const Child = @This(); const builtin = @import("builtin"); const native_os = builtin.os.tag; const std = @import("../std.zig"); const Io = std.Io; const process = std.process; const File = std.Io.File; const assert = std.debug.assert; const Allocator = std.mem.Allocator; pub const Id = switch (native_os) { .windows => std.os.windows.HANDLE, .wasi => void, else => std.posix.pid_t, }; /// After `wait` or `kill` is called, this becomes `null`. /// On Windows this is the hProcess. /// On POSIX this is the pid. id: ?Id, thread_handle: if (native_os == .windows) std.os.windows.HANDLE else void, /// The writing end of the child process's standard input pipe. /// Usage requires `process.SpawnOptions.StdIo.pipe`. stdin: ?File, /// The reading end of the child process's standard output pipe. /// Usage requires `process.SpawnOptions.StdIo.pipe`. stdout: ?File, /// The reading end of the child process's standard error pipe. /// Usage requires `process.SpawnOptions.StdIo.pipe`. stderr: ?File, /// This is available after calling wait if /// `request_resource_usage_statistics` was set to `true` before calling /// `spawn`. /// TODO move this data into `Term` resource_usage_statistics: ResourceUsageStatistics = .{}, request_resource_usage_statistics: bool, pub const ResourceUsageStatistics = struct { rusage: @TypeOf(rusage_init) = rusage_init, /// Returns the peak resident set size of the child process, in bytes, /// if available. pub inline fn getMaxRss(rus: ResourceUsageStatistics) ?usize { switch (native_os) { .dragonfly, .freebsd, .netbsd, .openbsd, .illumos, .linux, .serenity => { if (rus.rusage) |ru| { return @as(usize, @intCast(ru.maxrss)) * 1024; } else { return null; } }, .windows => { if (rus.rusage) |ru| { return ru.PeakWorkingSetSize; } else { return null; } }, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => { if (rus.rusage) |ru| { // Darwin oddly reports in bytes instead of kilobytes. return @as(usize, @intCast(ru.maxrss)); } else { return null; } }, else => return null, } } const rusage_init = switch (native_os) { .dragonfly, .freebsd, .netbsd, .openbsd, .illumos, .linux, .serenity, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, => @as(?std.posix.rusage, null), .windows => @as(?std.os.windows.VM_COUNTERS, null), else => {}, }; }; pub const Term = union(enum) { exited: u8, signal: std.posix.SIG, stopped: u32, unknown: u32, }; /// Requests for the operating system to forcibly terminate the child process, /// then blocks until it terminates, then cleans up all resources. /// /// Idempotent and does nothing after `wait` returns. /// /// Uncancelable. Ignores unexpected errors from the operating system. pub fn kill(child: *Child, io: Io) void { if (child.id == null) { assert(child.stdin == null); assert(child.stdout == null); assert(child.stderr == null); return; } io.vtable.childKill(io.userdata, child); assert(child.id == null); } pub const WaitError = error{ AccessDenied, } || Io.Cancelable || Io.UnexpectedError; /// Blocks until child process terminates and then cleans up all resources. pub fn wait(child: *Child, io: Io) WaitError!Term { assert(child.id != null); return io.vtable.childWait(io.userdata, child); } pub const CollectOutputError = error{ StreamTooLong, ConcurrencyUnavailable, } || Allocator.Error || Io.File.Reader.Error; pub const CollectOutputOptions = struct { stdout: *std.ArrayList(u8), stderr: *std.ArrayList(u8), allocator: Allocator, stdout_limit: Io.Limit = .unlimited, stderr_limit: Io.Limit = .unlimited, }; /// Collect the output from the process's stdout and stderr. Will return once /// all output has been collected. This does not mean that the process has /// ended. `wait` should still be called to wait for and clean up the process. /// /// The process must have been started with stdout and stderr set to /// `process.SpawnOptions.StdIo.pipe`. pub fn collectOutput(child: *const Child, io: Io, options: CollectOutputOptions) CollectOutputError!void { var stdout = try io.concurrent(collectStream, .{ io, options.allocator, child.stdout.?, options.stdout, options.stdout_limit, }); defer stdout.cancel(io) catch {}; var stderr = try io.concurrent(collectStream, .{ io, options.allocator, child.stderr.?, options.stderr, options.stderr_limit, }); defer stderr.cancel(io) catch {}; try stdout.await(io); try stderr.await(io); } fn collectStream(io: Io, gpa: Allocator, file: File, list: *std.ArrayList(u8), limit: Io.Limit) CollectOutputError!void { var fr = file.readerStreaming(io, &.{}); fr.interface.appendRemaining(gpa, list, limit) catch |err| switch (err) { error.ReadFailed => return fr.err.?, else => |e| return e, }; }