PranitInitial commit439f83c2 months agoHistory
440 lines · 12.3 KB · Zig
RawDownload
const std = @import("std");const builtin = @import("builtin");const process_supervisor = @import("../background/process_supervisor.zig"); const Allocator = std.mem.Allocator; pub const exit_marker = "__FX_EXIT_CODE__="; pub fn isValidPidText(pid: []const u8) bool {    if (pid.len == 0) return false;    for (pid) |byte| {        if (!std.ascii.isDigit(byte)) return false;    }    return true;} pub const ProviderError = Allocator.Error ||    std.process.SpawnError ||    std.Io.File.ReadStreamingError ||    std.Io.File.Writer.Error || error{    Unsupported,    SpawnFailed,    BackgroundWrapperNotReady,    BackgroundProcessIdentityIndeterminate,    BackgroundProcessIdentityMismatch,    BackgroundProcessIdentityUnavailable,    BackgroundReleaseFailed,    BackgroundLogUnavailable,    ProcessIdentityUnavailable,    ProcessIdentityUnsupported,    ProcessNotFound,    PermissionDenied,    Unexpected,    InvalidPid,}; pub const Isolation = union(enum) {    none,    macos_profile: []const u8,}; pub const OutputCapability = struct {    /// Opaque, borrowed output authority supplied by the host composition.    context: *const anyopaque,}; pub const SpawnRequest = struct {    cwd: []const u8,    output: OutputCapability,    isolation: Isolation,}; pub const CleanupStatus = enum {    confirmed,    timed_out,}; pub const OwnedProcess = struct {    context: *anyopaque,    wait_fn: *const fn (*anyopaque) void,    forget_fn: *const fn (*anyopaque) void,     /// Consumes the handle after the provider-owned child has exited.    pub fn wait(self: *OwnedProcess) void {        self.wait_fn(self.context);        self.* = undefined;    }     /// Consumes the handle without changing the child process lifecycle.    pub fn forget(self: *OwnedProcess) void {        self.forget_fn(self.context);        self.* = undefined;    }}; pub const PreparedProcess = struct {    context: *anyopaque,    /// Borrowed from `context` and valid until a consuming method succeeds.    pid: []const u8,    close_and_wait_fn: *const fn (        *anyopaque,        ?process_supervisor.ProcessInstanceToken,        i64,    ) CleanupStatus,    wait_for_exit_fn: *const fn (        *anyopaque,        ?process_supervisor.ProcessInstanceToken,        i64,    ) bool,    detach_reaper_fn: *const fn (*anyopaque) bool,    release_fn: *const fn (*anyopaque, []const u8) ProviderError!OwnedProcess,     /// Consumes the handle only when cleanup is confirmed.    pub fn closeAndWaitUnreleased(        self: *PreparedProcess,        process_token: ?process_supervisor.ProcessInstanceToken,        timeout_ms: i64,    ) CleanupStatus {        const status = self.close_and_wait_fn(            self.context,            process_token,            timeout_ms,        );        if (status == .confirmed) self.* = undefined;        return status;    }     /// Consumes the handle only when process exit is confirmed.    pub fn waitForUnreleasedExit(        self: *PreparedProcess,        process_token: ?process_supervisor.ProcessInstanceToken,        timeout_ms: i64,    ) bool {        const exited = self.wait_for_exit_fn(            self.context,            process_token,            timeout_ms,        );        if (exited) self.* = undefined;        return exited;    }     /// Consumes the handle only when the provider retains cleanup ownership.    pub fn detachUnreleasedReaper(self: *PreparedProcess) bool {        const detached = self.detach_reaper_fn(self.context);        if (detached) self.* = undefined;        return detached;    }     /// Releases the blocked command and consumes the prepared handle.    pub fn release(        self: *PreparedProcess,        original_command: []const u8,    ) ProviderError!OwnedProcess {        const owned = try self.release_fn(self.context, original_command);        self.* = undefined;        return owned;    }}; pub const Provider = struct {    /// Provider implementations own `context`; callers borrow it.    context: ?*anyopaque = null,    spawn_prepared_fn: *const fn (        ?*anyopaque,        Allocator,        SpawnRequest,    ) ProviderError!PreparedProcess,    capture_token_fn: *const fn (        ?*anyopaque,        Allocator,        []const u8,    ) ProviderError!process_supervisor.ProcessInstanceToken,    match_token_fn: *const fn (        ?*anyopaque,        Allocator,        []const u8,        process_supervisor.ProcessInstanceToken,    ) process_supervisor.TokenMatch,    signal_process_fn: *const fn (        ?*anyopaque,        Allocator,        []const u8,        process_supervisor.ProcessInstanceToken,    ) ProviderError!void,     pub fn spawnPrepared(        self: Provider,        alloc: Allocator,        request: SpawnRequest,    ) ProviderError!PreparedProcess {        return self.spawn_prepared_fn(            self.context,            alloc,            request,        );    }     pub fn captureToken(        self: Provider,        alloc: Allocator,        pid: []const u8,    ) ProviderError!process_supervisor.ProcessInstanceToken {        return self.capture_token_fn(self.context, alloc, pid);    }     pub fn matchToken(        self: Provider,        alloc: Allocator,        pid: []const u8,        expected: process_supervisor.ProcessInstanceToken,    ) process_supervisor.TokenMatch {        return self.match_token_fn(            self.context,            alloc,            pid,            expected,        );    }     pub fn signalProcess(        self: Provider,        alloc: Allocator,        pid: []const u8,        expected: process_supervisor.ProcessInstanceToken,    ) ProviderError!void {        return self.signal_process_fn(            self.context,            alloc,            pid,            expected,        );    }}; fn unsupportedSpawnPrepared(    _: ?*anyopaque,    _: Allocator,    _: SpawnRequest,) ProviderError!PreparedProcess {    return error.Unsupported;} fn unsupportedCaptureToken(    _: ?*anyopaque,    _: Allocator,    _: []const u8,) ProviderError!process_supervisor.ProcessInstanceToken {    return error.Unsupported;} fn unavailableMatchToken(    _: ?*anyopaque,    _: Allocator,    _: []const u8,    _: process_supervisor.ProcessInstanceToken,) process_supervisor.TokenMatch {    return .unavailable;} fn captureTokenThroughProcessSupervisor(    _: ?*anyopaque,    alloc: Allocator,    pid: []const u8,) ProviderError!process_supervisor.ProcessInstanceToken {    return process_supervisor.captureProcessInstanceToken(        alloc,        pid,    ) catch |err| switch (err) {        error.OutOfMemory => error.OutOfMemory,        error.InvalidPid => error.InvalidPid,        error.ProcessNotFound => error.ProcessNotFound,        error.ProcessIdentityUnavailable => error.ProcessIdentityUnavailable,        else => error.ProcessIdentityUnsupported,    };} fn matchTokenThroughProcessSupervisor(    _: ?*anyopaque,    alloc: Allocator,    pid: []const u8,    expected: process_supervisor.ProcessInstanceToken,) process_supervisor.TokenMatch {    return process_supervisor.matchProcessInstanceToken(        alloc,        pid,        expected,    );} fn unsupportedSignalProcess(    _: ?*anyopaque,    _: Allocator,    _: []const u8,    _: process_supervisor.ProcessInstanceToken,) ProviderError!void {    return error.Unsupported;} pub const unavailable_provider = Provider{    .spawn_prepared_fn = unsupportedSpawnPrepared,    .capture_token_fn = unsupportedCaptureToken,    .match_token_fn = unavailableMatchToken,    .signal_process_fn = unsupportedSignalProcess,}; pub const process_supervisor_test_provider = if (builtin.is_test)    Provider{        .spawn_prepared_fn = unsupportedSpawnPrepared,        .capture_token_fn = captureTokenThroughProcessSupervisor,        .match_token_fn = matchTokenThroughProcessSupervisor,        .signal_process_fn = unsupportedSignalProcess,    }else    unavailable_provider; test "unavailable provider does not consult process supervisor test hooks" {    const Stub = struct {        var calls: usize = 0;         fn capture(            _: Allocator,            _: []const u8,        ) anyerror!process_supervisor.ProcessInstanceToken {            calls += 1;            return error.ProcessNotFound;        }    };    Stub.calls = 0;    process_supervisor.process_token_capture_for_test = Stub.capture;    defer process_supervisor.process_token_capture_for_test = null;     try std.testing.expectError(        error.Unsupported,        unavailable_provider.captureToken(std.testing.allocator, "123"),    );    try std.testing.expectEqual(@as(usize, 0), Stub.calls);} test "provider routes lifecycle operations through one injected owner" {    const Fake = struct {        spawns: usize = 0,        releases: usize = 0,        waits: usize = 0,         fn spawn(            raw: ?*anyopaque,            _: Allocator,            request: SpawnRequest,        ) ProviderError!PreparedProcess {            const self: *@This() = @ptrCast(@alignCast(raw.?));            if (!std.mem.eql(u8, request.cwd, "/workspace") or                request.isolation != .none)            {                return error.SpawnFailed;            }            if (request.output.context != @as(*const anyopaque, @ptrCast(self))) {                return error.SpawnFailed;            }            self.spawns += 1;            return .{                .context = self,                .pid = "42",                .close_and_wait_fn = close,                .wait_for_exit_fn = waitForExit,                .detach_reaper_fn = detach,                .release_fn = release,            };        }         fn release(raw: *anyopaque, command: []const u8) ProviderError!OwnedProcess {            const self: *@This() = @ptrCast(@alignCast(raw));            if (!std.mem.eql(u8, "npm run dev", command)) {                return error.BackgroundReleaseFailed;            }            self.releases += 1;            return .{                .context = raw,                .wait_fn = wait,                .forget_fn = forget,            };        }         fn wait(raw: *anyopaque) void {            const self: *@This() = @ptrCast(@alignCast(raw));            self.waits += 1;        }         fn forget(_: *anyopaque) void {}         fn close(            _: *anyopaque,            _: ?process_supervisor.ProcessInstanceToken,            _: i64,        ) CleanupStatus {            return .timed_out;        }         fn waitForExit(            _: *anyopaque,            _: ?process_supervisor.ProcessInstanceToken,            _: i64,        ) bool {            return false;        }         fn detach(_: *anyopaque) bool {            return false;        }    };     var fake = Fake{};    var provider = unavailable_provider;    provider.context = &fake;    provider.spawn_prepared_fn = Fake.spawn;    var prepared = try provider.spawnPrepared(std.testing.allocator, .{        .cwd = "/workspace",        .output = .{ .context = &fake },        .isolation = .none,    });    try std.testing.expectEqualStrings("42", prepared.pid);    var owned = try prepared.release("npm run dev");    owned.wait();     try std.testing.expectEqual(@as(usize, 1), fake.spawns);    try std.testing.expectEqual(@as(usize, 1), fake.releases);    try std.testing.expectEqual(@as(usize, 1), fake.waits);} test "provider preserves macos isolation as typed request data" {    const Fake = struct {        fn spawn(            _: ?*anyopaque,            _: Allocator,            request: SpawnRequest,        ) ProviderError!PreparedProcess {            switch (request.isolation) {                .macos_profile => |path| if (!std.mem.eql(                    u8,                    path,                    "/private/tmp/fx.sb",                )) return error.SpawnFailed,                .none => return error.Unsupported,            }            return error.Unsupported;        }    };     var provider = unavailable_provider;    provider.spawn_prepared_fn = Fake.spawn;    var output_marker: u8 = 0;    try std.testing.expectError(error.Unsupported, provider.spawnPrepared(        std.testing.allocator,        .{            .cwd = "/workspace",            .output = .{ .context = &output_marker },            .isolation = .{ .macos_profile = "/private/tmp/fx.sb" },        },    ));}