zig/lib/std/process.zig
2026-01-08 05:06:31 +01:00

888 lines
34 KiB
Zig

const builtin = @import("builtin");
const native_os = builtin.os.tag;
const std = @import("std.zig");
const Io = std.Io;
const File = std.Io.File;
const fs = std.fs;
const mem = std.mem;
const math = std.math;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const testing = std.testing;
const posix = std.posix;
const windows = std.os.windows;
const unicode = std.unicode;
const max_path_bytes = std.fs.max_path_bytes;
pub const Child = @import("process/Child.zig");
pub const Args = @import("process/Args.zig");
pub const Environ = @import("process/Environ.zig");
pub const Preopens = @import("process/Preopens.zig");
/// This is the global, process-wide protection to coordinate stderr writes.
///
/// The primary motivation for recursive mutex here is so that a panic while
/// stderr mutex is held still dumps the stack trace and other debug
/// information.
pub var stderr_thread_mutex: std.Thread.Mutex.Recursive = .init;
/// A standard set of pre-initialized useful APIs for programs to take
/// advantage of. This is the type of the first parameter of the main function.
/// Applications wanting more flexibility can accept `Init.Minimal` instead.
///
/// Completion of https://github.com/ziglang/zig/issues/24510 will also allow
/// the second parameter of the main function to be a custom struct that
/// contain auto-parsed CLI arguments.
pub const Init = struct {
/// `Init` is a superset of `Minimal`; the latter is included here.
minimal: Minimal,
/// Permanent storage for the entire process, cleaned automatically on
/// exit. Not threadsafe.
arena: *std.heap.ArenaAllocator,
/// A default-selected general purpose allocator for temporary heap
/// allocations. Debug mode will set up leak checking if possible.
/// Threadsafe.
gpa: Allocator,
/// An appropriate default Io implementation based on the target
/// configuration. Debug mode will set up leak checking if possible.
io: Io,
/// Environment variables, initialized with `gpa`. Not threadsafe.
environ_map: *Environ.Map,
/// Named files that have been provided by the parent process. This is
/// mainly useful on WASI, but can be used on other systems to mimic the
/// behavior with respect to stdio.
preopens: Preopens,
/// Alternative to `Init` as the first parameter of the main function.
pub const Minimal = struct {
/// Environment variables.
environ: Environ,
/// Command line arguments.
args: Args,
};
};
pub const GetCwdError = posix.GetCwdError;
/// The result is a slice of `out_buffer`, from index `0`.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
pub fn getCwd(out_buffer: []u8) GetCwdError![]u8 {
return posix.getcwd(out_buffer);
}
// Same as GetCwdError, minus error.NameTooLong + Allocator.Error
pub const GetCwdAllocError = Allocator.Error || error{CurrentWorkingDirectoryUnlinked} || posix.UnexpectedError;
/// Caller must free the returned memory.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
pub fn getCwdAlloc(allocator: Allocator) GetCwdAllocError![]u8 {
// The use of max_path_bytes here is just a heuristic: most paths will fit
// in stack_buf, avoiding an extra allocation in the common case.
var stack_buf: [max_path_bytes]u8 = undefined;
var heap_buf: ?[]u8 = null;
defer if (heap_buf) |buf| allocator.free(buf);
var current_buf: []u8 = &stack_buf;
while (true) {
if (posix.getcwd(current_buf)) |slice| {
return allocator.dupe(u8, slice);
} else |err| switch (err) {
error.NameTooLong => {
// The path is too long to fit in stack_buf. Allocate geometrically
// increasing buffers until we find one that works
const new_capacity = current_buf.len * 2;
if (heap_buf) |buf| allocator.free(buf);
current_buf = try allocator.alloc(u8, new_capacity);
heap_buf = current_buf;
},
else => |e| return e,
}
}
}
test getCwdAlloc {
if (native_os == .wasi) return error.SkipZigTest;
const cwd = try getCwdAlloc(testing.allocator);
testing.allocator.free(cwd);
}
pub const UserInfo = struct {
uid: posix.uid_t,
gid: posix.gid_t,
};
/// POSIX function which gets a uid from username.
pub fn getUserInfo(name: []const u8) !UserInfo {
return switch (native_os) {
.linux,
.driverkit,
.ios,
.maccatalyst,
.macos,
.tvos,
.visionos,
.watchos,
.freebsd,
.netbsd,
.openbsd,
.haiku,
.illumos,
.serenity,
=> posixGetUserInfo(name),
else => @compileError("Unsupported OS"),
};
}
/// TODO this reads /etc/passwd. But sometimes the user/id mapping is in something else
/// like NIS, AD, etc. See `man nss` or look at an strace for `id myuser`.
pub fn posixGetUserInfo(io: Io, name: []const u8) !UserInfo {
const file = try Io.Dir.openFileAbsolute(io, "/etc/passwd", .{});
defer file.close(io);
var buffer: [4096]u8 = undefined;
var file_reader = file.reader(&buffer);
return posixGetUserInfoPasswdStream(name, &file_reader.interface) catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
error.EndOfStream => return error.UserNotFound,
error.CorruptPasswordFile => return error.CorruptPasswordFile,
};
}
fn posixGetUserInfoPasswdStream(name: []const u8, reader: *std.Io.Reader) !UserInfo {
const State = enum {
start,
wait_for_next_line,
skip_password,
read_user_id,
read_group_id,
};
var name_index: usize = 0;
var uid: posix.uid_t = 0;
var gid: posix.gid_t = 0;
sw: switch (State.start) {
.start => switch (try reader.takeByte()) {
':' => {
if (name_index == name.len) {
continue :sw .skip_password;
} else {
continue :sw .wait_for_next_line;
}
},
'\n' => return error.CorruptPasswordFile,
else => |byte| {
if (name_index == name.len or name[name_index] != byte) {
continue :sw .wait_for_next_line;
}
name_index += 1;
continue :sw .start;
},
},
.wait_for_next_line => switch (try reader.takeByte()) {
'\n' => {
name_index = 0;
continue :sw .start;
},
else => continue :sw .wait_for_next_line,
},
.skip_password => switch (try reader.takeByte()) {
'\n' => return error.CorruptPasswordFile,
':' => {
continue :sw .read_user_id;
},
else => continue :sw .skip_password,
},
.read_user_id => switch (try reader.takeByte()) {
':' => {
continue :sw .read_group_id;
},
'\n' => return error.CorruptPasswordFile,
else => |byte| {
const digit = switch (byte) {
'0'...'9' => byte - '0',
else => return error.CorruptPasswordFile,
};
{
const ov = @mulWithOverflow(uid, 10);
if (ov[1] != 0) return error.CorruptPasswordFile;
uid = ov[0];
}
{
const ov = @addWithOverflow(uid, digit);
if (ov[1] != 0) return error.CorruptPasswordFile;
uid = ov[0];
}
continue :sw .read_user_id;
},
},
.read_group_id => switch (try reader.takeByte()) {
'\n', ':' => return .{
.uid = uid,
.gid = gid,
},
else => |byte| {
const digit = switch (byte) {
'0'...'9' => byte - '0',
else => return error.CorruptPasswordFile,
};
{
const ov = @mulWithOverflow(gid, 10);
if (ov[1] != 0) return error.CorruptPasswordFile;
gid = ov[0];
}
{
const ov = @addWithOverflow(gid, digit);
if (ov[1] != 0) return error.CorruptPasswordFile;
gid = ov[0];
}
continue :sw .read_group_id;
},
},
}
comptime unreachable;
}
pub fn getBaseAddress() usize {
switch (native_os) {
.linux => {
const phdrs = std.posix.getSelfPhdrs();
var base: usize = 0;
for (phdrs) |phdr| switch (phdr.type) {
.LOAD => return base + phdr.vaddr,
.PHDR => base = @intFromPtr(phdrs.ptr) - phdr.vaddr,
else => {},
} else unreachable;
},
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
return @intFromPtr(&std.c._mh_execute_header);
},
.windows => return @intFromPtr(windows.kernel32.GetModuleHandleW(null)),
else => @compileError("Unsupported OS"),
}
}
/// Tells whether the target operating system supports replacing the current
/// process image. If this is `false` then calling `replace` or `replaceFile`
/// functions will return `error.OperationUnsupported`.
pub const can_replace = switch (native_os) {
.windows, .haiku, .wasi => false,
else => true,
};
/// Tells whether spawning child processes is supported.
pub const can_spawn = switch (native_os) {
.wasi, .ios, .tvos, .visionos, .watchos => false,
else => true,
};
pub const ReplaceError = error{
/// The target operating system cannot replace the process image with a new
/// one.
OperationUnsupported,
SystemResources,
AccessDenied,
PermissionDenied,
InvalidExe,
FileSystem,
IsDir,
FileNotFound,
NotDir,
FileBusy,
ProcessFdQuotaExceeded,
SystemFdQuotaExceeded,
} || Allocator.Error || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
pub const ReplaceOptions = struct {
argv: []const []const u8,
expand_arg0: ArgExpansion = .no_expand,
/// Replaces the environment when provided. The PATH value from here is
/// never used to resolve `argv[0]`.
environ_map: ?*const Environ.Map = null,
};
/// Replaces the current process image with the executed process. If this
/// function succeeds, it does not return.
///
/// `argv[0]` is the name of the process to replace the current one with. If it
/// is not already a file path (i.e. it contains '/'), it is resolved into a
/// file path based on PATH from the parent environment.
///
/// It is illegal to call this function in a fork() child.
pub fn replace(io: Io, options: ReplaceOptions) ReplaceError {
return io.vtable.processReplace(io.userdata, options);
}
/// Replaces the current process image with the executed process. If this
/// function succeeds, it does not return.
///
/// `argv[0]` is the file path of the process to replace the current one with,
/// relative to `dir`. It is *always* treated as a file path, even if it does
/// not contain '/'.
///
/// It is illegal to call this function in a fork() child.
pub fn replacePath(io: Io, dir: Io.Dir, options: ReplaceOptions) ReplaceError {
return io.vtable.processReplacePath(io.userdata, dir, options);
}
pub const ArgExpansion = enum { expand, no_expand };
/// File name extensions supported natively by `CreateProcess()` on Windows.
pub const WindowsExtension = enum { bat, cmd, com, exe };
pub const SpawnError = error{
/// The operating system does not support creating child processes.
OperationUnsupported,
OutOfMemory,
/// POSIX-only. `StdIo.ignore` was selected and opening `/dev/null` returned ENODEV.
NoDevice,
/// Windows-only. `cwd` or `argv` was provided and it was invalid WTF-8.
/// https://wtf-8.codeberg.page/
InvalidWtf8,
/// Windows-only. `cwd` was provided, but the path did not exist when spawning the child process.
CurrentWorkingDirectoryUnlinked,
/// Windows-only. NUL (U+0000), LF (U+000A), CR (U+000D) are not allowed
/// within arguments when executing a `.bat`/`.cmd` script.
/// - NUL/LF signifiies end of arguments, so anything afterwards
/// would be lost after execution.
/// - CR is stripped by `cmd.exe`, so any CR codepoints
/// would be lost after execution.
InvalidBatchScriptArg,
SystemResources,
AccessDenied,
PermissionDenied,
InvalidExe,
FileSystem,
IsDir,
FileNotFound,
NotDir,
FileBusy,
ProcessFdQuotaExceeded,
SystemFdQuotaExceeded,
ResourceLimitReached,
InvalidUserId,
InvalidProcessGroupId,
SymLinkLoop,
InvalidName,
/// An attempt was made to change the process group ID of one of the
/// children of the calling process and the child had already performed an
/// image replacement.
ProcessAlreadyExec,
} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
pub const SpawnOptions = struct {
argv: []const []const u8,
/// Set to change the current working directory when spawning the child process.
cwd: ?[]const u8 = null,
/// Set to change the current working directory when spawning the child process.
/// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190
/// Once that is done, `cwd` will be deprecated in favor of this field.
cwd_dir: ?Io.Dir = null,
/// Replaces the child environment when provided. The PATH value from here
/// is not used to resolve `argv[0]`; that resolution always uses parent
/// environment.
environ_map: ?*const Environ.Map = null,
expand_arg0: ArgExpansion = .no_expand,
/// When populated, a pipe will be created for the child process to
/// communicate progress back to the parent. The file descriptor of the
/// write end of the pipe will be specified in the `ZIG_PROGRESS`
/// environment variable inside the child process. The progress reported by
/// the child will be attached to this progress node in the parent process.
///
/// The child's progress tree will be grafted into the parent's progress tree,
/// by substituting this node with the child's root node.
progress_node: std.Progress.Node = std.Progress.Node.none,
stdin: StdIo = .inherit,
stdout: StdIo = .inherit,
stderr: StdIo = .inherit,
/// Set to true to obtain rusage information for the child process.
/// Depending on the target platform and implementation status, the
/// requested statistics may or may not be available. If they are
/// available, then the `resource_usage_statistics` field will be populated
/// after calling `wait`.
/// On Linux and Darwin, this obtains rusage statistics from wait4().
request_resource_usage_statistics: bool = false,
/// Set to change the user id when spawning the child process.
uid: ?posix.uid_t = null,
/// Set to change the group id when spawning the child process.
gid: ?posix.gid_t = null,
/// Set to change the process group id when spawning the child process.
pgid: ?posix.pid_t = null,
/// Start child process in suspended state.
/// For Posix systems it's started as if SIGSTOP was sent.
start_suspended: bool = false,
/// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess.
create_no_window: bool = false,
/// Darwin-only. Disable ASLR for the child process.
disable_aslr: bool = false,
/// Behavior of the child process's standard input, output, and error streams.
pub const StdIo = union(enum) {
/// Inherit the corresponding stream from the parent process.
inherit,
/// Pass an already open file from the parent to the child.
file: File,
/// Pass a null stream to the child process by opening "/dev/null" on POSIX
/// and "NUL" on Windows.
ignore,
/// Create a new pipe for the stream.
///
/// The corresponding field (`stdout`, `stderr`, or `stdin`) will be
/// assigned a `File` object that can be used to read from or write to the
/// pipe.
pipe,
/// Spawn the child process with the corresponding stream missing. This
/// will likely result in the child encountering EBADF if it tries to use
/// stdin, stdout, or stderr, or if only one stream is closed, it will
/// result in them getting mixed up. Generally, this option is for advanced
/// use cases only.
close,
};
};
/// Creates a child process.
///
/// `argv[0]` is the name of the program to execute. If it is not already a
/// file path (i.e. it contains '/'), it is resolved into a file path based on
/// PATH from the parent environment.
pub fn spawn(io: Io, options: SpawnOptions) SpawnError!Child {
return io.vtable.processSpawn(io.userdata, options);
}
/// Creates a child process.
///
/// `argv[0]` is the file path of the program to execute, relative to `dir`. It
/// is *always* treated as a file path, even if it does not contain '/'.
pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child {
return io.vtable.processSpawnPath(io.userdata, dir, options);
}
pub const RunError = posix.GetCwdError || posix.ReadError || SpawnError || posix.PollError || error{
StdoutStreamTooLong,
StderrStreamTooLong,
};
pub const RunOptions = struct {
argv: []const []const u8,
max_output_bytes: usize = 50 * 1024,
/// Set to change the current working directory when spawning the child process.
cwd: ?[]const u8 = null,
/// Set to change the current working directory when spawning the child process.
/// This is not yet implemented for Windows. See https://github.com/ziglang/zig/issues/5190
/// Once that is done, `cwd` will be deprecated in favor of this field.
cwd_dir: ?Io.Dir = null,
/// Replaces the child environment when provided. The PATH value from here
/// is not used to resolve `argv[0]`; that resolution always uses parent
/// environment.
environ_map: ?*const Environ.Map = null,
expand_arg0: ArgExpansion = .no_expand,
/// When populated, a pipe will be created for the child process to
/// communicate progress back to the parent. The file descriptor of the
/// write end of the pipe will be specified in the `ZIG_PROGRESS`
/// environment variable inside the child process. The progress reported by
/// the child will be attached to this progress node in the parent process.
///
/// The child's progress tree will be grafted into the parent's progress tree,
/// by substituting this node with the child's root node.
progress_node: std.Progress.Node = std.Progress.Node.none,
/// Windows-only. Sets the CREATE_NO_WINDOW flag in CreateProcess.
create_no_window: bool = true,
/// Darwin-only. Disable ASLR for the child process.
disable_aslr: bool = false,
};
pub const RunResult = struct {
term: Child.Term,
stdout: []u8,
stderr: []u8,
};
/// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
/// If it succeeds, the caller owns result.stdout and result.stderr memory.
pub fn run(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult {
var child = try spawn(io, .{
.argv = options.argv,
.cwd = options.cwd,
.cwd_dir = options.cwd_dir,
.environ_map = options.environ_map,
.expand_arg0 = options.expand_arg0,
.progress_node = options.progress_node,
.create_no_window = options.create_no_window,
.disable_aslr = options.disable_aslr,
.stdin = .ignore,
.stdout = .pipe,
.stderr = .pipe,
});
defer child.kill(io);
var stdout: std.ArrayList(u8) = .empty;
defer stdout.deinit(gpa);
var stderr: std.ArrayList(u8) = .empty;
defer stderr.deinit(gpa);
try child.collectOutput(gpa, &stdout, &stderr, options.max_output_bytes);
return .{
.stdout = try stdout.toOwnedSlice(gpa),
.stderr = try stderr.toOwnedSlice(gpa),
.term = try child.wait(io),
};
}
pub const TotalSystemMemoryError = error{
UnknownTotalSystemMemory,
};
/// Returns the total system memory, in bytes as a u64.
/// We return a u64 instead of usize due to PAE on ARM
/// and Linux's /proc/meminfo reporting more memory when
/// using QEMU user mode emulation.
pub fn totalSystemMemory() TotalSystemMemoryError!u64 {
switch (native_os) {
.linux => {
var info: std.os.linux.Sysinfo = undefined;
const result: usize = std.os.linux.sysinfo(&info);
if (std.os.linux.errno(result) != .SUCCESS) {
return error.UnknownTotalSystemMemory;
}
// Promote to u64 to avoid overflow on systems where info.totalram is a 32-bit usize
return @as(u64, info.totalram) * info.mem_unit;
},
.freebsd => {
var physmem: c_ulong = undefined;
var len: usize = @sizeOf(c_ulong);
posix.sysctlbynameZ("hw.physmem", &physmem, &len, null, 0) catch |err| switch (err) {
error.UnknownName => unreachable,
else => return error.UnknownTotalSystemMemory,
};
return @as(u64, @intCast(physmem));
},
// whole Darwin family
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
// "hw.memsize" returns uint64_t
var physmem: u64 = undefined;
var len: usize = @sizeOf(u64);
posix.sysctlbynameZ("hw.memsize", &physmem, &len, null, 0) catch |err| switch (err) {
error.PermissionDenied => unreachable, // only when setting values,
error.SystemResources => unreachable, // memory already on the stack
error.UnknownName => unreachable, // constant, known good value
else => return error.UnknownTotalSystemMemory,
};
return physmem;
},
.openbsd => {
const mib: [2]c_int = [_]c_int{
posix.CTL.HW,
posix.HW.PHYSMEM64,
};
var physmem: i64 = undefined;
var len: usize = @sizeOf(@TypeOf(physmem));
posix.sysctl(&mib, &physmem, &len, null, 0) catch |err| switch (err) {
error.NameTooLong => unreachable, // constant, known good value
error.PermissionDenied => unreachable, // only when setting values,
error.SystemResources => unreachable, // memory already on the stack
error.UnknownName => unreachable, // constant, known good value
else => return error.UnknownTotalSystemMemory,
};
assert(physmem >= 0);
return @as(u64, @bitCast(physmem));
},
.windows => {
var sbi: windows.SYSTEM_BASIC_INFORMATION = undefined;
const rc = windows.ntdll.NtQuerySystemInformation(
.SystemBasicInformation,
&sbi,
@sizeOf(windows.SYSTEM_BASIC_INFORMATION),
null,
);
if (rc != .SUCCESS) {
return error.UnknownTotalSystemMemory;
}
return @as(u64, sbi.NumberOfPhysicalPages) * sbi.PageSize;
},
else => return error.UnknownTotalSystemMemory,
}
}
/// Indicate intent to terminate with a successful exit code.
///
/// In debug builds, this is a no-op, so that the calling code's cleanup
/// mechanisms are tested and so that external tools checking for resource
/// leaks can be accurate. In release builds, this calls `exit` with code zero,
/// and does not return.
pub fn cleanExit(io: Io) void {
if (builtin.mode == .Debug) return;
_ = io.lockStderr(&.{}, .no_color) catch {};
exit(0);
}
/// Request ability to have more open file descriptors simultaneously.
///
/// On some systems, this raises the limit before seeing ProcessFdQuotaExceeded
/// errors. On other systems, this does nothing.
pub fn raiseFileDescriptorLimit() void {
const have_rlimit = posix.rlimit_resource != void;
if (!have_rlimit) return;
var lim = posix.getrlimit(.NOFILE) catch return; // Oh well; we tried.
if (native_os.isDarwin()) {
// On Darwin, `NOFILE` is bounded by a hardcoded value `OPEN_MAX`.
// According to the man pages for setrlimit():
// setrlimit() now returns with errno set to EINVAL in places that historically succeeded.
// It no longer accepts "rlim_cur = RLIM.INFINITY" for RLIM.NOFILE.
// Use "rlim_cur = min(OPEN_MAX, rlim_max)".
lim.max = @min(std.c.OPEN_MAX, lim.max);
}
if (lim.cur == lim.max) return;
// Do a binary search for the limit.
var min: posix.rlim_t = lim.cur;
var max: posix.rlim_t = 1 << 20;
// But if there's a defined upper bound, don't search, just set it.
if (lim.max != posix.RLIM.INFINITY) {
min = lim.max;
max = lim.max;
}
while (true) {
lim.cur = min + @divTrunc(max - min, 2); // on freebsd rlim_t is signed
if (posix.setrlimit(.NOFILE, lim)) |_| {
min = lim.cur;
} else |_| {
max = lim.cur;
}
if (min + 1 >= max) break;
}
}
test raiseFileDescriptorLimit {
raiseFileDescriptorLimit();
}
/// Logs an error and then terminates the process with exit code 1.
pub fn fatal(comptime format: []const u8, format_arguments: anytype) noreturn {
std.log.err(format, format_arguments);
exit(1);
}
pub const ExecutablePathBaseError = error{
FileNotFound,
AccessDenied,
/// The operating system does not support an executable learning its own
/// path.
OperationUnsupported,
NotDir,
SymLinkLoop,
InputOutput,
FileTooBig,
IsDir,
ProcessFdQuotaExceeded,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
NoSpaceLeft,
FileSystem,
BadPathName,
DeviceBusy,
SharingViolation,
PipeBusy,
NotLink,
PathAlreadyExists,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
ProcessNotFound,
/// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows
/// intercepts file system operations and makes them significantly slower
/// in addition to possibly failing with this error code.
AntivirusInterference,
/// On Windows, the volume does not contain a recognized file system. File
/// system drivers might not be loaded, or the volume may be corrupt.
UnrecognizedVolume,
PermissionDenied,
} || Io.Cancelable || Io.UnexpectedError;
pub const ExecutablePathAllocError = ExecutablePathBaseError || Allocator.Error;
pub fn executablePathAlloc(io: Io, allocator: Allocator) ExecutablePathAllocError![:0]u8 {
var buffer: [max_path_bytes]u8 = undefined;
const n = executablePath(io, &buffer) catch |err| switch (err) {
error.NameTooLong => unreachable,
else => |e| return e,
};
return allocator.dupeZ(u8, buffer[0..n]);
}
pub const ExecutablePathError = ExecutablePathBaseError || error{NameTooLong};
/// Get the path to the current executable, following symlinks.
///
/// This function may return an error if the current executable
/// was deleted after spawning.
///
/// Returned value is a slice of out_buffer.
///
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
///
/// On Linux, depends on procfs being mounted. If the currently executing binary has
/// been deleted, the file path looks something like "/a/b/c/exe (deleted)".
///
/// See also:
/// * `executableDirPath` - to obtain only the directory
/// * `openExecutable` - to obtain only an open file handle
pub fn executablePath(io: Io, out_buffer: []u8) ExecutablePathError!usize {
return io.vtable.processExecutablePath(io.userdata, out_buffer);
}
/// Get the directory path that contains the current executable.
///
/// Returns index into `out_buffer`.
///
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
pub fn executableDirPath(io: Io, out_buffer: []u8) ExecutablePathError!usize {
const n = try executablePath(io, out_buffer);
// Assert that the OS APIs return absolute paths, and therefore dirname
// will not return null.
return std.fs.path.dirname(out_buffer[0..n]).?.len;
}
/// Same as `executableDirPath` except allocates the result.
pub fn executableDirPathAlloc(io: Io, allocator: Allocator) ExecutablePathAllocError![]u8 {
var buffer: [max_path_bytes]u8 = undefined;
const dir_path_len = executableDirPath(io, &buffer) catch |err| switch (err) {
error.NameTooLong => unreachable,
else => |e| return e,
};
return allocator.dupe(u8, buffer[0..dir_path_len]);
}
pub const OpenExecutableError = File.OpenError || ExecutablePathError || File.LockError;
pub fn openExecutable(io: Io, flags: File.OpenFlags) OpenExecutableError!File {
return io.vtable.processExecutableOpen(io.userdata, flags);
}
/// Causes abnormal process termination.
///
/// If linking against libc, this calls `std.c.abort`. Otherwise it raises
/// SIGABRT followed by SIGKILL.
///
/// Invokes the current signal handler for SIGABRT, if any.
pub fn abort() noreturn {
@branchHint(.cold);
// MSVCRT abort() sometimes opens a popup window which is undesirable, so
// even when linking libc on Windows we use our own abort implementation.
// See https://github.com/ziglang/zig/issues/2071 for more details.
if (native_os == .windows) {
if (builtin.mode == .Debug and windows.peb().BeingDebugged != 0) {
@breakpoint();
}
windows.ntdll.RtlExitUserProcess(3);
}
if (!builtin.link_libc and native_os == .linux) {
// The Linux man page says that the libc abort() function
// "first unblocks the SIGABRT signal", but this is a footgun
// for user-defined signal handlers that want to restore some state in
// some program sections and crash in others.
// So, the user-installed SIGABRT handler is run, if present.
posix.raise(.ABRT) catch {};
// Disable all signal handlers.
const filledset = std.os.linux.sigfillset();
posix.sigprocmask(posix.SIG.BLOCK, &filledset, null);
// Only one thread may proceed to the rest of abort().
if (!builtin.single_threaded) {
const global = struct {
var abort_entered: bool = false;
};
while (@cmpxchgWeak(bool, &global.abort_entered, false, true, .seq_cst, .seq_cst)) |_| {}
}
// Install default handler so that the tkill below will terminate.
const sigact: posix.Sigaction = .{
.handler = .{ .handler = posix.SIG.DFL },
.mask = posix.sigemptyset(),
.flags = 0,
};
posix.sigaction(.ABRT, &sigact, null);
_ = std.os.linux.tkill(std.os.linux.gettid(), .ABRT);
var sigabrtmask = posix.sigemptyset();
posix.sigaddset(&sigabrtmask, .ABRT);
posix.sigprocmask(posix.SIG.UNBLOCK, &sigabrtmask, null);
// Beyond this point should be unreachable.
@as(*allowzero volatile u8, @ptrFromInt(0)).* = 0;
posix.raise(.KILL) catch {};
exit(127); // Pid 1 might not be signalled in some containers.
}
switch (native_os) {
.uefi, .wasi, .emscripten, .cuda, .amdhsa => @trap(),
else => posix.system.abort(),
}
}
/// Exits all threads of the program with the specified status code.
pub fn exit(status: u8) noreturn {
if (builtin.link_libc) {
std.c.exit(status);
} else switch (native_os) {
.windows => windows.ntdll.RtlExitUserProcess(status),
.wasi => std.os.wasi.proc_exit(status),
.linux => {
if (!builtin.single_threaded) std.os.linux.exit_group(status);
posix.system.exit(status);
},
.uefi => {
const uefi = std.os.uefi;
// exit() is only available if exitBootServices() has not been called yet.
// This call to exit should not fail, so we catch-ignore errors.
if (uefi.system_table.boot_services) |bs| {
bs.exit(uefi.handle, @enumFromInt(status), null) catch {};
}
// If we can't exit, reboot the system instead.
uefi.system_table.runtime_services.resetSystem(.cold, @enumFromInt(status), null);
},
else => posix.system.exit(status),
}
}
pub const SetCurrentDirError = error{
AccessDenied,
BadPathName,
FileNotFound,
FileSystem,
NameTooLong,
NoDevice,
NotDir,
OperationUnsupported,
UnrecognizedVolume,
} || Io.Cancelable || Io.UnexpectedError;
/// Changes the current working directory to the open directory handle.
/// Corresponds to "fchdir" in libc.
///
/// This modifies global process state and can have surprising effects in
/// multithreaded applications. Most applications and especially libraries
/// should not call this function as a general rule, however it can have use
/// cases in, for example, implementing a shell, or child process execution.
///
/// Calling this function makes code less portable and less reusable.
pub fn setCurrentDir(io: Io, dir: Io.Dir) !void {
return io.vtable.processSetCurrentDir(io.userdata, dir);
}