| 1 | const Child = @This(); |
| 2 | |
| 3 | const builtin = @import("builtin"); |
| 4 | const native_os = builtin.os.tag; |
| 5 | |
| 6 | const std = @import("../std.zig"); |
| 7 | const Io = std.Io; |
| 8 | const process = std.process; |
| 9 | const File = std.Io.File; |
| 10 | const assert = std.debug.assert; |
| 11 | const Allocator = std.mem.Allocator; |
| 12 | |
| 13 | pub const Id = switch (native_os) { |
| 14 | .windows => std.os.windows.HANDLE, |
| 15 | .wasi => void, |
| 16 | else => std.posix.pid_t, |
| 17 | }; |
| 18 | |
| 19 | /// After `wait` or `kill` is called, this becomes `null`. |
| 20 | /// On Windows this is the hProcess. |
| 21 | /// On POSIX this is the pid. |
| 22 | id: ?Id, |
| 23 | thread_handle: if (native_os == .windows) std.os.windows.HANDLE else void, |
| 24 | /// The writing end of the child process's standard input pipe. |
| 25 | /// Usage requires `process.SpawnOptions.StdIo.pipe`. |
| 26 | stdin: ?File, |
| 27 | /// The reading end of the child process's standard output pipe. |
| 28 | /// Usage requires `process.SpawnOptions.StdIo.pipe`. |
| 29 | stdout: ?File, |
| 30 | /// The reading end of the child process's standard error pipe. |
| 31 | /// Usage requires `process.SpawnOptions.StdIo.pipe`. |
| 32 | stderr: ?File, |
| 33 | /// This is available after calling wait if |
| 34 | /// `request_resource_usage_statistics` was set to `true` before calling |
| 35 | /// `spawn`. |
| 36 | /// TODO move this data into `Term` |
| 37 | resource_usage_statistics: ResourceUsageStatistics = .{}, |
| 38 | request_resource_usage_statistics: bool, |
| 39 | |
| 40 | pub const ResourceUsageStatistics = struct { |
| 41 | rusage: @TypeOf(rusage_init) = rusage_init, |
| 42 | |
| 43 | /// Returns the peak resident set size of the child process, in bytes, |
| 44 | /// if available. |
| 45 | pub inline fn getMaxRss(rus: ResourceUsageStatistics) ?usize { |
| 46 | switch (native_os) { |
| 47 | .dragonfly, .freebsd, .netbsd, .openbsd, .illumos, .linux, .serenity => { |
| 48 | if (rus.rusage) |ru| { |
| 49 | return @as(usize, @intCast(ru.maxrss)) * 1024; |
| 50 | } else { |
| 51 | return null; |
| 52 | } |
| 53 | }, |
| 54 | .windows => { |
| 55 | if (rus.rusage) |ru| { |
| 56 | return ru.PeakWorkingSetSize; |
| 57 | } else { |
| 58 | return null; |
| 59 | } |
| 60 | }, |
| 61 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => { |
| 62 | if (rus.rusage) |ru| { |
| 63 | // Darwin oddly reports in bytes instead of kilobytes. |
| 64 | return @as(usize, @intCast(ru.maxrss)); |
| 65 | } else { |
| 66 | return null; |
| 67 | } |
| 68 | }, |
| 69 | else => return null, |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | const rusage_init = switch (native_os) { |
| 74 | .dragonfly, |
| 75 | .freebsd, |
| 76 | .netbsd, |
| 77 | .openbsd, |
| 78 | .illumos, |
| 79 | .linux, |
| 80 | .serenity, |
| 81 | .driverkit, |
| 82 | .ios, |
| 83 | .maccatalyst, |
| 84 | .macos, |
| 85 | .tvos, |
| 86 | .visionos, |
| 87 | .watchos, |
| 88 | => @as(?std.posix.rusage, null), |
| 89 | .windows => @as(?std.os.windows.PROCESS.VM_COUNTERS, null), |
| 90 | else => {}, |
| 91 | }; |
| 92 | }; |
| 93 | |
| 94 | pub const Term = union(enum) { |
| 95 | exited: u8, |
| 96 | signal: std.posix.SIG, |
| 97 | stopped: std.posix.SIG, |
| 98 | unknown: u32, |
| 99 | |
| 100 | pub fn success(t: Term) bool { |
| 101 | return switch (t) { |
| 102 | .exited => |code| code == 0, |
| 103 | else => false, |
| 104 | }; |
| 105 | } |
| 106 | |
| 107 | pub fn format(t: Term, w: *Io.Writer) Io.Writer.Error!void { |
| 108 | switch (t) { |
| 109 | .exited => |code| return w.print("exited with code {d}", .{code}), |
| 110 | .signal => |sig| return w.print("terminated with signal {t}", .{sig}), |
| 111 | .stopped => |sig| return w.print("stopped with signal {t}", .{sig}), |
| 112 | .unknown => return w.writeAll("terminated unexpectedly"), |
| 113 | } |
| 114 | } |
| 115 | }; |
| 116 | |
| 117 | pub const Cwd = union(enum) { |
| 118 | /// CWD of the child is the same as the current CWD. |
| 119 | inherit, |
| 120 | /// On POSIX systems, `fchdir` is called after `fork` using this handle. |
| 121 | /// On Windows, the path is inferred from the provided handle and that path is used when calling `CreateProcessW`. |
| 122 | dir: Io.Dir, |
| 123 | /// On POSIX systems, `chdir` is called after `fork` using this path. |
| 124 | /// On Windows, this path is used when calling `CreateProcessW`. |
| 125 | path: []const u8, |
| 126 | }; |
| 127 | |
| 128 | /// Requests for the operating system to forcibly terminate the child process, |
| 129 | /// then blocks until it terminates, then cleans up all resources. |
| 130 | /// |
| 131 | /// Idempotent and does nothing after `wait` returns. |
| 132 | /// |
| 133 | /// Uncancelable. Ignores unexpected errors from the operating system. |
| 134 | pub fn kill(child: *Child, io: Io) void { |
| 135 | if (child.id == null) { |
| 136 | assert(child.stdin == null); |
| 137 | assert(child.stdout == null); |
| 138 | assert(child.stderr == null); |
| 139 | return; |
| 140 | } |
| 141 | io.vtable.childKill(io.userdata, child); |
| 142 | assert(child.id == null); |
| 143 | } |
| 144 | |
| 145 | pub const WaitError = error{ |
| 146 | AccessDenied, |
| 147 | } || Io.Cancelable || Io.UnexpectedError; |
| 148 | |
| 149 | /// Blocks until child process terminates and then cleans up all resources. |
| 150 | pub fn wait(child: *Child, io: Io) WaitError!Term { |
| 151 | assert(child.id != null); |
| 152 | return io.vtable.childWait(io.userdata, child); |
| 153 | } |
| 154 | |
| 155 | test { |
| 156 | _ = Term; |
| 157 | } |