os.process
import os
Everything here is re-exported by
os, soimport osis enough and the names are called asos.*. Importingos.processon 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
0rather 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) — Default15(SIGTERM), matching thekill(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:
0to test for the process, and9or15to 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 ownon_signal()callback and gets no chance to clean up. Its exit status is128 + 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
isolateworker isn’t affected by a handler registered here.
Note: a signal delivered during a blocking native call (
sleep(), a blockingProcess.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 (setenv_replace: truefor the child to receive only what’s inenv, nothing inherited).env_replace:?bool: seeenvabove. Defaultfalse.stdin,stdout,stderr:?string, one of'pipe'(readable/writable through the returnedProcess),'inherit'(shares this process’s own stream), or'null'(discarded).stdindefaults to'inherit';stdoutandstderrdefault to'pipe'. Do not confuse'null'withnil.'null'alludes to/dev/nullon 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 wayexec()’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) — Default15(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,9and15. Anything else raises. Signal0is 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