From 535da94536a005fb60e29f7bf902e49390b9cc10 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Wed, 22 Sep 2021 08:54:37 +0200 Subject: [PATCH] GET_CONFIG: add raw/variable-processed contents of all config files (#4528) We do this by adding to included_files as i3 processes the configs. This should allow for easy debugging, without having to change how i3 processes config files. related to #4192 --- RELEASE-NOTES-next | 1 + docs/ipc | 38 ++++++++++++++++++++++++--- i3-msg/main.c | 54 +++++++++++++++++++++++---------------- include/config_parser.h | 2 +- include/configuration.h | 2 ++ man/i3-msg.man | 6 ++++- src/config.c | 6 ++--- src/config_directives.c | 4 ++- src/config_parser.c | 14 +++++----- src/ipc.c | 17 +++++++++++- testcases/t/313-include.t | 27 ++++++++++++++++++-- 11 files changed, 130 insertions(+), 41 deletions(-) diff --git a/RELEASE-NOTES-next b/RELEASE-NOTES-next index 9b7f41ea9..f9c1a4f18 100644 --- a/RELEASE-NOTES-next +++ b/RELEASE-NOTES-next @@ -30,6 +30,7 @@ option is enabled and only then sets a screenshot as background. • default config: use dex for XDG autostart • docs/ipc: document scratchpad_state + • ipc: the GET_CONFIG request now returns all included files and their details • i3-nagbar: position on focused monitor by default • i3-nagbar: add option to position on primary monitor • alternate focusing tab/stack children-parent containers by clicking on their titlebars diff --git a/docs/ipc b/docs/ipc index a5c94459d..02c6f9a96 100644 --- a/docs/ipc +++ b/docs/ipc @@ -793,12 +793,44 @@ No payload. *Reply:* -The config reply is a map which currently only contains the "config" member, -which is a string containing the config file as loaded by i3 most recently. +The config reply is a map which contains the following fields: + +config (string):: + The top-level config file contents that i3 has loaded most recently. + This field is kept for backwards compatibility. See +included_configs+ + instead. +included_configs (array of maps):: + i3 adds one entry to this array for each config file it loads, in + order. The first entry’s +raw_contents+ are identical to the +config+ + field. + +Each +included_configs+ entry contains the following fields + +path (string):: + Absolute path name to the config file that i3 loaded. +raw_contents (string):: + The raw contents of the file as i3 read them. +variable_replaced_contents (string):: + The contents of the file after i3 replaced all variables. This is useful + for debugging variable replacement. *Example:* ------------------- -{ "config": "font pango:monospace 8\nbindsym Mod4+q exit\n" } +{ + "config": "include font.cfg\n", + "included_configs": [ + { + "path": "/home/michael/configfiles/i3/config", + "raw_contents": "include font.cfg\n", + "variable_replaced_contents": "include font.cfg\n" + }, + { + "path": "/home/michael/configfiles/i3/font.cfg", + "raw_contents": "set $font pango:monospace 8\nfont $font", + "variable_replaced_contents": "set pango:monospace 8 pango:monospace 8\nfont pango:monospace 8\n" + } + ], +} ------------------- [[_tick_reply]] diff --git a/i3-msg/main.c b/i3-msg/main.c index c1c8bb81b..239ac46fb 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -156,6 +156,7 @@ int main(int argc, char *argv[]) { char *payload = NULL; bool quiet = false; bool monitor = false; + bool raw_reply = false; static struct option long_options[] = { {"socket", required_argument, 0, 's'}, @@ -164,9 +165,10 @@ int main(int argc, char *argv[]) { {"quiet", no_argument, 0, 'q'}, {"monitor", no_argument, 0, 'm'}, {"help", no_argument, 0, 'h'}, + {"raw", no_argument, 0, 'r'}, {0, 0, 0, 0}}; - char *options_string = "s:t:vhqm"; + char *options_string = "s:t:vhqmr"; while ((o = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) { if (o == 's') { @@ -217,6 +219,8 @@ int main(int argc, char *argv[]) { return 0; } else if (o == '?') { exit(EXIT_FAILURE); + } else if (o == 'r') { + raw_reply = true; } } @@ -262,32 +266,38 @@ int main(int argc, char *argv[]) { /* 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_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); - - 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."); + if (!raw_reply) { + yajl_handle handle = yajl_alloc(&reply_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."); + } } - if (!quiet) { + if (!quiet || raw_reply) { printf("%.*s\n", reply_length, reply); } } 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."); + if (raw_reply) { + printf("%.*s\n", reply_length, reply); + } else { + 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."); + } } } else if (reply_type == I3_IPC_REPLY_TYPE_SUBSCRIBE) { do { diff --git a/include/config_parser.h b/include/config_parser.h index 7cdb5a193..00d01e459 100644 --- a/include/config_parser.h +++ b/include/config_parser.h @@ -103,4 +103,4 @@ typedef enum { * parsing. * */ -parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f); +parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f, IncludedFile *included_file); diff --git a/include/configuration.h b/include/configuration.h index 1e41893a7..be072cf87 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -77,6 +77,8 @@ struct Variable { */ struct IncludedFile { char *path; + char *raw_contents; + char *variable_replaced_contents; TAILQ_ENTRY(IncludedFile) files; }; diff --git a/man/i3-msg.man b/man/i3-msg.man index ce9b476d5..97013fa51 100644 --- a/man/i3-msg.man +++ b/man/i3-msg.man @@ -9,7 +9,7 @@ i3-msg - send messages to i3 window manager == SYNOPSIS -i3-msg [-q] [-v] [-h] [-s socket] [-t type] [message] +i3-msg [-q] [-v] [-h] [-s socket] [-t type] [-r] [message] == OPTIONS @@ -36,6 +36,10 @@ Instead of exiting right after receiving the first subscribed event, wait indefinitely for all of them. Can only be used with "-t subscribe". See the "subscribe" IPC message type below for details. +*-q, --raw*:: +Display the raw JSON reply instead of pretty-printing errors (for commands) or +displaying the top-level config file contents (for GET_CONFIG). + *message*:: Send ipc message, see below. diff --git a/src/config.c b/src/config.c index 7f7e0257e..c590f3c58 100644 --- a/src/config.c +++ b/src/config.c @@ -16,7 +16,6 @@ #include char *current_configpath = NULL; -char *current_config = NULL; Config config; struct modes_head modes; struct barconfig_head barconfigs = TAILQ_HEAD_INITIALIZER(barconfigs); @@ -234,6 +233,8 @@ bool load_configuration(const char *override_configpath, config_load_t load_type while (!TAILQ_EMPTY(&included_files)) { file = TAILQ_FIRST(&included_files); FREE(file->path); + FREE(file->raw_contents); + FREE(file->variable_replaced_contents); TAILQ_REMOVE(&included_files, file, files); FREE(file); } @@ -256,8 +257,7 @@ bool load_configuration(const char *override_configpath, config_load_t load_type .stack = &stack, }; SLIST_INIT(&(ctx.variables)); - FREE(current_config); - const int result = parse_file(&ctx, resolved_path); + const int result = parse_file(&ctx, resolved_path, file); free_variables(&ctx); if (result == -1) { die("Could not open configuration file: %s\n", strerror(errno)); diff --git a/src/config_directives.c b/src/config_directives.c index af3651813..3bb6e793f 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -63,7 +63,7 @@ CFGFUN(include, const char *pattern) { .stack = &stack, .variables = result->ctx->variables, }; - switch (parse_file(&ctx, resolved_path)) { + switch (parse_file(&ctx, resolved_path, file)) { case PARSE_FILE_SUCCESS: break; @@ -75,6 +75,8 @@ CFGFUN(include, const char *pattern) { result->has_errors = true; TAILQ_REMOVE(&included_files, file, files); FREE(file->path); + FREE(file->raw_contents); + FREE(file->variable_replaced_contents); FREE(file); break; diff --git a/src/config_parser.c b/src/config_parser.c index ff1132f68..860ea86f9 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -857,7 +857,7 @@ void free_variables(struct parser_ctx *ctx) { * parse_config and possibly launching i3-nagbar. * */ -parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f) { +parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f, IncludedFile *included_file) { int fd; struct stat stbuf; char *buf; @@ -891,13 +891,11 @@ parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f) { return PARSE_FILE_FAILED; } - if (current_config == NULL) { - current_config = scalloc(stbuf.st_size + 1, 1); - if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) { - return PARSE_FILE_FAILED; - } - rewind(fstr); + included_file->raw_contents = scalloc(stbuf.st_size + 1, 1); + if ((ssize_t)fread(included_file->raw_contents, 1, stbuf.st_size, fstr) != stbuf.st_size) { + return PARSE_FILE_FAILED; } + rewind(fstr); bool invalid_sets = false; @@ -1084,6 +1082,8 @@ parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f) { } } + included_file->variable_replaced_contents = sstrdup(new); + struct context *context = scalloc(1, sizeof(struct context)); context->filename = f; parse_config(ctx, new, context); diff --git a/src/ipc.c b/src/ipc.c index b50648e3e..084d171e1 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -1236,7 +1236,22 @@ IPC_HANDLER(get_config) { y(map_open); ystr("config"); - ystr(current_config); + IncludedFile *file = TAILQ_FIRST(&included_files); + ystr(file->raw_contents); + + ystr("included_configs"); + y(array_open); + TAILQ_FOREACH (file, &included_files, files) { + y(map_open); + ystr("path"); + ystr(file->path); + ystr("raw_contents"); + ystr(file->raw_contents); + ystr("variable_replaced_contents"); + ystr(file->variable_replaced_contents); + y(map_close); + } + y(array_close); y(map_close); diff --git a/testcases/t/313-include.t b/testcases/t/313-include.t index 3a511e516..9b8b0fabb 100644 --- a/testcases/t/313-include.t +++ b/testcases/t/313-include.t @@ -56,10 +56,11 @@ is(launch_get_border($config), 'normal', 'normal border'); ##################################################################### my ($fh, $filename) = tempfile(UNLINK => 1); -print $fh <<'EOT'; +my $varconfig = <<'EOT'; set $vartest special title for_window [title="$vartest"] border none EOT +print $fh $varconfig; $fh->flush; $config = < 1); my $socketpath = $tmpdir . "/config.sock"; ok(! -e $socketpath, "$socketpath does not exist yet"); +my ($indirectfh3, $indirectfilename3) = tempfile(UNLINK => 1); +my $indirectconfig = <flush; + $config = <get_config()->recv; is($config_reply->{config}, $config, 'GET_CONFIG returns the top-level config file'); +my $included = $config_reply->{included_configs}; +is(scalar @{$included}, 3, 'included_configs contains all 3 files'); +is($included->[0]->{raw_contents}, $config, 'included_configs->[0]->{raw_contents} contains top-level config'); +is($included->[1]->{raw_contents}, $indirectconfig, 'included_configs->[1]->{raw_contents} contains indirect config'); +is($included->[2]->{raw_contents}, $varconfig, 'included_configs->[2]->{raw_contents} contains variable config'); + +my $indirect_replaced_config = <[1]->{variable_replaced_contents}, $indirect_replaced_config, 'included_configs->[1]->{variable_replaced_contents} contains config with variables replaced'); + exit_gracefully($pid);