Processes
gnoblin.process.spawn(argv, options?) starts a program from an event handler or an operation callback and returns a
Process. Use it to start an application, run a helper and read its output, or ask a prompt program for an answer.
gnoblin.on("gnoblin.shortcut.activated", function(event)
local date = gnoblin.process.spawn({"date", "+%H:%M"}, {stdout = "capture"})
date:wait():on_complete(function(exit, err)
if err then
print("could not start date:", err.message)
elseif exit.exit_code == 0 then
print("time:", exit.stdout)
end
end)
end)argv holds 1 to 64 strings, at most 64 KiB in total. The first string is the program, found on PATH. The call
raises an error while the config file loads, and the process cannot start while the session is locked.
Options
| Option | Accepted values | Default | Meaning |
|---|---|---|---|
cwd |
absolute path | inherited | Working directory. |
env |
table of NAME = "value" strings |
none | Variables added to the session environment, at most 128. |
stdin |
string, at most 1 MiB | empty | Bytes written to standard input, which is then closed. |
stdout |
"inherit", "discard", "capture" |
"inherit" |
Where standard output goes. |
stderr |
"inherit", "discard", "capture" |
"inherit" |
Where standard error goes. |
output_limit |
integer from 0 to 4194304 | 65536 |
Bytes kept from each captured stream. |
timeout |
seconds, more than 0 up to 86400 | none | Time before Gnoblin ends the process. |
For stdout and stderr:
"inherit"writes to Gnoblin's own output, which the session log records."discard"drops the output."capture"keeps the output and returns it fromwait()as raw bytes, which need not be UTF-8. Output pastoutput_limitis read and dropped, so the program never blocks on a full pipe, and the matching_truncatedfield istrue.
When timeout passes, Gnoblin sends SIGTERM to the process group and SIGKILL two seconds later.
The Process object
| Field | Value |
|---|---|
id |
Identifier for this process in the session. |
alive |
True from spawn until the process has exited or failed to start. |
argv |
The command line it was started with. |
pid |
The process ID once it has started, otherwise nil. |
status |
"starting", "running", "exited", or "failed" when it could not start. |
exit_code |
The exit status after a normal exit. |
signal |
The signal that ended it, such as "term" or "kill", after it was killed. |
The fields are read-only and change as the process runs.
A Process is a live object, like a window: one handle for one
id, whose fields change as the process runs.
process:on(signal, fn)returns a subscription.onfails once the process has ended, and listening ends after"closed". The signals are:"closed"callsfn(process)once the process has ended, however it ended: it exited, was killed, or could not be started. It is the same signal as on windows, workspaces and outputs, so code can treat every object alike."changed"callsfn(process, changed)with the list of changed fields.
process:watch(field, fn)callsfn(process, value, old)whenpid,status,exit_codeorsignalchanges.id,argvandalivenever change, sowatchrefuses them.process.revisioncounts the changes of those fields, starting at 1.- The fields above say how it ended, and
wait()has the captured output.
local job = gnoblin.process.spawn({"sleep", "1"})
job:watch("status", function(p, status, old) print(old .. " -> " .. status) end)
job:on("closed", function(p) print(p.status, p.exit_code) end)Find a process again with these functions:
| Function | Result |
|---|---|
gnoblin.process.list() |
The processes this runtime started that have not ended, oldest first. |
gnoblin.process.by_id(id) |
The Process with this id, or nil when the runtime does not know it. |
Every call returns the same object for one id. A process that has ended is known only while something holds it, so
by_id can return nil for an old id.
Window.process holds the id of the Process that started a window's program. Resolve it with
gnoblin.process.by_id(window.process):
gnoblin.on("gnoblin.window.created", function(event)
local job = gnoblin.process.by_id(event.window.process)
if job then
print(event.window.title, "was started by", table.concat(job.argv, " "))
end
end)process:wait() returns an Operation that succeeds when the process ends, whatever its exit status. Every call returns
the same Operation. Its value has these fields:
| Field | Value |
|---|---|
exit_code |
The exit status, after a normal exit. |
signal |
The signal that ended the process, after it was killed. |
timed_out |
true when the timeout option ended the process. |
stdout, stderr |
The captured bytes, for a stream set to "capture". |
stdout_truncated |
true when standard output passed output_limit; also for stderr. |
The Operation fails with not_found when the program does not exist, and with another error code when it could not
start for another reason.
process:kill(signal?) sends a signal to the process group and returns an Operation. It fails with not_found once the
process has exited. Ending a process is allowed while the session is locked.
| Signal | Effect |
|---|---|
"term" (default) |
Asks the program to exit. |
"kill" |
Ends the program at once; it cannot clean up. |
"int", "hup" |
Interrupt and hang-up, which some programs handle. |
"usr1", "usr2" |
Program-defined signals. |
"stop", "cont" |
Pause the process group, and resume it. |
Process groups and reloads
Each process starts in its own session and process group, so kill and timeout also reach the programs it starts.
Gnoblin resets an inherited negative CPU nice value to 0 and keeps a positive one. At most 64 processes started this
way run at once.
A process keeps running when the config reloads. The Process object belongs to the runtime that started it, so the
reloaded config cannot wait for it or kill it.
A reload ends the handle:
- Its
"closed"listeners run with the reason"config_reloaded"as a second argument. statusstays"running", andaliveisfalse. When Gnoblin itself stops, it sends SIGTERM to every process that is still running.