fish-shell/src/fish_test_helper.cpp
ridiculousfish 7ee161af8d Fix the flaky tty_ownership test on Mac
The tty_ownership test was sometimes failing. In this test,
`fish_test_helper` creates a child and transfers the tty to it,
"abandoning" the tty. In some cases, the child was running before the
parent; the child claims the tty. When the parent tries to transfer it to
the child, it get SIGTTIN and stops. Fix this by ignoring SIGTTIN and
SIGTTOU.

This only affects macOS and BSDs.
2022-11-28 15:01:12 -08:00

286 lines
9.1 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 <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iterator> // for std::begin/end
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, nullptr, 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 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.
// 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, nullptr, &act);
if (act.sa_handler == SIG_IGN) {
print_signal(sig);
}
}
}
static void print_stop_cont() {
signal(SIGTSTP, [](int) {
// C++ compilers are awful and this is the dance we need to do to silence the "Unused
// result" warning. No, casting to (void) does *not* work. Please leave this.
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 sigkill_self() {
kill(getpid(), SIGKILL);
abort();
}
static void sigint_self() {
kill(getpid(), SIGINT);
abort();
}
static void stdin_make_nonblocking() {
const int fd = STDIN_FILENO;
// Catch SIGCONT so pause() wakes us up.
signal(SIGCONT, [](int) {});
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.
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[] = {
{"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"},
};
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;
}