// fish_test_helper is a little program with no fish dependencies that acts like certain other // programs, allowing fish to test its behavior. #include #include #include #include #include #include #include #include #include static void abandon_tty() { // The parent may get SIGSTOPed when it tries to call tcsetpgrp if the child has already done // it. Prevent this by ignoring signals. signal(SIGTTIN, SIG_IGN); signal(SIGTTOU, SIG_IGN); pid_t pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } // Both parent and child do the same thing. pid_t child = pid ? pid : getpid(); if (setpgid(child, child)) { perror("setpgid"); exit(EXIT_FAILURE); } // tcsetpgrp may fail in the parent if the child has already exited. // This is the benign race. (void)tcsetpgrp(STDIN_FILENO, child); // Parent waits for child to exit. if (pid > 0) { waitpid(child, NULL, 0); } } static void become_foreground_then_print_stderr() { if (tcsetpgrp(STDOUT_FILENO, getpgrp()) < 0) { perror("tcsetgrp"); exit(EXIT_FAILURE); } usleep(1000000 / 4); //.25 secs fprintf(stderr, "become_foreground_then_print_stderr done\n"); } static void nohup_wait() { pid_t init_parent = getppid(); if (signal(SIGHUP, SIG_IGN)) { perror("tcsetgrp"); exit(EXIT_FAILURE); } // Note: these silly close() calls are necessary to prevent our parent process (presumably fish) // from getting stuck in the "E" state ("Trying to exit"). This appears to be a (kernel?) bug on // macOS: the process is no longer running but is not a zombie either, and so cannot be reaped. // It is unclear why closing these fds successfully works around this issue. close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); // To avoid leaving fish_test_helpers around, we exit once our parent changes, meaning the fish // instance exited. while (getppid() == init_parent) { usleep(1000000 / 4); } } static void report_foreground_loop() { int was_fg = -1; const pid_t grp = getpgrp(); for (;;) { int is_fg = (tcgetpgrp(STDIN_FILENO) == grp); if (is_fg != was_fg) { was_fg = is_fg; if (fputs(is_fg ? "foreground\n" : "background\n", stderr) < 0) { return; } } usleep(1000000 / 2); } } static void report_foreground() { bool is_fg = (tcgetpgrp(STDIN_FILENO) == getpgrp()); fputs(is_fg ? "foreground\n" : "background\n", stderr); } static void sigint_parent() { // SIGINT the parent after a time, then exit int parent = getppid(); usleep(1000000 / 4); //.25 secs kill(parent, SIGINT); fprintf(stderr, "Sent SIGINT to %d\n", parent); } static void print_stdout_stderr() { fprintf(stdout, "stdout\n"); fprintf(stderr, "stderr\n"); fflush(NULL); } static void print_pid_then_sleep() { // On some systems getpid is a long, on others it's an int, let's just cast it. fprintf(stdout, "%ld\n", (long)getpid()); fflush(NULL); usleep(1000000 / 2); //.5 secs } static void print_pgrp() { fprintf(stdout, "%ld\n", (long)getpgrp()); } static void print_fds() { bool needs_space = false; for (int fd = 0; fd <= 100; fd++) { if (fcntl(fd, F_GETFD) >= 0) { fprintf(stdout, "%s%d", needs_space ? " " : "", fd); needs_space = true; } } fputc('\n', stdout); } static void print_signal(int sig) { // Print a signal description to stderr. const char *s = strsignal(sig); if (s) { fprintf(stderr, "%s", s); if (strchr(s, ':') == NULL) { fprintf(stderr, ": %d", sig); } fprintf(stderr, "\n"); } } static void print_blocked_signals() { sigset_t sigs; sigemptyset(&sigs); if (sigprocmask(SIG_SETMASK, NULL, &sigs)) { perror("sigprocmask"); exit(EXIT_FAILURE); } // There is no obviously portable way to get the maximum number of signals. // POSIX says sigqueue(2) can be used with signo 0 to validate the pid and signo parameters, // but it is missing from OpenBSD and returns ENOSYS (not implemented) under WSL. // Here we limit it to 32 because strsignal on OpenBSD returns "Unknown signal" for anything // above, while NetBSD taps out at 63, and Linux at 64. for (int sig = 1; sig < 33; sig++) { if (sigismember(&sigs, sig)) { print_signal(sig); } } } static void print_ignored_signals() { for (int sig = 1; sig < 33; sig++) { struct sigaction act = {}; sigaction(sig, NULL, &act); if (act.sa_handler == SIG_IGN) { print_signal(sig); } } } static void sigtstp_handler(int x) { (void)!write(STDOUT_FILENO, "SIGTSTP\n", strlen("SIGTSTP\n")); kill(getpid(), SIGSTOP); } static void sigcont_handler(int x) { (void)!write(STDOUT_FILENO, "SIGCONT\n", strlen("SIGCONT\n")); } static void print_stop_cont() { signal(SIGTSTP, &sigtstp_handler); signal(SIGCONT, &sigcont_handler); char buff[1]; for (;;) { if (read(STDIN_FILENO, buff, sizeof buff) >= 0) { exit(0); } } } static void sigkill_self() { kill(getpid(), SIGKILL); usleep(20000000); // 20 secs abort(); } static void sigint_self() { kill(getpid(), SIGINT); usleep(20000000); // 20 secs abort(); } static void do_nothing(int x) { } static void stdin_make_nonblocking() { const int fd = STDIN_FILENO; // Catch SIGCONT so pause() wakes us up. signal(SIGCONT, &do_nothing); for (;;) { int flags = fcntl(fd, F_GETFL, 0); fprintf(stdout, "stdin was %sblocking\n", (flags & O_NONBLOCK) ? "non" : ""); if (fcntl(fd, F_SETFL, flags | O_NONBLOCK)) { perror("fcntl"); exit(EXIT_FAILURE); } pause(); } } static void show_help(); /// A thing that fish_test_helper can do. typedef struct { /// The argument to match against. const char *arg; /// Function to invoke. void (*func)(); /// Description of what this does. const char *desc; } fth_command_t; static fth_command_t s_commands[] = { {"abandon_tty", abandon_tty, "Create a new pgroup and transfer tty ownership to it"}, {"become_foreground_then_print_stderr", become_foreground_then_print_stderr, "Claim the terminal (tcsetpgrp) and then print to stderr"}, {"nohup_wait", nohup_wait, "Ignore SIGHUP and just wait"}, {"report_foreground", report_foreground, "Report to stderr whether we own the terminal"}, {"report_foreground_loop", report_foreground_loop, "Continually report to stderr whether we own the terminal"}, {"sigint_parent", sigint_parent, "Wait .25 seconds, then SIGINT the parent process"}, {"print_stdout_stderr", print_stdout_stderr, "Print 'stdout' to stdout and 'stderr' to stderr"}, {"print_pid_then_sleep", print_pid_then_sleep, "Print our pid, then sleep for .5 seconds"}, {"print_pgrp", print_pgrp, "Print our pgroup to stdout"}, {"print_fds", print_fds, "Print the list of active FDs to stdout"}, {"print_blocked_signals", print_blocked_signals, "Print to stdout the name(s) of blocked signals"}, {"print_ignored_signals", print_ignored_signals, "Print to stdout the name(s) of ignored signals"}, {"print_stop_cont", print_stop_cont, "Print when we get SIGTSTP and SIGCONT, exiting on input"}, {"sigint_self", sigint_self, "Send SIGINT to self"}, {"sigkill_self", sigkill_self, "Send SIGKILL to self"}, {"stdin_make_nonblocking", stdin_make_nonblocking, "Print if stdin is blocking and then make it nonblocking"}, {"help", show_help, "Print list of fish_test_helper commands"}, {NULL, NULL, NULL}, }; static void show_help() { printf("fish_test_helper: helper utility for fish\n\n"); printf("Commands\n"); printf("--------\n"); for (int i = 0; s_commands[i].arg; i++) { printf(" %s:\n %s\n\n", s_commands[i].arg, s_commands[i].desc); } } int main(int argc, char *argv[]) { if (argc <= 1) { fprintf(stderr, "No commands given.\n"); return 0; } for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "help") || !strcmp(argv[i], "-h")) { show_help(); return 0; } const fth_command_t *found = NULL; for (int j = 0; s_commands[j].arg; j++) { if (!strcmp(argv[i], s_commands[j].arg)) { found = &s_commands[j]; break; } } if (found) { found->func(); } else { fprintf(stderr, "%s: Unknown command: %s\n", argv[0], argv[i]); return EXIT_FAILURE; } } return 0; }