Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

os.process

import os

Everything here is re-exported by os, so import os is enough and the names are called as os.*. Importing os.process on its own works too and reaches the same definitions.

Process identity, subprocess execution, and signal handling.

Functions

exec()

os.exec(...args: list) -> string

Executes the given shell (or command prompt for Windows) commands and returns a dictionary containing the exit code and the output as string.

exec() accepts multiple string arguments and concatenates them together with a space in between before executing.

The returned dictionary contains the following:

  • exit_code: number - The exit code of the program. Any number other than zero (0) should be treated as a failure. - output: string - The output of the program.

exec() is the simple, blocking one-shot convenience: run a command, wait for it, get back its exit code and combined output. spawn() is for everything exec() doesn’t cover: streaming a child’s stdout/stderr as it’s produced, writing to its stdin, overriding its working directory or environment, or checking on it without blocking.

Example,

%> os.exec('ls', '-l')
{exit_code: 1, output: 'total 48
-rw-r--r--@ 1 username  staff  705 Aug 27  2021 buggy.zu
-rw-r--r--  1 username  staff  197 Mar  5 05:13 myprogram.zu'}

Parameters

  • cmd (...string)

Returns string

Note: this blocks until the command finishes and buffers its entire output in memory; for a long-running command, one you need to feed input to, or one whose output you want to process as it arrives, use spawn() instead.

exit()

os.exit(code: number)

Exit the current process and quits the Zuri runtime.

Parameters

  • code (number)

Returns

set_exit_code()

os.set_exit_code(code: number)

Records the status the process should end with, without ending it.

Unlike exit(), the program carries on: the rest of the script, every at_exit() handler, and an uncaught error’s report all still get their turn, and the status is applied once they are done.

import os

os.at_exit(@{
  echo 'one check did not pass'
  os.set_exit_code(1)
})

echo 'working'
working
one check did not pass

Parameters

  • code (number)

Note: an uncaught error still ends the program with 1, whatever was recorded here.

Note: the last call wins.

at_exit()

os.at_exit(callback: function)

Registers callback to run when the program ends.

Handlers run last registered first, whether the program reached the end of its script, called exit(), or stopped on an uncaught error. Registering several is fine, and a handler may register another one which will also run.

import os

var handle = file('report.txt', 'w')

os.at_exit(@{
  handle.close()
  echo 'report written'
})

handle.write('done')
echo 'working'
working
report written

Parameters

  • callback (function)

Note: a handler that raises is reported on standard error and the remaining handlers still run, so one failed cleanup cannot cancel the others.

Note: exit() called from inside a handler exits immediately with that code; the handlers that have not run yet do not run.

Note: a handler does not run when the process is killed by a signal nothing handled, since nothing gets to run at that point.

pid()

os.pid() -> int

The current process’s id.

Returns int

ppid()

os.ppid() -> int

The current process’s parent’s id.

Returns int

Note: Windows has no call that asks for this directly. It is answered there by searching a snapshot of the running processes for this one and reading the parent recorded against it, which yields 0 rather than raising if the snapshot cannot be taken or this process is not in it.

kill()

os.kill(pid: int, signal: ?int) -> bool

Sends signal to the process identified by pid.

Signal 0 sends nothing. It runs the same existence and permission checks every other signal does and then stops, which makes it the way to ask whether a process is still running and still yours to signal:

var alive = true

catch {
  os.kill(os.pid(), 0)
} as e {
  alive = false
}

echo alive  # true

Parameters

  • pid (int)
  • signal (?int) — Default 15 (SIGTERM), matching the kill(1) command’s own default.

Returns bool

Raises Error if pid doesn’t name a process this one has permission to signal, or if signal is one this platform cannot carry out.

Note: Windows has no way to signal a process by id, so it accepts only the numbers it can carry out exactly: 0 to test for the process, and 9 or 15 to end it. Every other number raises there rather than ending the process anyway, because ending a process outright is a different outcome from asking it to stop, not a near-enough one. A process ended this way never runs its own on_signal() callback and gets no chance to clean up. Its exit status is 128 + signal, the same number a Unix caller would have read for it.

on_signal()

os.on_signal(name: string, callback: function) -> bool

Registers callback to run when this process receives the named signal, for handling things like Ctrl+C gracefully instead of being torn down immediately.

callback takes no arguments. It runs on the main thread, between bytecode instructions: never from a background thread: so it’s safe for it to do anything ordinary Zuri code can do, including raising, which propagates the same way an uncaught error from any other code would.

What the callback returns decides what happens next

Returning a truthy value means the callback has taken responsibility for the signal, and the program carries on:

os.on_signal('INT', def {
  echo 'ignoring Ctrl+C, still working'
  return true
})

Returning anything falsy means it hasn’t, and the signal then gets the default action it would have had if nothing were registered: for 'INT', 'TERM' and 'HUP', terminating the process with the conventional 128 + signal number status. A callback that just cleans up and falls off the end returns nil, which is falsy, so it shuts down without needing to call exit():

os.on_signal('INT', def {
  echo 'shutting down...'
  flush_caches()
})

That default is deliberate. A handler that only logged and always kept running would leave Ctrl+C unable to stop the program at all, which is a much worse thing to do by accident than to shut down.

Truthiness here is the language’s own, so return 0 and return '' decline exactly as return false does. Call os.exit() when you want a specific exit status instead.

Supported signal names are 'INT' (Ctrl+C), 'TERM', 'HUP' (Unix only), and 'BREAK' (Windows only); names are matched case-insensitively, and carry no SIG prefix. Registering a new callback for a name that already has one replaces it: there’s no stacking of multiple handlers for the same signal.

On Windows

Windows has no signals. A callback registered here becomes a console control handler, so it runs for console events and nothing else: 'INT' for Ctrl+C, 'BREAK' for Ctrl+Break, and 'TERM' for the console window being closed. Nothing another process does with kill() ever reaches it, including this process calling kill() on its own id.

'TERM' is weaker there than it looks. The console is closing either way, so Windows allows the callback a few seconds and then ends the process regardless of what it returned: a truthy return cannot keep the program running, the way it can for a real SIGTERM. Treat 'TERM' on Windows as a short window to flush what matters, not as a signal you can decline.

Parameters

  • name (string)
  • callback (function)

Returns bool

Raises Error if name isn’t a recognized signal name, or isn’t supported on the current platform.

Note: only takes effect for the main thread’s own execution; code running inside an isolate worker isn’t affected by a handler registered here.

Note: a signal delivered during a blocking native call (sleep(), a blocking Process.wait(), blocking file/network I/O, …) isn’t seen until that call returns and bytecode execution resumes, the same limitation every cooperative signal-handling system has.

spawn()

os.spawn(cmd: string, args: ?list, options: ?dict) -> Process

Spawns cmd as a new subprocess and returns a Process handle to it immediately, without waiting for it to finish.

cmd names a program to run, not a command line for a shell to interpret. Nothing here expands a variable, splits on whitespace, follows a pipe or knows what a shell builtin is, so a name only a shell would recognise raises rather than running: echo is a builtin on Windows with no executable behind it, and cat is not present there at all. Use exec() when a shell is what you want.

options may include:

  • cwd: ?string: the child’s working directory. Defaults to this process’s own current working directory.
  • env: ?dict: extra environment variables for the child, merged into a copy of this process’s own environment by default (set env_replace: true for the child to receive only what’s in env, nothing inherited).
  • env_replace: ?bool: see env above. Default false.
  • stdin, stdout, stderr: ?string, one of 'pipe' (readable/writable through the returned Process), 'inherit' (shares this process’s own stream), or 'null' (discarded). stdin defaults to 'inherit'; stdout and stderr default to 'pipe'. Do not confuse 'null' with nil. 'null' alludes to /dev/null on unix devices which has the same behavior.

Example,

%> var p = os.spawn('grep', ['zuri'], { stdin: 'pipe' })
%> p.write_stdin('hello zuri\nhello world\n')
true
%> p.close_stdin()
%> p.read_stdout().to_string()
hello zuri

%> p.wait()
0

Parameters

  • cmd (string)
  • args (?list) — The command’s own arguments (not shell- interpreted the way exec()’s combined string is: each element is passed to the child as one literal argument).
  • options (?dict)

Returns Process

Raises Error if cmd couldn’t be spawned at all (not found, no permission, …).

Classes

Process

class os.Process

A running (or finished) child process created by spawn().

Note: constructed only by spawn() itself: there’s no meaningful way to build one by hand since there’s no “spawn it later” step to defer.

Constructor

os.Process(_ptr)

Process.write_stdin()

os.Process.write_stdin(data) -> bool

Writes data to the child’s stdin.

Parameters

  • data (string|bytes)

Returns bool

Raises Error if this process’s stdin wasn’t opened with stdin: 'pipe', or has already been closed via close_stdin().

Process.close_stdin()

os.Process.close_stdin()

Closes the write half of this process’s stdin, so the child sees EOF on it. Needed for any child that reads its own stdin until EOF: the pipe only actually closes once this is called (or the Process itself is garbage collected).

Returns

Process.read_stdout()

os.Process.read_stdout(length: ?int) -> bytes

Reads from the child’s stdout. While the child runs, this blocks until at least one byte has been produced or its stdout has hit EOF, and returns what has arrived so far. Once wait() or try_wait() has seen the child exit, it returns everything the child wrote that has not been read yet, waiting for its stdout to hit EOF first.

Parameters

  • length (?int) — The maximum number of bytes to return; all available output if not given.

Returns bytes

Raises Error if this process’s stdout wasn’t opened with stdout: 'pipe'.

Process.read_stderr()

os.Process.read_stderr(length: ?int) -> bytes

Same as read_stdout(), for the child’s stderr instead.

Parameters

  • length (?int)

Returns bytes

Raises Error if this process’s stderr wasn’t opened with stderr: 'pipe'.

Process.wait()

os.Process.wait(timeout_ms: ?int)

Blocks until the child exits (or timeout_ms elapses, whichever comes first) and returns its exit code.

Parameters

  • timeout_ms (?int) — Wait indefinitely if not given.

Returns — ?int: The exit code, or nil if timeout_ms elapsed before the child exited.

Process.try_wait()

os.Process.try_wait()

Checks whether the child has exited yet, without blocking.

Returns — ?int: The exit code if it has already exited, nil if it’s still running.

Process.kill()

os.Process.kill(signal: ?int) -> bool

Sends signal to this child process.

Parameters

  • signal (?int) — Default 15 (SIGTERM).

Returns bool

Raises Error if signal is one this platform cannot carry out.

Note: Windows accepts only the numbers it can carry out exactly, the same three the free kill() function takes there: 0, 9 and 15. Anything else raises. Signal 0 is a no-op on a child, since holding this handle is already proof the process exists.

Process.pid()

os.Process.pid() -> int

This child process’s id.

Returns int


2021, Richard Ore and Zuri contributors