mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-11-29 05:03:46 +08:00
288 lines
9.1 KiB
C++
288 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 <signal.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;
|
|
}
|