diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 6a16b89c..39f1f9d4 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -180,6 +180,7 @@ this is for compatibility with Openbox. no yes [see details below] + [see details below] ``` @@ -316,6 +317,29 @@ this is for compatibility with Openbox. --cancel-label="%n" ``` +** + Set command to be invoked for displaying errors in the config files, + it is executed when errors are detected on startup and reconfigure. + The errors are sent to STDIN of the program, and a SIGTERM is sent to + it if the process is still running when a reconfigure is triggered. + + The default error command is: + ``` + labnag \\ + --message 'Config Error' \\ + --button-dismiss 'Close' \\ + --layer overlay \\ + --timeout 0 \\ + --detailed-message + ``` + + Example using `zenity`: + ``` + + zenity --title='Config Error' --text-info + + ``` + ## PLACEMENT ``` diff --git a/include/common/log.h b/include/common/log.h new file mode 100644 index 00000000..c20efda9 --- /dev/null +++ b/include/common/log.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_LOG_H +#define LABWC_LOG_H + +#include +#include "common/buf.h" + +struct buf *log_get_buf(void); +void log_set_error(enum wlr_log_importance importance); +void log_reset(void); +void nag_show(void); +void nag_show_callback(void *data); + +#define nag_log(verb, fmt, ...) \ +do { \ + wlr_log(verb, fmt, ##__VA_ARGS__); \ + log_set_error(verb); \ + buf_add_fmt(log_get_buf(), fmt "\n", ##__VA_ARGS__); \ +} while (0) + +#endif /* LABWC_LOG_H */ diff --git a/include/common/spawn.h b/include/common/spawn.h index 4df5ed0b..d7cbda5e 100644 --- a/include/common/spawn.h +++ b/include/common/spawn.h @@ -22,6 +22,19 @@ void spawn_async_no_shell(char const *command); */ void spawn_sync_no_shell(char const *command); +/** + * spawn_piped_async_no_shell - execute asynchronously + * @command: command to be executed + * @pipe_fd_w: set to the write end of a pipe + * connected to stdin of the command + * Notes: + * The returned pid_t has to be waited for to + * not produce zombies and the pipe_fd_w has to + * be closed. spawn_piped_close() can be used + * to ensure both. + */ +pid_t spawn_piped_async_no_shell(const char *command, int *pipe_fd_w); + /** * spawn_piped - execute asynchronously * @command: command to be executed diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 9c2183a8..320fbdda 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -87,6 +87,7 @@ struct rcxml { bool xwayland_persistence; bool primary_selection; char *prompt_command; + char *error_command; /* placement */ enum lab_placement_policy placement_policy; diff --git a/src/action.c b/src/action.c index 265ff6e7..0f88e8a2 100644 --- a/src/action.c +++ b/src/action.c @@ -13,6 +13,7 @@ #include "common/buf.h" #include "common/macros.h" #include "common/list.h" +#include "common/log.h" #include "common/mem.h" #include "common/parse-bool.h" #include "common/spawn.h" @@ -347,7 +348,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char } else if (!strcasecmp(content, "current")) { action_arg_add_int(action, argument, CYCLE_WORKSPACE_CURRENT); } else { - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } goto cleanup; @@ -360,7 +361,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char } else if (!strcasecmp(content, "focused")) { action_arg_add_int(action, argument, CYCLE_OUTPUT_FOCUSED); } else { - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } goto cleanup; @@ -371,7 +372,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char } else if (!strcasecmp(content, "current")) { action_arg_add_int(action, argument, CYCLE_APP_ID_CURRENT); } else { - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } goto cleanup; @@ -401,7 +402,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char if (!strcmp(argument, "direction")) { enum view_axis axis = view_axis_parse(content); if (axis == VIEW_AXIS_NONE || axis == VIEW_AXIS_INVALID) { - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } else { action_arg_add_int(action, argument, axis); @@ -415,7 +416,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char if (mode != LAB_SSD_MODE_INVALID) { action_arg_add_int(action, argument, mode); } else { - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } goto cleanup; @@ -430,7 +431,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char enum lab_edge edge = lab_edge_parse(content, /*tiled*/ true, /*any*/ false); if (edge == LAB_EDGE_NONE || edge == LAB_EDGE_CENTER) { - wlr_log(WLR_ERROR, + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } else { @@ -497,7 +498,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char enum lab_edge edge = lab_edge_parse(content, /*tiled*/ false, /*any*/ false); if (edge == LAB_EDGE_NONE) { - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } else { action_arg_add_int(action, argument, edge); @@ -521,7 +522,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char enum lab_placement_policy policy = view_placement_parse(content); if (policy == LAB_PLACE_INVALID) { - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } else { action_arg_add_int(action, argument, policy); @@ -542,7 +543,7 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char goto cleanup; } - wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s'", + nag_log(WLR_ERROR, "Invalid argument for action %s: '%s'", action_names[action->type], argument); cleanup: diff --git a/src/common/log.c b/src/common/log.c new file mode 100644 index 00000000..e59daacc --- /dev/null +++ b/src/common/log.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L +#include "common/log.h" +#include "common/spawn.h" +#include +#include +#include +#include "common/buf.h" +#include "config/rcxml.h" + +static struct buf log_buf = BUF_INIT; +static bool has_error = false; +static pid_t pid = 0; + +struct buf * +log_get_buf(void) +{ + return &log_buf; +} + +void +log_set_error(enum wlr_log_importance importance) +{ + if (!has_error && importance == WLR_ERROR) { + has_error = true; + } +} + +void +log_reset(void) +{ + has_error = false; + buf_reset(&log_buf); + if (pid > 0) { + if (!waitpid(pid, NULL, WNOHANG)) { + kill(pid, SIGTERM); + /* waitpid() is done in a generic SIGCHLD handler in src/server.c */ + } + } + pid = 0; +} + +void +nag_show(void) +{ + if (!has_error) { + return; + } + int pipe_w; + pid = spawn_piped_async_no_shell(rc.error_command, &pipe_w); + if (pid > 0) { + ssize_t bytes = write(pipe_w, log_buf.data, log_buf.len); + if (bytes < 0) { + wlr_log(WLR_ERROR, "Failed to write errors to process: %s", + rc.error_command); + } + spawn_piped_close(pid, pipe_w); + } else if (pid < 0) { + wlr_log(WLR_ERROR, "Failed to launch process: %s", rc.error_command); + } +} + +void +nag_show_callback(void *data) +{ + nag_show(); +} diff --git a/src/common/meson.build b/src/common/meson.build index 4cf52023..ccab92cf 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -8,6 +8,7 @@ labwc_sources += files( 'font.c', 'graphic-helpers.c', 'lab-scene-rect.c', + 'log.c', 'match.c', 'mem.c', 'nodename.c', diff --git a/src/common/spawn.c b/src/common/spawn.c index 6073d6ae..4dec77b9 100644 --- a/src/common/spawn.c +++ b/src/common/spawn.c @@ -155,6 +155,82 @@ spawn_primary_client(const char *command) } } +pid_t +spawn_piped_async_no_shell(const char *command, int *pipe_fd_w) +{ + assert(command); + + GError *err = NULL; + gchar **argv = NULL; + + /* Use glib's shell-parse to mimic Openbox's behaviour */ + g_shell_parse_argv((gchar *)command, NULL, &argv, &err); + if (err) { + g_message("%s", err->message); + g_error_free(err); + return -1; + } + + int pipe_rw[2]; + if (pipe(pipe_rw) != 0) { + wlr_log(WLR_ERROR, "unable to pipe()"); + g_strfreev(argv); + return -1; + } + + pid_t child = 0; + child = fork(); + if (child < 0) { + wlr_log(WLR_ERROR, "unable to fork() child"); + close(pipe_rw[0]); + close(pipe_rw[1]); + g_strfreev(argv); + return child; + } + + if (child == 0) { + /* Child */ + reset_signals_and_limits(); + + /* + * replace stdout and stderr with /dev/null + * and stdin with the read end of the pipe + */ + close(pipe_rw[1]); + dup2(pipe_rw[0], STDIN_FILENO); + close(pipe_rw[0]); + + int dev_null = open("/dev/null", O_WRONLY); + if (dev_null < 0) { + wlr_log_errno(WLR_ERROR, "opening /dev/null failed"); + /* + * Just close stdout and stderr and + * hope $command can deal with that. + */ + close(STDOUT_FILENO); + close(STDERR_FILENO); + } else { + dup2(dev_null, STDOUT_FILENO); + dup2(dev_null, STDERR_FILENO); + close(dev_null); + } + execvp(argv[0], argv); + _exit(1); + } + /* labwc */ + close(pipe_rw[0]); + g_strfreev(argv); + + /* + * Prevent leaking the write end of the pipe to further + * children forked during the lifetime of the descriptor. + */ + set_cloexec(pipe_rw[1]); + *pipe_fd_w = pipe_rw[1]; + + return child; +} + pid_t spawn_piped(const char *command, int *pipe_fd) { diff --git a/src/config/rcxml.c b/src/config/rcxml.c index ab0639d1..1d9afff4 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -15,6 +15,7 @@ #include "common/buf.h" #include "common/dir.h" #include "common/list.h" +#include "common/log.h" #include "common/macros.h" #include "common/mem.h" #include "common/nodename.h" @@ -1167,6 +1168,8 @@ entry(xmlNode *node, char *nodename, char *content) } else if (!strcasecmp(nodename, "promptCommand.core")) { xstrdup_replace(rc.prompt_command, content); + } else if (!strcasecmp(nodename, "errorCommand.core")) { + xstrdup_replace(rc.error_command, content); } else if (!strcmp(nodename, "policy.placement")) { enum lab_placement_policy policy = view_placement_parse(content); @@ -1476,7 +1479,7 @@ rcxml_parse_xml(struct buf *b) int options = 0; xmlDoc *d = xmlReadMemory(b->data, b->len, NULL, NULL, options); if (!d) { - wlr_log(WLR_ERROR, "error parsing config file"); + nag_log(WLR_ERROR, "error parsing config file"); return; } xmlNode *root = xmlDocGetRootElement(d); @@ -1802,6 +1805,15 @@ post_processing(void) "--layer overlay " "--timeout 0"); } + if (!rc.error_command) { + rc.error_command = + xstrdup("labnag " + "--message 'Config Error' " + "--button-dismiss 'Close' " + "--layer overlay " + "--timeout 0 " + "--detailed-message"); + } if (!rc.fallback_app_icon_name) { rc.fallback_app_icon_name = xstrdup("labwc"); } @@ -2030,7 +2042,7 @@ rcxml_read(const char *filename) continue; } - wlr_log(WLR_INFO, "read config file %s", path->string); + nag_log(WLR_INFO, "read config file %s", path->string); rcxml_parse_xml(&b); buf_reset(&b); @@ -2052,6 +2064,7 @@ rcxml_finish(void) zfree(rc.font_menuitem.name); zfree(rc.font_osd.name); zfree(rc.prompt_command); + zfree(rc.error_command); zfree(rc.theme_name); zfree(rc.icon_theme_name); zfree(rc.fallback_app_icon_name); diff --git a/src/server.c b/src/server.c index d0c8c04c..79e2eb8f 100644 --- a/src/server.c +++ b/src/server.c @@ -52,6 +52,7 @@ #endif #include "action.h" +#include "common/log.h" #include "common/macros.h" #include "common/mem.h" #include "common/scene-helpers.h" @@ -98,6 +99,7 @@ reload_config_and_theme(void) scaled_buffer_invalidate_sharing(); rcxml_finish(); + log_reset(); rcxml_read(rc.config_file); theme_finish(rc.theme); theme_init(rc.theme, rc.theme_name); @@ -119,6 +121,8 @@ reload_config_and_theme(void) resize_indicator_reconfigure(); kde_server_decoration_update_default(); workspaces_reconfigure(); + + wl_event_loop_add_idle(server.wl_event_loop, nag_show_callback, NULL); } static int @@ -809,6 +813,7 @@ server_init(void) #if HAVE_XWAYLAND xwayland_server_init(server.compositor); #endif + wl_event_loop_add_idle(server.wl_event_loop, nag_show_callback, NULL); } void