From d72113be395891f4289f0d86a32a37a99a4d5b38 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 01:11:33 +0200 Subject: Add -ipc option to control gpu-screen-recorder over a unix domain socket The socket accepts the same commands as the signal handlers, except that saving a replay takes an arbitrary number of seconds instead of only the fixed times that the signals provide. Requests and replies are newline terminated json objects and every request is replied to, parsed with the sj.h json library that was added to external. Requests are handled on a thread that only runs while the recorder exists, so the recorder control fields are now atomics, which they have to be now that another thread writes them. --- src/cli/ipc.c | 555 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 src/cli/ipc.c (limited to 'src/cli/ipc.c') diff --git a/src/cli/ipc.c b/src/cli/ipc.c new file mode 100644 index 0000000..6d555c0 --- /dev/null +++ b/src/cli/ipc.c @@ -0,0 +1,555 @@ +#include "../../include/cli/ipc.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/replay_save.h" +#include "../../include/log.h" + +#define SJ_IMPL +#include "../../external/sj.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GSR_IPC_MAX_REQUEST_NAME_SIZE 64 +#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6) +#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + 128) +#define GSR_IPC_SEND_TIMEOUT_MILLISECONDS 1000 +#define GSR_IPC_SOCKET_MODE 0600 + +typedef struct { + int64_t id; + char name[GSR_IPC_MAX_REQUEST_NAME_SIZE]; + sj_Value data; + 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') + return false; + } + return true; +} + +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; + + if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + struct pollfd poll_fd; + poll_fd.fd = fd; + poll_fd.events = POLLOUT; + poll_fd.revents = 0; + + const int poll_result = poll(&poll_fd, 1, GSR_IPC_SEND_TIMEOUT_MILLISECONDS); + if(poll_result == -1 && errno == EINTR) + continue; + + if(poll_result <= 0) + return false; + + continue; + } + + return false; + } + return true; +} + +static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool success, const char *error_message) { + char reply[GSR_IPC_MAX_REPLY_SIZE]; + int reply_size = 0; + + if(success) { + 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 : ""); + reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"error\",\"data\":\"%s\"}\n", id, escaped_error_message); + } + + if(reply_size < 0 || reply_size >= (int)sizeof(reply)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a reply to request %" PRIi64, id); + return false; + } + + return ipc_send_all(client->fd, reply, reply_size); +} + +static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, char *error_message, size_t error_message_size) { + memset(request, 0, sizeof(*request)); + + sj_Reader reader = sj_reader(data, size); + const sj_Value root = sj_read(&reader); + if(root.type != SJ_OBJECT) { + snprintf(error_message, error_message_size, "expected the request to be a json object"); + return false; + } + + sj_Value id_value; + sj_Value name_value; + bool has_id = false; + bool has_name = false; + + sj_Value key; + sj_Value value; + while(sj_iter_object(&reader, root, &key, &value)) { + if(json_string_equals(&key, "id")) { + id_value = value; + has_id = true; + } else if(json_string_equals(&key, "name")) { + name_value = value; + has_name = true; + } else if(json_string_equals(&key, "data")) { + request->data = value; + request->has_data = true; + } + } + + if(reader.error) { + snprintf(error_message, error_message_size, "failed to parse the request: %s", reader.error); + return false; + } + + if(!has_id) { + snprintf(error_message, error_message_size, "the request is missing the 'id' field"); + return false; + } + + if(id_value.type != SJ_NUMBER || !json_number_to_int64(&id_value, &request->id)) { + snprintf(error_message, error_message_size, "expected 'id' to be an integer"); + return false; + } + + if(!has_name) { + snprintf(error_message, error_message_size, "the request is missing the 'name' field"); + return false; + } + + if(name_value.type != SJ_STRING) { + snprintf(error_message, error_message_size, "expected 'name' to be a string"); + return false; + } + + snprintf(request->name, sizeof(request->name), "%.*s", (int)(name_value.end - name_value.start), name_value.start); + return true; +} + +static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, int *seconds, char *error_message, size_t error_message_size) { + *seconds = GSR_SAVE_REPLAY_SECONDS_FULL; + if(!request->has_data || request->data.type == SJ_NULL) + 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) { + 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; + } + + *seconds = data_seconds; + return true; +} + +static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, char *error_message, size_t error_message_size) { + if(strcmp(request->name, "stop") == 0) + return self->handlers.stop(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "toggle-pause") == 0) + return self->handlers.toggle_pause(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "toggle-replay-recording") == 0) + return self->handlers.toggle_replay_recording(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "save-replay") == 0) { + int seconds = GSR_SAVE_REPLAY_SECONDS_FULL; + if(!ipc_request_get_save_replay_seconds(request, &seconds, error_message, error_message_size)) + return false; + + return self->handlers.save_replay(seconds, error_message, error_message_size, self->handlers.userdata); + } + + snprintf(error_message, error_message_size, "unknown request name '%s'", request->name); + return false; +} + +static bool ipc_client_on_request(gsr_ipc *self, gsr_ipc_client *client) { + if(client->request_too_large) + return ipc_client_send_reply(client, 0, false, "the request is too large"); + + if(string_is_only_whitespace(client->request, client->request_size)) + return ipc_client_send_reply(client, 0, false, "the request is empty"); + + char error_message[GSR_IPC_MAX_ERROR_MESSAGE_SIZE]; + error_message[0] = '\0'; + + gsr_ipc_request request; + if(!ipc_request_parse(client->request, client->request_size, &request, error_message, sizeof(error_message))) + return ipc_client_send_reply(client, request.id, false, error_message); + + if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) + return ipc_client_send_reply(client, request.id, false, error_message); + + return ipc_client_send_reply(client, request.id, true, NULL); +} + +static bool ipc_client_on_byte(gsr_ipc *self, gsr_ipc_client *client, char c) { + if(c != '\n') { + if(client->request_size < GSR_IPC_MAX_REQUEST_SIZE) + client->request[client->request_size++] = c; + else + client->request_too_large = true; + return true; + } + + const bool keep_client = ipc_client_on_request(self, client); + client->request_size = 0; + client->request_too_large = false; + return keep_client; +} + +static bool ipc_client_receive(gsr_ipc *self, gsr_ipc_client *client) { + for(;;) { + char buffer[1024]; + const ssize_t bytes_read = recv(client->fd, buffer, sizeof(buffer), 0); + if(bytes_read == 0) + return false; + + if(bytes_read == -1) { + if(errno == EINTR) + continue; + + return errno == EAGAIN || errno == EWOULDBLOCK; + } + + for(ssize_t i = 0; i < bytes_read; ++i) { + if(!ipc_client_on_byte(self, client, buffer[i])) + return false; + } + } +} + +static bool fd_set_cloexec(int fd) { + const int flags = fcntl(fd, F_GETFD); + return flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1; +} + +static bool fd_set_nonblocking(int fd) { + const int flags = fcntl(fd, F_GETFL); + return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1; +} + +static void ipc_add_client(gsr_ipc *self, int client_fd) { + if(self->num_clients == GSR_IPC_MAX_CLIENTS) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: too many ipc clients are connected, rejecting the new connection"); + close(client_fd); + return; + } + + if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to setup the ipc client socket, error: %s", strerror(errno)); + close(client_fd); + return; + } + + gsr_ipc_client *client = &self->clients[self->num_clients]; + client->fd = client_fd; + client->request_size = 0; + client->request_too_large = false; + ++self->num_clients; +} + +static void ipc_accept_client(gsr_ipc *self) { + const int client_fd = accept(self->socket_fd, NULL, NULL); + if(client_fd == -1) + return; + + ipc_add_client(self, client_fd); +} + +static void ipc_remove_client(gsr_ipc *self, int index) { + close(self->clients[index].fd); + for(int i = index; i < self->num_clients - 1; ++i) { + self->clients[i] = self->clients[i + 1]; + } + --self->num_clients; +} + +static void* ipc_thread(void *userdata) { + gsr_ipc *self = userdata; + struct pollfd poll_fds[2 + GSR_IPC_MAX_CLIENTS]; + + for(;;) { + poll_fds[0].fd = self->wakeup_pipe[0]; + poll_fds[0].events = POLLIN; + poll_fds[0].revents = 0; + poll_fds[1].fd = self->socket_fd; + poll_fds[1].events = POLLIN; + poll_fds[1].revents = 0; + + const int num_polled_clients = self->num_clients; + for(int i = 0; i < num_polled_clients; ++i) { + poll_fds[2 + i].fd = self->clients[i].fd; + poll_fds[2 + i].events = POLLIN; + poll_fds[2 + i].revents = 0; + } + + if(poll(poll_fds, 2 + num_polled_clients, -1) == -1) { + if(errno == EINTR) + continue; + + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to poll the ipc sockets, error: %s", strerror(errno)); + break; + } + + if(poll_fds[0].revents != 0) + break; + + if(poll_fds[1].revents & POLLIN) + ipc_accept_client(self); + + for(int i = num_polled_clients - 1; i >= 0; --i) { + bool keep_client = true; + if(poll_fds[2 + i].revents & POLLIN) + keep_client = ipc_client_receive(self, &self->clients[i]); + else if(poll_fds[2 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) + keep_client = false; + + if(!keep_client) + ipc_remove_client(self, i); + } + } + + return NULL; +} + +static bool ipc_socket_filepath_in_use(const char *socket_filepath) { + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath); + + const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if(fd == -1) + return true; + + const bool in_use = connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == 0; + close(fd); + return in_use; +} + +static bool ipc_bind(gsr_ipc *self, const struct sockaddr_un *addr) { + const mode_t prev_mask = umask(0777 & ~GSR_IPC_SOCKET_MODE); + int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); + if(bind_result == -1 && errno == EADDRINUSE && !ipc_socket_filepath_in_use(self->socket_filepath)) { + unlink(self->socket_filepath); + bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); + } + const int bind_error = errno; + umask(prev_mask); + + if(bind_result == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to bind the ipc socket to \"%s\", error: %s", self->socket_filepath, strerror(bind_error)); + return false; + } + + self->socket_bound = true; + return true; +} + +static void ipc_close(gsr_ipc *self) { + for(int i = 0; i < self->num_clients; ++i) { + close(self->clients[i].fd); + } + self->num_clients = 0; + + for(int i = 0; i < 2; ++i) { + if(self->wakeup_pipe[i] != -1) { + close(self->wakeup_pipe[i]); + self->wakeup_pipe[i] = -1; + } + } + + if(self->socket_fd != -1) { + close(self->socket_fd); + self->socket_fd = -1; + } + + if(self->socket_bound) { + unlink(self->socket_filepath); + self->socket_bound = false; + } +} + +int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { + memset(self, 0, sizeof(*self)); + self->socket_fd = -1; + self->wakeup_pipe[0] = -1; + self->wakeup_pipe[1] = -1; + + 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, "gsr_ipc_init: the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath); + goto err; + } + + snprintf(self->socket_filepath, sizeof(self->socket_filepath), "%s", socket_filepath); + + if(pipe(self->wakeup_pipe) == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc wakeup pipe, error: %s", strerror(errno)); + self->wakeup_pipe[0] = -1; + self->wakeup_pipe[1] = -1; + goto err; + } + + if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1])) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to setup the ipc wakeup pipe, error: %s", strerror(errno)); + goto err; + } + + self->socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if(self->socket_fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc socket, error: %s", strerror(errno)); + goto err; + } + + if(!ipc_bind(self, &addr)) + goto err; + + if(listen(self->socket_fd, GSR_IPC_MAX_CLIENTS) == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to listen on the ipc socket, error: %s", strerror(errno)); + goto err; + } + + self->initialized = true; + return GSR_ERROR_OK; + + err: + ipc_close(self); + return GSR_ERROR_GENERIC; +} + +void gsr_ipc_deinit(gsr_ipc *self) { + if(!self->initialized) + return; + + gsr_ipc_stop(self); + ipc_close(self); + self->initialized = false; +} + +int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers) { + if(!self->initialized) + return GSR_ERROR_OK; + + self->handlers = *handlers; + + /* Block all signals in the ipc thread to keep the signal handlers running on the main thread */ + sigset_t all_signals; + sigset_t prev_signals; + sigfillset(&all_signals); + pthread_sigmask(SIG_SETMASK, &all_signals, &prev_signals); + const int thread_create_result = pthread_create(&self->thread, NULL, ipc_thread, self); + pthread_sigmask(SIG_SETMASK, &prev_signals, NULL); + + if(thread_create_result != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_start: failed to create the ipc thread, error: %s", strerror(thread_create_result)); + return GSR_ERROR_GENERIC; + } + + self->thread_running = true; + return GSR_ERROR_OK; +} + +void gsr_ipc_stop(gsr_ipc *self) { + if(!self->thread_running) + return; + + const char wakeup_value = 1; + ssize_t bytes_written = 0; + do { + bytes_written = write(self->wakeup_pipe[1], &wakeup_value, 1); + } while(bytes_written == -1 && errno == EINTR); + + if(bytes_written == -1) + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_stop: failed to wake up the ipc thread, error: %s", strerror(errno)); + + pthread_join(self->thread, NULL); + self->thread_running = false; +} -- cgit v1.2.3 From 9b799f6e45c5e7a44432824f69d9632ff5848e6c Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 01:35:27 +0200 Subject: Only remove the ipc socket path when an unused socket exists there The socket that a killed GPU Screen Recorder instance leaves behind is removed so that the same ipc socket path can be used again, but this was done for any type of file, so using the path of a regular file as the ipc socket path deleted that file. --- src/cli/ipc.c | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) (limited to 'src/cli/ipc.c') diff --git a/src/cli/ipc.c b/src/cli/ipc.c index 6d555c0..5a43283 100644 --- a/src/cli/ipc.c +++ b/src/cli/ipc.c @@ -412,13 +412,32 @@ static bool ipc_socket_filepath_in_use(const char *socket_filepath) { return in_use; } +/* Removes the socket that a GPU Screen Recorder instance that was killed left behind, so the same ipc socket path can be used again */ +static bool ipc_remove_unused_socket(const char *socket_filepath) { + struct stat file_stat; + if(lstat(socket_filepath, &file_stat) == -1) + return true; + + if(!S_ISSOCK(file_stat.st_mode)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: can't use \"%s\" as the ipc socket path because a file that isn't a socket already exists there", socket_filepath); + return false; + } + + if(ipc_socket_filepath_in_use(socket_filepath)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: another program is already listening on \"%s\"", socket_filepath); + return false; + } + + unlink(socket_filepath); + return true; +} + static bool ipc_bind(gsr_ipc *self, const struct sockaddr_un *addr) { + if(!ipc_remove_unused_socket(self->socket_filepath)) + return false; + const mode_t prev_mask = umask(0777 & ~GSR_IPC_SOCKET_MODE); - int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); - if(bind_result == -1 && errno == EADDRINUSE && !ipc_socket_filepath_in_use(self->socket_filepath)) { - unlink(self->socket_filepath); - bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); - } + const int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); const int bind_error = errno; umask(prev_mask); -- cgit v1.2.3 From f547fcc354671c672fffb20092e72d231e6ed1e4 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 01:36:33 +0200 Subject: Add gsr-cli program that sends ipc commands to gpu-screen-recorder gsr-cli sends a command to the ipc socket of a running GPU Screen Recorder and exits with 0 when it replied with ok, printing the reason to stderr when it didn't, which means the commands don't have to be sent blindly like signals are. The status command checks if GPU Screen Recorder is running by connecting to the socket without sending a command, so it's also correct when a killed instance left the socket behind. The json helpers moved into src/json.c to be shared between the two programs. --- README.md | 5 +- gpu-screen-recorder.1 | 7 ++ gsr-cli.1 | 100 +++++++++++++++++ include/json.h | 15 +++ meson.build | 4 +- project.conf | 2 +- src/cli/ipc.c | 74 ++----------- src/gsr_cli/main.c | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/json.c | 69 ++++++++++++ 9 files changed, 503 insertions(+), 71 deletions(-) create mode 100644 gsr-cli.1 create mode 100644 include/json.h create mode 100644 src/gsr_cli/main.c create mode 100644 src/json.c (limited to 'src/cli/ipc.c') diff --git a/README.md b/README.md index 089076e..1b7502d 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,9 @@ To stop recording send SIGINT to gpu screen recorder. You can do this by running To pause/unpause recording send SIGUSR2 to gpu screen recorder. You can do this by running `pkill -SIGUSR2 -f "^gpu-screen-recorder"`. This is only applicable and useful when recording (not streaming nor replay).\ There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section).\ GPU Screen Recorder can also be controlled with json messages over a unix domain socket by launching it with the `-ipc` option, for example `-ipc "$XDG_RUNTIME_DIR/gsr.sock"`. -This gives the same control as the signals, except that a replay can be saved with an arbitrary number of seconds, for example `echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock"`. -See the `IPC` section in the man page (`man gpu-screen-recorder`) for the full protocol. +This gives the same control as the signals, except that a replay can be saved with an arbitrary number of seconds and that you get a reply that says if the command succeeded.\ +The `gsr-cli` program sends these commands, for example `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30`. It can also check if GPU Screen Recorder is running with `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status`.\ +See `man gsr-cli` for all commands and the `IPC` section in `man gpu-screen-recorder` for the protocol, if you want to talk to the socket directly instead of using `gsr-cli`. ## Simple way to run replay without gui Run the script `scripts/start-replay.sh` to start replay and then `scripts/save-replay.sh` to save a replay and `scripts/stop-replay.sh` to stop the replay. The videos are saved to `$HOME/Videos`. You can use these scripts to start replay at system startup if you add `scripts/start-replay.sh` to startup (this can be done differently depending on your desktop environment / window manager) and then go into diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index 036197b..af723e8 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -476,6 +476,10 @@ option is used GPU Screen Recorder also listens for commands on a unix domain so except that a replay can be saved with an arbitrary number of seconds. The socket can only be used by the user that started GPU Screen Recorder and it's removed when GPU Screen Recorder exits. .PP +.BR gsr\-cli (1) +is a program that sends these commands and reports the reply, so the protocol below only has to be +implemented by programs that want to talk to the socket directly. +.PP Requests and replies are json objects terminated by a newline. Every request is replied to. A request has these fields: .TP @@ -687,6 +691,9 @@ The bug may have been previously reported or may not be related to gpu-screen-r .SH COPYRIGHT Copyright © dec05eba. Licensed under GPL-3.0-only. .SH SEE ALSO +.BR gsr\-cli (1), +.BR gsr\-kms\-server (1) +.PP .UR https://git.dec05eba.com/gpu-screen-recorder Project homepage .UE diff --git a/gsr-cli.1 b/gsr-cli.1 new file mode 100644 index 0000000..0c6084e --- /dev/null +++ b/gsr-cli.1 @@ -0,0 +1,100 @@ +.TH GSR\-CLI 1 "2026-08-02" "5.15.3" "GPU Screen Recorder CLI Manual" +.SH NAME +gsr\-cli \- Control a running GPU Screen Recorder instance +.SH SYNOPSIS +.B gsr\-cli +.B \-ipc +.I socket_path +.I command +.RI [ command_argument ] +.PP +.B gsr\-cli +.B \-h +| +.B \-\-help +.SH DESCRIPTION +.B gsr\-cli +sends a command to a +.BR gpu\-screen\-recorder (1) +instance that was started with the +.B \-ipc +option and reports the reply. This gives the same control as sending signals to GPU Screen Recorder, +except that a replay can be saved with an arbitrary number of seconds and that the result of the +command is known instead of being sent blindly. +.PP +.B gsr\-cli +exits with 0 when the command succeeded and 1 when it failed. When a command fails the reason +is printed to stderr. +.SH OPTIONS +.TP +.BI \-ipc " socket_path" +The unix domain socket that GPU Screen Recorder was started with. Required. +.SH COMMANDS +.TP +.B status +Check if a GPU Screen Recorder instance is listening on the socket, which is done by connecting +to the socket without sending a command. Prints +.B running +or +.B "not running" +and exits with 0 when it's running. A socket file that was left behind by a GPU Screen Recorder +instance that was killed counts as not running. +.TP +.B stop +Stop and save recording (stop without save in replay mode). +.TP +.B toggle-pause +Pause/unpause recording (not for streaming/replay). +.TP +.B toggle-replay-recording +Start/stop regular recording during replay/streaming, which requires GPU Screen Recorder to run +with the +.B \-ro +option. +.TP +.BR save-replay " [" \fIseconds\fR ] +Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay +buffer is saved when no number of seconds is given. +.SH EXAMPLES +Start GPU Screen Recorder in replay mode with an ipc socket: +.RS +.nf +gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" & +.fi +.RE +.PP +Save the last 30 seconds: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 +.fi +.RE +.PP +Start replay only when it isn't already running: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh +.fi +.RE +.SH NOTES +.IP \(bu 3 +The socket can only be used by the user that started GPU Screen Recorder. +.IP \(bu 3 +A command is replied to as soon as GPU Screen Recorder accepts it. Saving a video finishes +after the reply, and the path to the saved video is printed by GPU Screen Recorder itself. +.IP \(bu 3 +GPU Screen Recorder creates the socket before it has finished starting up, so a command that is +sent while it's starting up is replied to when the recording starts. +.SH AUTHORS +gsr\-cli was written by the GPU Screen Recorder contributors. +.SH REPORTING BUGS +Report bugs to: +.UR mailto:dec05eba@protonmail.com +dec05eba@protonmail.com +.UE . +.SH COPYRIGHT +Copyright © dec05eba. Licensed under GPL-3.0-only. +.SH SEE ALSO +.BR gpu\-screen\-recorder (1) +.PP +Project homepage: diff --git a/include/json.h b/include/json.h new file mode 100644 index 0000000..3bf8a9e --- /dev/null +++ b/include/json.h @@ -0,0 +1,15 @@ +#ifndef GSR_JSON_H +#define GSR_JSON_H + +#include +#include +#include +#include "../external/sj.h" + +bool gsr_json_string_equals(const sj_Value *value, const char *str); +/* Fails if |value| is not a json number without a fractional part */ +bool gsr_json_number_to_int64(const sj_Value *value, int64_t *result); +/* An escaped string can become 6 times as large as |str|. The result is truncated when it doesn't fit in |buffer| */ +void gsr_json_escape_string(char *buffer, size_t buffer_size, const char *str); + +#endif /* GSR_JSON_H */ diff --git a/meson.build b/meson.build index bddfc02..7b0c14e 100644 --- a/meson.build +++ b/meson.build @@ -31,6 +31,7 @@ src = [ 'src/replay_buffer/replay_buffer_ram.c', 'src/replay_buffer/replay_buffer_disk.c', 'src/log.c', + 'src/json.c', 'src/ffmpeg_utils.c', 'src/recorder/audio_codec.c', 'src/recorder/video_codec.c', @@ -131,10 +132,11 @@ endif add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: 'c') executable('gsr-kms-server', 'kms/server/kms_server.c', dependencies : dependency('libdrm'), c_args : '-fstack-protector-all', install : true) +executable('gsr-cli', ['src/gsr_cli/main.c', 'src/json.c', 'src/log.c'], install : true) executable('gpu-screen-recorder', src, dependencies : dep, install : true) install_headers('plugin/plugin.h', install_dir : 'include/gsr') -install_man('gpu-screen-recorder.1', 'gsr-kms-server.1') +install_man('gpu-screen-recorder.1', 'gsr-cli.1', 'gsr-kms-server.1') install_subdir('scripts', install_dir: 'share/gpu-screen-recorder') if get_option('systemd') == true diff --git a/project.conf b/project.conf index 0e0ed9c..38af0ba 100644 --- a/project.conf +++ b/project.conf @@ -8,7 +8,7 @@ platforms = ["posix"] version = "c++17" [config] -ignore_dirs = ["kms/server", "build", "debug-build", "plugin/examples"] +ignore_dirs = ["kms/server", "src/gsr_cli", "build", "debug-build", "plugin/examples"] #error_on_warning = "true" [define] 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 -#include #include #include #include @@ -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 . +*/ + +#include "../../include/json.h" +#include "../../include/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 [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 \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 +#include +#include +#include + +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'; + } +} -- cgit v1.2.3 From fbc31a9561d16418226c0efa65e2a73923516dbd Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 14:56:08 +0200 Subject: ipc: response with video path in save recording/replay --- README.md | 5 + gpu-screen-recorder.1 | 37 ++- gsr-cli.1 | 31 ++- include/cli/ipc.h | 46 ++++ include/recorder/recorder.h | 7 + src/cli/ipc.c | 545 ++++++++++++++++++++++++++++++++++++++------ src/cli/main.c | 72 +++++- src/recorder/recorder.c | 166 +++++++++----- src/recorder/replay_save.c | 2 +- tools/gsr-cli/main.c | 103 ++++++--- 10 files changed, 843 insertions(+), 171 deletions(-) (limited to 'src/cli/ipc.c') diff --git a/README.md b/README.md index 055bddd..9715748 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,18 @@ and then use the `gsr-cli` program to send commands to it:\ gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" start-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" stop-replay-recording gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-pause +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" set-paused true gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" stop gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status ``` This gives the same control as the signals, with these differences: * `save-replay` takes the number of seconds to save, instead of the fixed times that the signals provide. The whole replay buffer is saved when no number of seconds is given. * `gsr-cli` exits with 0 only when the command succeeded and prints the reason to stderr when it didn't, so commands don't have to be sent blindly. +* `stop`, `save-replay` and `stop-replay-recording` are replied to when the file they save has been saved (and after the `-sc` script has been started), and `gsr-cli` prints the path of the saved file. +* `set-paused`, `start-replay-recording` and `stop-replay-recording` set an absolute state instead of toggling, so the result doesn't depend on the current state. * `status` prints `running` or `not running` and exits with 0 when GPU Screen Recorder is running, which a script can use to only start replay when it isn't already running: `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh`. * The commands are sent to one specific GPU Screen Recorder instance instead of every instance that `pkill` matches. diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index af723e8..67857a8 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -506,27 +506,54 @@ or .B data Optional. For an .B error -result this is a string that describes what went wrong. +result this is a string that describes what went wrong. For an +.B ok +result to +.BR stop ", " save-replay " or " stop-replay-recording +this is a string with the path of the saved file. +.PP +The +.BR stop ", " save-replay " and " stop-replay-recording +requests save a file, and they are replied to when the file has been saved and the script given with the +.B \-sc +option has been started. The other requests are replied to as soon as they are accepted. .PP These requests are available: .TP .B stop -Stop and save recording (stop without save in replay mode). +Stop and save recording (stop without save in replay mode). The reply contains the path of the +saved file, except in replay mode where nothing is saved. .TP .B save-replay Save replay (replay mode only). .B data is the number of seconds to save, which has to be larger than 0. The whole replay buffer is saved when .B data -is omitted or null. +is omitted or null. The reply contains the path of the saved file. .TP .B toggle-pause Pause/unpause recording (not for streaming/replay). .TP +.B set-paused +Pause/unpause recording (not for streaming/replay). +.B data +has to be true to pause or false to unpause. Unlike +.B toggle-pause +this doesn't fail when the recording is already paused/unpaused. +.TP .B toggle-replay-recording Start/stop regular recording during replay/streaming, which requires the .B \-ro option. +.TP +.B start-replay-recording +Start regular recording during replay/streaming, which requires the +.B \-ro +option. Does nothing when a recording is already running. +.TP +.B stop-replay-recording +Stop the regular recording that runs during replay/streaming. Fails when no recording is running. +The reply contains the path of the saved file. .PP Example: .nf @@ -535,10 +562,10 @@ gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_ echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" .RE .fi -which replies with: +which replies with the following when the replay has been saved: .nf .RS -{"id":1,"result":"ok"} +{"id":1,"result":"ok","data":"/home/user/Videos/Replay_2026-08-05_14-04-22.mp4"} .RE .fi .SH EXAMPLES diff --git a/gsr-cli.1 b/gsr-cli.1 index 0c6084e..0a05df9 100644 --- a/gsr-cli.1 +++ b/gsr-cli.1 @@ -41,20 +41,39 @@ and exits with 0 when it's running. A socket file that was left behind by a GPU instance that was killed counts as not running. .TP .B stop -Stop and save recording (stop without save in replay mode). +Stop and save recording (stop without save in replay mode). Waits until the recording has been +saved and prints the path of the saved file. Nothing is printed in replay mode since nothing is +saved. .TP .B toggle-pause Pause/unpause recording (not for streaming/replay). .TP +.BR set-paused " true|false" +Pause/unpause recording (not for streaming/replay). Unlike +.B toggle-pause +this doesn't fail when the recording is already paused/unpaused, so the result doesn't depend on +the current state. +.TP .B toggle-replay-recording Start/stop regular recording during replay/streaming, which requires GPU Screen Recorder to run with the .B \-ro option. .TP +.B start-replay-recording +Start regular recording during replay/streaming, which requires GPU Screen Recorder to run +with the +.B \-ro +option. Does nothing when a recording is already running. +.TP +.B stop-replay-recording +Stop the regular recording that runs during replay/streaming. Waits until the recording has been +saved and prints the path of the saved file. Fails when no recording is running. +.TP .BR save-replay " [" \fIseconds\fR ] Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay -buffer is saved when no number of seconds is given. +buffer is saved when no number of seconds is given. Waits until the replay has been saved and +prints the path of the saved file. .SH EXAMPLES Start GPU Screen Recorder in replay mode with an ipc socket: .RS @@ -80,8 +99,12 @@ gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh .IP \(bu 3 The socket can only be used by the user that started GPU Screen Recorder. .IP \(bu 3 -A command is replied to as soon as GPU Screen Recorder accepts it. Saving a video finishes -after the reply, and the path to the saved video is printed by GPU Screen Recorder itself. +The +.BR stop ", " save-replay " and " stop-replay-recording +commands are replied to when the file has been saved and the script given with the +.B \-sc +option has been started, and the reply contains the path of the saved file. The other commands are +replied to as soon as GPU Screen Recorder accepts them. .IP \(bu 3 GPU Screen Recorder creates the socket before it has finished starting up, so a command that is sent while it's starting up is replied to when the recording starts. diff --git a/include/cli/ipc.h b/include/cli/ipc.h index 0862c71..6008db9 100644 --- a/include/cli/ipc.h +++ b/include/cli/ipc.h @@ -3,43 +3,82 @@ #include #include +#include #include #include #define GSR_IPC_MAX_CLIENTS 8 #define GSR_IPC_MAX_REQUEST_SIZE 4096 #define GSR_IPC_MAX_ERROR_MESSAGE_SIZE 256 +#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6) +#define GSR_IPC_MAX_ESCAPED_DATA_SIZE PATH_MAX +#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + GSR_IPC_MAX_ESCAPED_DATA_SIZE + 128) +#define GSR_IPC_CLIENT_SEND_BUFFER_SIZE (GSR_IPC_MAX_REPLY_SIZE*2) /* These are called from the ipc thread while the recording is running. Return false and write a message to |error_message| to reply to the request with an error. + The reply to the requests that match a |gsr_ipc_deferred_request_type| is not sent when the handler + succeeds. It's sent when the matching gsr_ipc_complete_request is called. */ typedef struct { bool (*stop)(char *error_message, size_t error_message_size, void *userdata); bool (*toggle_pause)(char *error_message, size_t error_message_size, void *userdata); + bool (*set_paused)(bool paused, char *error_message, size_t error_message_size, void *userdata); bool (*toggle_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + bool (*start_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + bool (*stop_replay_recording)(char *error_message, size_t error_message_size, void *userdata); /* |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved */ bool (*save_replay)(int seconds, char *error_message, size_t error_message_size, void *userdata); void *userdata; } gsr_ipc_handlers; +/* Requests that are replied to when the file they save has been saved (see gsr_ipc_complete_request) */ +typedef enum { + GSR_IPC_DEFERRED_REQUEST_STOP, + GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, + GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, + GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT +} gsr_ipc_deferred_request_type; + +typedef enum { + GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY, + GSR_IPC_DEFERRED_REQUEST_STATE_PENDING, + GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED +} gsr_ipc_deferred_request_state; + +typedef struct { + gsr_ipc_deferred_request_state state; + int client_fd; + int64_t request_id; + bool success; + bool has_filepath; + char filepath[PATH_MAX]; +} gsr_ipc_deferred_request; + typedef struct { int fd; char request[GSR_IPC_MAX_REQUEST_SIZE]; size_t request_size; bool request_too_large; + char send_buffer[GSR_IPC_CLIENT_SEND_BUFFER_SIZE]; + size_t send_buffer_size; } gsr_ipc_client; /* Receives newline terminated json requests on a unix domain socket and replies to every request */ typedef struct { bool initialized; int socket_fd; + int poll_fd; bool socket_bound; char socket_filepath[PATH_MAX]; int wakeup_pipe[2]; gsr_ipc_client clients[GSR_IPC_MAX_CLIENTS]; int num_clients; gsr_ipc_handlers handlers; + gsr_ipc_deferred_request deferred_requests[GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT]; + pthread_mutex_t deferred_requests_mutex; + bool deferred_requests_mutex_created; pthread_t thread; bool thread_running; } gsr_ipc; @@ -53,4 +92,11 @@ int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers); /* Stops handling requests. Does nothing if the ipc hasn't been started */ void gsr_ipc_stop(gsr_ipc *self); +/* + Sends the reply to the pending request of |type|, or does nothing when there is no pending request of that type. + |filepath| is the path of the saved file and it can be NULL when nothing was saved. + This is safe to call from any thread. Does nothing if |self| hasn't been initialized. +*/ +void gsr_ipc_complete_request(gsr_ipc *self, gsr_ipc_deferred_request_type type, bool success, const char *filepath); + #endif /* GSR_CLI_IPC_H */ diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h index bbd37e1..887b4fe 100644 --- a/include/recorder/recorder.h +++ b/include/recorder/recorder.h @@ -47,7 +47,14 @@ int gsr_recorder_run(gsr_recorder *self); /* These are safe to call from a signal handler or from another thread */ void gsr_recorder_stop(gsr_recorder *self); void gsr_recorder_toggle_pause(gsr_recorder *self); +/* Does nothing when the recording is already paused/unpaused */ +void gsr_recorder_set_paused(gsr_recorder *self, bool paused); void gsr_recorder_toggle_replay_recording(gsr_recorder *self); +/* Does nothing when a recording is already running */ +void gsr_recorder_start_replay_recording(gsr_recorder *self); +/* Does nothing when no recording is running */ +void gsr_recorder_stop_replay_recording(gsr_recorder *self); +bool gsr_recorder_is_replay_recording(const gsr_recorder *self); /* |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ void gsr_recorder_save_replay(gsr_recorder *self, int seconds); diff --git a/src/cli/ipc.c b/src/cli/ipc.c index b9d0c51..ff4b852 100644 --- a/src/cli/ipc.c +++ b/src/cli/ipc.c @@ -17,12 +17,21 @@ #include #include +#ifdef __linux__ +#include +#else +#include +#include +#endif + #define GSR_IPC_MAX_REQUEST_NAME_SIZE 64 -#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6) -#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + 128) -#define GSR_IPC_SEND_TIMEOUT_MILLISECONDS 1000 +#define GSR_IPC_MAX_EVENTS (2 + GSR_IPC_MAX_CLIENTS*2) +#define GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS 1000 #define GSR_IPC_SOCKET_MODE 0600 +#define GSR_IPC_WAKEUP_QUIT (1 << 0) +#define GSR_IPC_WAKEUP_COMPLETED_REQUEST (1 << 1) + typedef struct { int64_t id; char name[GSR_IPC_MAX_REQUEST_NAME_SIZE]; @@ -30,6 +39,12 @@ typedef struct { bool has_data; } gsr_ipc_request; +typedef struct { + int fd; + bool readable; + bool writable; +} gsr_ipc_event; + 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') @@ -38,7 +53,103 @@ static bool string_is_only_whitespace(const char *str, size_t size) { return true; } -static bool ipc_send_all(int fd, const char *data, size_t size) { +static bool fd_set_cloexec(int fd) { + const int flags = fcntl(fd, F_GETFD); + return flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1; +} + +static bool fd_set_nonblocking(int fd) { + const int flags = fcntl(fd, F_GETFL); + return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1; +} + +#ifdef __linux__ +static bool ipc_poller_init(gsr_ipc *self) { + self->poll_fd = epoll_create1(EPOLL_CLOEXEC); + if(self->poll_fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create an epoll instance, error: %s", strerror(errno)); + return false; + } + return true; +} + +static bool ipc_poller_add(gsr_ipc *self, int fd) { + struct epoll_event event; + memset(&event, 0, sizeof(event)); + event.events = EPOLLIN | EPOLLET; + event.data.fd = fd; + return epoll_ctl(self->poll_fd, EPOLL_CTL_ADD, fd, &event) == 0; +} + +static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) { + struct epoll_event event; + memset(&event, 0, sizeof(event)); + event.events = EPOLLIN | EPOLLET | (enable ? EPOLLOUT : 0); + event.data.fd = fd; + return epoll_ctl(self->poll_fd, EPOLL_CTL_MOD, fd, &event) == 0; +} + +/* Returns the number of events, or -1 on failure. Waits until at least one event is available */ +static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) { + struct epoll_event platform_events[GSR_IPC_MAX_EVENTS]; + if(events_capacity > GSR_IPC_MAX_EVENTS) + events_capacity = GSR_IPC_MAX_EVENTS; + + const int num_events = epoll_wait(self->poll_fd, platform_events, events_capacity, -1); + if(num_events == -1) + return errno == EINTR ? 0 : -1; + + for(int i = 0; i < num_events; ++i) { + events[i].fd = platform_events[i].data.fd; + events[i].readable = platform_events[i].events & (EPOLLIN | EPOLLHUP | EPOLLERR); + events[i].writable = platform_events[i].events & EPOLLOUT; + } + return num_events; +} +#else +static bool ipc_poller_init(gsr_ipc *self) { + self->poll_fd = kqueue(); + if(self->poll_fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a kqueue instance, error: %s", strerror(errno)); + return false; + } + fd_set_cloexec(self->poll_fd); + return true; +} + +static bool ipc_poller_add(gsr_ipc *self, int fd) { + struct kevent change; + EV_SET(&change, fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, NULL); + return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1; +} + +static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) { + struct kevent change; + EV_SET(&change, fd, EVFILT_WRITE, enable ? (EV_ADD | EV_CLEAR) : EV_DELETE, 0, 0, NULL); + return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1; +} + +/* Returns the number of events, or -1 on failure. Waits until at least one event is available */ +static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) { + struct kevent platform_events[GSR_IPC_MAX_EVENTS]; + if(events_capacity > GSR_IPC_MAX_EVENTS) + events_capacity = GSR_IPC_MAX_EVENTS; + + const int num_events = kevent(self->poll_fd, NULL, 0, platform_events, events_capacity, NULL); + if(num_events == -1) + return errno == EINTR ? 0 : -1; + + for(int i = 0; i < num_events; ++i) { + events[i].fd = platform_events[i].ident; + events[i].readable = platform_events[i].filter == EVFILT_READ; + events[i].writable = platform_events[i].filter == EVFILT_WRITE; + } + return num_events; +} +#endif + +/* Only used when the ipc thread exits, to not lose replies that haven't been fully sent yet */ +static bool ipc_send_all_blocking(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); @@ -56,7 +167,7 @@ static bool ipc_send_all(int fd, const char *data, size_t size) { poll_fd.events = POLLOUT; poll_fd.revents = 0; - const int poll_result = poll(&poll_fd, 1, GSR_IPC_SEND_TIMEOUT_MILLISECONDS); + const int poll_result = poll(&poll_fd, 1, GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS); if(poll_result == -1 && errno == EINTR) continue; @@ -71,11 +182,76 @@ static bool ipc_send_all(int fd, const char *data, size_t size) { return true; } -static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool success, const char *error_message) { +static bool ipc_client_send_data(gsr_ipc *self, gsr_ipc_client *client, const char *data, size_t size) { + size_t offset = 0; + if(client->send_buffer_size == 0) { + while(offset < size) { + const ssize_t bytes_written = send(client->fd, data + offset, size - offset, MSG_NOSIGNAL); + if(bytes_written > 0) { + offset += bytes_written; + continue; + } + + if(bytes_written == -1 && errno == EINTR) + continue; + + if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) + break; + + return false; + } + } + + const size_t bytes_remaining = size - offset; + if(bytes_remaining == 0) + return true; + + if(client->send_buffer_size + bytes_remaining > sizeof(client->send_buffer)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: an ipc client isn't reading replies fast enough, disconnecting it"); + return false; + } + + const bool send_buffer_was_empty = client->send_buffer_size == 0; + memcpy(client->send_buffer + client->send_buffer_size, data + offset, bytes_remaining); + client->send_buffer_size += bytes_remaining; + return !send_buffer_was_empty || ipc_poller_set_write_notify(self, client->fd, true); +} + +static bool ipc_client_flush_send_buffer(gsr_ipc *self, gsr_ipc_client *client) { + if(client->send_buffer_size == 0) + return true; + + size_t offset = 0; + while(offset < client->send_buffer_size) { + const ssize_t bytes_written = send(client->fd, client->send_buffer + offset, client->send_buffer_size - offset, MSG_NOSIGNAL); + if(bytes_written > 0) { + offset += bytes_written; + continue; + } + + if(bytes_written == -1 && errno == EINTR) + continue; + + if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) + break; + + return false; + } + + memmove(client->send_buffer, client->send_buffer + offset, client->send_buffer_size - offset); + client->send_buffer_size -= offset; + return client->send_buffer_size != 0 || ipc_poller_set_write_notify(self, client->fd, false); +} + +static bool ipc_client_send_reply(gsr_ipc *self, gsr_ipc_client *client, int64_t id, bool success, const char *error_message, const char *data) { char reply[GSR_IPC_MAX_REPLY_SIZE]; int reply_size = 0; - if(success) { + if(success && data) { + char escaped_data[GSR_IPC_MAX_ESCAPED_DATA_SIZE]; + gsr_json_escape_string(escaped_data, sizeof(escaped_data), data); + reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\",\"data\":\"%s\"}\n", id, escaped_data); + } else if(success) { reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\"}\n", id); } else { char escaped_error_message[GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE]; @@ -88,7 +264,7 @@ static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool succe return false; } - return ipc_send_all(client->fd, reply, reply_size); + return ipc_client_send_data(self, client, reply, reply_size); } static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, char *error_message, size_t error_message_size) { @@ -165,6 +341,76 @@ static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, return true; } +static bool ipc_request_get_set_paused_state(const gsr_ipc_request *request, bool *paused, char *error_message, size_t error_message_size) { + if(request->has_data && request->data.type == SJ_BOOL) { + *paused = gsr_json_string_equals(&request->data, "true"); + return true; + } + + snprintf(error_message, error_message_size, "expected 'data' to be true to pause or false to unpause"); + return false; +} + +static bool ipc_request_name_to_deferred_request_type(const char *name, gsr_ipc_deferred_request_type *type) { + if(strcmp(name, "stop") == 0) { + *type = GSR_IPC_DEFERRED_REQUEST_STOP; + return true; + } + + if(strcmp(name, "save-replay") == 0) { + *type = GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY; + return true; + } + + if(strcmp(name, "stop-replay-recording") == 0) { + *type = GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING; + return true; + } + + return false; +} + +static const char* deferred_request_already_pending_error(gsr_ipc_deferred_request_type type) { + switch(type) { + case GSR_IPC_DEFERRED_REQUEST_STOP: return "GPU Screen Recorder is already stopping"; + case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "a replay is already being saved"; + case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "the recording is already being stopped"; + case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break; + } + return "the request is already being handled"; +} + +static const char* deferred_request_failed_error(gsr_ipc_deferred_request_type type) { + switch(type) { + case GSR_IPC_DEFERRED_REQUEST_STOP: return "failed to save the recording"; + case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "failed to save the replay"; + case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "failed to save the recording"; + case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break; + } + return "the request failed"; +} + +static bool ipc_set_deferred_request_pending(gsr_ipc *self, gsr_ipc_deferred_request_type type, int client_fd, int64_t request_id) { + pthread_mutex_lock(&self->deferred_requests_mutex); + gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type]; + const bool was_empty = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + if(was_empty) { + deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_PENDING; + deferred_request->client_fd = client_fd; + deferred_request->request_id = request_id; + deferred_request->success = false; + deferred_request->has_filepath = false; + } + pthread_mutex_unlock(&self->deferred_requests_mutex); + return was_empty; +} + +static void ipc_clear_deferred_request(gsr_ipc *self, gsr_ipc_deferred_request_type type) { + pthread_mutex_lock(&self->deferred_requests_mutex); + self->deferred_requests[type].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + pthread_mutex_unlock(&self->deferred_requests_mutex); +} + static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, char *error_message, size_t error_message_size) { if(strcmp(request->name, "stop") == 0) return self->handlers.stop(error_message, error_message_size, self->handlers.userdata); @@ -172,9 +418,23 @@ static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, ch if(strcmp(request->name, "toggle-pause") == 0) return self->handlers.toggle_pause(error_message, error_message_size, self->handlers.userdata); + if(strcmp(request->name, "set-paused") == 0) { + bool paused = false; + if(!ipc_request_get_set_paused_state(request, &paused, error_message, error_message_size)) + return false; + + return self->handlers.set_paused(paused, error_message, error_message_size, self->handlers.userdata); + } + if(strcmp(request->name, "toggle-replay-recording") == 0) return self->handlers.toggle_replay_recording(error_message, error_message_size, self->handlers.userdata); + if(strcmp(request->name, "start-replay-recording") == 0) + return self->handlers.start_replay_recording(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "stop-replay-recording") == 0) + return self->handlers.stop_replay_recording(error_message, error_message_size, self->handlers.userdata); + if(strcmp(request->name, "save-replay") == 0) { int seconds = GSR_SAVE_REPLAY_SECONDS_FULL; if(!ipc_request_get_save_replay_seconds(request, &seconds, error_message, error_message_size)) @@ -189,22 +449,35 @@ static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, ch static bool ipc_client_on_request(gsr_ipc *self, gsr_ipc_client *client) { if(client->request_too_large) - return ipc_client_send_reply(client, 0, false, "the request is too large"); + return ipc_client_send_reply(self, client, 0, false, "the request is too large", NULL); if(string_is_only_whitespace(client->request, client->request_size)) - return ipc_client_send_reply(client, 0, false, "the request is empty"); + return ipc_client_send_reply(self, client, 0, false, "the request is empty", NULL); char error_message[GSR_IPC_MAX_ERROR_MESSAGE_SIZE]; error_message[0] = '\0'; gsr_ipc_request request; if(!ipc_request_parse(client->request, client->request_size, &request, error_message, sizeof(error_message))) - return ipc_client_send_reply(client, request.id, false, error_message); + return ipc_client_send_reply(self, client, request.id, false, error_message, NULL); - if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) - return ipc_client_send_reply(client, request.id, false, error_message); + /* The pending deferred request has to be registered before the handler starts the operation, + otherwise the operation could finish before the reply to it gets registered */ + gsr_ipc_deferred_request_type deferred_request_type; + const bool reply_is_deferred = ipc_request_name_to_deferred_request_type(request.name, &deferred_request_type); + if(reply_is_deferred && !ipc_set_deferred_request_pending(self, deferred_request_type, client->fd, request.id)) + return ipc_client_send_reply(self, client, request.id, false, deferred_request_already_pending_error(deferred_request_type), NULL); - return ipc_client_send_reply(client, request.id, true, NULL); + if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) { + if(reply_is_deferred) + ipc_clear_deferred_request(self, deferred_request_type); + return ipc_client_send_reply(self, client, request.id, false, error_message, NULL); + } + + if(reply_is_deferred) + return true; + + return ipc_client_send_reply(self, client, request.id, true, NULL, NULL); } static bool ipc_client_on_byte(gsr_ipc *self, gsr_ipc_client *client, char c) { @@ -243,16 +516,6 @@ static bool ipc_client_receive(gsr_ipc *self, gsr_ipc_client *client) { } } -static bool fd_set_cloexec(int fd) { - const int flags = fcntl(fd, F_GETFD); - return flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1; -} - -static bool fd_set_nonblocking(int fd) { - const int flags = fcntl(fd, F_GETFL); - return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1; -} - static void ipc_add_client(gsr_ipc *self, int client_fd) { if(self->num_clients == GSR_IPC_MAX_CLIENTS) { gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: too many ipc clients are connected, rejecting the new connection"); @@ -260,7 +523,7 @@ static void ipc_add_client(gsr_ipc *self, int client_fd) { return; } - if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd)) { + if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd) || !ipc_poller_add(self, client_fd)) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to setup the ipc client socket, error: %s", strerror(errno)); close(client_fd); return; @@ -270,70 +533,175 @@ static void ipc_add_client(gsr_ipc *self, int client_fd) { client->fd = client_fd; client->request_size = 0; client->request_too_large = false; + client->send_buffer_size = 0; ++self->num_clients; } -static void ipc_accept_client(gsr_ipc *self) { - const int client_fd = accept(self->socket_fd, NULL, NULL); - if(client_fd == -1) - return; +static void ipc_accept_clients(gsr_ipc *self) { + for(;;) { + const int client_fd = accept(self->socket_fd, NULL, NULL); + if(client_fd == -1) { + if(errno == EINTR) + continue; + return; + } - ipc_add_client(self, client_fd); + ipc_add_client(self, client_fd); + } } static void ipc_remove_client(gsr_ipc *self, int index) { - close(self->clients[index].fd); + const int client_fd = self->clients[index].fd; + + pthread_mutex_lock(&self->deferred_requests_mutex); + for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) { + if(self->deferred_requests[i].state != GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY && self->deferred_requests[i].client_fd == client_fd) + self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + } + pthread_mutex_unlock(&self->deferred_requests_mutex); + + close(client_fd); for(int i = index; i < self->num_clients - 1; ++i) { self->clients[i] = self->clients[i + 1]; } --self->num_clients; } -static void* ipc_thread(void *userdata) { - gsr_ipc *self = userdata; - struct pollfd poll_fds[2 + GSR_IPC_MAX_CLIENTS]; +static int ipc_find_client_index_by_fd(const gsr_ipc *self, int fd) { + for(int i = 0; i < self->num_clients; ++i) { + if(self->clients[i].fd == fd) + return i; + } + return -1; +} + +static void ipc_send_completed_request_replies(gsr_ipc *self) { + for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) { + pthread_mutex_lock(&self->deferred_requests_mutex); + const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i]; + if(deferred_request.state == GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED) + self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + pthread_mutex_unlock(&self->deferred_requests_mutex); + + if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED) + continue; + + const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd); + if(client_index == -1) + continue; + + const char *error_message = deferred_request_failed_error(i); + const char *filepath = deferred_request.has_filepath ? deferred_request.filepath : NULL; + if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, deferred_request.success, error_message, filepath)) + ipc_remove_client(self, client_index); + } +} + +static void ipc_fail_pending_requests(gsr_ipc *self) { + for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) { + pthread_mutex_lock(&self->deferred_requests_mutex); + const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i]; + self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + pthread_mutex_unlock(&self->deferred_requests_mutex); + + if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_PENDING) + continue; + + const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd); + if(client_index == -1) + continue; + + if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, false, "GPU Screen Recorder exited before the request finished", NULL)) + ipc_remove_client(self, client_index); + } +} + +static void ipc_flush_clients_blocking(gsr_ipc *self) { + for(int i = 0; i < self->num_clients; ++i) { + gsr_ipc_client *client = &self->clients[i]; + if(client->send_buffer_size > 0) + ipc_send_all_blocking(client->fd, client->send_buffer, client->send_buffer_size); + client->send_buffer_size = 0; + } +} +static int ipc_drain_wakeup_pipe(gsr_ipc *self) { + int wakeup_flags = 0; for(;;) { - poll_fds[0].fd = self->wakeup_pipe[0]; - poll_fds[0].events = POLLIN; - poll_fds[0].revents = 0; - poll_fds[1].fd = self->socket_fd; - poll_fds[1].events = POLLIN; - poll_fds[1].revents = 0; - - const int num_polled_clients = self->num_clients; - for(int i = 0; i < num_polled_clients; ++i) { - poll_fds[2 + i].fd = self->clients[i].fd; - poll_fds[2 + i].events = POLLIN; - poll_fds[2 + i].revents = 0; + char buffer[64]; + const ssize_t bytes_read = read(self->wakeup_pipe[0], buffer, sizeof(buffer)); + if(bytes_read == -1 && errno == EINTR) + continue; + + if(bytes_read <= 0) + break; + + for(ssize_t i = 0; i < bytes_read; ++i) { + if(buffer[i] == 'q') + wakeup_flags |= GSR_IPC_WAKEUP_QUIT; + else if(buffer[i] == 'c') + wakeup_flags |= GSR_IPC_WAKEUP_COMPLETED_REQUEST; } + } + return wakeup_flags; +} - if(poll(poll_fds, 2 + num_polled_clients, -1) == -1) { - if(errno == EINTR) - continue; +static void ipc_wakeup_thread(gsr_ipc *self, char wakeup_value) { + ssize_t bytes_written = 0; + do { + bytes_written = write(self->wakeup_pipe[1], &wakeup_value, 1); + } while(bytes_written == -1 && errno == EINTR); - gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to poll the ipc sockets, error: %s", strerror(errno)); + if(bytes_written == -1) + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wake up the ipc thread, error: %s", strerror(errno)); +} + +static void* ipc_thread(void *userdata) { + gsr_ipc *self = userdata; + gsr_ipc_event events[GSR_IPC_MAX_EVENTS]; + bool running = true; + + while(running) { + const int num_events = ipc_poller_wait(self, events, GSR_IPC_MAX_EVENTS); + if(num_events == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wait for ipc events, error: %s", strerror(errno)); break; } - if(poll_fds[0].revents != 0) - break; + for(int i = 0; i < num_events; ++i) { + if(events[i].fd == self->wakeup_pipe[0]) { + const int wakeup_flags = ipc_drain_wakeup_pipe(self); + if(wakeup_flags & GSR_IPC_WAKEUP_COMPLETED_REQUEST) + ipc_send_completed_request_replies(self); + if(wakeup_flags & GSR_IPC_WAKEUP_QUIT) + running = false; + continue; + } - if(poll_fds[1].revents & POLLIN) - ipc_accept_client(self); + if(events[i].fd == self->socket_fd) { + ipc_accept_clients(self); + continue; + } + + const int client_index = ipc_find_client_index_by_fd(self, events[i].fd); + if(client_index == -1) + continue; - for(int i = num_polled_clients - 1; i >= 0; --i) { + gsr_ipc_client *client = &self->clients[client_index]; bool keep_client = true; - if(poll_fds[2 + i].revents & POLLIN) - keep_client = ipc_client_receive(self, &self->clients[i]); - else if(poll_fds[2 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) - keep_client = false; + if(events[i].readable) + keep_client = ipc_client_receive(self, client); + if(keep_client && events[i].writable) + keep_client = ipc_client_flush_send_buffer(self, client); if(!keep_client) - ipc_remove_client(self, i); + ipc_remove_client(self, client_index); } } + ipc_send_completed_request_replies(self); + ipc_fail_pending_requests(self); + ipc_flush_clients_blocking(self); return NULL; } @@ -403,6 +771,11 @@ static void ipc_close(gsr_ipc *self) { } } + if(self->poll_fd != -1) { + close(self->poll_fd); + self->poll_fd = -1; + } + if(self->socket_fd != -1) { close(self->socket_fd); self->socket_fd = -1; @@ -412,11 +785,17 @@ static void ipc_close(gsr_ipc *self) { unlink(self->socket_filepath); self->socket_bound = false; } + + if(self->deferred_requests_mutex_created) { + pthread_mutex_destroy(&self->deferred_requests_mutex); + self->deferred_requests_mutex_created = false; + } } int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { memset(self, 0, sizeof(*self)); self->socket_fd = -1; + self->poll_fd = -1; self->wakeup_pipe[0] = -1; self->wakeup_pipe[1] = -1; @@ -430,6 +809,12 @@ int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { snprintf(self->socket_filepath, sizeof(self->socket_filepath), "%s", socket_filepath); + if(pthread_mutex_init(&self->deferred_requests_mutex, NULL) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the deferred requests mutex"); + goto err; + } + self->deferred_requests_mutex_created = true; + if(pipe(self->wakeup_pipe) == -1) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc wakeup pipe, error: %s", strerror(errno)); self->wakeup_pipe[0] = -1; @@ -437,7 +822,7 @@ int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { goto err; } - if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1])) { + if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1]) || !fd_set_nonblocking(self->wakeup_pipe[0])) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to setup the ipc wakeup pipe, error: %s", strerror(errno)); goto err; } @@ -448,6 +833,14 @@ int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { goto err; } + if(!ipc_poller_init(self)) + goto err; + + if(!ipc_poller_add(self, self->wakeup_pipe[0]) || !ipc_poller_add(self, self->socket_fd)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to register the ipc sockets for events, error: %s", strerror(errno)); + goto err; + } + if(!ipc_bind(self, &addr)) goto err; @@ -500,15 +893,27 @@ void gsr_ipc_stop(gsr_ipc *self) { if(!self->thread_running) return; - const char wakeup_value = 1; - ssize_t bytes_written = 0; - do { - bytes_written = write(self->wakeup_pipe[1], &wakeup_value, 1); - } while(bytes_written == -1 && errno == EINTR); - - if(bytes_written == -1) - gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_stop: failed to wake up the ipc thread, error: %s", strerror(errno)); - + ipc_wakeup_thread(self, 'q'); pthread_join(self->thread, NULL); self->thread_running = false; } + +void gsr_ipc_complete_request(gsr_ipc *self, gsr_ipc_deferred_request_type type, bool success, const char *filepath) { + if(!self->initialized) + return; + + pthread_mutex_lock(&self->deferred_requests_mutex); + gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type]; + const bool was_pending = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_PENDING; + if(was_pending) { + deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED; + deferred_request->success = success; + deferred_request->has_filepath = filepath != NULL; + if(filepath) + snprintf(deferred_request->filepath, sizeof(deferred_request->filepath), "%s", filepath); + } + pthread_mutex_unlock(&self->deferred_requests_mutex); + + if(was_pending) + ipc_wakeup_thread(self, 'c'); +} diff --git a/src/cli/main.c b/src/cli/main.c index d34e8cc..de014a7 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -137,6 +137,17 @@ static bool ipc_toggle_pause_handler(char *error_message, size_t error_message_s return true; } +static bool ipc_set_paused_handler(bool paused, char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(settings->is_replaying) { + snprintf(error_message, error_message_size, "pausing is not supported when recording a replay"); + return false; + } + + gsr_recorder_set_paused(recorder, paused); + return true; +} + static bool ipc_toggle_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) { const gsr_recorder_settings *settings = userdata; if(!settings->replay_recording_directory) { @@ -148,6 +159,33 @@ static bool ipc_toggle_replay_recording_handler(char *error_message, size_t erro return true; } +static bool ipc_start_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(!settings->replay_recording_directory) { + snprintf(error_message, error_message_size, "option -ro is required to start a recording"); + return false; + } + + gsr_recorder_start_replay_recording(recorder); + return true; +} + +static bool ipc_stop_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(!settings->replay_recording_directory) { + snprintf(error_message, error_message_size, "option -ro is required to start a recording"); + return false; + } + + if(!gsr_recorder_is_replay_recording(recorder)) { + snprintf(error_message, error_message_size, "no recording is running"); + return false; + } + + gsr_recorder_stop_replay_recording(recorder); + return true; +} + static bool ipc_save_replay_handler(int seconds, char *error_message, size_t error_message_size, void *userdata) { const gsr_recorder_settings *settings = userdata; if(!settings->is_replaying) { @@ -295,18 +333,26 @@ static void screenshot_saved_callback(const char *filepath, void *userdata) { run_recording_saved_script_async(recording_saved_script, filepath, "screenshot"); } +typedef struct { + const char *recording_saved_script; + gsr_ipc *ipc; +} recorder_callbacks_context; + static void replay_saved_callback(const char *filepath, void *userdata) { - const char *recording_saved_script = userdata; + recorder_callbacks_context *context = userdata; if(!filepath) { printf("gsr error: Failed to save replay\n"); fflush(stdout); + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, false, NULL); return; } puts(filepath); fflush(stdout); - if(recording_saved_script) - run_recording_saved_script_async(recording_saved_script, filepath, "replay"); + if(context->recording_saved_script) + run_recording_saved_script_async(context->recording_saved_script, filepath, "replay"); + + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, true, filepath); } static void recording_started_callback(const char *filepath, void *userdata) { @@ -318,17 +364,20 @@ static void recording_started_callback(const char *filepath, void *userdata) { } static void recording_stopped_callback(const char *filepath, void *userdata) { - const char *recording_saved_script = userdata; + recorder_callbacks_context *context = userdata; if(!filepath) { printf("gsr error: Failed to save recording\n"); fflush(stdout); + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, false, NULL); return; } puts(filepath); fflush(stdout); - if(recording_saved_script) - run_recording_saved_script_async(recording_saved_script, filepath, "regular"); + if(context->recording_saved_script) + run_recording_saved_script_async(context->recording_saved_script, filepath, "regular"); + + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, true, filepath); } #ifdef GSR_APP_AUDIO @@ -410,12 +459,16 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture recorder_params.pipewire_audio = &pipewire_audio; #endif + recorder_callbacks_context callbacks_context; + callbacks_context.recording_saved_script = arg_parser->settings.recording_saved_script; + callbacks_context.ipc = ipc; + gsr_recorder_callbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.replay_saved = replay_saved_callback; callbacks.recording_started = recording_started_callback; callbacks.recording_stopped = recording_stopped_callback; - callbacks.userdata = (void*)arg_parser->settings.recording_saved_script; + callbacks.userdata = &callbacks_context; int error = GSR_ERROR_OK; recorder = gsr_recorder_create(&recorder_params, &callbacks, &error); @@ -427,9 +480,13 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture gsr_recorder_stop(recorder); gsr_ipc_handlers ipc_handlers; + memset(&ipc_handlers, 0, sizeof(ipc_handlers)); ipc_handlers.stop = ipc_stop_handler; ipc_handlers.toggle_pause = ipc_toggle_pause_handler; + ipc_handlers.set_paused = ipc_set_paused_handler; ipc_handlers.toggle_replay_recording = ipc_toggle_replay_recording_handler; + ipc_handlers.start_replay_recording = ipc_start_replay_recording_handler; + ipc_handlers.stop_replay_recording = ipc_stop_replay_recording_handler; ipc_handlers.save_replay = ipc_save_replay_handler; ipc_handlers.userdata = &arg_parser->settings; @@ -437,6 +494,7 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture if(run_result == GSR_ERROR_OK) run_result = gsr_recorder_run(recorder); + gsr_ipc_complete_request(ipc, GSR_IPC_DEFERRED_REQUEST_STOP, true, arg_parser->settings.is_replaying ? NULL : arg_parser->settings.filename); gsr_ipc_stop(ipc); gsr_recorder_destroy(recorder); recorder = NULL; diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 4426ac5..4bffb8e 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -35,6 +35,15 @@ #define GSR_VIDEO_STREAM_INDEX 0 +#define GSR_SET_PAUSED_REQUEST_NONE -1 +#define GSR_SET_PAUSED_REQUEST_UNPAUSE 0 +#define GSR_SET_PAUSED_REQUEST_PAUSE 1 + +#define GSR_REPLAY_RECORDING_REQUEST_NONE 0 +#define GSR_REPLAY_RECORDING_REQUEST_TOGGLE 1 +#define GSR_REPLAY_RECORDING_REQUEST_START 2 +#define GSR_REPLAY_RECORDING_REQUEST_STOP 3 + struct gsr_recorder { gsr_recorder_settings settings; gsr_recorder_callbacks callbacks; @@ -88,7 +97,9 @@ struct gsr_recorder { atomic_int running; atomic_int toggle_pause; - atomic_int toggle_replay_recording; + atomic_int set_paused_request; + atomic_int replay_recording_request; + atomic_int replay_recording_state; atomic_int save_replay_seconds; bool should_stop_error; bool force_iframe_frame; @@ -403,7 +414,9 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->audio_input_tracks = params->audio_input_tracks; atomic_init(&self->running, 1); atomic_init(&self->toggle_pause, 0); - atomic_init(&self->toggle_replay_recording, 0); + atomic_init(&self->set_paused_request, GSR_SET_PAUSED_REQUEST_NONE); + atomic_init(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); + atomic_init(&self->replay_recording_state, 0); atomic_init(&self->save_replay_seconds, 0); self->audio_max_frame_size = 1024; int error_code = GSR_ERROR_GENERIC; @@ -625,78 +638,99 @@ static void recorder_capture_and_encode_frame(gsr_recorder *self, bool damaged) } static void recorder_apply_pause_toggle(gsr_recorder *self) { - if(atomic_load(&self->toggle_pause) == 1 && !self->settings.is_replaying) { - self->paused = !self->paused; + const bool toggle_pause = atomic_exchange(&self->toggle_pause, 0) == 1; + const int set_paused_request = atomic_exchange(&self->set_paused_request, GSR_SET_PAUSED_REQUEST_NONE); + if(self->settings.is_replaying) + return; + + bool new_paused = self->paused; + if(toggle_pause) + new_paused = !new_paused; + if(set_paused_request != GSR_SET_PAUSED_REQUEST_NONE) + new_paused = set_paused_request == GSR_SET_PAUSED_REQUEST_PAUSE; + + if(new_paused != self->paused) { + self->paused = new_paused; gsr_recording_clock_set_paused(self->recording_clock, self->paused); gsr_log(GSR_LOG_LEVEL_INFO, self->paused ? "Paused" : "Unpaused"); - atomic_store(&self->toggle_pause, 0); } } static void recorder_apply_replay_recording_toggle(gsr_recorder *self) { - if(atomic_load(&self->toggle_replay_recording) && !self->settings.replay_recording_directory) { - atomic_store(&self->toggle_replay_recording, 0); - if(self->callbacks.recording_started) + const int request = atomic_exchange(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); + if(request == GSR_REPLAY_RECORDING_REQUEST_NONE) + return; + + if(!self->settings.replay_recording_directory) { + if(request != GSR_REPLAY_RECORDING_REQUEST_STOP && self->callbacks.recording_started) self->callbacks.recording_started(NULL, self->callbacks.userdata); + return; } - if(atomic_load(&self->toggle_replay_recording) && self->settings.replay_recording_directory) { - atomic_store(&self->toggle_replay_recording, 0); - const bool new_replay_recording_state = !self->replay_recording; - if(new_replay_recording_state) { - gsr_audio_capture_lock_filter(&self->audio_capture); - self->num_replay_recording_items = 0; - const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); - if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { - const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); - if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { - char ts_filepath[PATH_MAX + 4]; - snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); - gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); - } - - if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { - self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; - ++self->num_replay_recording_items; - } + bool new_replay_recording_state = !self->replay_recording; + if(request == GSR_REPLAY_RECORDING_REQUEST_START) + new_replay_recording_state = true; + else if(request == GSR_REPLAY_RECORDING_REQUEST_STOP) + new_replay_recording_state = false; - for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { - const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; - const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); - if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { - self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; - ++self->num_replay_recording_items; - } - } + if(new_replay_recording_state == self->replay_recording) + return; - self->replay_recording = true; - self->force_iframe_frame = true; - gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); - if(self->callbacks.recording_started) - self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); - } else { - if(self->callbacks.recording_started) - self->callbacks.recording_started(NULL, self->callbacks.userdata); + if(new_replay_recording_state) { + gsr_audio_capture_lock_filter(&self->audio_capture); + self->num_replay_recording_items = 0; + const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); + if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { + const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); + if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { + char ts_filepath[PATH_MAX + 4]; + snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); + gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); } - gsr_audio_capture_unlock_filter(&self->audio_capture); - } else if(self->replay_recording_output.av_format_context) { - for(size_t i = 0; i < self->num_replay_recording_items; ++i) { - gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + + if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; + ++self->num_replay_recording_items; } - self->num_replay_recording_items = 0; - if(gsr_recording_output_stop(&self->replay_recording_output)) { - gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); - if(self->callbacks.recording_stopped) - self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); - } else { - if(self->callbacks.recording_stopped) - self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { + const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; + const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); + if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; + ++self->num_replay_recording_items; + } } - self->replay_recording = false; - self->replay_recording_filepath[0] = '\0'; + self->replay_recording = true; + atomic_store(&self->replay_recording_state, 1); + self->force_iframe_frame = true; + gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); + if(self->callbacks.recording_started) + self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); } + gsr_audio_capture_unlock_filter(&self->audio_capture); + } else if(self->replay_recording_output.av_format_context) { + for(size_t i = 0; i < self->num_replay_recording_items; ++i) { + gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + } + self->num_replay_recording_items = 0; + + if(gsr_recording_output_stop(&self->replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + } + + self->replay_recording = false; + atomic_store(&self->replay_recording_state, 0); + self->replay_recording_filepath[0] = '\0'; } } @@ -905,8 +939,24 @@ void gsr_recorder_toggle_pause(gsr_recorder *self) { atomic_store(&self->toggle_pause, 1); } +void gsr_recorder_set_paused(gsr_recorder *self, bool paused) { + atomic_store(&self->set_paused_request, paused ? GSR_SET_PAUSED_REQUEST_PAUSE : GSR_SET_PAUSED_REQUEST_UNPAUSE); +} + void gsr_recorder_toggle_replay_recording(gsr_recorder *self) { - atomic_store(&self->toggle_replay_recording, 1); + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_TOGGLE); +} + +void gsr_recorder_start_replay_recording(gsr_recorder *self) { + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_START); +} + +void gsr_recorder_stop_replay_recording(gsr_recorder *self) { + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_STOP); +} + +bool gsr_recorder_is_replay_recording(const gsr_recorder *self) { + return atomic_load(&self->replay_recording_state) == 1; } void gsr_recorder_save_replay(gsr_recorder *self, int seconds) { diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c index 1d33437..c3a1bea 100644 --- a/src/recorder/replay_save.c +++ b/src/recorder/replay_save.c @@ -140,7 +140,7 @@ bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_co if(self->video_start_iterator.packet_index == (size_t)-1) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded"); gsr_replay_save_cleanup(self); - return true; + return false; } self->video_pts_offset = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, self->video_start_iterator)->pts; diff --git a/tools/gsr-cli/main.c b/tools/gsr-cli/main.c index b536ceb..e430964 100644 --- a/tools/gsr-cli/main.c +++ b/tools/gsr-cli/main.c @@ -14,8 +14,9 @@ #define GSR_CLI_REQUEST_ID 1 #define GSR_CLI_MAX_REQUEST_SIZE 256 -#define GSR_CLI_MAX_REPLY_SIZE 4096 +#define GSR_CLI_MAX_REPLY_SIZE 8192 #define GSR_CLI_REPLY_TIMEOUT_SECONDS 10 +#define GSR_CLI_NO_REPLY_TIMEOUT 0 static void usage(void) { printf("usage: gsr-cli -ipc [command_argument]\n"); @@ -31,14 +32,24 @@ static void usage(void) { 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(" Stop and save the recording (stop without save in replay mode). Waits until the recording\n"); + printf(" has been saved and prints the path of the saved file.\n"); printf(" toggle-pause\n"); printf(" Pause/unpause the recording (not for streaming/replay).\n"); + printf(" set-paused true|false\n"); + printf(" Pause/unpause the recording (not for streaming/replay). Unlike toggle-pause this doesn't\n"); + printf(" fail when the recording is already paused/unpaused.\n"); printf(" toggle-replay-recording\n"); printf(" Start/stop a regular recording during replay/streaming.\n"); + printf(" start-replay-recording\n"); + printf(" Start a regular recording during replay/streaming. Does nothing when a recording is already running.\n"); + printf(" stop-replay-recording\n"); + printf(" Stop the regular recording that runs during replay/streaming. Waits until the recording\n"); + printf(" has been saved and prints the path of the saved file.\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(" saved when no number of seconds is given. Waits until the replay has been saved and prints\n"); + printf(" the path of the saved file.\n"); printf("\n"); printf("EXAMPLES:\n"); printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n"); @@ -58,7 +69,7 @@ static bool string_to_int64(const char *str, int64_t *result) { } /* 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) { +static int ipc_connect(const char *socket_filepath, int reply_timeout_seconds) { struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; @@ -74,9 +85,12 @@ static int ipc_connect(const char *socket_filepath) { } struct timeval timeout; - timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; + timeout.tv_sec = reply_timeout_seconds; timeout.tv_usec = 0; - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + if(reply_timeout_seconds != GSR_CLI_NO_REPLY_TIMEOUT) + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); if(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == -1) { @@ -140,6 +154,23 @@ static bool ipc_receive_reply(int fd, char *reply, size_t reply_capacity, size_t } } +static void print_json_string(const char *str, size_t size) { + for(size_t i = 0; i < size; ++i) { + char c = str[i]; + if(c == '\\' && i + 1 < size) { + ++i; + switch(str[i]) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = str[i]; break; + } + } + putchar(c); + } + putchar('\n'); +} + /* 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); @@ -185,8 +216,11 @@ static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) return 1; } - if(gsr_json_string_equals(&result_value, "ok")) + if(gsr_json_string_equals(&result_value, "ok")) { + if(has_data && data_value.type == SJ_STRING) + print_json_string(data_value.start, data_value.end - data_value.start); 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); @@ -197,7 +231,7 @@ static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) } static int status_command(const char *socket_filepath) { - const int fd = ipc_connect(socket_filepath); + const int fd = ipc_connect(socket_filepath, GSR_CLI_REPLY_TIMEOUT_SECONDS); if(fd == -1) { printf("not running\n"); fflush(stdout); @@ -210,20 +244,8 @@ static int status_command(const char *socket_filepath) { 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); +static int send_request(const char *socket_filepath, const char *request, int reply_timeout_seconds) { + const int fd = ipc_connect(socket_filepath, reply_timeout_seconds); 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; @@ -259,9 +281,31 @@ int main(int argc, char **argv) { const char *socket_filepath = argv[2]; const char *command = argv[3]; const char *command_argument = argc == 5 ? argv[4] : NULL; + char request[GSR_CLI_MAX_REQUEST_SIZE]; - if(strcmp(command, "save-replay") == 0) - return send_command(socket_filepath, command, command_argument); + if(strcmp(command, "save-replay") == 0) { + if(command_argument) { + int64_t seconds = 0; + if(!string_to_int64(command_argument, &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'", command_argument); + return 1; + } + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, seconds); + } else { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID); + } + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); + } + + if(strcmp(command, "set-paused") == 0) { + if(!command_argument || (strcmp(command_argument, "true") != 0 && strcmp(command_argument, "false") != 0)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the 'set-paused' command expects either true or false as the argument"); + usage(); + return 1; + } + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"set-paused\",\"data\":%s}\n", GSR_CLI_REQUEST_ID, command_argument); + return send_request(socket_filepath, request, GSR_CLI_REPLY_TIMEOUT_SECONDS); + } if(command_argument) { gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take an argument", command); @@ -272,8 +316,15 @@ int main(int argc, char **argv) { 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); + if(strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0 || strcmp(command, "start-replay-recording") == 0) { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, command); + return send_request(socket_filepath, request, GSR_CLI_REPLY_TIMEOUT_SECONDS); + } + + if(strcmp(command, "stop") == 0 || strcmp(command, "stop-replay-recording") == 0) { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, command); + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); + } gsr_log(GSR_LOG_LEVEL_ERROR, "invalid command '%s'", command); usage(); -- cgit v1.2.3 From 3a1ce0e17f209702095aaa86fedeb74400b3d039 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 15:26:36 +0200 Subject: ipc: save-replay: add restart-replay option --- gpu-screen-recorder.1 | 18 ++++++++++++-- gsr-cli.1 | 16 +++++++++++-- include/cli/ipc.h | 7 ++++-- include/recorder/recorder.h | 11 +++++++-- src/cli/ipc.c | 57 +++++++++++++++++++++++++++++++++++++++------ src/cli/main.c | 12 ++++++---- src/recorder/recorder.c | 9 +++++-- tools/gsr-cli/main.c | 56 ++++++++++++++++++++++++++++++++++---------- 8 files changed, 153 insertions(+), 33 deletions(-) (limited to 'src/cli/ipc.c') diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index 67857a8..9f461cc 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -527,8 +527,22 @@ saved file, except in replay mode where nothing is saved. .B save-replay Save replay (replay mode only). .B data -is the number of seconds to save, which has to be larger than 0. The whole replay buffer is saved when +is an object with these optional fields: +.RS +.TP +.B seconds +The number of seconds to save, which has to be larger than 0. +.TP +.B restart-replay +true/false, which overrides the +.B \-restart\-replay\-on\-save +option for this save. Just like that option, the replay buffer is only cleared when the whole replay buffer is saved. +.RE +.IP +The whole replay buffer is saved when .B data +is omitted or null, or when +.B seconds is omitted or null. The reply contains the path of the saved file. .TP .B toggle-pause @@ -559,7 +573,7 @@ Example: .nf .RS gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" & -echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" +echo '{"id":1,"name":"save-replay","data":{"seconds":30}}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" .RE .fi which replies with the following when the replay has been saved: diff --git a/gsr-cli.1 b/gsr-cli.1 index 0a05df9..14d1f51 100644 --- a/gsr-cli.1 +++ b/gsr-cli.1 @@ -6,7 +6,7 @@ gsr\-cli \- Control a running GPU Screen Recorder instance .B \-ipc .I socket_path .I command -.RI [ command_argument ] +.RI [ command_arguments... ] .PP .B gsr\-cli .B \-h @@ -70,10 +70,15 @@ option. Does nothing when a recording is already running. Stop the regular recording that runs during replay/streaming. Waits until the recording has been saved and prints the path of the saved file. Fails when no recording is running. .TP -.BR save-replay " [" \fIseconds\fR ] +.BR save-replay " [" \fIseconds\fR "] [" restart-replay=true|false ] Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay buffer is saved when no number of seconds is given. Waits until the replay has been saved and prints the path of the saved file. +.B restart-replay +overrides the +.B \-restart\-replay\-on\-save +option of GPU Screen Recorder for this save, which clears the replay buffer after the whole replay +buffer has been saved. .SH EXAMPLES Start GPU Screen Recorder in replay mode with an ipc socket: .RS @@ -89,6 +94,13 @@ gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 .fi .RE .PP +Save the whole replay buffer and clear it: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay restart-replay=true +.fi +.RE +.PP Start replay only when it isn't already running: .RS .nf diff --git a/include/cli/ipc.h b/include/cli/ipc.h index 6008db9..0c4c076 100644 --- a/include/cli/ipc.h +++ b/include/cli/ipc.h @@ -28,8 +28,11 @@ typedef struct { bool (*toggle_replay_recording)(char *error_message, size_t error_message_size, void *userdata); bool (*start_replay_recording)(char *error_message, size_t error_message_size, void *userdata); bool (*stop_replay_recording)(char *error_message, size_t error_message_size, void *userdata); - /* |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved */ - bool (*save_replay)(int seconds, char *error_message, size_t error_message_size, void *userdata); + /* + |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved. + |restart_replay| overrides the -restart-replay-on-save option for this save when |has_restart_replay| is set. + */ + bool (*save_replay)(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata); void *userdata; } gsr_ipc_handlers; diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h index 887b4fe..5721a98 100644 --- a/include/recorder/recorder.h +++ b/include/recorder/recorder.h @@ -55,7 +55,14 @@ void gsr_recorder_start_replay_recording(gsr_recorder *self); /* Does nothing when no recording is running */ void gsr_recorder_stop_replay_recording(gsr_recorder *self); bool gsr_recorder_is_replay_recording(const gsr_recorder *self); -/* |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ -void gsr_recorder_save_replay(gsr_recorder *self, int seconds); +#define GSR_RESTART_REPLAY_USE_OPTION -1 +#define GSR_RESTART_REPLAY_DISABLE 0 +#define GSR_RESTART_REPLAY_ENABLE 1 + +/* + |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer. + |restart_replay| overrides the -restart-replay-on-save option for this save when it's not GSR_RESTART_REPLAY_USE_OPTION. +*/ +void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay); #endif /* GSR_RECORDER_RECORDER_H */ diff --git a/src/cli/ipc.c b/src/cli/ipc.c index ff4b852..2fb3000 100644 --- a/src/cli/ipc.c +++ b/src/cli/ipc.c @@ -37,6 +37,7 @@ typedef struct { char name[GSR_IPC_MAX_REQUEST_NAME_SIZE]; sj_Value data; bool has_data; + const char *request_end; } gsr_ipc_request; typedef struct { @@ -269,6 +270,7 @@ static bool ipc_client_send_reply(gsr_ipc *self, gsr_ipc_client *client, int64_t static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, char *error_message, size_t error_message_size) { memset(request, 0, sizeof(*request)); + request->request_end = data + size; sj_Reader reader = sj_reader(data, size); const sj_Value root = sj_read(&reader); @@ -326,18 +328,57 @@ static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, return true; } -static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, int *seconds, char *error_message, size_t error_message_size) { +static bool json_value_to_save_replay_seconds(const sj_Value *value, int *seconds, char *error_message, size_t error_message_size) { + int64_t data_seconds = 0; + if(!gsr_json_number_to_int64(value, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) { + snprintf(error_message, error_message_size, "expected the number of seconds to save to be larger than 0"); + return false; + } + + *seconds = data_seconds; + return true; +} + +static bool ipc_request_get_save_replay_options(const gsr_ipc_request *request, int *seconds, bool *has_restart_replay, bool *restart_replay, char *error_message, size_t error_message_size) { *seconds = GSR_SAVE_REPLAY_SECONDS_FULL; + *has_restart_replay = false; + *restart_replay = false; if(!request->has_data || request->data.type == SJ_NULL) return true; - int64_t data_seconds = 0; - 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"); + if(request->data.type != SJ_OBJECT) { + snprintf(error_message, error_message_size, "expected 'data' to be an object with the optional fields 'seconds' and 'restart-replay'"); + return false; + } + + sj_Reader reader = sj_reader(request->data.start, request->request_end - request->data.start); + const sj_Value data = sj_read(&reader); + + sj_Value key; + sj_Value value; + while(sj_iter_object(&reader, data, &key, &value)) { + if(gsr_json_string_equals(&key, "seconds")) { + if(value.type == SJ_NULL) + continue; + + if(!json_value_to_save_replay_seconds(&value, seconds, error_message, error_message_size)) + return false; + } else if(gsr_json_string_equals(&key, "restart-replay")) { + if(value.type != SJ_BOOL) { + snprintf(error_message, error_message_size, "expected 'restart-replay' to be true or false"); + return false; + } + + *has_restart_replay = true; + *restart_replay = gsr_json_string_equals(&value, "true"); + } + } + + if(reader.error) { + snprintf(error_message, error_message_size, "failed to parse 'data': %s", reader.error); return false; } - *seconds = data_seconds; return true; } @@ -437,10 +478,12 @@ static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, ch if(strcmp(request->name, "save-replay") == 0) { int seconds = GSR_SAVE_REPLAY_SECONDS_FULL; - if(!ipc_request_get_save_replay_seconds(request, &seconds, error_message, error_message_size)) + bool has_restart_replay = false; + bool restart_replay = false; + if(!ipc_request_get_save_replay_options(request, &seconds, &has_restart_replay, &restart_replay, error_message, error_message_size)) return false; - return self->handlers.save_replay(seconds, error_message, error_message_size, self->handlers.userdata); + return self->handlers.save_replay(seconds, has_restart_replay, restart_replay, error_message, error_message_size, self->handlers.userdata); } snprintf(error_message, error_message_size, "unknown request name '%s'", request->name); diff --git a/src/cli/main.c b/src/cli/main.c index de014a7..ac7de24 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -59,7 +59,7 @@ static void toggle_replay_recording_handler(int signal_value) { static void save_replay_seconds_handler(gsr_recorder *rec, int seconds) { if(rec) - gsr_recorder_save_replay(rec, seconds); + gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION); else pending_save_replay_seconds = seconds; } @@ -78,7 +78,7 @@ static void apply_pending_signals(gsr_recorder *rec) { if(pending_save_replay_seconds != 0) { const int seconds = pending_save_replay_seconds; pending_save_replay_seconds = 0; - gsr_recorder_save_replay(rec, seconds); + gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION); } } @@ -186,14 +186,18 @@ static bool ipc_stop_replay_recording_handler(char *error_message, size_t error_ return true; } -static bool ipc_save_replay_handler(int seconds, char *error_message, size_t error_message_size, void *userdata) { +static bool ipc_save_replay_handler(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata) { const gsr_recorder_settings *settings = userdata; if(!settings->is_replaying) { snprintf(error_message, error_message_size, "option -r is required to save a replay"); return false; } - gsr_recorder_save_replay(recorder, seconds); + int restart_replay_request = GSR_RESTART_REPLAY_USE_OPTION; + if(has_restart_replay) + restart_replay_request = restart_replay ? GSR_RESTART_REPLAY_ENABLE : GSR_RESTART_REPLAY_DISABLE; + + gsr_recorder_save_replay(recorder, seconds, restart_replay_request); return true; } diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index fc0ccf0..3f1331f 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -101,6 +101,7 @@ struct gsr_recorder { atomic_int replay_recording_request; atomic_int replay_recording_state; atomic_int save_replay_seconds; + atomic_int save_replay_restart_replay; bool should_stop_error; bool force_iframe_frame; int audio_max_frame_size; @@ -418,6 +419,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r atomic_init(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); atomic_init(&self->replay_recording_state, 0); atomic_init(&self->save_replay_seconds, 0); + atomic_init(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); self->audio_max_frame_size = 1024; int error_code = GSR_ERROR_GENERIC; self->hdr = video_codec_is_hdr(params->settings->video_codec); @@ -748,11 +750,13 @@ static void recorder_poll_replay_save(gsr_recorder *self) { current_save_replay_seconds += self->settings.keyint; atomic_store(&self->save_replay_seconds, 0); + const int restart_replay_request = atomic_exchange(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); + const bool restart_replay = restart_replay_request == GSR_RESTART_REPLAY_USE_OPTION ? self->settings.restart_replay_on_save : restart_replay_request == GSR_RESTART_REPLAY_ENABLE; const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); if(!replay_start_result && self->callbacks.replay_saved) self->callbacks.replay_saved(NULL, self->callbacks.userdata); - if(self->settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { + if(restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { pthread_mutex_lock(&self->encoder.replay_mutex); gsr_replay_buffer_clear(self->encoder.replay_buffer); pthread_mutex_unlock(&self->encoder.replay_mutex); @@ -959,6 +963,7 @@ bool gsr_recorder_is_replay_recording(const gsr_recorder *self) { return atomic_load(&self->replay_recording_state) == 1; } -void gsr_recorder_save_replay(gsr_recorder *self, int seconds) { +void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay) { + atomic_store(&self->save_replay_restart_replay, restart_replay); atomic_store(&self->save_replay_seconds, seconds); } diff --git a/tools/gsr-cli/main.c b/tools/gsr-cli/main.c index e430964..fbfc2fa 100644 --- a/tools/gsr-cli/main.c +++ b/tools/gsr-cli/main.c @@ -19,7 +19,7 @@ #define GSR_CLI_NO_REPLY_TIMEOUT 0 static void usage(void) { - printf("usage: gsr-cli -ipc [command_argument]\n"); + printf("usage: gsr-cli -ipc [command_arguments...]\n"); printf("\n"); printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n"); printf("\n"); @@ -46,14 +46,16 @@ static void usage(void) { printf(" stop-replay-recording\n"); printf(" Stop the regular recording that runs during replay/streaming. Waits until the recording\n"); printf(" has been saved and prints the path of the saved file.\n"); - printf(" save-replay [seconds]\n"); + printf(" save-replay [seconds] [restart-replay=true|false]\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. Waits until the replay has been saved and prints\n"); - printf(" the path of the saved file.\n"); + printf(" the path of the saved file. restart-replay overrides the -restart-replay-on-save option\n"); + printf(" of GPU Screen Recorder for this save.\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"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay restart-replay=true\n"); fflush(stdout); } @@ -272,7 +274,7 @@ int main(int argc, char **argv) { return 1; } - if(argc > 5) { + if(argc > 6) { gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments"); usage(); return 1; @@ -280,23 +282,53 @@ int main(int argc, char **argv) { const char *socket_filepath = argv[2]; const char *command = argv[3]; - const char *command_argument = argc == 5 ? argv[4] : NULL; + const char *command_argument = argc >= 5 ? argv[4] : NULL; + const char *command_argument2 = argc >= 6 ? argv[5] : NULL; char request[GSR_CLI_MAX_REQUEST_SIZE]; if(strcmp(command, "save-replay") == 0) { - if(command_argument) { - int64_t seconds = 0; - if(!string_to_int64(command_argument, &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'", command_argument); + bool has_seconds = false; + int64_t seconds = 0; + const char *restart_replay = NULL; + + const char *command_arguments[2] = { command_argument, command_argument2 }; + for(int i = 0; i < 2; ++i) { + const char *argument = command_arguments[i]; + if(!argument) + continue; + + if(strncmp(argument, "restart-replay=", 15) == 0) { + restart_replay = argument + 15; + if(strcmp(restart_replay, "true") != 0 && strcmp(restart_replay, "false") != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected restart-replay to be either true or false, got: '%s'", restart_replay); + return 1; + } + } else if(!has_seconds && string_to_int64(argument, &seconds) && seconds > 0 && seconds <= INT_MAX) { + has_seconds = true; + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the argument to be the number of seconds to save (an integer larger than 0) or restart-replay=true|false, got: '%s'", argument); return 1; } - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, seconds); - } else { - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID); } + + if(restart_replay && has_seconds) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 ",\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, seconds, restart_replay); + else if(restart_replay) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, restart_replay); + else if(has_seconds) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 "}}\n", GSR_CLI_REQUEST_ID, seconds); + else + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID); + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); } + if(command_argument2) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take more than one argument", command); + usage(); + return 1; + } + if(strcmp(command, "set-paused") == 0) { if(!command_argument || (strcmp(command_argument, "true") != 0 && strcmp(command_argument, "false") != 0)) { gsr_log(GSR_LOG_LEVEL_ERROR, "the 'set-paused' command expects either true or false as the argument"); -- cgit v1.2.3