Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072const std = @import("std");const builtin = @import("builtin");const darwin_process_spawn = @import("../shared/darwin_process_spawn.zig");const io_mod = @import("../shared/io.zig");
const Allocator = std.mem.Allocator;
const DarwinPipeIdentity = struct { handle: u64, peer_handle: u64,
fn eql(self: DarwinPipeIdentity, other: DarwinPipeIdentity) bool { return self.handle == other.handle and self.peer_handle == other.peer_handle; }};
/// Kernel-owned command membership that survives environment replacement,/// exec, process-group changes, and forks. The caller owns `deinit`.pub const DarwinProcessWitness = struct { supervisor_fd: ?std.posix.fd_t, child_fd: ?std.posix.fd_t, descendant_fd: std.posix.fd_t, identity: DarwinPipeIdentity,
pub fn init() !DarwinProcessWitness { if (comptime builtin.os.tag != .macos) return error.ProcessTreeUnsupported; var pipe: [2]std.posix.fd_t = undefined; switch (std.posix.errno(std.posix.system.pipe(&pipe))) { .SUCCESS => {}, else => |err| return std.posix.unexpectedErrno(err), } errdefer closeFd(pipe[0]); errdefer closeFd(pipe[1]); try setCloseOnExec(pipe[0]); try setCloseOnExec(pipe[1]); return .{ .supervisor_fd = pipe[0], .child_fd = pipe[1], .descendant_fd = darwin_process_spawn.inherited_fd_target(pipe[1]), .identity = try captureDarwinPipeIdentity(std.c.getpid(), pipe[1]), }; }
pub fn childFd(self: DarwinProcessWitness) std.posix.fd_t { return self.child_fd orelse unreachable; }
pub fn closeChildCopy(self: *DarwinProcessWitness) void { const fd = self.child_fd orelse return; closeFd(fd); self.child_fd = null; }
pub fn deinit(self: *DarwinProcessWitness) void { if (self.child_fd) |fd| closeFd(fd); if (self.supervisor_fd) |fd| closeFd(fd); self.* = undefined; }};
const Identity = union(enum) { linux_start_ticks: u64, macos_unique_id: u64,
fn eql(self: Identity, other: Identity) bool { return switch (self) { .linux_start_ticks => |ticks| switch (other) { .linux_start_ticks => |other_ticks| ticks == other_ticks, else => false, }, .macos_unique_id => |unique_id| switch (other) { .macos_unique_id => |other_unique_id| unique_id == other_unique_id, else => false, }, }; }};
const TrackedProcess = struct { pid: std.posix.pid_t, identity: Identity,};
const ProcessSnapshot = struct { identity: Identity, parent_pid: std.posix.pid_t, parent_unique_id: ?u64 = null, started_at_us: ?u64 = null, zombie: bool = false,};
pub const DeliverySummary = struct { delivered: usize = 0, incomplete: bool = false,};
const ProcessGroupState = union(enum) { found: std.posix.pid_t, vanished, unavailable,};
const SystemSignalEffects = struct { fn capture(alloc: Allocator, pid: std.posix.pid_t) !ProcessSnapshot { return captureSnapshot(alloc, pid); }
fn processGroup(pid: std.posix.pid_t) ProcessGroupState { return inspectProcessGroup(pid); }
fn send(pid: std.posix.pid_t, signal: std.posix.SIG) std.posix.KillError!void { return std.posix.kill(pid, signal); }};
/// Tracks a command root and descendants across new sessions or process/// groups. Identity checks guard both traversal and signaling against PID/// reuse.pub const Tracker = struct { alloc: Allocator, root: ?TrackedProcess = null, processes: std.ArrayList(TrackedProcess) = .empty, macos_child_buffer: []std.posix.pid_t = &.{}, macos_pid_buffer: []std.posix.pid_t = &.{}, darwin_process_witness: ?DarwinPipeIdentity = null, darwin_process_witness_fd: ?std.posix.fd_t = null, macos_root_started_at_us: ?u64 = null,
pub fn init(alloc: Allocator) !Tracker { var tracker = Tracker{ .alloc = alloc }; errdefer tracker.deinit(); if (comptime builtin.os.tag == .macos) { const reported = Darwin.proc_listchildpids(0, null, 0); const capacity: usize = if (reported > 0) @max(@as(usize, @intCast(reported)) + 256, 1024) else 1024; tracker.macos_child_buffer = try alloc.alloc( std.posix.pid_t, capacity, ); const process_count = Darwin.proc_listallpids(null, 0); const process_capacity: usize = if (process_count > 0) @max(@as(usize, @intCast(process_count)) + 256, 1024) else 1024; tracker.macos_pid_buffer = try alloc.alloc( std.posix.pid_t, process_capacity, ); } return tracker; }
pub fn deinit(self: *Tracker) void { self.processes.deinit(self.alloc); if (self.macos_child_buffer.len > 0) { self.alloc.free(self.macos_child_buffer); } if (self.macos_pid_buffer.len > 0) { self.alloc.free(self.macos_pid_buffer); } self.* = undefined; }
pub fn bindProcessWitness( self: *Tracker, witness: *const DarwinProcessWitness, ) void { if (comptime builtin.os.tag != .macos) return; self.darwin_process_witness = witness.identity; self.darwin_process_witness_fd = witness.descendant_fd; }
pub fn refresh(self: *Tracker, root_pid: std.posix.pid_t) !void { const root_snapshot: ?ProcessSnapshot = captureSnapshot(self.alloc, root_pid) catch |err| switch (err) { error.ProcessNotFound => null, else => return err, }; var traverse_root = false; if (root_snapshot) |snapshot| { if (self.root) |root| { traverse_root = root.pid == root_pid and root.identity.eql(snapshot.identity); } else { self.root = .{ .pid = root_pid, .identity = snapshot.identity, }; self.macos_root_started_at_us = snapshot.started_at_us; traverse_root = true; } } if (traverse_root) try self.appendDirectChildren(self.root.?); if (comptime builtin.os.tag == .macos) { if (root_snapshot == null) try self.refreshLineageProcesses(); }
var parent_index: usize = 0; while (parent_index < self.processes.items.len) : (parent_index += 1) { const parent = self.processes.items[parent_index]; const actual = captureSnapshot(self.alloc, parent.pid) catch continue; if (!shouldTraverseParent(parent.identity, actual.identity)) continue; try self.appendDirectChildren(parent); } }
pub fn refreshAdditionalRoot( self: *Tracker, root_pid: std.posix.pid_t, ) !void { const snapshot = captureSnapshot(self.alloc, root_pid) catch |err| switch (err) { error.ProcessNotFound => return, else => return err, }; try self.appendDirectChildren(.{ .pid = root_pid, .identity = snapshot.identity, }); }
pub fn refreshLineageProcesses(self: *Tracker) !void { if (comptime builtin.os.tag != .macos) return; const reported = Darwin.proc_listallpids(null, 0); if (reported <= 0) return; const required = @as(usize, @intCast(reported)) + 256; if (required > self.macos_pid_buffer.len) { self.macos_pid_buffer = try self.alloc.realloc( self.macos_pid_buffer, required, ); } const count = Darwin.proc_listallpids( self.macos_pid_buffer.ptr, @intCast(self.macos_pid_buffer.len * @sizeOf(std.posix.pid_t)), ); if (count <= 0) return; const process_count = @min( @as(usize, @intCast(count)), self.macos_pid_buffer.len, ); var changed = true; while (changed) { changed = false; for (self.macos_pid_buffer[0..process_count]) |pid| { if (pid <= 0 or pid == std.c.getpid()) continue; if (try self.trackLineageProcess(pid)) changed = true; } } }
pub fn signalAll(self: *Tracker, signal: std.posix.SIG) usize { return self.signalProcessesChecked(signal, null).delivered; }
pub fn signalOutsideProcessGroup( self: *Tracker, signal: std.posix.SIG, preserved_group: std.posix.pid_t, ) usize { return self.signalProcessesChecked(signal, preserved_group).delivered; }
pub fn signalOutsideProcessGroupChecked( self: *Tracker, signal: std.posix.SIG, preserved_group: std.posix.pid_t, ) DeliverySummary { return self.signalProcessesChecked(signal, preserved_group); }
fn signalProcessesChecked( self: *Tracker, signal: std.posix.SIG, preserved_group: ?std.posix.pid_t, ) DeliverySummary { return self.signalProcessesWith( signal, preserved_group, SystemSignalEffects, ); }
fn signalProcessesWith( self: *Tracker, signal: std.posix.SIG, preserved_group: ?std.posix.pid_t, comptime Effects: type, ) DeliverySummary { var summary: DeliverySummary = .{}; var index = self.processes.items.len; while (index > 0) { index -= 1; self.signalTrackedProcessWith( self.processes.items[index], signal, preserved_group, &summary, Effects, ); } if (self.root) |root| { self.signalTrackedProcessWith( root, signal, preserved_group, &summary, Effects, ); } return summary; }
fn signalTrackedProcessWith( self: *Tracker, process: TrackedProcess, signal: std.posix.SIG, preserved_group: ?std.posix.pid_t, summary: *DeliverySummary, comptime Effects: type, ) void { const actual = Effects.capture(self.alloc, process.pid) catch |err| { if (err != error.ProcessNotFound) summary.incomplete = true; return; }; if (!process.identity.eql(actual.identity)) return; if (actual.zombie) return; const process_group = switch (Effects.processGroup(process.pid)) { .found => |value| value, .vanished => return, .unavailable => { summary.incomplete = true; return; }, }; if (!shouldSignalProcess(process_group, preserved_group)) return; Effects.send(process.pid, signal) catch |err| switch (err) { error.ProcessNotFound => return, else => { summary.incomplete = true; return; }, }; summary.delivered += 1; }
pub fn anyAlive(self: *Tracker) bool { if (self.root) |root| { const actual: ?ProcessSnapshot = captureSnapshot(self.alloc, root.pid) catch null; if (actual) |snapshot| { if (root.identity.eql(snapshot.identity) and snapshotIsAlive(snapshot)) return true; } } for (self.processes.items) |process| { const actual = captureSnapshot(self.alloc, process.pid) catch continue; if (process.identity.eql(actual.identity) and snapshotIsAlive(actual)) return true; } return false; }
fn appendDirectChildren( self: *Tracker, parent: TrackedProcess, ) !void { switch (builtin.os.tag) { .linux => try self.appendLinuxChildren(parent), .macos => try self.appendMacOSChildren(parent), else => return error.ProcessTreeUnsupported, } }
fn appendLinuxChildren( self: *Tracker, parent: TrackedProcess, ) !void { if (comptime builtin.os.tag != .linux) return error.ProcessTreeUnsupported; if (!try self.parentIdentityMatches(parent)) return; const task_path = try std.fmt.allocPrint( self.alloc, "/proc/{d}/task", .{parent.pid}, ); defer self.alloc.free(task_path); var task_dir = (try openLinuxProcDir(task_path)) orelse return; defer task_dir.close(io_mod.getIo()); var tasks = task_dir.iterate(); while (try tasks.next(io_mod.getIo())) |entry| { const tid = std.fmt.parseInt( std.posix.pid_t, entry.name, 10, ) catch continue; if (tid <= 0) continue; try self.appendLinuxTaskChildren(parent, tid); } }
fn appendLinuxTaskChildren( self: *Tracker, parent: TrackedProcess, tid: std.posix.pid_t, ) !void { const path = try std.fmt.allocPrint( self.alloc, "/proc/{d}/task/{d}/children", .{ parent.pid, tid }, ); defer self.alloc.free(path); var file = (try openLinuxProcFile(path)) orelse return; defer file.close(io_mod.getIo()); var buffer: [64 * 1024]u8 = undefined; const read_len = readLinuxChildrenFile(file, &buffer) catch |err| switch (err) { error.ProcessNotFound => return, else => return err, }; if (!try self.parentIdentityMatches(parent)) return; var children = std.mem.tokenizeAny(u8, buffer[0..read_len], " \t\r\n"); while (children.next()) |pid_text| { const pid = std.fmt.parseInt(std.posix.pid_t, pid_text, 10) catch continue; try self.trackChild(pid, parent.pid); } }
fn appendMacOSChildren( self: *Tracker, parent: TrackedProcess, ) !void { if (comptime builtin.os.tag != .macos) return error.ProcessTreeUnsupported; if (!try self.parentIdentityMatches(parent)) return; const count = Darwin.proc_listchildpids( parent.pid, self.macos_child_buffer.ptr, @intCast(self.macos_child_buffer.len * @sizeOf(std.posix.pid_t)), ); if (count <= 0) return; const child_count = @min( @as(usize, @intCast(count)), self.macos_child_buffer.len, ); if (!try self.parentIdentityMatches(parent)) return; for (self.macos_child_buffer[0..child_count]) |pid| { if (pid > 0) try self.trackChild(pid, parent.pid); } }
fn parentIdentityMatches(self: *Tracker, parent: TrackedProcess) !bool { const snapshot = captureSnapshot(self.alloc, parent.pid) catch |err| switch (err) { error.ProcessNotFound => return false, else => return err, }; return parent.identity.eql(snapshot.identity); }
fn trackChild( self: *Tracker, pid: std.posix.pid_t, expected_parent_pid: std.posix.pid_t, ) !void { const snapshot = captureSnapshot(self.alloc, pid) catch |err| switch (err) { error.ProcessNotFound => return, else => return err, }; if (!snapshotBelongsToParent(snapshot, expected_parent_pid)) return; if (self.root) |root| { if (root.pid == pid and root.identity.eql(snapshot.identity)) return; } for (self.processes.items) |*process| { if (process.pid != pid) continue; process.identity = snapshot.identity; return; } try self.processes.append(self.alloc, .{ .pid = pid, .identity = snapshot.identity, }); }
fn trackLineageProcess(self: *Tracker, pid: std.posix.pid_t) !bool { const snapshot = captureSnapshot(self.alloc, pid) catch return false; if (!couldBelongByStart( self.macos_root_started_at_us, snapshot.started_at_us, )) return false; const parent_unique_id = snapshot.parent_unique_id orelse return false; if (!self.containsMacOSUniqueId(parent_unique_id) and !try self.processHasBoundWitness(pid)) { return false; } if (self.root) |root| { if (root.pid == pid and root.identity.eql(snapshot.identity)) return false; } for (self.processes.items) |*process| { if (process.pid != pid) continue; if (process.identity.eql(snapshot.identity)) return false; process.identity = snapshot.identity; return true; } try self.processes.append(self.alloc, .{ .pid = pid, .identity = snapshot.identity, }); return true; }
fn processHasBoundWitness(self: *Tracker, pid: std.posix.pid_t) !bool { if (comptime builtin.os.tag != .macos) return false; const expected = self.darwin_process_witness orelse return false; const fd = self.darwin_process_witness_fd orelse return false; const actual = captureDarwinPipeIdentity(pid, fd) catch return false; return expected.eql(actual); }
fn containsMacOSUniqueId(self: *Tracker, unique_id: u64) bool { if (self.root) |root| { if (identityHasMacOSUniqueId(root.identity, unique_id)) return true; } for (self.processes.items) |process| { if (identityHasMacOSUniqueId(process.identity, unique_id)) return true; } return false; }};
fn identityHasMacOSUniqueId(identity: Identity, unique_id: u64) bool { return switch (identity) { .macos_unique_id => |actual| actual == unique_id, else => false, };}
fn setCloseOnExec(fd: std.posix.fd_t) !void { while (true) switch (std.posix.errno(std.posix.system.fcntl( fd, std.posix.F.SETFD, @as(usize, std.posix.FD_CLOEXEC), ))) { .SUCCESS => return, .INTR => continue, else => return error.FileControlFailed, };}
fn closeFd(fd: std.posix.fd_t) void { switch (std.posix.errno(std.posix.system.close(fd))) { .SUCCESS, .INTR => {}, else => {}, }}
fn captureDarwinPipeIdentity( pid: std.posix.pid_t, fd: std.posix.fd_t,) !DarwinPipeIdentity { if (comptime builtin.os.tag != .macos) return error.ProcessTreeUnsupported; var info: Darwin.PipeFdInfo = undefined; const read_len = Darwin.proc_pidfdinfo( pid, fd, Darwin.proc_pid_fd_pipe_info, &info, @sizeOf(Darwin.PipeFdInfo), ); if (read_len == 0) return error.ProcessNotFound; if (read_len != @sizeOf(Darwin.PipeFdInfo)) { return error.ProcessIdentityUnavailable; } return .{ .handle = info.pipeinfo.pipe_handle, .peer_handle = info.pipeinfo.pipe_peerhandle, };}
fn couldBelongByStart(root_started_at_us: ?u64, candidate_started_at_us: ?u64) bool { const root = root_started_at_us orelse return true; const candidate = candidate_started_at_us orelse return false; return candidate >= root;}
fn darwinStartTimeUs(seconds: u64, microseconds: u64) u64 { const scaled = @mulWithOverflow(seconds, @as(u64, std.time.us_per_s)); if (scaled[1] != 0) return std.math.maxInt(u64); const total = @addWithOverflow(scaled[0], microseconds); return if (total[1] == 0) total[0] else std.math.maxInt(u64);}
fn shouldTraverseParent(expected: Identity, actual: Identity) bool { return expected.eql(actual);}
fn snapshotBelongsToParent( snapshot: ProcessSnapshot, expected_parent_pid: std.posix.pid_t,) bool { return snapshot.parent_pid == expected_parent_pid;}
fn shouldSignalProcess( process_group: ?std.posix.pid_t, preserved_group: ?std.posix.pid_t,) bool { const preserved = preserved_group orelse return true; const actual = process_group orelse return false; return actual != preserved;}
fn inspectProcessGroup(pid: std.posix.pid_t) ProcessGroupState { if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) { return .unavailable; } const process_group = getpgid(pid); if (process_group >= 0) return .{ .found = process_group }; return switch (std.c.errno(process_group)) { .SRCH => .vanished, else => .unavailable, };}
extern "c" fn getpgid(pid: std.posix.pid_t) std.posix.pid_t;
fn readLinuxChildrenFile(file: std.Io.File, buffer: []u8) !usize { if (comptime builtin.os.tag != .linux) return error.ProcessTreeUnsupported; while (true) { const result = std.posix.system.read(file.handle, buffer.ptr, buffer.len); switch (std.posix.errno(result)) { .SUCCESS => return @intCast(result), .INTR => continue, .SRCH, .NOENT => return error.ProcessNotFound, else => return error.ProcessTreeInspectionFailed, } }}
fn openLinuxProcDir(path: []const u8) !?std.Io.Dir { if (comptime builtin.os.tag != .linux) return error.ProcessTreeUnsupported; // A process can disappear between identity validation and opening its // procfs entry. The POSIX wrapper maps Linux's ESRCH to FileNotFound; // std.Io currently treats ESRCH from directory opens as unexpected. const fd = std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY, .NOFOLLOW = true, .DIRECTORY = true, .CLOEXEC = true, }, 0) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; return .{ .handle = fd };}
fn openLinuxProcFile(path: []const u8) !?std.Io.File { if (comptime builtin.os.tag != .linux) return error.ProcessTreeUnsupported; const fd = std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY, .NOFOLLOW = true, .CLOEXEC = true, }, 0) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; return .{ .handle = fd, .flags = .{ .nonblocking = false }, };}
test "Linux proc helpers treat missing process data as vanished" { if (builtin.os.tag != .linux) return error.SkipZigTest; try std.testing.expect( (try openLinuxProcDir("/proc/self/fx-process-tree-missing")) == null, ); try std.testing.expect( (try openLinuxProcFile("/proc/self/fx-process-tree-missing")) == null, );}
test "process-group exclusion preserves the captured command grace" { try std.testing.expect(shouldSignalProcess(41, null)); try std.testing.expect(!shouldSignalProcess(41, 41)); try std.testing.expect(shouldSignalProcess(42, 41)); try std.testing.expect(!shouldSignalProcess(null, 41));}
test "stale process identities cannot become traversal roots" { try std.testing.expect(shouldTraverseParent( .{ .linux_start_ticks = 41 }, .{ .linux_start_ticks = 41 }, )); try std.testing.expect(!shouldTraverseParent( .{ .linux_start_ticks = 41 }, .{ .linux_start_ticks = 42 }, )); try std.testing.expect(!shouldTraverseParent( .{ .linux_start_ticks = 41 }, .{ .macos_unique_id = 41 }, ));}
test "child admission binds the observed process to its expected parent" { const snapshot = ProcessSnapshot{ .identity = .{ .linux_start_ticks = 42 }, .parent_pid = 17, }; try std.testing.expect(snapshotBelongsToParent(snapshot, 17)); try std.testing.expect(!snapshotBelongsToParent(snapshot, 18));}
test "macOS lineage identity matches only the same unique process" { try std.testing.expect(identityHasMacOSUniqueId( .{ .macos_unique_id = 42 }, 42, )); try std.testing.expect(!identityHasMacOSUniqueId( .{ .macos_unique_id = 42 }, 43, )); try std.testing.expect(!identityHasMacOSUniqueId( .{ .linux_start_ticks = 42 }, 42, ));}
test "checked signal delivery distinguishes vanished stale and failed targets" { const FakeEffects = struct { fn capture(_: Allocator, pid: std.posix.pid_t) !ProcessSnapshot { return switch (pid) { 11 => error.ProcessNotFound, 12 => error.ProcessIdentityUnavailable, 13 => .{ .identity = .{ .linux_start_ticks = 113 }, .parent_pid = 1, }, else => .{ .identity = .{ .linux_start_ticks = @intCast(pid) }, .parent_pid = 1, }, }; }
fn processGroup(pid: std.posix.pid_t) ProcessGroupState { return switch (pid) { 14 => .vanished, 15 => .unavailable, 16 => .{ .found = 41 }, else => .{ .found = pid + 100 }, }; }
fn send(pid: std.posix.pid_t, _: std.posix.SIG) std.posix.KillError!void { return switch (pid) { 17 => error.PermissionDenied, 18 => error.ProcessNotFound, else => {}, }; } };
var tracker = Tracker{ .alloc = std.testing.allocator }; defer tracker.deinit(); for (10..19) |pid| { try tracker.processes.append(std.testing.allocator, .{ .pid = @intCast(pid), .identity = .{ .linux_start_ticks = pid }, }); }
const summary = tracker.signalProcessesWith( std.posix.SIG.TERM, 41, FakeEffects, ); try std.testing.expectEqual(@as(usize, 1), summary.delivered); try std.testing.expect(summary.incomplete);}
test "checked signal delivery keeps vanished stale and excluded targets complete" { const FakeEffects = struct { fn capture(_: Allocator, pid: std.posix.pid_t) !ProcessSnapshot { if (pid == 21) return error.ProcessNotFound; return .{ .identity = .{ .linux_start_ticks = if (pid == 22) 122 else @as(u64, @intCast(pid)) }, .parent_pid = 1, }; }
fn processGroup(pid: std.posix.pid_t) ProcessGroupState { return switch (pid) { 23 => .vanished, else => .{ .found = 41 }, }; }
fn send(_: std.posix.pid_t, _: std.posix.SIG) std.posix.KillError!void { return; } };
var tracker = Tracker{ .alloc = std.testing.allocator }; defer tracker.deinit(); for (21..25) |pid| { try tracker.processes.append(std.testing.allocator, .{ .pid = @intCast(pid), .identity = .{ .linux_start_ticks = pid }, }); }
const summary = tracker.signalProcessesWith( std.posix.SIG.TERM, 41, FakeEffects, ); try std.testing.expectEqual(@as(usize, 0), summary.delivered); try std.testing.expect(!summary.incomplete);}
fn captureSnapshot(alloc: Allocator, pid: std.posix.pid_t) !ProcessSnapshot { return switch (builtin.os.tag) { .linux => try captureLinuxSnapshot(alloc, pid), .macos => try captureMacOSSnapshot(pid), else => error.ProcessTreeUnsupported, };}
fn captureLinuxSnapshot(alloc: Allocator, pid: std.posix.pid_t) !ProcessSnapshot { if (comptime builtin.os.tag != .linux) return error.ProcessTreeUnsupported; const path = try std.fmt.allocPrint(alloc, "/proc/{d}/stat", .{pid}); defer alloc.free(path); var file = (try openLinuxProcFile(path)) orelse return error.ProcessNotFound; defer file.close(io_mod.getIo()); var buffer: [4096]u8 = undefined; const read_len = try readLinuxProcFile(file, &buffer); const stat = buffer[0..read_len]; const close_paren = std.mem.lastIndexOfScalar(u8, stat, ')') orelse return error.ProcessIdentityUnavailable; var fields = std.mem.tokenizeScalar(u8, stat[close_paren + 1 ..], ' '); var field_number: usize = 3; var parent_pid: ?std.posix.pid_t = null; var zombie = false; while (fields.next()) |field| : (field_number += 1) { if (field_number == 3) zombie = field.len == 1 and field[0] == 'Z'; if (field_number == 4) { parent_pid = std.fmt.parseInt(std.posix.pid_t, field, 10) catch return error.ProcessIdentityUnavailable; } if (field_number == 22) { const start_ticks = std.fmt.parseUnsigned(u64, field, 10) catch return error.ProcessIdentityUnavailable; return .{ .identity = .{ .linux_start_ticks = start_ticks }, .parent_pid = parent_pid orelse return error.ProcessIdentityUnavailable, .zombie = zombie, }; } } return error.ProcessIdentityUnavailable;}
fn readLinuxProcFile(file: std.Io.File, buffer: []u8) !usize { if (comptime builtin.os.tag != .linux) return error.ProcessTreeUnsupported; while (true) { const result = std.posix.system.read(file.handle, buffer.ptr, buffer.len); switch (std.posix.errno(result)) { .SUCCESS => { const read_len: usize = @intCast(result); if (read_len == 0) return error.ProcessNotFound; return read_len; }, .INTR => continue, .SRCH => return error.ProcessNotFound, else => return error.ProcessIdentityUnavailable, } }}
fn captureMacOSSnapshot(pid: std.posix.pid_t) !ProcessSnapshot { if (comptime builtin.os.tag != .macos) return error.ProcessTreeUnsupported; var unique: Darwin.ProcUniqueIdentifierInfo = undefined; const unique_len = Darwin.proc_pidinfo( pid, Darwin.proc_pid_unique_identifier_info, 0, &unique, @sizeOf(Darwin.ProcUniqueIdentifierInfo), ); if (unique_len == 0) return error.ProcessNotFound; if (unique_len != @sizeOf(Darwin.ProcUniqueIdentifierInfo)) { return error.ProcessIdentityUnavailable; } var info: Darwin.ProcBsdInfo = undefined; const read_len = Darwin.proc_pidinfo( pid, 3, 0, &info, @sizeOf(Darwin.ProcBsdInfo), ); if (read_len == 0) return error.ProcessNotFound; if (read_len != @sizeOf(Darwin.ProcBsdInfo)) { return error.ProcessIdentityUnavailable; } return .{ .identity = .{ .macos_unique_id = unique.p_uniqueid }, .parent_pid = @intCast(info.pbi_ppid), .parent_unique_id = unique.p_puniqueid, .started_at_us = darwinStartTimeUs( info.pbi_start_tvsec, info.pbi_start_tvusec, ), .zombie = info.pbi_status == Darwin.process_status_zombie, };}
pub fn processIsAlive(alloc: Allocator, pid: std.posix.pid_t) !bool { const snapshot = captureSnapshot(alloc, pid) catch |err| switch (err) { error.ProcessNotFound => return false, else => return err, }; return snapshotIsAlive(snapshot);}
fn snapshotIsAlive(snapshot: ProcessSnapshot) bool { return !snapshot.zombie;}
const Darwin = struct { // Stable libproc process-identity flavor; the SDK omits this constant from // its public header, but XNU defines the record as API with a fixed size. const proc_pid_unique_identifier_info: c_int = 17; const proc_pid_fd_pipe_info: c_int = 6; const process_status_zombie: u32 = 5;
const ProcFileInfo = extern struct { fi_openflags: u32, fi_status: u32, fi_offset: i64, fi_type: i32, fi_guardflags: u32, };
const VinfoStat = extern struct { vst_dev: u32, vst_mode: u16, vst_nlink: u16, vst_ino: u64, vst_uid: u32, vst_gid: u32, vst_atime: i64, vst_atimensec: i64, vst_mtime: i64, vst_mtimensec: i64, vst_ctime: i64, vst_ctimensec: i64, vst_birthtime: i64, vst_birthtimensec: i64, vst_size: i64, vst_blocks: i64, vst_blksize: i32, vst_flags: u32, vst_gen: u32, vst_rdev: u32, vst_qspare: [2]i64, };
const PipeInfo = extern struct { pipe_stat: VinfoStat, pipe_handle: u64, pipe_peerhandle: u64, pipe_status: i32, rfu_1: i32, };
const PipeFdInfo = extern struct { pfi: ProcFileInfo, pipeinfo: PipeInfo, };
const ProcUniqueIdentifierInfo = extern struct { p_uuid: [16]u8, p_uniqueid: u64, p_puniqueid: u64, p_idversion: i32, p_orig_ppidversion: i32, p_reserve2: u64, p_reserve3: u64, };
const ProcBsdInfo = extern struct { pbi_flags: u32, pbi_status: u32, pbi_xstatus: u32, pbi_pid: u32, pbi_ppid: u32, pbi_uid: u32, pbi_gid: u32, pbi_ruid: u32, pbi_rgid: u32, pbi_svuid: u32, pbi_svgid: u32, rfu_1: u32, pbi_comm: [16]u8, pbi_name: [32]u8, pbi_nfiles: u32, pbi_pgid: u32, pbi_pjobc: u32, e_tdev: u32, e_tpgid: u32, pbi_nice: i32, pbi_start_tvsec: u64, pbi_start_tvusec: u64, };
extern "c" fn proc_listchildpids( ppid: c_int, buffer: ?*anyopaque, buffersize: c_int, ) c_int;
extern "c" fn proc_listallpids( buffer: ?*anyopaque, buffersize: c_int, ) c_int;
extern "c" fn proc_pidinfo( pid: c_int, flavor: c_int, arg: u64, buffer: *anyopaque, buffersize: c_int, ) c_int;
extern "c" fn proc_pidfdinfo( pid: c_int, fd: c_int, flavor: c_int, buffer: *anyopaque, buffersize: c_int, ) c_int;};
test "tracked identity distinguishes process instances" { const linux = Identity{ .linux_start_ticks = 42 }; try std.testing.expect(linux.eql(.{ .linux_start_ticks = 42 })); try std.testing.expect(!linux.eql(.{ .linux_start_ticks = 43 })); try std.testing.expect(!linux.eql(.{ .macos_unique_id = 42 }));}
test "zombie snapshots are terminal process state" { const live = ProcessSnapshot{ .identity = .{ .linux_start_ticks = 1 }, .parent_pid = 1, }; var zombie = live; zombie.zombie = true; try std.testing.expect(snapshotIsAlive(live)); try std.testing.expect(!snapshotIsAlive(zombie));}
test "Darwin witness scan excludes processes older than command root" { try std.testing.expect(couldBelongByStart(null, null)); try std.testing.expect(!couldBelongByStart(100, null)); try std.testing.expect(!couldBelongByStart(100, 99)); try std.testing.expect(couldBelongByStart(100, 100)); try std.testing.expect(couldBelongByStart(100, 101)); try std.testing.expectEqual(@as(u64, 2_000_003), darwinStartTimeUs(2, 3)); try std.testing.expectEqual( std.math.maxInt(u64), darwinStartTimeUs(std.math.maxInt(u64), 1), );}