aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli/ipc.c74
-rw-r--r--src/gsr_cli/main.c298
-rw-r--r--src/json.c69
3 files changed, 374 insertions, 67 deletions
diff --git a/src/cli/ipc.c b/src/cli/ipc.c
index 5a43283..b9d0c51 100644
--- a/src/cli/ipc.c
+++ b/src/cli/ipc.c
@@ -1,13 +1,10 @@
#include "../../include/cli/ipc.h"
#include "../../include/recorder/error.h"
#include "../../include/recorder/replay_save.h"
+#include "../../include/json.h"
#include "../../include/log.h"
-#define SJ_IMPL
-#include "../../external/sj.h"
-
#include <stdio.h>
-#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
@@ -33,63 +30,6 @@ typedef struct {
bool has_data;
} gsr_ipc_request;
-static void json_escape_string(char *buffer, size_t buffer_size, const char *str) {
- char escape_buffer[8];
- size_t offset = 0;
- buffer[0] = '\0';
-
- for(size_t i = 0; str[i] != '\0'; ++i) {
- const unsigned char c = str[i];
- const char *escaped = escape_buffer;
- switch(c) {
- case '"': escaped = "\\\""; break;
- case '\\': escaped = "\\\\"; break;
- case '\n': escaped = "\\n"; break;
- case '\r': escaped = "\\r"; break;
- case '\t': escaped = "\\t"; break;
- default: {
- if(c < 0x20)
- snprintf(escape_buffer, sizeof(escape_buffer), "\\u%04x", c);
- else
- snprintf(escape_buffer, sizeof(escape_buffer), "%c", c);
- break;
- }
- }
-
- const size_t escaped_size = strlen(escaped);
- if(offset + escaped_size >= buffer_size)
- break;
-
- memcpy(buffer + offset, escaped, escaped_size);
- offset += escaped_size;
- buffer[offset] = '\0';
- }
-}
-
-static bool json_string_equals(const sj_Value *value, const char *str) {
- const size_t value_size = value->end - value->start;
- return strlen(str) == value_size && memcmp(value->start, str, value_size) == 0;
-}
-
-static bool json_number_to_int64(const sj_Value *value, int64_t *result) {
- char buffer[32];
- const size_t value_size = value->end - value->start;
- if(value_size == 0 || value_size >= sizeof(buffer))
- return false;
-
- memcpy(buffer, value->start, value_size);
- buffer[value_size] = '\0';
-
- char *number_end = NULL;
- errno = 0;
- const long long parsed_value = strtoll(buffer, &number_end, 10);
- if(errno != 0 || number_end != buffer + value_size)
- return false;
-
- *result = parsed_value;
- return true;
-}
-
static bool string_is_only_whitespace(const char *str, size_t size) {
for(size_t i = 0; i < size; ++i) {
if(str[i] != ' ' && str[i] != '\t' && str[i] != '\r' && str[i] != '\n')
@@ -139,7 +79,7 @@ static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool succe
reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\"}\n", id);
} else {
char escaped_error_message[GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE];
- json_escape_string(escaped_error_message, sizeof(escaped_error_message), error_message ? error_message : "");
+ gsr_json_escape_string(escaped_error_message, sizeof(escaped_error_message), error_message ? error_message : "");
reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"error\",\"data\":\"%s\"}\n", id, escaped_error_message);
}
@@ -169,13 +109,13 @@ static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request,
sj_Value key;
sj_Value value;
while(sj_iter_object(&reader, root, &key, &value)) {
- if(json_string_equals(&key, "id")) {
+ if(gsr_json_string_equals(&key, "id")) {
id_value = value;
has_id = true;
- } else if(json_string_equals(&key, "name")) {
+ } else if(gsr_json_string_equals(&key, "name")) {
name_value = value;
has_name = true;
- } else if(json_string_equals(&key, "data")) {
+ } else if(gsr_json_string_equals(&key, "data")) {
request->data = value;
request->has_data = true;
}
@@ -191,7 +131,7 @@ static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request,
return false;
}
- if(id_value.type != SJ_NUMBER || !json_number_to_int64(&id_value, &request->id)) {
+ if(!gsr_json_number_to_int64(&id_value, &request->id)) {
snprintf(error_message, error_message_size, "expected 'id' to be an integer");
return false;
}
@@ -216,7 +156,7 @@ static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request,
return true;
int64_t data_seconds = 0;
- if(request->data.type != SJ_NUMBER || !json_number_to_int64(&request->data, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) {
+ if(!gsr_json_number_to_int64(&request->data, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) {
snprintf(error_message, error_message_size, "expected 'data' to be the number of seconds to save, which has to be larger than 0");
return false;
}
diff --git a/src/gsr_cli/main.c b/src/gsr_cli/main.c
new file mode 100644
index 0000000..fb0fcbf
--- /dev/null
+++ b/src/gsr_cli/main.c
@@ -0,0 +1,298 @@
+/*
+ Copyright (C) 2020 dec05eba
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "../../include/json.h"
+#include "../../include/log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/un.h>
+
+#define GSR_CLI_REQUEST_ID 1
+#define GSR_CLI_MAX_REQUEST_SIZE 256
+#define GSR_CLI_MAX_REPLY_SIZE 4096
+#define GSR_CLI_REPLY_TIMEOUT_SECONDS 10
+
+static void usage(void) {
+ printf("usage: gsr-cli -ipc <socket_path> <command> [command_argument]\n");
+ printf("\n");
+ printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n");
+ printf("\n");
+ printf("OPTIONS:\n");
+ printf(" -ipc <socket_path>\n");
+ printf(" The unix domain socket that GPU Screen Recorder was started with. Required.\n");
+ printf("\n");
+ printf("COMMANDS:\n");
+ printf(" status\n");
+ printf(" Check if a GPU Screen Recorder instance is listening on the socket. Prints \"running\" or\n");
+ printf(" \"not running\" and exits with 0 when it's running.\n");
+ printf(" stop\n");
+ printf(" Stop and save the recording (stop without save in replay mode).\n");
+ printf(" toggle-pause\n");
+ printf(" Pause/unpause the recording (not for streaming/replay).\n");
+ printf(" toggle-replay-recording\n");
+ printf(" Start/stop a regular recording during replay/streaming.\n");
+ printf(" save-replay [seconds]\n");
+ printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n");
+ printf(" saved when no number of seconds is given.\n");
+ printf("\n");
+ printf("EXAMPLES:\n");
+ printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n");
+ printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay 30\n");
+ fflush(stdout);
+}
+
+static bool string_to_int64(const char *str, int64_t *result) {
+ char *number_end = NULL;
+ errno = 0;
+ const long long parsed_value = strtoll(str, &number_end, 10);
+ if(errno != 0 || number_end == str || *number_end != '\0')
+ return false;
+
+ *result = parsed_value;
+ return true;
+}
+
+/* Returns the socket, or -1 on failure. Only logs an error when the failure isn't a missing GPU Screen Recorder instance */
+static int ipc_connect(const char *socket_filepath) {
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ if(snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath) >= (int)sizeof(addr.sun_path)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath);
+ return -1;
+ }
+
+ const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if(fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create a socket, error: %s", strerror(errno));
+ return -1;
+ }
+
+ struct timeval timeout;
+ timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS;
+ timeout.tv_usec = 0;
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+
+ if(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == -1) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+static bool ipc_send_all(int fd, const char *data, size_t size) {
+ size_t offset = 0;
+ while(offset < size) {
+ const ssize_t bytes_written = send(fd, data + offset, size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to send the request, error: %s", strerror(errno));
+ return false;
+ }
+ return true;
+}
+
+/* Reads until a newline. |reply_size| is set to the size of the reply, excluding the newline */
+static bool ipc_receive_reply(int fd, char *reply, size_t reply_capacity, size_t *reply_size) {
+ size_t offset = 0;
+ for(;;) {
+ if(offset == reply_capacity) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is too large");
+ return false;
+ }
+
+ const ssize_t bytes_read = recv(fd, reply + offset, reply_capacity - offset, 0);
+ if(bytes_read == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder closed the connection before replying");
+ return false;
+ }
+
+ if(bytes_read == -1) {
+ if(errno == EINTR)
+ continue;
+
+ if(errno == EAGAIN || errno == EWOULDBLOCK)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "timed out after %d seconds waiting for a reply", GSR_CLI_REPLY_TIMEOUT_SECONDS);
+ else
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to receive the reply, error: %s", strerror(errno));
+ return false;
+ }
+
+ const char *newline = memchr(reply + offset, '\n', bytes_read);
+ offset += bytes_read;
+ if(newline) {
+ *reply_size = newline - reply;
+ return true;
+ }
+ }
+}
+
+/* Returns the exit code that gsr-cli should exit with */
+static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) {
+ sj_Reader reader = sj_reader(reply, reply_size);
+ const sj_Value root = sj_read(&reader);
+ if(root.type != SJ_OBJECT) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "expected the reply to be a json object, got: %.*s", (int)reply_size, reply);
+ return 1;
+ }
+
+ int64_t id = 0;
+ bool has_id = false;
+ sj_Value result_value;
+ bool has_result = false;
+ sj_Value data_value;
+ bool has_data = false;
+
+ sj_Value key;
+ sj_Value value;
+ while(sj_iter_object(&reader, root, &key, &value)) {
+ if(gsr_json_string_equals(&key, "id")) {
+ has_id = gsr_json_number_to_int64(&value, &id);
+ } else if(gsr_json_string_equals(&key, "result")) {
+ result_value = value;
+ has_result = value.type == SJ_STRING;
+ } else if(gsr_json_string_equals(&key, "data")) {
+ data_value = value;
+ has_data = true;
+ }
+ }
+
+ if(reader.error) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to parse the reply: %s", reader.error);
+ return 1;
+ }
+
+ if(!has_id || id != request_id) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "received a reply to another request: %.*s", (int)reply_size, reply);
+ return 1;
+ }
+
+ if(!has_result) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is missing the 'result' field: %.*s", (int)reply_size, reply);
+ return 1;
+ }
+
+ if(gsr_json_string_equals(&result_value, "ok"))
+ return 0;
+
+ if(has_data && data_value.type == SJ_STRING)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "%.*s", (int)(data_value.end - data_value.start), data_value.start);
+ else
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the request failed: %.*s", (int)reply_size, reply);
+
+ return 1;
+}
+
+static int status_command(const char *socket_filepath) {
+ const int fd = ipc_connect(socket_filepath);
+ if(fd == -1) {
+ printf("not running\n");
+ fflush(stdout);
+ return 1;
+ }
+
+ close(fd);
+ printf("running\n");
+ fflush(stdout);
+ return 0;
+}
+
+static int send_command(const char *socket_filepath, const char *name, const char *seconds_str) {
+ char request[GSR_CLI_MAX_REQUEST_SIZE];
+ if(seconds_str) {
+ int64_t seconds = 0;
+ if(!string_to_int64(seconds_str, &seconds) || seconds <= 0 || seconds > INT_MAX) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", seconds_str);
+ return 1;
+ }
+ snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, name, seconds);
+ } else {
+ snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, name);
+ }
+
+ const int fd = ipc_connect(socket_filepath);
+ if(fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to connect to \"%s\". Is GPU Screen Recorder running with the -ipc option?", socket_filepath);
+ return 1;
+ }
+
+ int exit_code = 1;
+ char reply[GSR_CLI_MAX_REPLY_SIZE];
+ size_t reply_size = 0;
+ if(ipc_send_all(fd, request, strlen(request)) && ipc_receive_reply(fd, reply, sizeof(reply), &reply_size))
+ exit_code = ipc_handle_reply(reply, reply_size, GSR_CLI_REQUEST_ID);
+
+ close(fd);
+ return exit_code;
+}
+
+int main(int argc, char **argv) {
+ if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
+ usage();
+ return 0;
+ }
+
+ if(argc < 4 || strcmp(argv[1], "-ipc") != 0) {
+ usage();
+ return 1;
+ }
+
+ if(argc > 5) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments");
+ usage();
+ return 1;
+ }
+
+ const char *socket_filepath = argv[2];
+ const char *command = argv[3];
+ const char *command_argument = argc == 5 ? argv[4] : NULL;
+
+ if(strcmp(command, "save-replay") == 0)
+ return send_command(socket_filepath, command, command_argument);
+
+ if(command_argument) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take an argument", command);
+ usage();
+ return 1;
+ }
+
+ if(strcmp(command, "status") == 0)
+ return status_command(socket_filepath);
+
+ if(strcmp(command, "stop") == 0 || strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0)
+ return send_command(socket_filepath, command, NULL);
+
+ gsr_log(GSR_LOG_LEVEL_ERROR, "invalid command '%s'", command);
+ usage();
+ return 1;
+}
diff --git a/src/json.c b/src/json.c
new file mode 100644
index 0000000..f7d1cd7
--- /dev/null
+++ b/src/json.c
@@ -0,0 +1,69 @@
+#include "../include/json.h"
+
+#define SJ_IMPL
+#include "../external/sj.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+bool gsr_json_string_equals(const sj_Value *value, const char *str) {
+ const size_t value_size = value->end - value->start;
+ return strlen(str) == value_size && memcmp(value->start, str, value_size) == 0;
+}
+
+bool gsr_json_number_to_int64(const sj_Value *value, int64_t *result) {
+ if(value->type != SJ_NUMBER)
+ return false;
+
+ char buffer[32];
+ const size_t value_size = value->end - value->start;
+ if(value_size == 0 || value_size >= sizeof(buffer))
+ return false;
+
+ memcpy(buffer, value->start, value_size);
+ buffer[value_size] = '\0';
+
+ char *number_end = NULL;
+ errno = 0;
+ const long long parsed_value = strtoll(buffer, &number_end, 10);
+ if(errno != 0 || number_end != buffer + value_size)
+ return false;
+
+ *result = parsed_value;
+ return true;
+}
+
+void gsr_json_escape_string(char *buffer, size_t buffer_size, const char *str) {
+ char escape_buffer[8];
+ size_t offset = 0;
+ buffer[0] = '\0';
+
+ for(size_t i = 0; str[i] != '\0'; ++i) {
+ const unsigned char c = str[i];
+ const char *escaped = escape_buffer;
+ switch(c) {
+ case '"': escaped = "\\\""; break;
+ case '\\': escaped = "\\\\"; break;
+ case '\n': escaped = "\\n"; break;
+ case '\r': escaped = "\\r"; break;
+ case '\t': escaped = "\\t"; break;
+ default: {
+ if(c < 0x20)
+ snprintf(escape_buffer, sizeof(escape_buffer), "\\u%04x", c);
+ else
+ snprintf(escape_buffer, sizeof(escape_buffer), "%c", c);
+ break;
+ }
+ }
+
+ const size_t escaped_size = strlen(escaped);
+ if(offset + escaped_size >= buffer_size)
+ break;
+
+ memcpy(buffer + offset, escaped, escaped_size);
+ offset += escaped_size;
+ buffer[offset] = '\0';
+ }
+}