timeout — run a command with a time limit, for WASIX
GNU coreutils' timeout forks the command and signals it. WASIX has no fork
for a plain wasm module, so an earlier version of this package was built on
posix_spawnp. That works for a child that sleeps or reads — and fails for a
child that spins:
$ timeout 2 sh -c 'while :; do :; done'
…never returns; the guest keeps running until the caller gives up
Why a spinning child could not be stopped
posix_spawn creates a process the runtime signals unreliably. Two observations,
both measured on the same runtime:
| child created with | kill(child, TERM) | kill(child, KILL) |
|---|---|---|
posix_spawnp | never delivered to a spinning child; also terminates the caller | never delivered |
fork() + execvp() | child dies, caller survives | child dies, caller survives |
So the fix is the primitive, not the signal: this build forks and execs the
command exactly as GNU does. fork on WASIX is implemented on top of
stack_checkpoint, which is why the module is asyncified — without
instrumentation every run exits 45 before it can start a child.
timeout 2 sh -c 'while :; do :; done' now returns in about 2.1 s, with status
124.
What it does when the child will not die
After the deadline it sends the signal, waits a grace period (1 s, or
--kill-after), escalates to KILL once, waits 250 ms more, and then stops
waiting — it reports the timeout and leaves the process for the runtime to reap.
Waiting forever is never the right answer: the caller asked for a deadline.
Interface
timeout [OPTION] DURATION COMMAND [ARG]...
-k, --kill-after=DURATION wait this long after the first signal before KILL
-s, --signal=SIGNAL signal to send on timeout (default TERM)
--preserve-status exit with the command's status instead of 124
-v, --verbose report each signal sent
--foreground accepted for compatibility
--help, --version
DURATION is a number with an optional s, m, h or d suffix, fractions
included (0.5, 2.5s, 1m).
Exit status: 124 on timeout, 125 if timeout itself fails, 126 if the command
cannot be invoked, 127 if it is not found, otherwise the command's own status.
Verified
| command | result |
|---|---|
timeout 2 sh -c 'while :; do :; done' | 124, ~2.1 s, process gone |
timeout 2 sh -c 'sh -c "while :; do :; done"' | 124, ~2.0 s |
timeout 2 sleep 5 | 124, ~2.0 s |
timeout 5 sleep 1 | 0 |
timeout 5 true | 0 |
timeout 5 prog 42 | 42 |
timeout 1 /nonexistent | 127 |
timeout -k 1 1 spin | 124 after the kill-after grace |
One reporting note: a shell that clamps exit statuses above 79 (some WASIX
shells do) will show 79 where the process really exited 124. The process
status itself is correct — the wasmer CLI reports 124.
Building
./build.sh
One C file, src/timeout.c, 207 KiB after wasm-opt --asyncify --enable-threads --enable-bulk-memory. Usual wasix flags:
-matomics -mbulk-memory -pthread -mthread-model posix -ftls-model=local-exec,
-Wl,--shared-memory -Wl,--import-memory -Wl,--max-memory=4294967296, and the
__heap_base/__stack_pointer/__wasm_init_tls/__tls_* exports.
License: MIT.