aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2026-08-05 14:56:08 +0200
committerdec05eba <dec05eba@protonmail.com>2026-08-05 14:56:08 +0200
commitfbc31a9561d16418226c0efa65e2a73923516dbd (patch)
tree1c515cd13ee8af93a665d0587fb002745a01422d
parenta522748ed2fc04e4ccad26c1da1056472cb28bc2 (diff)
ipc: response with video path in save recording/replay
-rw-r--r--README.md5
-rw-r--r--gpu-screen-recorder.137
-rw-r--r--gsr-cli.131
-rw-r--r--include/cli/ipc.h46
-rw-r--r--include/recorder/recorder.h7
-rw-r--r--src/cli/ipc.c545
-rw-r--r--src/cli/main.c72
-rw-r--r--src/recorder/recorder.c166
-rw-r--r--src/recorder/replay_save.c2
-rw-r--r--tools/gsr-cli/main.c103
10 files changed, 843 insertions, 171 deletions
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 <stdbool.h>
#include <stddef.h>
+#include <stdint.h>
#include <limits.h>
#include <pthread.h>
#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 <sys/stat.h>
#include <sys/un.h>
+#ifdef __linux__
+#include <sys/epoll.h>
+#else
+#include <sys/event.h>
+#include <sys/time.h>
+#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 <socket_path> <command> [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();