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