diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli/ipc.c | 74 | ||||
| -rw-r--r-- | src/gsr_cli/main.c | 298 | ||||
| -rw-r--r-- | src/json.c | 69 |
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'; + } +} |
