Notify when commands cannot execute
1d08f0a2b8409ca8c64f6c096d9e22e600a09807
1 parent
8e37882f
PKGBUILD
+1 -0
| 10 | 10 | options=('!debug') |
|
| 11 | 11 | backup=('usr/share/wayland-sessions/swm.desktop') |
|
| 12 | 12 | depends=( |
|
| 13 | 13 | 'glibc' |
|
| 14 | 14 | 'libinput' |
|
| 15 | + | 'libnotify' |
|
| 15 | 16 | 'libxcb' |
|
| 16 | 17 | 'libxkbcommon' |
|
| 17 | 18 | 'wayland' |
|
| 18 | 19 | 'wlroots0.20' |
|
| 19 | 20 | 'xcb-util-wm' |
swm.c
+24 -4
| 582 | 582 | ); |
|
| 583 | 583 | static void print_status(void); |
|
| 584 | 584 | static void publish_status(void); |
|
| 585 | 585 | static void status_idle_notify(void *data); |
|
| 586 | 586 | static void publish_windows(const char *runtime); |
|
| 587 | + | static void execute_command(char *const argv[], const char *context); |
|
| 587 | 588 | static void prepare_child(void); |
|
| 588 | 589 | static void power_manager_set_mode(struct wl_listener *listener, void *data); |
|
| 589 | 590 | static void quit(const arg_t *arg); |
|
| 590 | 591 | static void raise_client(const arg_t *arg); |
|
| 591 | 592 | static void render_monitor(struct wl_listener *listener, void *data); |
| 1923 | 1924 | ||
| 1924 | 1925 | expand_argv(p, argv, storage); |
|
| 1925 | 1926 | ||
| 1926 | 1927 | prepare_child(); |
|
| 1927 | 1928 | setsid(); |
|
| 1928 | - | execvp(argv[0], argv); |
|
| 1929 | - | die("autostart: execvp %s:", argv[0]); |
|
| 1929 | + | execute_command(argv, "autostart"); |
|
| 1930 | 1930 | } |
|
| 1931 | 1931 | while (*++p) |
|
| 1932 | 1932 | ; |
|
| 1933 | 1933 | } |
|
| 1934 | 1934 | } |
| 4732 | 4732 | } |
|
| 4733 | 4733 | fflush(stdout); |
|
| 4734 | 4734 | workspace_broadcast(); |
|
| 4735 | 4735 | } |
|
| 4736 | 4736 | ||
| 4737 | + | /* Replace a command child with its program or an error notification. */ |
|
| 4738 | + | [[noreturn]] void execute_command(char *const argv[], const char *context) { |
|
| 4739 | + | char message[MAX_COMMAND_SIZE]; |
|
| 4740 | + | int error; |
|
| 4741 | + | ||
| 4742 | + | execvp(argv[0], argv); |
|
| 4743 | + | error = errno; |
|
| 4744 | + | fprintf(stderr, "swm: %s: cannot execute %s: %s\n", context, argv[0], strerror(error)); |
|
| 4745 | + | snprintf(message, sizeof(message), "Cannot execute %s: %s", argv[0], strerror(error)); |
|
| 4746 | + | execlp( |
|
| 4747 | + | "notify-send", |
|
| 4748 | + | "notify-send", |
|
| 4749 | + | "--app-name=swm", |
|
| 4750 | + | "--urgency=critical", |
|
| 4751 | + | "Command failed", |
|
| 4752 | + | message, |
|
| 4753 | + | nullptr |
|
| 4754 | + | ); |
|
| 4755 | + | _exit(127); |
|
| 4756 | + | } |
|
| 4757 | + | ||
| 4737 | 4758 | /* Restore process state that child commands should not inherit from swm. */ |
|
| 4738 | 4759 | void prepare_child(void) { |
|
| 4739 | 4760 | if (sigprocmask(SIG_SETMASK, &original_signal_mask, nullptr) < 0) |
|
| 4740 | 4761 | die("sigprocmask:"); |
|
| 4741 | 4762 | signal(SIGPIPE, SIG_DFL); |
| 5978 | 5999 | expand_argv(arg->v, argv, storage); |
|
| 5979 | 6000 | ||
| 5980 | 6001 | prepare_child(); |
|
| 5981 | 6002 | dup2(STDERR_FILENO, STDOUT_FILENO); |
|
| 5982 | 6003 | setsid(); |
|
| 5983 | - | execvp(argv[0], argv); |
|
| 5984 | - | die("execvp %s failed:", argv[0]); |
|
| 6004 | + | execute_command(argv, "key binding"); |
|
| 5985 | 6005 | } |
|
| 5986 | 6006 | if (pid > 0 && selmon && selmon->ws) { |
|
| 5987 | 6007 | ps = pool_take(&pending_spawn_pool); |
|
| 5988 | 6008 | ||
| 5989 | 6009 | if (!ps) |
test/run.py
+5 -0
| 211 | 211 | "sh", |
|
| 212 | 212 | "wl-paste", |
|
| 213 | 213 | ) |
|
| 214 | 214 | for name in stub_commands: |
|
| 215 | 215 | (stubs / name).symlink_to("/bin/true") |
|
| 216 | + | notification = stubs / "notify-send" |
|
| 217 | + | notification.write_text( |
|
| 218 | + | "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$SWM_TEST_DIR/notification\"\n" |
|
| 219 | + | ) |
|
| 220 | + | notification.chmod(0o755) |
|
| 216 | 221 | if integration: |
|
| 217 | 222 | for executable in ("pywayland-scanner", "timeout"): |
|
| 218 | 223 | if not shutil.which(executable): |
|
| 219 | 224 | raise Failure(f"required tool not found: {executable}") |
|
| 220 | 225 | generate_python_protocols(os.environ.get("PKG_CONFIG", "pkg-config")) |
test/unit.c
+46 -0
| 425 | 425 | } |
|
| 426 | 426 | assert(wl_list_empty(&pending_spawns)); |
|
| 427 | 427 | selmon = nullptr; |
|
| 428 | 428 | } |
|
| 429 | 429 | ||
| 430 | + | /* Verify a failed keybinding command produces a desktop notification. */ |
|
| 431 | + | static void test_command_failure_notification(void) { |
|
| 432 | + | const char *stub_path = getenv("SWM_TEST_STUB_PATH"); |
|
| 433 | + | const char *test_dir = getenv("SWM_TEST_DIR"); |
|
| 434 | + | const char *old_path = getenv("PATH"); |
|
| 435 | + | char *saved_path; |
|
| 436 | + | char notification[PATH_MAX]; |
|
| 437 | + | char contents[1024]; |
|
| 438 | + | char *command[] = { "swm-command-that-does-not-exist", nullptr }; |
|
| 439 | + | FILE *file; |
|
| 440 | + | pid_t pid; |
|
| 441 | + | size_t length; |
|
| 442 | + | int status; |
|
| 443 | + | ||
| 444 | + | assert(stub_path && test_dir && old_path); |
|
| 445 | + | saved_path = strdup(old_path); |
|
| 446 | + | assert(saved_path); |
|
| 447 | + | assert( |
|
| 448 | + | snprintf(notification, sizeof(notification), "%s/notification", test_dir) < |
|
| 449 | + | (int)sizeof(notification) |
|
| 450 | + | ); |
|
| 451 | + | unlink(notification); |
|
| 452 | + | assert(setenv("PATH", stub_path, 1) == 0); |
|
| 453 | + | pid = fork(); |
|
| 454 | + | assert(pid >= 0); |
|
| 455 | + | ||
| 456 | + | if (pid == 0) |
|
| 457 | + | execute_command(command, "key binding"); |
|
| 458 | + | assert(waitpid(pid, &status, 0) == pid); |
|
| 459 | + | assert(WIFEXITED(status) && WEXITSTATUS(status) == 0); |
|
| 460 | + | file = fopen(notification, "r"); |
|
| 461 | + | assert(file); |
|
| 462 | + | length = fread(contents, 1, sizeof(contents) - 1, file); |
|
| 463 | + | assert(length > 0); |
|
| 464 | + | contents[length] = '\0'; |
|
| 465 | + | assert(fclose(file) == 0); |
|
| 466 | + | assert(strstr(contents, "--app-name=swm")); |
|
| 467 | + | assert(strstr(contents, "--urgency=critical")); |
|
| 468 | + | assert(strstr(contents, "Command failed")); |
|
| 469 | + | assert(strstr(contents, "swm-command-that-does-not-exist")); |
|
| 470 | + | assert(setenv("PATH", saved_path, 1) == 0); |
|
| 471 | + | free(saved_path); |
|
| 472 | + | unlink(notification); |
|
| 473 | + | } |
|
| 474 | + | ||
| 430 | 475 | /* Verify client commands remain safe when no client can receive them. */ |
|
| 431 | 476 | static void test_commands_without_clients(void) { |
|
| 432 | 477 | struct wlr_output output = {}; |
|
| 433 | 478 | monitor_t monitor = {}; |
|
| 434 | 479 | arg_t argument; |
| 874 | 919 | { "rules", test_rule_application }, |
|
| 875 | 920 | { "focus", test_focus_queries }, |
|
| 876 | 921 | { "window-state", test_window_state_storage }, |
|
| 877 | 922 | { "child-reaping", test_child_reaping }, |
|
| 878 | 923 | { "spawn-tracking", test_spawn_tracking }, |
|
| 924 | + | { "command-failure-notification", test_command_failure_notification }, |
|
| 879 | 925 | { "commands-without-clients", test_commands_without_clients }, |
|
| 880 | 926 | { "small-helpers", test_small_helpers }, |
|
| 881 | 927 | { "client-accessors", test_client_accessors }, |
|
| 882 | 928 | { "autostart", test_autostart_execution }, |
|
| 883 | 929 | { "pointer-gestures", test_pointer_gestures }, |