Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/pipe.toit
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ PROCESS-EXIT-CODE-MASK_ ::= 0xff
PROCESS-SIGNAL-SHIFT_ ::= 10
PROCESS-SIGNAL-MASK_ ::= 0xff

SIGNAL-KILL_ ::= 9
SIGNAL-TERMINATE_ ::= 15

READ-EVENT_ ::= 1 << 0
WRITE-EVENT_ ::= 1 << 1
CLOSE-EVENT_ ::= 1 << 2
Expand Down Expand Up @@ -282,6 +285,64 @@ class Process:
*/
stderr -> Stream?: return fork-data_[2]

/**
Requests that the child process terminate.

On POSIX systems the default is a catchable termination request. If $hard is
true, a signal that cannot be caught is sent instead.

Windows has no equivalent catchable termination request for arbitrary child
processes, so it always terminates the process immediately there.
*/
kill --hard/bool=false -> none:
signal-value := hard ? SIGNAL-KILL_ : SIGNAL-TERMINATE_
if sdk-system.platform == sdk-system.PLATFORM-WINDOWS:
signal-value = SIGNAL-KILL_
signal signal-value

/**
Requests that the child process terminate and waits for it to exit.

On POSIX systems the initial request is catchable. If $hard-after-ms is
provided and the process has not exited after that many milliseconds, a
signal that cannot be caught is sent. A value of 0 sends only the hard
termination signal. If $hard-after-ms is null, the call waits indefinitely
for the process to exit voluntarily.

Windows has no equivalent catchable termination request for arbitrary child
processes, so it always terminates the process immediately before waiting.
*/
kill --wait/True --hard-after-ms/int?=null -> none:
if hard-after-ms and hard-after-ms < 0: throw "OUT_OF_RANGE"

if hard-after-ms == 0 or sdk-system.platform == sdk-system.PLATFORM-WINDOWS:
kill --hard
this.wait
return

kill
if not hard-after-ms:
this.wait
return

escalation-deadline := Time.monotonic-us + hard-after-ms * 1_000
outer-deadline := Task.current.deadline
escalation-is-first := not outer-deadline or escalation-deadline <= outer-deadline
error := catch --unwind=(: it != DEADLINE-EXCEEDED-ERROR or not escalation-is-first):
with-timeout --ms=hard-after-ms: this.wait
if error:
kill --hard
this.wait

/**
Sends the platform-specific signal $value to the child process.

POSIX systems accept their native signal numbers. Windows only supports
signal 9, which terminates the process immediately.
*/
signal value/int -> none:
kill_ pid value

/**
Waits for the process to finish and returns the raw exit value.

Expand Down
92 changes: 92 additions & 0 deletions tests/process_kill_test.toit
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (C) 2026 Toitware ApS.
// Use of this source code is governed by a Zero-Clause BSD license that can
// be found in the tests/TESTS_LICENSE file.

import expect show *
import host.pipe
import system show platform PLATFORM-FREERTOS PLATFORM-WINDOWS

SIGKILL ::= 9
SIGTERM ::= 15

main:
// FreeRTOS cannot launch host subprocesses.
if platform == PLATFORM-FREERTOS: return

test-kill
test-hard-kill
test-kill-and-wait
test-invalid-grace-period
test-signal
if platform != PLATFORM-WINDOWS:
test-ignored-signal
test-handled-signal
test-outer-timeout

test-kill:
process := pipe.fork --create-stdin "cat" ["cat"]
process.kill
exit-value := with-timeout --ms=1_000: process.wait
expected-signal := platform == PLATFORM-WINDOWS ? SIGKILL : SIGTERM
expect-equals expected-signal (pipe.exit-signal exit-value)

test-hard-kill:
process := pipe.fork --create-stdin "cat" ["cat"]
process.kill --hard
exit-value := with-timeout --ms=1_000: process.wait
expect-equals SIGKILL (pipe.exit-signal exit-value)

test-kill-and-wait:
process := pipe.fork --create-stdin "cat" ["cat"]
process.kill --wait
expected-signal := platform == PLATFORM-WINDOWS ? SIGKILL : SIGTERM
expect-equals expected-signal process.exit-signal

test-invalid-grace-period:
process := pipe.fork --create-stdin "cat" ["cat"]
expect-throw "OUT_OF_RANGE": process.kill --wait --hard-after-ms=-1
process.kill --hard
process.wait

test-signal:
process := pipe.fork --create-stdin "cat" ["cat"]
process.signal SIGKILL
exit-value := with-timeout --ms=1_000: process.wait
expect-equals SIGKILL (pipe.exit-signal exit-value)

test-ignored-signal:
// Wait for the shell to install the handler before sending the signal.
process := pipe.fork
--create-stdout
"sh"
["sh", "-c", "trap '' TERM; echo ready; exec sleep 60"]
process.stdout.in.read

process.kill --wait --hard-after-ms=20
expect-equals SIGKILL process.exit-signal

test-handled-signal:
// A process that handles SIGTERM must not receive the scheduled SIGKILL.
process := pipe.fork
--create-stdout
"sh"
["sh", "-c", "trap 'exit 42' TERM; echo ready; while :; do :; done"]
process.stdout.in.read

process.kill --wait --hard-after-ms=100
expect-equals 42 process.exit-code

test-outer-timeout:
// An outer deadline must abort the wait without triggering early escalation.
process := pipe.fork
--create-stdout
"sh"
["sh", "-c", "trap '' TERM; echo ready; exec sleep 60"]
process.stdout.in.read

expect-throw DEADLINE-EXCEEDED-ERROR:
with-timeout --ms=5:
process.kill --wait --hard-after-ms=50

process.kill --hard
process.wait