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