diff --git a/.gitignore b/.gitignore index 420cca7..6fd430d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.* !.gitignore *.o +*.dSYM/ *~ *.1 *.1.md diff --git a/attach.c b/attach.c index 2de74ac..55f183e 100644 --- a/attach.c +++ b/attach.c @@ -14,7 +14,9 @@ */ static struct termios cur_term; /* 1 if the window size changed */ -static int win_changed; +static volatile sig_atomic_t win_changed; +/* Non-zero if a fatal signal was received; stores the signal number. */ +static volatile sig_atomic_t die_signal; /* Socket creation time, used to compute session age in messages. */ time_t session_start; @@ -27,58 +29,90 @@ char const *clear_csi_data(void) return "\033[999H\r\n"; } -/* Write buf to fd handling partial writes. Exit on failure. */ -void write_buf_or_fail(int fd, const void *buf, size_t count) +/* Exit promptly once the main thread notices a fatal signal. + * If terminal output itself is wedged, skip stdio entirely. */ +static void exit_for_deferred_signal(int can_print) +{ + int sig = die_signal; + char age[32]; + + if (!sig) + return; + if (!can_print) { + tcsetattr(0, TCSANOW, &orig_term); + _exit(1); + } + session_age(age, sizeof(age)); + if (sig == SIGHUP || sig == SIGINT) + printf("%s[%s: session '%s' detached after %s]\r\n", + clear_csi_data(), progname, session_shortname(), age); + else + printf("%s[%s: session '%s' got signal %d - exiting after %s]\r\n", + clear_csi_data(), progname, session_shortname(), sig, age); + exit(1); +} + +/* Write all of buf to fd, retrying on short writes and EINTR. +** Returns 0 on success, -1 on failure (errno is set). */ +static int write_all(int fd, const void *buf, size_t count) { while (count != 0) { ssize_t ret = write(fd, buf, count); - if (ret >= 0) { + if (ret > 0) { buf = (const char *)buf + ret; count -= ret; - } else if (ret < 0 && errno == EINTR) + } else if (ret < 0 && errno == EINTR) { + if (die_signal) + return -1; continue; - else { - if (session_start) { - char age[32]; - session_age(age, sizeof(age)); - printf - ("%s[%s: session '%s' write failed after %s]\r\n", - clear_csi_data(), progname, - session_shortname(), age); - } else { - printf("%s[%s: write failed]\r\n", - clear_csi_data(), progname); - } - exit(1); + } else { + /* ret == 0 (no progress) or ret < 0 (real error) */ + if (ret == 0) + errno = EIO; + return -1; } } + return 0; +} + +/* Write buf to fd handling partial writes. Exit on failure. */ +void write_buf_or_fail(int fd, const void *buf, size_t count) +{ + if (write_all(fd, buf, count) < 0) { + exit_for_deferred_signal(fd != 1); + if (session_start) { + char age[32]; + session_age(age, sizeof(age)); + printf + ("%s[%s: session '%s' write failed after %s]\r\n", + clear_csi_data(), progname, + session_shortname(), age); + } else { + printf("%s[%s: write failed]\r\n", + clear_csi_data(), progname); + } + exit(1); + } } /* Write pkt to fd. Exit on failure. */ void write_packet_or_fail(int fd, const struct packet *pkt) { - while (1) { - ssize_t ret = write(fd, pkt, sizeof(struct packet)); - - if (ret == sizeof(struct packet)) - return; - else if (ret < 0 && errno == EINTR) - continue; - else { - if (session_start) { - char age[32]; - session_age(age, sizeof(age)); - printf - ("%s[%s: session '%s' write failed after %s]\r\n", - clear_csi_data(), progname, - session_shortname(), age); - } else { - printf("%s[%s: write failed]\r\n", - clear_csi_data(), progname); - } - exit(1); + if (write_all(fd, pkt, sizeof(struct packet)) < 0) { + exit_for_deferred_signal(fd != 1); + if (session_start) { + char age[32]; + session_age(age, sizeof(age)); + printf + ("%s[%s: session '%s' write failed after %s]\r\n", + clear_csi_data(), progname, + session_shortname(), age); + } else { + printf("%s[%s: write failed]\r\n", + clear_csi_data(), progname); } + exit(1); } } @@ -149,26 +183,15 @@ void session_age(char *buf, size_t size) format_age(now > session_start ? now - session_start : 0, buf, size); } -/* Signal */ +/* Signal -- only set a flag; all non-trivial work happens in the main loop. */ static RETSIGTYPE die(int sig) { - char age[32]; - session_age(age, sizeof(age)); - /* Print a nice pretty message for some things. */ - if (sig == SIGHUP || sig == SIGINT) - printf("%s[%s: session '%s' detached after %s]\r\n", - clear_csi_data(), progname, session_shortname(), age); - else - printf - ("%s[%s: session '%s' got signal %d - exiting after %s]\r\n", - clear_csi_data(), progname, session_shortname(), sig, age); - exit(1); + die_signal = sig; } -/* Window size change. */ +/* Window size change -- only set a flag. */ static RETSIGTYPE win_change(ATTRIBUTE_UNUSED int sig) { - signal(SIGWINCH, win_change); win_changed = 1; } @@ -344,14 +367,32 @@ int attach_main(int noerror) /* Set a trap to restore the terminal when we die. */ atexit(restore_term); - /* Set some signals. */ - signal(SIGPIPE, SIG_IGN); - signal(SIGXFSZ, SIG_IGN); - signal(SIGHUP, die); - signal(SIGTERM, die); - signal(SIGINT, die); - signal(SIGQUIT, die); - signal(SIGWINCH, win_change); + /* Set some signals using sigaction to avoid SA_RESTART ambiguity. */ + { + struct sigaction sa_ign, sa_die, sa_winch; + + memset(&sa_ign, 0, sizeof(sa_ign)); + sa_ign.sa_handler = SIG_IGN; + sigemptyset(&sa_ign.sa_mask); + sigaction(SIGPIPE, &sa_ign, NULL); + sigaction(SIGXFSZ, &sa_ign, NULL); + + memset(&sa_die, 0, sizeof(sa_die)); + sa_die.sa_handler = die; + sigemptyset(&sa_die.sa_mask); + /* No SA_RESTART: let select() return EINTR so the loop + * notices die_signal promptly. */ + sigaction(SIGHUP, &sa_die, NULL); + sigaction(SIGTERM, &sa_die, NULL); + sigaction(SIGINT, &sa_die, NULL); + sigaction(SIGQUIT, &sa_die, NULL); + + memset(&sa_winch, 0, sizeof(sa_winch)); + sa_winch.sa_handler = win_change; + sigemptyset(&sa_winch.sa_mask); + sa_winch.sa_flags = SA_RESTART; /* benign — don't interrupt I/O */ + sigaction(SIGWINCH, &sa_winch, NULL); + } /* Set raw mode. */ cur_term.c_iflag &= @@ -396,10 +437,16 @@ int attach_main(int noerror) while (1) { int n; + exit_for_deferred_signal(1); + FD_ZERO(&readfds); FD_SET(0, &readfds); FD_SET(s, &readfds); n = select(s + 1, &readfds, NULL, NULL, NULL); + + /* Check for deferred fatal signal. */ + exit_for_deferred_signal(1); + if (n < 0 && errno != EINTR && errno != EAGAIN) { char age[32]; session_age(age, sizeof(age)); @@ -425,6 +472,8 @@ int attach_main(int noerror) } exit(0); } else if (len < 0) { + if (errno == EINTR) + continue; char age[32]; session_age(age, sizeof(age)); printf @@ -445,6 +494,8 @@ int attach_main(int noerror) memset(pkt.u.buf, 0, sizeof(pkt.u.buf)); len = read(0, pkt.u.buf, sizeof(pkt.u.buf)); + if (len < 0 && errno == EINTR) + continue; if (len <= 0) exit(1); @@ -497,11 +548,7 @@ int push_main() } pkt.len = len; - len = write(s, &pkt, sizeof(struct packet)); - if (len != sizeof(struct packet)) { - if (len >= 0) - errno = EPIPE; - + if (write_all(s, &pkt, sizeof(struct packet)) < 0) { printf("%s: %s: %s\n", progname, sockname, strerror(errno)); return 1; @@ -521,9 +568,9 @@ static int send_kill(int sig) memset(&pkt, 0, sizeof(pkt)); pkt.type = MSG_KILL; pkt.len = (unsigned char)sig; - ret = write(s, &pkt, sizeof(pkt)); + ret = write_all(s, &pkt, sizeof(pkt)); close(s); - return (ret == sizeof(pkt)) ? 0 : -1; + return ret; } static int session_gone(void) diff --git a/tests/preload_short_write.c b/tests/preload_short_write.c new file mode 100644 index 0000000..11828d3 --- /dev/null +++ b/tests/preload_short_write.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include + +static int did_inject; + +static ssize_t real_write(int fd, const void *buf, size_t count) +{ + return syscall(SYS_write, fd, buf, count); +} + +static int should_inject(int fd, size_t count) +{ + struct stat st; + + if (did_inject || count <= 1) + return 0; + if (!getenv("ATCH_FAULT_SHORT_WRITE_ONCE")) + return 0; + if (fstat(fd, &st) < 0) + return 0; + return S_ISSOCK(st.st_mode); +} + +static ssize_t short_write_impl(int fd, const void *buf, size_t count) +{ + if (should_inject(fd, count)) { + did_inject = 1; + return real_write(fd, buf, 1); + } + return real_write(fd, buf, count); +} + +#ifdef __APPLE__ +#define DYLD_INTERPOSE(_replacement, _replacee) \ + __attribute__((used)) static struct { \ + const void *replacement; \ + const void *replacee; \ + } _interpose_##_replacee \ + __attribute__((section("__DATA,__interpose"))) = { \ + (const void *)(unsigned long)&_replacement, \ + (const void *)(unsigned long)&_replacee \ + } + +ssize_t interposed_write(int fd, const void *buf, size_t count) +{ + return short_write_impl(fd, buf, count); +} + +DYLD_INTERPOSE(interposed_write, write); +#else +ssize_t write(int fd, const void *buf, size_t count) +{ + return short_write_impl(fd, buf, count); +} +#endif diff --git a/tests/test.sh b/tests/test.sh index 0c954c0..1578859 100644 --- a/tests/test.sh +++ b/tests/test.sh @@ -720,6 +720,100 @@ assert_contains "no args: shows Usage:" "Usage:" "$out" run "$ATCH" --help assert_contains "help: shows tail command" "tail" "$out" +# ── 23. fault injection: short socket writes are retried ─────────────────── +# Force the first packet write to a socket to complete with 1 byte. +# Verifies write_all() retries correctly instead of treating short writes +# as fatal. + +TESTS_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +OS_NAME=$(uname -s) + +FAULT_LIB= +build_short_write_injector() { + [ -n "$FAULT_LIB" ] && return 0 + case "$OS_NAME" in + Darwin) + FAULT_LIB="$TESTDIR/libshortwrite.dylib" + cc -dynamiclib -O2 -Wall -o "$FAULT_LIB" \ + "$TESTS_DIR/preload_short_write.c" >/dev/null 2>&1 ;; + *) + FAULT_LIB="$TESTDIR/libshortwrite.so" + cc -shared -fPIC -O2 -Wall -o "$FAULT_LIB" \ + "$TESTS_DIR/preload_short_write.c" -ldl >/dev/null 2>&1 ;; + esac +} + +with_short_socket_write() { + build_short_write_injector || return 1 + case "$OS_NAME" in + Darwin) + env DYLD_INSERT_LIBRARIES="$FAULT_LIB" \ + DYLD_FORCE_FLAT_NAMESPACE=1 \ + ATCH_FAULT_SHORT_WRITE_ONCE=1 "$@" ;; + *) + env LD_PRELOAD="$FAULT_LIB" \ + ATCH_FAULT_SHORT_WRITE_ONCE=1 "$@" ;; + esac +} + +"$ATCH" start short-push sh -c 'cat' +wait_socket short-push +out=$(printf 'short-write-marker\n' | with_short_socket_write \ + "$ATCH" push short-push 2>&1) +prc=$? +assert_exit "fault: push retries short socket write" 0 "$prc" +sleep 0.2 +assert_contains "fault: push data reaches session after short write" \ + "short-write-marker" "$(cat "$HOME/.cache/atch/short-push.log" 2>/dev/null)" +tidy short-push + +"$ATCH" start short-kill sleep 999 +wait_socket short-kill +out=$(with_short_socket_write "$ATCH" kill short-kill 2>&1) +krc=$? +assert_exit "fault: kill retries short socket write" 0 "$krc" +run "$ATCH" list +assert_not_contains "fault: session is gone after short-write kill" \ + "short-kill" "$out" +"$ATCH" kill -f short-kill >/dev/null 2>&1 || true + +# ── 24. signal safety (forkpty harness) ──────────────────────────────────── +# Builds and runs a C test binary that uses forkpty() to send signals +# to the exact atch attach PID. Skips gracefully if cc is unavailable. + +TESTS_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +SIGNAL_HARNESS="$TESTDIR/test_signal" + +if cc -o "$SIGNAL_HARNESS" "$TESTS_DIR/test_signal.c" -lutil 2>/dev/null; then + "$ATCH" start sig-harness sleep 9999 + wait_socket sig-harness + + sig_out=$("$SIGNAL_HARNESS" "$ATCH" sig-harness 2>&1) + + # Fold harness results into main TAP stream (avoid subshell pipe) + sig_tmpfile="$TESTDIR/sig_out.txt" + echo "$sig_out" > "$sig_tmpfile" + while IFS= read -r line; do + case "$line" in + ok\ *) + desc=$(echo "$line" | sed 's/^ok [0-9]* - //') + ok "signal: $desc" + ;; + not\ ok\ *) + desc=$(echo "$line" | sed 's/^not ok [0-9]* - //') + fail "signal: $desc" + ;; + "#"*) + printf "%s\n" "$line" + ;; + esac + done < "$sig_tmpfile" + + tidy sig-harness +else + ok "signal: SKIP — cc not available, cannot build forkpty harness" +fi + # ── summary ────────────────────────────────────────────────────────────────── printf "\n1..%d\n" "$T" diff --git a/tests/test_signal.c b/tests/test_signal.c new file mode 100644 index 0000000..813e56b --- /dev/null +++ b/tests/test_signal.c @@ -0,0 +1,381 @@ +/* + * test_signal.c — deterministic signal-safety tests for atch attach. + * + * Uses forkpty() to create a real PTY, execs atch attach in the child, + * and sends signals to the exact child PID from the parent. No pkill, + * no script, no heuristics. + * + * Build: cc -o test_signal tests/test_signal.c -lutil + * Usage: ./test_signal + * + * Requires a running atch session named "sig-test-session". + * The wrapper script tests/test_signal.sh handles setup/teardown. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#else +#include +#endif + +static int pass_count = 0; +static int fail_count = 0; +static int test_num = 0; + +static void ok(const char *desc) +{ + test_num++; + pass_count++; + printf("ok %d - %s\n", test_num, desc); +} + +static void fail(const char *desc, const char *detail) +{ + test_num++; + fail_count++; + printf("not ok %d - %s\n", test_num, desc); + if (detail) + printf(" # %s\n", detail); +} + +/* Wait for child to exit within timeout_ms, draining master_fd to prevent + * the child from blocking on PTY writes (e.g. during session log replay). + * Pass master_fd=-1 if the master is already closed. + * Returns exit status or -1 on timeout. */ +static int wait_exit(pid_t pid, int timeout_ms, int master_fd) +{ + int elapsed = 0; + int status; + char drain[4096]; + + while (elapsed < timeout_ms) { + pid_t r = waitpid(pid, &status, WNOHANG); + if (r == pid) { + if (WIFEXITED(status)) + return WEXITSTATUS(status); + if (WIFSIGNALED(status)) + return 128 + WTERMSIG(status); + return -1; + } + /* Drain PTY output to prevent child blocking on write */ + if (master_fd >= 0) { + struct pollfd pfd = { .fd = master_fd, .events = POLLIN }; + if (poll(&pfd, 1, 0) > 0) + (void)read(master_fd, drain, sizeof(drain)); + } + usleep(10000); /* 10ms */ + elapsed += 10; + } + return -1; /* timed out */ +} + +/* Check if pid is still alive. */ +static int is_alive(pid_t pid) +{ + return kill(pid, 0) == 0; +} + +/* Read available data from fd into buf (non-blocking). */ +static ssize_t drain_fd(int fd, char *buf, size_t size, int timeout_ms) +{ + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + ssize_t total = 0; + + while (total < (ssize_t)size - 1) { + int r = poll(&pfd, 1, timeout_ms); + if (r <= 0) + break; + ssize_t n = read(fd, buf + total, size - 1 - total); + if (n <= 0) + break; + total += n; + timeout_ms = 50; /* short timeout for subsequent reads */ + } + buf[total] = '\0'; + return total; +} + +/* + * Fork a child that execs atch attach . + * Returns child PID, sets *master_fd to the PTY master. + */ +static pid_t spawn_attach(const char *atch_bin, const char *session, + int *master_fd) +{ + int master; + pid_t pid = forkpty(&master, NULL, NULL, NULL); + + if (pid < 0) { + perror("forkpty"); + exit(1); + } + + if (pid == 0) { + /* child — exec atch attach */ + execl(atch_bin, atch_bin, "attach", session, (char *)NULL); + perror("execl"); + _exit(127); + } + + /* parent */ + *master_fd = master; + + /* Make master non-blocking for drain_fd */ + int flags = fcntl(master, F_GETFL); + if (flags >= 0) + fcntl(master, F_SETFL, flags | O_NONBLOCK); + + return pid; +} + +/* + * Test: SIGWINCH does not kill the attach process. + * After receiving SIGWINCH, the child must still be alive. + */ +static void test_sigwinch_survives(const char *atch_bin, const char *session) +{ + int master; + pid_t pid = spawn_attach(atch_bin, session, &master); + + /* Let attach settle */ + usleep(300000); + + if (!is_alive(pid)) { + fail("sigwinch: child alive before signal", "child died during attach"); + close(master); + return; + } + + /* Send SIGWINCH */ + kill(pid, SIGWINCH); + usleep(200000); + + if (is_alive(pid)) + ok("sigwinch: child survives SIGWINCH"); + else + fail("sigwinch: child survives SIGWINCH", "child died after SIGWINCH"); + + /* Send burst of SIGWINCH */ + for (int i = 0; i < 10; i++) { + kill(pid, SIGWINCH); + usleep(10000); + } + usleep(200000); + + if (is_alive(pid)) + ok("sigwinch: child survives SIGWINCH burst (10x)"); + else + fail("sigwinch: child survives SIGWINCH burst", "child died during burst"); + + /* Clean up: send SIGTERM to exit */ + kill(pid, SIGTERM); + wait_exit(pid, 2000, master); + close(master); +} + +/* + * Test: SIGTERM causes prompt exit (no deadlock). + */ +static void test_sigterm_exits(const char *atch_bin, const char *session) +{ + int master; + pid_t pid = spawn_attach(atch_bin, session, &master); + + usleep(300000); + + if (!is_alive(pid)) { + fail("sigterm: child alive before signal", "child died during attach"); + close(master); + return; + } + + kill(pid, SIGTERM); + + /* Wait with PTY master still open — child must exit from the signal + * handler, not from EOF on the terminal. Parent drains the PTY to + * prevent the child blocking on replay writes. */ + int status = wait_exit(pid, 3000, master); + close(master); + + if (status == -1) { + fail("sigterm: child exits within 3s (master still open)", + "child hung (possible deadlock)"); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + } else { + ok("sigterm: child exits promptly after SIGTERM"); + } + + if (!is_alive(pid)) + ok("sigterm: child is dead after SIGTERM"); + else + fail("sigterm: child is dead after SIGTERM", "child still alive"); +} + +/* + * Test: SIGHUP causes prompt exit (simulates SSH disconnect). + */ +static void test_sighup_exits(const char *atch_bin, const char *session) +{ + int master; + pid_t pid = spawn_attach(atch_bin, session, &master); + + usleep(300000); + + if (!is_alive(pid)) { + fail("sighup: child alive before signal", "child died during attach"); + close(master); + return; + } + + kill(pid, SIGHUP); + + /* Wait with PTY master still open — child must exit from the signal + * handler, not from EOF on the terminal. Parent drains the PTY to + * prevent the child blocking on replay writes. */ + int status = wait_exit(pid, 3000, master); + close(master); + + if (status == -1) { + fail("sighup: child exits within 3s (master still open)", + "child hung (possible deadlock)"); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + } else { + ok("sighup: child exits promptly after SIGHUP"); + } + + if (!is_alive(pid)) + ok("sighup: child is dead after SIGHUP"); + else + fail("sighup: child is dead after SIGHUP", "child still alive"); +} + +/* + * Test: SIGTERM still exits promptly when attach is blocked writing to + * its own PTY. The parent intentionally does not drain the PTY master. + */ +static void test_sigterm_exits_while_stdout_blocked(const char *atch_bin, + const char *session) +{ + int master; + pid_t pid = spawn_attach(atch_bin, session, &master); + + /* Give the noisy session time to fill the PTY and block the child. */ + usleep(1000000); + + if (!is_alive(pid)) { + fail("sigterm: child alive before blocked-write signal", + "child died during noisy attach"); + close(master); + return; + } + + kill(pid, SIGTERM); + + /* Keep the PTY master open but undrained. If attach retries EINTR + * forever in write_all(), this wait will time out. */ + int status = wait_exit(pid, 3000, -1); + close(master); + + if (status == -1) { + fail("sigterm: child exits within 3s while stdout blocked", + "child hung in blocked write"); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + } else { + ok("sigterm: child exits promptly while stdout blocked"); + } + + if (!is_alive(pid)) + ok("sigterm: child is dead after blocked-write SIGTERM"); + else + fail("sigterm: child is dead after blocked-write SIGTERM", + "child still alive"); +} + +/* + * Test: detach character (^\, 0x1c) causes clean detach. + * The child should exit, and the session should still be running. + */ +static void test_detach_char(const char *atch_bin, const char *session) +{ + int master; + pid_t pid = spawn_attach(atch_bin, session, &master); + + usleep(300000); + + if (!is_alive(pid)) { + fail("detach: child alive before detach char", "child died during attach"); + close(master); + return; + } + + /* Send detach character: ^\ (0x1c) */ + char detach = 0x1c; + if (write(master, &detach, 1) < 0) { + fail("detach: write detach char to PTY", strerror(errno)); + close(master); + return; + } + + /* Give atch time to process the detach and exit */ + usleep(500000); + + /* Read any output from PTY before closing */ + char buf[4096]; + drain_fd(master, buf, sizeof(buf), 200); + close(master); + + int status = wait_exit(pid, 3000, -1); + if (status == -1) { + fail("detach: child exits after detach char", "child hung"); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + } else { + ok("detach: child exits after detach char"); + } + + if (!is_alive(pid)) + ok("detach: child is dead after detach"); + else + fail("detach: child is dead after detach", "child still alive"); +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, + "Usage: %s [session-name] [noisy-session]\n", + argv[0]); + return 1; + } + + const char *atch_bin = argv[1]; + const char *session = argc > 2 ? argv[2] : "sig-test-session"; + const char *noisy_session = argc > 3 ? argv[3] : "sig-noisy-session"; + + printf("TAP version 13\n"); + + test_sigwinch_survives(atch_bin, session); + test_sigterm_exits(atch_bin, session); + test_sighup_exits(atch_bin, session); + test_sigterm_exits_while_stdout_blocked(atch_bin, noisy_session); + test_detach_char(atch_bin, session); + + printf("\n1..%d\n", test_num); + printf("# %d passed, %d failed\n", pass_count, fail_count); + + return fail_count > 0 ? 1 : 0; +} diff --git a/tests/test_signal.sh b/tests/test_signal.sh new file mode 100755 index 0000000..3df1e15 --- /dev/null +++ b/tests/test_signal.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# Signal-safety integration tests for atch attach. +# Uses a forkpty()-based C harness for exact PID targeting. +# +# Usage: sh tests/test_signal.sh +# Builds the test harness automatically if needed. + +ATCH="${1:-./atch}" +TESTS_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +TESTDIR=$(mktemp -d) +export HOME="$TESTDIR" +SESSION="sig-test-session" +NOISY_SESSION="sig-noisy-session" + +trap '"$ATCH" kill "$NOISY_SESSION" >/dev/null 2>&1 || true; "$ATCH" kill "$SESSION" >/dev/null 2>&1 || true; rm -rf "$TESTDIR"' EXIT + +# Build the harness (graceful skip if cc fails) +HARNESS="$TESTDIR/test_signal" +if ! cc -o "$HARNESS" "$TESTS_DIR/test_signal.c" -lutil 2>/dev/null; then + echo "1..0 # SKIP cannot build forkpty harness" + exit 0 +fi + +# Start a background session for the tests to attach to +"$ATCH" start "$SESSION" sleep 9999 || { + echo "1..0 # SKIP failed to start test session" + exit 0 +} + +"$ATCH" start "$NOISY_SESSION" sh -c 'yes X' || { + echo "1..0 # SKIP failed to start noisy test session" + exit 0 +} + +# Wait for socket +i=0 +while [ $i -lt 20 ]; do + [ -S "$HOME/.cache/atch/$SESSION" ] && break + sleep 0.05 + i=$((i + 1)) +done + +if [ ! -S "$HOME/.cache/atch/$SESSION" ] || + [ ! -S "$HOME/.cache/atch/$NOISY_SESSION" ]; then + echo "1..0 # SKIP session socket did not appear" + exit 0 +fi + +# Run the harness +"$HARNESS" "$ATCH" "$SESSION" "$NOISY_SESSION"