tiny.sandbox.command
Defined in tiny.sandbox.
API (8)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
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
| Definitions | 8 |
|---|---|
| Public names | 8 |
| Members | 16 |
| Version | 26.7.0 |
| Revision | daab053ee433 |