Skip to documentation
SLOP

tiny.sandbox.command

Reference tiny.sandbox command

Defined in tiny.sandbox.

API (8)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.sandboxcommand
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallstest sourcelib.sandbox.src.commandtest: command deadline kills descenda...test sourcelib.sandbox.src.commandtest: isolated command lookup ignores...private sourcelib.sandbox.src.command.Deadlineexpiredprivate sourcelib.sandbox.src.command.Deadlineinitprivate sourcelib.sandbox.src.commandchildProcessGroupprivate sourcelib.sandbox.src.commandstopChildprivate sourcelib.sandbox.src.commandtimeoutFromMillisecondsprivate sourcelib.sandbox.src.commandwaitUntilDeadlinecommandrun
Static calls · unresolved targets: 1 · external targets: 18.

Source: lib/sandbox/src/command.zig

zig
const std = @import("std");const builtin = @import("builtin");const sys = @import("sys");const Allocator = std.mem.Allocator;const poll_ms = 25;const Deadline = struct {    clock: sys.time.AwakeClock,    end: sys.time.AwakeInstant,    fn init(clock: sys.time.AwakeClock, timeout_ms: u64) sys.time.ClockError!Deadline {        const timeout = sys.time.Duration.fromMilliseconds(@max(timeout_ms, 1));        return .{            .clock = clock,            .end = (try clock.now()).deadlineAfter(timeout),        };    }    fn expired(self: Deadline) sys.time.ClockError!bool {        return (try self.clock.now()).reached(self.end);    }    fn remainingMilliseconds(self: Deadline) sys.time.ClockError!?u64 {        const remaining = (try self.clock.now()).remainingUntil(self.end);        if (remaining.isZero()) return null;        return remaining.asMillisecondsCeil();    }};pub const Status = union(enum) {    term: sys.process.Termination,    pub fn exitCode(self: Status) ?i64 {        return switch (self) {            .term => |term| sys.process.exitCode(term),        };    }};pub const Interrupt = struct {    ptr: ?*const anyopaque = null,    checkFn: ?*const fn (?*const anyopaque) anyerror!void = null,    pub fn check(self: Interrupt) !void {        const checkFn = self.checkFn orelse return;        try checkFn(self.ptr);    }};pub const Result = struct {    term: sys.process.Termination,    stdout: []u8,    stderr: []u8,    pub fn deinit(self: *Result, allocator: Allocator) void {        allocator.free(self.stdout);        allocator.free(self.stderr);        self.* = undefined;    }};pub const DescriptorPolicy = enum {    inherited,    isolated,};pub const Options = struct {    argv: []const []const u8,    cwd: sys.process.Child.Cwd,    environ_map: ?*const sys.process.Environ.Map = null,    stdout_limit: usize,    stderr_limit: usize,    timeout_ms: u64,    interrupt: Interrupt = .{},    awake_clock: sys.time.AwakeClock = .system(),    descriptor_policy: DescriptorPolicy = .inherited,};pub fn run(allocator: Allocator, options: Options) !Result {    var io_state = sys.thread.initThreadedIo(allocator, .{});    defer io_state.deinit();    const io = io_state.io();    var child = switch (options.descriptor_policy) {        .inherited => try sys.process.spawn(io, .{            .argv = options.argv,            .cwd = options.cwd,            .environ_map = options.environ_map,            .stdin = .ignore,            .stdout = .pipe,            .stderr = .pipe,            .pgid = childProcessGroup(),        }),        .isolated => isolated: {            var empty_environment = sys.process.Environ.Map.init(allocator);            defer empty_environment.deinit();            switch (options.cwd) {                .inherit => {},                else => return error.DescriptorIsolationFailed,            }            break :isolated try sys.process.spawnCapturedIsolated(allocator, .{                .argv = options.argv,                .environment = options.environ_map orelse &empty_environment,            });        },    };    var multi_reader_buffer: std.Io.File.MultiReader.Buffer(2) = undefined;    var multi_reader: std.Io.File.MultiReader = undefined;    multi_reader.init(allocator, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });    defer multi_reader.deinit();    defer sys.process.killAndReap(&child, io);    const stdout_reader = multi_reader.reader(0);    const stderr_reader = multi_reader.reader(1);    const deadline = try Deadline.init(options.awake_clock, options.timeout_ms);    while (true) {        options.interrupt.check() catch |err| {            stopChild(&child, io);            return err;        };        if (try deadline.expired()) {            stopChild(&child, io);            return error.Timeout;        }        multi_reader.fill(64, timeoutFromMilliseconds(io, poll_ms)) catch |err| switch (err) {            error.Timeout => continue,            error.EndOfStream => break,            else => |actual| return actual,        };        if (stdout_reader.buffered().len > options.stdout_limit) {            stopChild(&child, io);            return error.StdoutStreamTooLong;        }        if (stderr_reader.buffered().len > options.stderr_limit) {            stopChild(&child, io);            return error.StderrStreamTooLong;        }    }    try multi_reader.checkAnyError();    const term = try waitUntilDeadline(        &child,        io,        deadline,        options.interrupt,    );    const stdout = try multi_reader.toOwnedSlice(0);    errdefer allocator.free(stdout);    const stderr = try multi_reader.toOwnedSlice(1);    return .{        .term = term,        .stdout = stdout,        .stderr = stderr,    };}fn childProcessGroup() @TypeOf(@as(sys.process.SpawnOptions, undefined).pgid) {    if (comptime builtin.os.tag == .windows) return null;    return 0;}fn stopChild(child: *sys.process.Child, io: std.Io) void {    const child_id = child.id orelse return;    sys.process.signalChildGroup(child_id, .terminate) catch sys.process.requestTermination(child, io);    sys.process.signalChildGroup(child_id, .kill) catch sys.process.forceKillChildId(child_id);}fn waitUntilDeadline(    child: *sys.process.Child,    io: std.Io,    deadline: Deadline,    interrupt: Interrupt,) !sys.process.Termination {    const Outcome = union(enum) {        exit: sys.process.WaitError!sys.process.Termination,        tick: std.Io.Cancelable!void,    };    var outcomes: [2]Outcome = undefined;    var select = std.Io.Select(Outcome).init(io, &outcomes);    select.async(.exit, waitChild, .{ child, io });    while (true) {        interrupt.check() catch |err| {            stopChild(child, io);            select.cancelDiscard();            return err;        };        const remaining_ms = try deadline.remainingMilliseconds() orelse {            stopChild(child, io);            select.cancelDiscard();            return error.Timeout;        };        select.async(.tick, waitTick, .{ io, @min(remaining_ms, poll_ms) });        switch (try select.await()) {            .exit => |result| {                select.cancelDiscard();                return try result;            },            .tick => |result| try result,        }    }}fn waitChild(    child: *sys.process.Child,    io: std.Io,) sys.process.WaitError!sys.process.Termination {    return try child.wait(io);}fn waitTick(io: std.Io, milliseconds: u64) std.Io.Cancelable!void {    return try timeoutFromMilliseconds(io, milliseconds).sleep(io);}fn timeoutFromMilliseconds(io: std.Io, milliseconds: u64) std.Io.Timeout {    const max_milliseconds: u64 = @intCast(std.math.maxInt(i64));    const bounded_ms = @min(milliseconds, max_milliseconds);    const duration: std.Io.Clock.Duration = .{        .clock = .awake,        .raw = std.Io.Duration.fromMilliseconds(@intCast(bounded_ms)),    };    return .{ .deadline = std.Io.Clock.Timestamp.fromNow(io, duration) };}test "command deadline kills descendants after output streams close" {    if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) {        return error.SkipZigTest;    }    var temporary = std.testing.tmpDir(.{});    defer temporary.cleanup();    const root = try temporary.dir.realPathFileAlloc(        std.Options.debug_io,        ".",        std.testing.allocator,    );    defer std.testing.allocator.free(root);    const marker = try std.fs.path.join(        std.testing.allocator,        &.{ root, "survived" },    );    defer std.testing.allocator.free(marker);    const argv = [_][]const u8{        "sh",        "-c",        "exec 1>&- 2>&-; (sleep .2; : > \"$1\") & wait",        "sandbox-command",        marker,    };    const started = try sys.time.awakeNow();    try std.testing.expectError(error.Timeout, run(std.testing.allocator, .{        .argv = &argv,        .cwd = .{ .path = root },        .stdout_limit = 4096,        .stderr_limit = 4096,        .timeout_ms = 25,    }));    const elapsed = try (try sys.time.awakeNow()).elapsedSince(started);    try std.testing.expect(elapsed.asMillisecondsFloor() < 1_000);    sys.time.sleepMilliseconds(300);    const survived = exists: {        std.Io.Dir.cwd().access(std.Options.debug_io, marker, .{}) catch            break :exists false;        break :exists true;    };    try std.testing.expect(!survived);}test "command deadline ignores wall jumps and suspend gaps" {    var clock = sys.time.FakeClock.zero();    const deadline = try Deadline.init(clock.awakeClock(), 10);    clock.setWall(.fromNanoseconds(std.math.maxInt(u64)));    clock.suspendGap(.fromMilliseconds(20));    try std.testing.expect(!try deadline.expired());    clock.advance(.fromMilliseconds(10));    try std.testing.expect(try deadline.expired());}test "isolated command lookup ignores replacement child PATH" {    if (comptime builtin.os.tag != .linux) return error.SkipZigTest;    if (comptime !sys.fs.FilePermissions.has_executable_bit) {        return error.SkipZigTest;    }    var temporary = std.testing.tmpDir(.{});    defer temporary.cleanup();    const root = try temporary.dir.realPathFileAlloc(        std.Options.debug_io,        ".",        std.testing.allocator,    );    defer std.testing.allocator.free(root);    try temporary.dir.writeFile(std.Options.debug_io, .{        .sub_path = "sh",        .data = "#!/bin/sh\nprintf fake",    });    try temporary.dir.setFilePermissions(        std.Options.debug_io,        "sh",        .executable_file,        .{},    );    var environment = sys.process.Environ.Map.init(std.testing.allocator);    defer environment.deinit();    try environment.put("PATH", root);    const argv = [_][]const u8{        "sh",        "-c",        "printf '%s' \"$PATH\"",    };    var result = try run(std.testing.allocator, .{        .argv = &argv,        .cwd = .inherit,        .environ_map = &environment,        .stdout_limit = 4096,        .stderr_limit = 4096,        .timeout_ms = 1_000,        .descriptor_policy = .isolated,    });    defer result.deinit(std.testing.allocator);    try std.testing.expectEqual(@as(i64, 0), result.term.exited);    try std.testing.expectEqualStrings(root, result.stdout);}

Source: lib/sandbox/src/root.zig:32

zig
pub const command = @import("command.zig");

Audit

Definitions8
Public names8
Members16
Version26.7.0
Revisiondaab053ee433