gate DIAGNOSTIC (remove with #359): sigwho.py names the sender of the SIGTERM
The gate step runs make test under a Python wrapper that blocks SIGTERM, SIGINT and SIGHUP, waits for one with sigwaitinfo, prints the sender's pid, uid and command line (if visible in this pid namespace) and the process table, forwards the signal to make, waits, and dies by the same signal so the runner reports what it reported before. Thirteen runs have died by SIGTERM 35 s after the root shards start; nothing in the tree or the runner sends it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+57
−1
.amadan/ci.d/10-gate+2 −1.amadan/sigwho.py+55 −0
CI failed — run details
CI log
=== gate === sigwho: pid 4 ppid 2 pgid 0 sid 0; child pid 16 .amadan/test-gate.sh gate: 2026-10-24T02:13:20Z on aarch64, 32 cpus, 62014 MB available, go1.26.8 linux/arm64 gate: +0s start launchctx gate: +0s start vet gate: +0s start gofmt gate: +1s start rest gate: +24s start internal_console@0 gate: +24s start internal_console@1 gate: +24s start internal_console@2 gate: +24s start internal_console@3 gate: +24s start internal_console@4 gate: +24s start internal_console@5 gate: +24s start internal_console@6 gate: +24s start internal_console@7 gate: +24s waiting for internal_console's shards before the next split package gate: +187s start root@0 gate: +187s start root@1 gate: +187s start root@2 gate: +187s waiting for root's shards before the next split package amadan: step gate failed: signal: terminated amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/.amadan/ci.d/10-gate b/.amadan/ci.d/10-gate| index daf132329..b6963187c 100755 |
| --- a/.amadan/ci.d/10-gate |
| +++ b/.amadan/ci.d/10-gate |
| @@ -51,6 +51,7 @@ set -eu |
| go build ./... & |
| build=$! |
| status=0 |
| -make test || status=$? |
| +# DIAGNOSTIC (remove with #359): sigwho.py reports who sends the SIGTERM. |
| +python3 .amadan/sigwho.py make test || status=$? |
| wait "$build" || status=1 |
| exit "$status" |
diff --git a/.amadan/sigwho.py b/.amadan/sigwho.py| new file mode 100644 |
| index 000000000..bc76b81e3 |
| --- /dev/null |
| +++ b/.amadan/sigwho.py |
| @@ -0,0 +1,55 @@ |
| +#!/usr/bin/env python3 |
| +"""Diagnostic wrapper (remove when #359 is answered): runs its arguments as a |
| +child and, when SIGTERM/SIGINT/SIGHUP arrives at THIS process, prints who sent |
| +it (si_pid, si_uid, the sender's cmdline if it is visible in this pid |
| +namespace) and the process table, forwards the signal to the child, waits, |
| +then dies by the same signal so the runner sees what it saw before.""" |
| +import os, signal, subprocess, sys, time |
| + |
| +SIGS = {signal.SIGTERM, signal.SIGINT, signal.SIGHUP} |
| + |
| +def rd(p): |
| + try: |
| + with open(p, 'rb') as f: |
| + return f.read().replace(b'\0', b' ').decode('utf-8', 'replace').strip() |
| + except OSError as e: |
| + return f'<{e.__class__.__name__}>' |
| + |
| +def main(): |
| + signal.pthread_sigmask(signal.SIG_BLOCK, SIGS) |
| + child = subprocess.Popen(sys.argv[1:], preexec_fn=lambda: signal.pthread_sigmask(signal.SIG_UNBLOCK, SIGS)) |
| + print(f"sigwho: pid {os.getpid()} ppid {os.getppid()} pgid {os.getpgid(0)} sid {os.getsid(0)}; child pid {child.pid}", flush=True) |
| + t0 = time.time() |
| + while True: |
| + info = signal.sigtimedwait(SIGS, 1.0) |
| + if info is not None: |
| + sig = info.si_signo |
| + print(f"sigwho: +{time.time()-t0:.1f}s received signal {sig} from pid {info.si_pid} uid {info.si_uid} code {info.si_code}", flush=True) |
| + print(f"sigwho: sender cmdline: {rd(f'/proc/{info.si_pid}/cmdline')} | comm: {rd(f'/proc/{info.si_pid}/comm')} | status ppid: {[l for l in rd(f'/proc/{info.si_pid}/status').splitlines() if l.startswith('PPid')]}", flush=True) |
| + try: |
| + ps = subprocess.run(['ps', '-eo', 'pid,ppid,pgid,sid,etimes,comm,args', '--sort=pid'], capture_output=True, text=True, timeout=5).stdout |
| + lines = ps.splitlines() |
| + print("sigwho: process table (" + str(len(lines)-1) + " processes):", flush=True) |
| + for l in lines[:120]: |
| + print(" " + l[:200], flush=True) |
| + except Exception as e: |
| + print(f"sigwho: ps failed: {e}", flush=True) |
| + try: |
| + child.send_signal(sig) |
| + except ProcessLookupError: |
| + pass |
| + try: |
| + child.wait(timeout=20) |
| + except subprocess.TimeoutExpired: |
| + child.kill() |
| + signal.pthread_sigmask(signal.SIG_UNBLOCK, SIGS) |
| + signal.signal(sig, signal.SIG_DFL) |
| + os.kill(os.getpid(), sig) |
| + time.sleep(1) |
| + sys.exit(128 + sig) |
| + rc = child.poll() |
| + if rc is not None: |
| + sys.exit(rc if rc >= 0 else 128 - rc) |
| + |
| +if __name__ == '__main__': |
| + main() |