From a6d8ed9b1ac6efa507d65b752758522bcfcc5213 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 19 Aug 2017 17:29:03 +0200 Subject: [PATCH] Introduce the GET_CONFIG IPC request This introduces memory usage by one copy of the config file, which is an acceptable trade-off for being able to easily revert data loss. The default config is 6KB, user configs will be in the same ballpark. fixes #2856 --- AnyEvent-I3/lib/AnyEvent/I3.pm | 19 ++++++++- i3-msg/main.c | 77 +++++++++++++++++++++++++++++----- include/configuration.h | 1 + include/i3/ipc.h | 4 ++ src/config.c | 1 + src/config_parser.c | 5 +++ src/ipc.c | 24 ++++++++++- testcases/t/268-ipc-config.t | 55 ++++++++++++++++++++++++ 8 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 testcases/t/268-ipc-config.t diff --git a/AnyEvent-I3/lib/AnyEvent/I3.pm b/AnyEvent-I3/lib/AnyEvent/I3.pm index 875f3790..5ce3c016 100644 --- a/AnyEvent-I3/lib/AnyEvent/I3.pm +++ b/AnyEvent-I3/lib/AnyEvent/I3.pm @@ -95,10 +95,13 @@ use constant TYPE_GET_TREE => 4; use constant TYPE_GET_MARKS => 5; use constant TYPE_GET_BAR_CONFIG => 6; use constant TYPE_GET_VERSION => 7; +use constant TYPE_GET_BINDING_MODES => 8; +use constant TYPE_GET_CONFIG => 9; our %EXPORT_TAGS = ( 'all' => [ qw(i3 TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS - TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION) + TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION + TYPE_GET_BINDING_MODES TYPE_GET_CONFIG) ] ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } ); @@ -501,6 +504,20 @@ sub get_version { return $cv; } +=head2 get_config + +Gets the raw last read config from i3. Requires i3 >= 4.14 + +=cut +sub get_config { + my ($self) = @_; + + $self->_ensure_connection; + + $self->message(TYPE_GET_CONFIG); +} + + =head2 command($content) Makes i3 execute the given command diff --git a/i3-msg/main.c b/i3-msg/main.c index 4bc3c149..1a172789 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -119,6 +119,43 @@ static yajl_callbacks reply_callbacks = { .yajl_end_map = reply_end_map_cb, }; +/******************************************************************************* + * Config reply callbacks + *******************************************************************************/ + +static char *config_last_key = NULL; + +static int config_string_cb(void *params, const unsigned char *val, size_t len) { + char *str = scalloc(len + 1, 1); + strncpy(str, (const char *)val, len); + if (strcmp(config_last_key, "config") == 0) { + fprintf(stdout, "%s", str); + } + free(str); + return 1; +} + +static int config_start_map_cb(void *params) { + return 1; +} + +static int config_end_map_cb(void *params) { + return 1; +} + +static int config_map_key_cb(void *params, const unsigned char *keyVal, size_t keyLen) { + config_last_key = scalloc(keyLen + 1, 1); + strncpy(config_last_key, (const char *)keyVal, keyLen); + return 1; +} + +static yajl_callbacks config_callbacks = { + .yajl_string = config_string_cb, + .yajl_start_map = config_start_map_cb, + .yajl_map_key = config_map_key_cb, + .yajl_end_map = config_end_map_cb, +}; + int main(int argc, char *argv[]) { #if defined(__OpenBSD__) if (pledge("stdio rpath unix", NULL) == -1) @@ -150,25 +187,27 @@ int main(int argc, char *argv[]) { free(socket_path); socket_path = sstrdup(optarg); } else if (o == 't') { - if (strcasecmp(optarg, "command") == 0) + if (strcasecmp(optarg, "command") == 0) { message_type = I3_IPC_MESSAGE_TYPE_COMMAND; - else if (strcasecmp(optarg, "get_workspaces") == 0) + } else if (strcasecmp(optarg, "get_workspaces") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_WORKSPACES; - else if (strcasecmp(optarg, "get_outputs") == 0) + } else if (strcasecmp(optarg, "get_outputs") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_OUTPUTS; - else if (strcasecmp(optarg, "get_tree") == 0) + } else if (strcasecmp(optarg, "get_tree") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_TREE; - else if (strcasecmp(optarg, "get_marks") == 0) + } else if (strcasecmp(optarg, "get_marks") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_MARKS; - else if (strcasecmp(optarg, "get_bar_config") == 0) + } else if (strcasecmp(optarg, "get_bar_config") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_BAR_CONFIG; - else if (strcasecmp(optarg, "get_binding_modes") == 0) + } else if (strcasecmp(optarg, "get_binding_modes") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_BINDING_MODES; - else if (strcasecmp(optarg, "get_version") == 0) + } else if (strcasecmp(optarg, "get_version") == 0) { message_type = I3_IPC_MESSAGE_TYPE_GET_VERSION; - else { + } else if (strcasecmp(optarg, "get_config") == 0) { + message_type = I3_IPC_MESSAGE_TYPE_GET_CONFIG; + } else { printf("Unknown message type\n"); - printf("Known types: command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version\n"); + printf("Known types: command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config\n"); exit(EXIT_FAILURE); } } else if (o == 'q') { @@ -241,7 +280,7 @@ int main(int argc, char *argv[]) { errx(EXIT_FAILURE, "IPC: Received reply of type %d but expected %d", reply_type, message_type); /* For the reply of commands, have a look if that command was successful. * If not, nicely format the error message. */ - if (reply_type == I3_IPC_MESSAGE_TYPE_COMMAND) { + if (reply_type == I3_IPC_REPLY_TYPE_COMMAND) { yajl_handle handle = yajl_alloc(&reply_callbacks, NULL, NULL); yajl_status state = yajl_parse(handle, (const unsigned char *)reply, reply_length); yajl_free(handle); @@ -256,8 +295,24 @@ int main(int argc, char *argv[]) { /* NB: We still fall-through and print the reply, because even if one * command failed, that doesn’t mean that all commands failed. */ + } else if (reply_type == I3_IPC_REPLY_TYPE_CONFIG) { + yajl_handle handle = yajl_alloc(&config_callbacks, NULL, NULL); + yajl_status state = yajl_parse(handle, (const unsigned char *)reply, reply_length); + yajl_free(handle); + + switch (state) { + case yajl_status_ok: + break; + case yajl_status_client_canceled: + case yajl_status_error: + errx(EXIT_FAILURE, "IPC: Could not parse JSON reply."); + } + + goto exit; } printf("%.*s\n", reply_length, reply); + +exit: free(reply); close(sockfd); diff --git a/include/configuration.h b/include/configuration.h index 66628eeb..4f6e5ce8 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -21,6 +21,7 @@ typedef struct Config Config; typedef struct Barconfig Barconfig; extern char *current_configpath; +extern char *current_config; extern Config config; extern SLIST_HEAD(modes_head, Mode) modes; extern TAILQ_HEAD(barconfig_head, Barconfig) barconfigs; diff --git a/include/i3/ipc.h b/include/i3/ipc.h index 249cc32e..e3891454 100644 --- a/include/i3/ipc.h +++ b/include/i3/ipc.h @@ -54,6 +54,9 @@ typedef struct i3_ipc_header { /** Request a list of configured binding modes. */ #define I3_IPC_MESSAGE_TYPE_GET_BINDING_MODES 8 +/** Request the raw last loaded i3 config. */ +#define I3_IPC_MESSAGE_TYPE_GET_CONFIG 9 + /* * Messages from i3 to clients * @@ -67,6 +70,7 @@ typedef struct i3_ipc_header { #define I3_IPC_REPLY_TYPE_BAR_CONFIG 6 #define I3_IPC_REPLY_TYPE_VERSION 7 #define I3_IPC_REPLY_TYPE_BINDING_MODES 8 +#define I3_IPC_REPLY_TYPE_CONFIG 9 /* * Events from i3 to clients. Events have the first bit set high. diff --git a/src/config.c b/src/config.c index d4441d5d..7e08b520 100644 --- a/src/config.c +++ b/src/config.c @@ -13,6 +13,7 @@ #include char *current_configpath = NULL; +char *current_config = NULL; Config config; struct modes_head modes; struct barconfig_head barconfigs = TAILQ_HEAD_INITIALIZER(barconfigs); diff --git a/src/config_parser.c b/src/config_parser.c index 60b27815..e72923d6 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -898,6 +898,11 @@ bool parse_file(const char *f, bool use_nagbar) { if ((fstr = fdopen(fd, "r")) == NULL) die("Could not fdopen: %s\n", strerror(errno)); + FREE(current_config); + current_config = scalloc(stbuf.st_size + 1, 1); + fread(current_config, 1, stbuf.st_size, fstr); + rewind(fstr); + while (!feof(fstr)) { if (!continuation) continuation = buffer; diff --git a/src/ipc.c b/src/ipc.c index bb20b340..18d6075d 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -1087,9 +1087,30 @@ IPC_HANDLER(subscribe) { ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply); } +/* + * Returns the raw last loaded i3 configuration file contents. + */ +IPC_HANDLER(get_config) { + yajl_gen gen = ygenalloc(); + + y(map_open); + + ystr("config"); + ystr(current_config); + + y(map_close); + + const unsigned char *payload; + ylength length; + y(get_buf, &payload, &length); + + ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_CONFIG, payload); + y(free); +} + /* The index of each callback function corresponds to the numeric * value of the message type (see include/i3/ipc.h) */ -handler_t handlers[9] = { +handler_t handlers[10] = { handle_command, handle_get_workspaces, handle_subscribe, @@ -1099,6 +1120,7 @@ handler_t handlers[9] = { handle_get_bar_config, handle_get_version, handle_get_binding_modes, + handle_get_config, }; /* diff --git a/testcases/t/268-ipc-config.t b/testcases/t/268-ipc-config.t new file mode 100644 index 00000000..bb578e13 --- /dev/null +++ b/testcases/t/268-ipc-config.t @@ -0,0 +1,55 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • http://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • http://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • http://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Verifies that the config file is returned raw via the IPC interface. +# Ticket: #2856 +# Bug still in: 4.13-133-ge4da07e7 +use i3test i3_autostart => 0; +use File::Temp qw(tempdir); + +my $tmpdir = tempdir(CLEANUP => 1); +my $socketpath = $tmpdir . "/config.sock"; +ok(! -e $socketpath, "$socketpath does not exist yet"); + +my $config = <<'EOT'; +# i3 config file (v4) +font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 + +nop foo \ +continued + +set $var normal title +for_window [title="$vartest"] border none +EOT + +$config .= "ipc-socket $socketpath"; + +my $pid = launch_with_config($config, dont_add_socket_path => 1, dont_create_temp_dir => 1); +get_socket_path(0); +my $i3 = i3(get_socket_path()); +$i3->connect->recv; + +my $cv = AE::cv; +my $timer = AE::timer 0.5, 0, sub { $cv->send(0); }; + +my $last_config = $i3->get_config()->recv; +chomp($last_config->{config}); +is($last_config->{config}, $config, + 'received config is not equal to written config'); + +exit_gracefully($pid); + +done_testing;