timeout package icon
icedcoffee/timeout
Public
wasmer run icedcoffee/timeout

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 withkill(child, TERM)kill(child, KILL)
posix_spawnpnever delivered to a spinning child; also terminates the callernever delivered
fork() + execvp()child dies, caller surviveschild 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

commandresult
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 5124, ~2.0 s
timeout 5 sleep 10
timeout 5 true0
timeout 5 prog 4242
timeout 1 /nonexistent127
timeout -k 1 1 spin124 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.

GNU-compatible timeout(1) for WASIX - stops a CPU-bound loop, not just a sleeping child


Github