const std = @import("std");const background_process_provider = @import( "../execution/background_process_provider.zig",);const io_mod = @import("../shared/io.zig");const session_child_store = @import("../session/session_child_store.zig"); const Allocator = std.mem.Allocator; pub const Output = union(enum) { managed_session: struct { capability: *session_child_store.SessionChildCapability, file: session_child_store.ManagedFile, name: []u8, display_path: []u8, }, external: struct { file: std.Io.File, path: []u8, }, pub fn childStdioFile(self: *const Output) std.Io.File { return switch (self.*) { .managed_session => |value| value.file.childStdioFile(), .external => |value| value.file, }; } pub fn providerCapability( self: *const Output, ) background_process_provider.OutputCapability { return .{ .context = self }; } pub fn childStdioFileForProvider( capability: background_process_provider.OutputCapability, ) std.Io.File { const self: *const Output = @ptrCast(@alignCast(capability.context)); return self.childStdioFile(); } pub fn displayPath(self: *const Output) []const u8 { return switch (self.*) { .managed_session => |value| value.display_path, .external => |value| value.path, }; } pub fn managedLogName(self: *const Output) ?[]const u8 { return switch (self.*) { .managed_session => |value| value.name, .external => null, }; } pub fn deinit(self: *Output, alloc: Allocator, remove: bool) void { switch (self.*) { .managed_session => |*value| { value.file.deinit(); if (remove) { value.capability.delete( .background_logs, value.name, ) catch {}; } alloc.free(value.name); alloc.free(value.display_path); }, .external => |value| { value.file.close(io_mod.getIo()); if (remove) { std.Io.Dir.deleteFileAbsolute( io_mod.getIo(), value.path, ) catch {}; } alloc.free(value.path); }, } self.* = undefined; }}; pub fn prepareManaged( alloc: Allocator, capability: *session_child_store.SessionChildCapability,) !Output { var attempt: usize = 0; while (attempt < 16) : (attempt += 1) { const name = try randomLogName(alloc); var file = capability.createExclusiveFile( alloc, .background_logs, name, ) catch |err| switch (err) { error.PathAlreadyExists => { alloc.free(name); continue; }, else => { alloc.free(name); return err; }, }; errdefer file.deinit(); const display_path = try alloc.dupe( u8, file.displayPath() orelse return error.SessionChildStoreFailed, ); return .{ .managed_session = .{ .capability = capability, .file = file, .name = name, .display_path = display_path, } }; } return error.BackgroundIdentityUnavailable;} pub fn prepareExternal(alloc: Allocator) !Output { const root = io_mod.getenv("TMPDIR") orelse "/tmp"; var attempt: usize = 0; while (attempt < 16) : (attempt += 1) { const name = try randomLogName(alloc); defer alloc.free(name); const path = try std.fs.path.join(alloc, &.{ root, name }); const file = std.Io.Dir.createFileAbsolute( io_mod.getIo(), path, .{ .read = true, .truncate = false, .exclusive = true, .permissions = std.Io.File.Permissions.fromMode(0o600), }, ) catch |err| switch (err) { error.PathAlreadyExists => { alloc.free(path); continue; }, else => { alloc.free(path); return err; }, }; return .{ .external = .{ .file = file, .path = path } }; } return error.BackgroundIdentityUnavailable;} fn randomLogName(alloc: Allocator) ![]u8 { var random: [16]u8 = undefined; try std.Io.randomSecure(io_mod.getIo(), &random); var encoded: [32]u8 = undefined; const alphabet = "0123456789abcdef"; for (random, 0..) |byte, index| { encoded[index * 2] = alphabet[byte >> 4]; encoded[index * 2 + 1] = alphabet[byte & 0x0f]; } return std.fmt.allocPrint( alloc, "fx-cmd-{s}.log", .{encoded[0..]}, );} test "background launch output removes external log on cancellation" { const alloc = std.testing.allocator; var output = try prepareExternal(alloc); const path = try alloc.dupe(u8, output.displayPath()); defer alloc.free(path); output.deinit(alloc, true); try std.testing.expectError( error.FileNotFound, std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}), );} test "background launch output removes managed log on cancellation" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir( io_mod.getIo(), "session", std.Io.File.Permissions.fromMode(0o700), ); const display_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "session"); defer alloc.free(display_path); var session_dir = try tmp.dir.openDir(io_mod.getIo(), "session", .{ .iterate = true, .follow_symlinks = false, }); defer session_dir.close(io_mod.getIo()); var capability = try session_child_store.SessionChildCapability.initForTesting( alloc, session_dir, display_path, .writable, .{}, ); defer capability.deinit(); var output = try prepareManaged(alloc, &capability); const name = try alloc.dupe(u8, output.managedLogName().?); defer alloc.free(name); output.deinit(alloc, true); try std.testing.expectError( error.FileNotFound, capability.stat(.background_logs, name), );}
const std = @import("std");
const background_process_provider = @import(
"../execution/background_process_provider.zig",
);
const io_mod = @import("../shared/io.zig");
const session_child_store = @import("../session/session_child_store.zig");
const Allocator = std.mem.Allocator;
pub const Output = union(enum) {
managed_session: struct {
capability: *session_child_store.SessionChildCapability,
file: session_child_store.ManagedFile,
name: []u8,
display_path: []u8,
},
external: struct {
file: std.Io.File,
path: []u8,
},
pub fn childStdioFile(self: *const Output) std.Io.File {
return switch (self.*) {
.managed_session => |value| value.file.childStdioFile(),
.external => |value| value.file,
};
}
pub fn providerCapability(
self: *const Output,
) background_process_provider.OutputCapability {
return .{ .context = self };
}
pub fn childStdioFileForProvider(
capability: background_process_provider.OutputCapability,
) std.Io.File {
const self: *const Output = @ptrCast(@alignCast(capability.context));
return self.childStdioFile();
}
pub fn displayPath(self: *const Output) []const u8 {
return switch (self.*) {
.managed_session => |value| value.display_path,
.external => |value| value.path,
};
}
pub fn managedLogName(self: *const Output) ?[]const u8 {
return switch (self.*) {
.managed_session => |value| value.name,
.external => null,
};
}
pub fn deinit(self: *Output, alloc: Allocator, remove: bool) void {
switch (self.*) {
.managed_session => |*value| {
value.file.deinit();
if (remove) {
value.capability.delete(
.background_logs,
value.name,
) catch {};
}
alloc.free(value.name);
alloc.free(value.display_path);
},
.external => |value| {
value.file.close(io_mod.getIo());
if (remove) {
std.Io.Dir.deleteFileAbsolute(
io_mod.getIo(),
value.path,
) catch {};
}
alloc.free(value.path);
},
}
self.* = undefined;
}
};
pub fn prepareManaged(
alloc: Allocator,
capability: *session_child_store.SessionChildCapability,
) !Output {
var attempt: usize = 0;
while (attempt < 16) : (attempt += 1) {
const name = try randomLogName(alloc);
var file = capability.createExclusiveFile(
alloc,
.background_logs,
name,
) catch |err| switch (err) {
error.PathAlreadyExists => {
alloc.free(name);
continue;
},
else => {
alloc.free(name);
return err;
},
};
errdefer file.deinit();
const display_path = try alloc.dupe(
u8,
file.displayPath() orelse return error.SessionChildStoreFailed,
);
return .{ .managed_session = .{
.capability = capability,
.file = file,
.name = name,
.display_path = display_path,
} };
}
return error.BackgroundIdentityUnavailable;
}
pub fn prepareExternal(alloc: Allocator) !Output {
const root = io_mod.getenv("TMPDIR") orelse "/tmp";
var attempt: usize = 0;
while (attempt < 16) : (attempt += 1) {
const name = try randomLogName(alloc);
defer alloc.free(name);
const path = try std.fs.path.join(alloc, &.{ root, name });
const file = std.Io.Dir.createFileAbsolute(
io_mod.getIo(),
path,
.{
.read = true,
.truncate = false,
.exclusive = true,
.permissions = std.Io.File.Permissions.fromMode(0o600),
},
) catch |err| switch (err) {
error.PathAlreadyExists => {
alloc.free(path);
continue;
},
else => {
alloc.free(path);
return err;
},
};
return .{ .external = .{ .file = file, .path = path } };
}
return error.BackgroundIdentityUnavailable;
}
fn randomLogName(alloc: Allocator) ![]u8 {
var random: [16]u8 = undefined;
try std.Io.randomSecure(io_mod.getIo(), &random);
var encoded: [32]u8 = undefined;
const alphabet = "0123456789abcdef";
for (random, 0..) |byte, index| {
encoded[index * 2] = alphabet[byte >> 4];
encoded[index * 2 + 1] = alphabet[byte & 0x0f];
}
return std.fmt.allocPrint(
alloc,
"fx-cmd-{s}.log",
.{encoded[0..]},
);
}
test "background launch output removes external log on cancellation" {
const alloc = std.testing.allocator;
var output = try prepareExternal(alloc);
const path = try alloc.dupe(u8, output.displayPath());
defer alloc.free(path);
output.deinit(alloc, true);
try std.testing.expectError(
error.FileNotFound,
std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}),
);
}
test "background launch output removes managed log on cancellation" {
const alloc = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.createDir(
io_mod.getIo(),
"session",
std.Io.File.Permissions.fromMode(0o700),
);
const display_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "session");
defer alloc.free(display_path);
var session_dir = try tmp.dir.openDir(io_mod.getIo(), "session", .{
.iterate = true,
.follow_symlinks = false,
});
defer session_dir.close(io_mod.getIo());
var capability = try session_child_store.SessionChildCapability.initForTesting(
alloc,
session_dir,
display_path,
.writable,
.{},
);
defer capability.deinit();
var output = try prepareManaged(alloc, &capability);
const name = try alloc.dupe(u8, output.managedLogName().?);
defer alloc.free(name);
output.deinit(alloc, true);
try std.testing.expectError(
error.FileNotFound,
capability.stat(.background_logs, name),
);
}