fish-shell/src/fish_test_helper.cpp
ridiculousfish ed51e2baac Prevent hanging when restoring the foreground process group at exit
When fish starts, it notices which pgroup owns the tty, and then it
restores that pgroup's tty ownership when it exits. However if fish does
not own the tty, then (on Mac at least) the tcsetpgrp call triggers a
SIGSTOP and fish will hang while trying to exit.

The first change is to ignore SIGTTOU instead of defaulting it. This
prevents the hang; however it risks re-introducing #7060.

The second change somewhat mitigates the risk of the first: only do the
restore if the initial pgroup is different than fish's pgroup. This
prevents some useless calls which might potentially steal the tty from
another process (e.g. in #7060).
2021-04-05 17:44:14 -07:00

227 lines
7.0 KiB
C++

// fish_test_helper is a little program with no fish dependencies that acts like certain other
// programs, allowing fish to test its behavior.
#include <fcntl.h>
#include <unistd.h>
#include <algorithm>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iterator> // for std::begin/end
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 auto 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(nullptr);
}
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", static_cast<long>(getpid()));
fflush(nullptr);
usleep(1000000 / 2); //.5 secs
}
static void print_pgrp() { fprintf(stdout, "%ld\n", static_cast<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.
if (const char *s = strsignal(sig)) {
fprintf(stderr, "%s", s);
if (strchr(s, ':') == nullptr) {
fprintf(stderr, ": %d", sig);
}
fprintf(stderr, "\n");
}
}
static void print_blocked_signals() {
sigset_t sigs;
sigemptyset(&sigs);
if (sigprocmask(SIG_SETMASK, nullptr, &sigs)) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}
// There is no obviously portable way to get the maximum number of signals.
// Here we limit it to 32 because strsignal on OpenBSD returns "Unknown signal" for anything
// above.
// NetBSD taps out at 63, 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, nullptr, &act);
if (act.sa_handler == SIG_IGN) {
print_signal(sig);
}
}
}
static void print_stop_cont() {
signal(SIGTSTP, [](int) {
auto __attribute__((unused)) _ = write(STDOUT_FILENO, "SIGTSTP\n", strlen("SIGTSTP\n"));
kill(getpid(), SIGSTOP);
});
signal(SIGCONT, [](int) {
auto __attribute__((unused)) _ = write(STDOUT_FILENO, "SIGCONT\n", strlen("SIGCONT\n"));
});
char buff[1];
for (;;) {
if (read(STDIN_FILENO, buff, sizeof buff) >= 0) {
exit(0);
}
}
}
static void show_help();
/// A thing that fish_test_helper can do.
struct fth_command_t {
/// The argument to match against.
const char *arg;
/// Function to invoke.
void (*func)();
/// Description of what this does.
const char *desc;
};
static fth_command_t s_commands[] = {
{"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"},
{"help", show_help, "Print list of fish_test_helper commands"},
};
static void show_help() {
printf("fish_test_helper: helper utility for fish\n\n");
printf("Commands\n");
printf("--------\n");
for (const auto &cmd : s_commands) {
printf(" %s:\n %s\n\n", cmd.arg, cmd.desc);
}
}
int main(int argc, char *argv[]) {
std::sort(std::begin(s_commands), std::end(s_commands),
[](const fth_command_t &lhs, const fth_command_t &rhs) {
return strcmp(lhs.arg, rhs.arg) < 0;
});
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 = nullptr;
for (const auto &cmd : s_commands) {
if (!strcmp(argv[i], cmd.arg)) {
found = &cmd;
break;
}
}
if (found) {
found->func();
} else {
fprintf(stderr, "%s: Unknown command: %s\n", argv[0], argv[i]);
return EXIT_FAILURE;
}
}
return 0;
}