Notify when commands cannot execute

1d08f0a2b8409ca8c64f6c096d9e22e600a09807
Alexis Sellier committed ago 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 },