213 lines
9.2 KiB
Zig
213 lines
9.2 KiB
Zig
//! Process-level runtime types: what a user program receives at entry (`Init`,
|
|
//! the argv contract) and the process end of the lifecycle
|
|
//! (docs/process-lifecycle.md) — today the exit reason a supervisor reads to
|
|
//! decide restart; signals and the stop sequence land here with M17.4. Mirrors
|
|
//! the spirit of `std.process.Init.Minimal` in danos terms — std's `Args` holds
|
|
//! no data on freestanding targets, so the type is danos's own.
|
|
|
|
const std = @import("std");
|
|
const abi = @import("abi");
|
|
const sc = @import("system-call");
|
|
const ipc = @import("ipc");
|
|
const time = @import("time");
|
|
|
|
/// Everything a program receives at entry. Passed to
|
|
/// `pub fn main(init: runtime.process.Init)`; programs that need nothing keep
|
|
/// `pub fn main() void`. An `environment` field is added here once the kernel
|
|
/// passes a non-empty envp (today it is always empty — see docs/sysv.md).
|
|
pub const Init = struct {
|
|
arguments: Arguments,
|
|
};
|
|
|
|
/// The process arguments (argc/argv), parsed from the kernel-built System V
|
|
/// entry block. The bytes live in the entry block at the top of the stack page,
|
|
/// NUL-terminated, valid for the process's lifetime.
|
|
pub const Arguments = struct {
|
|
/// argc — at least 1: argument 0 is the path or name this binary was
|
|
/// spawned as.
|
|
count: usize,
|
|
/// The argv pointers in the entry block (NULL-terminated after `count`
|
|
/// entries).
|
|
vector: [*]const [*:0]const u8,
|
|
|
|
/// Argument `index` (0 = the program's own path/name), or null if out of
|
|
/// range.
|
|
pub fn get(arguments: Arguments, index: usize) ?[:0]const u8 {
|
|
if (index >= arguments.count) return null;
|
|
return std.mem.span(arguments.vector[index]);
|
|
}
|
|
|
|
pub fn iterate(arguments: Arguments) Iterator {
|
|
return .{ .arguments = arguments };
|
|
}
|
|
|
|
pub const Iterator = struct {
|
|
arguments: Arguments,
|
|
index: usize = 0,
|
|
|
|
pub fn next(iterator: *Iterator) ?[:0]const u8 {
|
|
const argument = iterator.arguments.get(iterator.index) orelse return null;
|
|
iterator.index += 1;
|
|
return argument;
|
|
}
|
|
};
|
|
};
|
|
|
|
/// How a process ended — what a supervisor's restart policy reads: a clean exit
|
|
/// meant to stop, a fault wants a restart with backoff, killed means the
|
|
/// supervisor did it itself (docs/process-lifecycle.md).
|
|
pub const ExitReason = abi.ExitReason;
|
|
|
|
/// How dead child `id` ended. Ask after the exit notification arrives — the
|
|
/// kernel records the reason before it posts the notification, so this never
|
|
/// races it. Returns null for an id that never lived, is still alive, was
|
|
/// evicted from the kernel's bounded record, or is not this process's child
|
|
/// (the same authority gate as `kill`).
|
|
pub fn exitReason(id: u32) ?ExitReason {
|
|
const r = sc.systemCall1(.process_exit_reason, id);
|
|
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
|
return @enumFromInt(r);
|
|
}
|
|
|
|
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
|
/// names, message delivery. A signal is a one-way coalescing statement — never a
|
|
/// question (liveness is the zero-length ping call) and never kill (that is
|
|
/// `system.kill`, unhandleable by definition).
|
|
pub const Signal = abi.Signal;
|
|
|
|
/// The coalesced set of signals one notification delivered: two pending
|
|
/// terminates arrive as one. Decode a received badge with `signalsFrom`.
|
|
pub const SignalSet = struct {
|
|
pending: u32,
|
|
|
|
pub fn has(set: SignalSet, signal: Signal) bool {
|
|
return set.pending & (@as(u32, 1) << @intFromEnum(signal)) != 0;
|
|
}
|
|
};
|
|
|
|
/// Nominate `endpoint` as this process's signal endpoint. Signals posted while
|
|
/// unbound have pended; they are delivered immediately on bind, coalesced.
|
|
pub fn bindSignals(endpoint: usize) bool {
|
|
return sc.systemCall1(.signal_bind, endpoint) == 0;
|
|
}
|
|
|
|
/// Decode a received badge into the signals it delivered, or null if it is not
|
|
/// a signal notification.
|
|
pub fn signalsFrom(badge: u64) ?SignalSet {
|
|
if (badge & abi.notify_badge_bit == 0 or badge & abi.notify_signal_bit == 0) return null;
|
|
return .{ .pending = @truncate(badge & ~(abi.notify_badge_bit | abi.notify_signal_bit)) };
|
|
}
|
|
|
|
/// Post `signal` to child `id` (or to yourself). Supervisor-gated, like kill;
|
|
/// non-blocking, always — a statement, not a conversation.
|
|
pub fn sendSignal(id: u32, signal: Signal) bool {
|
|
return sc.systemCall2(.process_signal, id, @intFromEnum(signal)) == 0;
|
|
}
|
|
|
|
/// The standard stop sequence (docs/process-lifecycle.md): terminate, wait up to
|
|
/// `deadline_ms` for the exit notification on `exit_endpoint` (the endpoint the
|
|
/// child was spawned with), then kill. Any *other* notifications arriving on
|
|
/// that endpoint while stopping are consumed and dropped — a supervisor with
|
|
/// concurrent traffic implements the same sequence inside its own event loop
|
|
/// (arm `system.timerOnce`, keep serving) instead of calling this.
|
|
pub fn stop(id: u32, deadline_ms: u64, exit_endpoint: usize) void {
|
|
_ = sendSignal(id, .terminate);
|
|
_ = time.timerOnce(exit_endpoint, deadline_ms);
|
|
var receive: [8]u8 = undefined;
|
|
while (true) {
|
|
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
|
if (got.isChildExit() and got.childProcessId() == id) return;
|
|
if (got.isTimer()) break; // the deadline passed first — escalate
|
|
}
|
|
_ = kill(id);
|
|
while (true) {
|
|
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
|
if (got.isChildExit() and got.childProcessId() == id) return;
|
|
}
|
|
}
|
|
|
|
/// Subscribe `endpoint` to published exit events: every process death posts an
|
|
/// asynchronous notification with the same badge encoding as a supervisor's exit
|
|
/// notice (decode with `ipc.Received.isChildExit`/`childProcessId`). For stateful
|
|
/// services: release what the dead client held — file handles, subscriptions —
|
|
/// because a service must never depend on clients cleaning up after themselves
|
|
/// (docs/process-lifecycle.md). Ungated, like `system.processes`.
|
|
pub fn subscribeExits(endpoint: usize) bool {
|
|
return sc.systemCall1(.process_subscribe, endpoint) == 0;
|
|
}
|
|
|
|
// --- raw process syscalls, formerly in the system.zig dumping ground ---
|
|
|
|
/// One `processes` entry — re-exported from the shared ABI so a program can declare its
|
|
/// snapshot buffer without importing `abi` itself.
|
|
pub const ProcessDescriptor = abi.ProcessDescriptor;
|
|
|
|
/// Give up the rest of this quantum.
|
|
pub fn yield() void {
|
|
_ = sc.systemCall0(.yield);
|
|
}
|
|
|
|
/// End the process. Never returns.
|
|
pub fn exit(code: usize) noreturn {
|
|
_ = sc.systemCall1(.exit, code);
|
|
unreachable; // the kernel never returns from exit
|
|
}
|
|
|
|
/// Start the binary bundled in the initial-ramdisk under `name` as a new ring-3 process,
|
|
/// returning the child's process id (or null). argv[0] is `name`, and the caller becomes
|
|
/// its **supervisor** — the only process allowed to `kill` it.
|
|
pub fn spawn(name: []const u8) ?u32 {
|
|
return spawnSupervised(name, &.{}, null);
|
|
}
|
|
|
|
/// Like `spawn`, but hands the child argv[1..] (argv[0] is still `name`).
|
|
pub fn spawnWithArguments(name: []const u8, arguments: []const []const u8) ?u32 {
|
|
return spawnSupervised(name, arguments, null);
|
|
}
|
|
|
|
/// The full spawn: argv[1..] for the child, and an optional endpoint the kernel notifies
|
|
/// when the child ends (any way — clean exit, fault, or `kill`), delivered via
|
|
/// `ipc.replyWait` as a child-exit badge (`ipc.Received.isChildExit`/`childProcessId`), so
|
|
/// one endpoint can supervise many children. Returns the child's process id, or null.
|
|
pub fn spawnSupervised(name: []const u8, arguments: []const []const u8, exit_endpoint: ?usize) ?u32 {
|
|
var blob: [256]u8 = undefined;
|
|
var len: usize = 0;
|
|
for (arguments, 0..) |argument, i| {
|
|
if (i != 0) {
|
|
if (len >= blob.len) return null;
|
|
blob[len] = 0;
|
|
len += 1;
|
|
}
|
|
if (len + argument.len > blob.len) return null;
|
|
@memcpy(blob[len..][0..argument.len], argument);
|
|
len += argument.len;
|
|
}
|
|
const r = sc.systemCall5(.system_spawn, @intFromPtr(name.ptr), name.len, if (len == 0) 0 else @intFromPtr(&blob), len, exit_endpoint orelse abi.no_cap);
|
|
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
|
return @intCast(r);
|
|
}
|
|
|
|
/// Snapshot the process table into `out` and return the total number of live processes
|
|
/// (which may exceed `out.len`; call again with a larger buffer). Kernel tasks are
|
|
/// included, with an empty name. The primitive `ps` is built on.
|
|
pub fn processes(out: []ProcessDescriptor) usize {
|
|
return sc.systemCall2(.process_enumerate, @intFromPtr(out.ptr), out.len);
|
|
}
|
|
|
|
/// Whether a process spawned under `name` (its argv[0]) is currently alive.
|
|
pub fn isProcessRunning(name: []const u8) bool {
|
|
var table: [32]ProcessDescriptor = undefined;
|
|
const total = processes(&table);
|
|
for (table[0..@min(total, table.len)]) |descriptor| {
|
|
if (std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// End process `id`. Only its supervisor — the process that spawned it — may; anyone else
|
|
/// gets false, as does a stale or unknown id. Delivery is prompt but asynchronous, like a
|
|
/// signal. True means the kill is accepted and irrevocable.
|
|
pub fn kill(id: u32) bool {
|
|
return sc.systemCall1(.process_kill, id) == 0;
|
|
}
|