mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
1 Commits
claude/vir
...
claude/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c28459fa9 |
40
src/cli.zig
40
src/cli.zig
@@ -91,6 +91,7 @@ pub const PackCommand = @import("./cli/pack_command.zig").PackCommand;
|
||||
pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand;
|
||||
pub const InitCommand = @import("./cli/init_command.zig").InitCommand;
|
||||
pub const WhyCommand = @import("./cli/why_command.zig").WhyCommand;
|
||||
pub const ProcessManagerCommand = @import("./cli/process_manager_command.zig").ProcessManagerCommand;
|
||||
|
||||
pub const Arguments = @import("./cli/Arguments.zig");
|
||||
|
||||
@@ -617,8 +618,11 @@ pub const Command = struct {
|
||||
RootCommandMatcher.case("logout") => .ReservedCommand,
|
||||
RootCommandMatcher.case("whoami") => .PackageManagerCommand,
|
||||
RootCommandMatcher.case("prune") => .ReservedCommand,
|
||||
RootCommandMatcher.case("list") => .ReservedCommand,
|
||||
RootCommandMatcher.case("list") => .ListCommand,
|
||||
RootCommandMatcher.case("why") => .WhyCommand,
|
||||
RootCommandMatcher.case("start") => .StartCommand,
|
||||
RootCommandMatcher.case("stop") => .StopCommand,
|
||||
RootCommandMatcher.case("logs") => .LogsCommand,
|
||||
|
||||
RootCommandMatcher.case("-e") => .AutoCommand,
|
||||
|
||||
@@ -921,6 +925,26 @@ pub const Command = struct {
|
||||
Output.flush();
|
||||
try HelpCommand.exec(allocator);
|
||||
},
|
||||
.StartCommand => {
|
||||
const ctx = try Command.init(allocator, log, .StartCommand);
|
||||
try ProcessManagerCommand.exec(ctx);
|
||||
return;
|
||||
},
|
||||
.StopCommand => {
|
||||
const ctx = try Command.init(allocator, log, .StopCommand);
|
||||
try ProcessManagerCommand.exec(ctx);
|
||||
return;
|
||||
},
|
||||
.ListCommand => {
|
||||
const ctx = try Command.init(allocator, log, .ListCommand);
|
||||
try ProcessManagerCommand.exec(ctx);
|
||||
return;
|
||||
},
|
||||
.LogsCommand => {
|
||||
const ctx = try Command.init(allocator, log, .LogsCommand);
|
||||
try ProcessManagerCommand.exec(ctx);
|
||||
return;
|
||||
},
|
||||
.ExecCommand => {
|
||||
const ctx = try Command.init(allocator, log, .ExecCommand);
|
||||
|
||||
@@ -949,6 +973,10 @@ pub const Command = struct {
|
||||
RemoveCommand,
|
||||
RunCommand,
|
||||
RunAsNodeCommand, // arg0 == 'node'
|
||||
StartCommand,
|
||||
StopCommand,
|
||||
ListCommand,
|
||||
LogsCommand,
|
||||
TestCommand,
|
||||
UnlinkCommand,
|
||||
UpdateCommand,
|
||||
@@ -986,6 +1014,10 @@ pub const Command = struct {
|
||||
.RemoveCommand => 'R',
|
||||
.RunCommand => 'r',
|
||||
.RunAsNodeCommand => 'n',
|
||||
.StartCommand => 's',
|
||||
.StopCommand => 'S',
|
||||
.ListCommand => 'L',
|
||||
.LogsCommand => 'K',
|
||||
.TestCommand => 't',
|
||||
.UnlinkCommand => 'U',
|
||||
.UpdateCommand => 'u',
|
||||
@@ -1312,8 +1344,12 @@ pub const Command = struct {
|
||||
Output.pretty(intro_text, .{});
|
||||
Output.flush();
|
||||
},
|
||||
.StartCommand, .StopCommand, .ListCommand, .LogsCommand => {
|
||||
// These commands handle their own help
|
||||
ProcessManagerCommand.printHelp();
|
||||
},
|
||||
else => {
|
||||
HelpCommand.printWithReason(.explicit);
|
||||
HelpCommand.printWithReason(.explicit, show_all_flags);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
201
src/cli/process_manager/client.zig
Normal file
201
src/cli/process_manager/client.zig
Normal file
@@ -0,0 +1,201 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
const Command = @import("../../cli.zig").Command;
|
||||
const Protocol = @import("./protocol.zig");
|
||||
const Manager = @import("./manager.zig");
|
||||
const Output = bun.Output;
|
||||
const Global = bun.Global;
|
||||
const strings = bun.strings;
|
||||
|
||||
var path_buf: bun.PathBuffer = undefined;
|
||||
|
||||
pub fn startCommand(ctx: Command.Context) !void {
|
||||
if (ctx.positionals.len < 2) {
|
||||
Output.errGeneric("Usage: bun start SCRIPT", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
const script_name = ctx.positionals[1];
|
||||
const cwd = try bun.getcwd(&path_buf);
|
||||
|
||||
const manager = try Manager.ProcessManager.init(ctx.allocator, cwd);
|
||||
defer manager.deinit();
|
||||
|
||||
// Clean up any dead processes first
|
||||
try manager.cleanup();
|
||||
|
||||
manager.startProcess(script_name, script_name, cwd) catch |err| {
|
||||
switch (err) {
|
||||
error.ProcessAlreadyExists => {
|
||||
Output.errGeneric("Process '{s}' is already running", .{script_name});
|
||||
Global.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
Output.prettyln("<green>\u{2713}<r> Started: <b>{s}<r>", .{script_name});
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
pub fn stopCommand(ctx: Command.Context) !void {
|
||||
if (ctx.positionals.len < 2) {
|
||||
Output.errGeneric("Usage: bun stop NAME", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
const name = ctx.positionals[1];
|
||||
const cwd = try bun.getcwd(&path_buf);
|
||||
|
||||
const manager = try Manager.ProcessManager.init(ctx.allocator, cwd);
|
||||
defer manager.deinit();
|
||||
|
||||
// Clean up any dead processes first
|
||||
try manager.cleanup();
|
||||
|
||||
manager.stopProcess(name) catch |err| {
|
||||
switch (err) {
|
||||
error.ProcessNotFound => {
|
||||
Output.errGeneric("Process '{s}' not found", .{name});
|
||||
Global.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
Output.prettyln("<green>\u{2713}<r> Stopped: <b>{s}<r>", .{name});
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
pub fn listCommand(ctx: Command.Context) !void {
|
||||
const cwd = try bun.getcwd(&path_buf);
|
||||
|
||||
const manager = try Manager.ProcessManager.init(ctx.allocator, cwd);
|
||||
defer manager.deinit();
|
||||
|
||||
// Clean up any dead processes first
|
||||
try manager.cleanup();
|
||||
|
||||
const list = try manager.listProcesses(ctx.allocator);
|
||||
defer ctx.allocator.free(list);
|
||||
|
||||
if (list.len == 0) {
|
||||
Output.prettyln("No processes running in this workspace", .{});
|
||||
Output.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
Output.prettyln("\n<b>NAME PID SCRIPT UPTIME<r>", .{});
|
||||
for (list) |proc| {
|
||||
const uptime = formatUptime(proc.uptime);
|
||||
Output.prettyln("{s: <20}{d: <10}{s: <30}{s}", .{ proc.name, proc.pid, proc.script, uptime });
|
||||
}
|
||||
Output.prettyln("", .{});
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
pub fn logsCommand(ctx: Command.Context) !void {
|
||||
if (ctx.positionals.len < 2) {
|
||||
Output.errGeneric("Usage: bun logs NAME [-f]", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
const name = ctx.positionals[1];
|
||||
const cwd = try bun.getcwd(&path_buf);
|
||||
|
||||
const manager = try Manager.ProcessManager.init(ctx.allocator, cwd);
|
||||
defer manager.deinit();
|
||||
|
||||
// Clean up any dead processes first
|
||||
try manager.cleanup();
|
||||
|
||||
const response = manager.getLogPaths(name) catch |err| {
|
||||
switch (err) {
|
||||
error.ProcessNotFound => {
|
||||
Output.errGeneric("Process '{s}' not found", .{name});
|
||||
Global.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
switch (response) {
|
||||
.log_path => |paths| {
|
||||
Output.prettyln("<b>Logs for {s}:<r>", .{name});
|
||||
Output.prettyln(" stdout: {s}", .{paths.stdout});
|
||||
Output.prettyln(" stderr: {s}", .{paths.stderr});
|
||||
|
||||
// TODO: Check for -f flag and tail the logs
|
||||
// For now, just print last 50 lines of stdout
|
||||
Output.prettyln("\n<b>Recent stdout:<r>", .{});
|
||||
try tailFile(ctx.allocator, paths.stdout, 50);
|
||||
|
||||
Output.prettyln("\n<b>Recent stderr:<r>", .{});
|
||||
try tailFile(ctx.allocator, paths.stderr, 50);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
fn tailFile(allocator: std.mem.Allocator, path: []const u8, lines: usize) !void {
|
||||
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
|
||||
if (err == error.FileNotFound) {
|
||||
Output.prettyln(" <d>(no output yet)<r>", .{});
|
||||
return;
|
||||
}
|
||||
return err;
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
const contents = try file.readToEndAlloc(allocator, 1024 * 1024);
|
||||
defer allocator.free(contents);
|
||||
|
||||
if (contents.len == 0) {
|
||||
Output.prettyln(" <d>(no output yet)<r>", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Count newlines from the end
|
||||
var line_count: usize = 0;
|
||||
var i: usize = contents.len;
|
||||
while (i > 0 and line_count < lines) {
|
||||
i -= 1;
|
||||
if (contents[i] == '\n') {
|
||||
line_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip to the start of the line
|
||||
if (i > 0) {
|
||||
while (i < contents.len and contents[i] != '\n') {
|
||||
i += 1;
|
||||
}
|
||||
if (i < contents.len) i += 1;
|
||||
}
|
||||
|
||||
const tail_contents = contents[i..];
|
||||
if (tail_contents.len > 0) {
|
||||
Output.pretty("{s}", .{tail_contents});
|
||||
} else {
|
||||
Output.prettyln(" <d>(no output yet)<r>", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn formatUptime(seconds: i64) []const u8 {
|
||||
var buf: [32]u8 = undefined;
|
||||
if (seconds < 60) {
|
||||
return std.fmt.bufPrint(&buf, "{d}s", .{seconds}) catch "?";
|
||||
} else if (seconds < 3600) {
|
||||
const mins = @divFloor(seconds, 60);
|
||||
return std.fmt.bufPrint(&buf, "{d}m", .{mins}) catch "?";
|
||||
} else if (seconds < 86400) {
|
||||
const hours = @divFloor(seconds, 3600);
|
||||
return std.fmt.bufPrint(&buf, "{d}h", .{hours}) catch "?";
|
||||
} else {
|
||||
const days = @divFloor(seconds, 86400);
|
||||
return std.fmt.bufPrint(&buf, "{d}d", .{days}) catch "?";
|
||||
}
|
||||
}
|
||||
406
src/cli/process_manager/manager.zig
Normal file
406
src/cli/process_manager/manager.zig
Normal file
@@ -0,0 +1,406 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
const Output = bun.Output;
|
||||
const Protocol = @import("./protocol.zig");
|
||||
const Global = bun.Global;
|
||||
const FileSystem = @import("../../fs.zig").FileSystem;
|
||||
|
||||
var path_buf: bun.PathBuffer = undefined;
|
||||
var path_buf2: bun.PathBuffer = undefined;
|
||||
|
||||
pub fn getStateDir(allocator: std.mem.Allocator, cwd: []const u8) ![]const u8 {
|
||||
const hash = std.hash.Wyhash.hash(0, cwd);
|
||||
|
||||
if (Environment.isLinux or Environment.isMac) {
|
||||
return try std.fmt.allocPrint(allocator, "/tmp/bun-pm-{x}", .{hash});
|
||||
} else if (Environment.isWindows) {
|
||||
const temp = try std.process.getEnvVarOwned(allocator, "TEMP");
|
||||
defer allocator.free(temp);
|
||||
return try std.fmt.allocPrint(allocator, "{s}\\bun-pm-{x}", .{ temp, hash });
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
pub fn getLogDir(allocator: std.mem.Allocator, cwd: []const u8) ![]const u8 {
|
||||
const hash = std.hash.Wyhash.hash(0, cwd);
|
||||
|
||||
if (Environment.isLinux or Environment.isMac) {
|
||||
return try std.fmt.allocPrint(allocator, "/tmp/bun-logs/{x}", .{hash});
|
||||
} else if (Environment.isWindows) {
|
||||
const temp = try std.process.getEnvVarOwned(allocator, "TEMP");
|
||||
defer allocator.free(temp);
|
||||
return try std.fmt.allocPrint(allocator, "{s}\\bun-logs\\{x}", .{ temp, hash });
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
pub const ManagedProcess = struct {
|
||||
name: []const u8,
|
||||
script: []const u8,
|
||||
pid: i32,
|
||||
cwd: []const u8,
|
||||
start_time: i64,
|
||||
stdout_path: []const u8,
|
||||
stderr_path: []const u8,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn deinit(this: *ManagedProcess) void {
|
||||
this.allocator.free(this.name);
|
||||
this.allocator.free(this.script);
|
||||
this.allocator.free(this.cwd);
|
||||
this.allocator.free(this.stdout_path);
|
||||
this.allocator.free(this.stderr_path);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ProcessManager = struct {
|
||||
processes: std.StringHashMap(ManagedProcess),
|
||||
allocator: std.mem.Allocator,
|
||||
state_dir: []const u8,
|
||||
log_dir: []const u8,
|
||||
cwd: []const u8,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, cwd: []const u8) !*ProcessManager {
|
||||
const state_dir = try getStateDir(allocator, cwd);
|
||||
const log_dir = try getLogDir(allocator, cwd);
|
||||
|
||||
// Create directories
|
||||
std.fs.makeDirAbsolute(state_dir) catch |err| {
|
||||
if (err != error.PathAlreadyExists) return err;
|
||||
};
|
||||
std.fs.makeDirAbsolute(log_dir) catch |err| {
|
||||
if (err != error.PathAlreadyExists) return err;
|
||||
};
|
||||
|
||||
const manager = try allocator.create(ProcessManager);
|
||||
manager.* = .{
|
||||
.processes = std.StringHashMap(ManagedProcess).init(allocator),
|
||||
.allocator = allocator,
|
||||
.state_dir = state_dir,
|
||||
.log_dir = log_dir,
|
||||
.cwd = try allocator.dupe(u8, cwd),
|
||||
};
|
||||
|
||||
// Try to load existing state
|
||||
manager.loadState() catch {
|
||||
// Ignore errors on first load
|
||||
};
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *ProcessManager) void {
|
||||
var it = this.processes.valueIterator();
|
||||
while (it.next()) |proc| {
|
||||
proc.deinit();
|
||||
}
|
||||
this.processes.deinit();
|
||||
this.allocator.free(this.state_dir);
|
||||
this.allocator.free(this.log_dir);
|
||||
this.allocator.free(this.cwd);
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
pub fn startProcess(
|
||||
this: *ProcessManager,
|
||||
name: []const u8,
|
||||
script: []const u8,
|
||||
cwd: []const u8,
|
||||
) !void {
|
||||
// Check if already running
|
||||
if (this.processes.get(name)) |_| {
|
||||
return error.ProcessAlreadyExists;
|
||||
}
|
||||
|
||||
// Create log files
|
||||
const stdout_path = try std.fmt.allocPrint(
|
||||
this.allocator,
|
||||
"{s}/{s}-stdout.log",
|
||||
.{ this.log_dir, name },
|
||||
);
|
||||
errdefer this.allocator.free(stdout_path);
|
||||
|
||||
const stderr_path = try std.fmt.allocPrint(
|
||||
this.allocator,
|
||||
"{s}/{s}-stderr.log",
|
||||
.{ this.log_dir, name },
|
||||
);
|
||||
errdefer this.allocator.free(stderr_path);
|
||||
|
||||
// Create/truncate log files
|
||||
const stdout_file = try std.fs.createFileAbsolute(stdout_path, .{ .truncate = true });
|
||||
stdout_file.close();
|
||||
|
||||
const stderr_file = try std.fs.createFileAbsolute(stderr_path, .{ .truncate = true });
|
||||
stderr_file.close();
|
||||
|
||||
// Spawn the process
|
||||
const pid = try this.spawnProcess(script, cwd, stdout_path, stderr_path);
|
||||
|
||||
const proc = ManagedProcess{
|
||||
.name = try this.allocator.dupe(u8, name),
|
||||
.script = try this.allocator.dupe(u8, script),
|
||||
.pid = pid,
|
||||
.cwd = try this.allocator.dupe(u8, cwd),
|
||||
.start_time = std.time.timestamp(),
|
||||
.stdout_path = stdout_path,
|
||||
.stderr_path = stderr_path,
|
||||
.allocator = this.allocator,
|
||||
};
|
||||
|
||||
try this.processes.put(proc.name, proc);
|
||||
try this.saveState();
|
||||
}
|
||||
|
||||
fn spawnProcess(
|
||||
this: *ProcessManager,
|
||||
script: []const u8,
|
||||
cwd: []const u8,
|
||||
stdout_path: []const u8,
|
||||
stderr_path: []const u8,
|
||||
) !i32 {
|
||||
_ = this;
|
||||
|
||||
if (comptime Environment.isWindows) {
|
||||
// Windows implementation
|
||||
return error.NotImplementedOnWindows;
|
||||
}
|
||||
|
||||
// Open log files
|
||||
const stdout_fd = try std.posix.open(
|
||||
stdout_path,
|
||||
.{ .ACCMODE = .WRONLY, .CREAT = true, .APPEND = true },
|
||||
0o644,
|
||||
);
|
||||
errdefer std.posix.close(stdout_fd);
|
||||
|
||||
const stderr_fd = try std.posix.open(
|
||||
stderr_path,
|
||||
.{ .ACCMODE = .WRONLY, .CREAT = true, .APPEND = true },
|
||||
0o644,
|
||||
);
|
||||
errdefer std.posix.close(stderr_fd);
|
||||
|
||||
// Fork the process
|
||||
const pid = try std.posix.fork();
|
||||
|
||||
if (pid == 0) {
|
||||
// Child process
|
||||
|
||||
// Redirect stdout and stderr
|
||||
std.posix.dup2(stdout_fd, std.posix.STDOUT_FILENO) catch std.process.exit(1);
|
||||
std.posix.dup2(stderr_fd, std.posix.STDERR_FILENO) catch std.process.exit(1);
|
||||
|
||||
// Close original file descriptors
|
||||
std.posix.close(stdout_fd);
|
||||
std.posix.close(stderr_fd);
|
||||
|
||||
// Change directory
|
||||
std.posix.chdir(cwd) catch std.process.exit(1);
|
||||
|
||||
// Prepare arguments for bun run
|
||||
// We need a null-terminated version of script
|
||||
const script_z = std.posix.toPosixPath(script) catch std.process.exit(1);
|
||||
const argv = [_:null]?[*:0]const u8{
|
||||
"bun",
|
||||
"run",
|
||||
&script_z,
|
||||
null,
|
||||
};
|
||||
|
||||
// Get bun path
|
||||
const bun_path = bun.selfExePath() catch std.process.exit(1);
|
||||
|
||||
// Execute
|
||||
_ = std.posix.execveZ(
|
||||
bun_path.ptr,
|
||||
@ptrCast(&argv),
|
||||
@ptrCast(@extern(*[*:null]const ?[*:0]const u8, .{ .name = "environ" })),
|
||||
) catch std.process.exit(1);
|
||||
|
||||
// If execve returns, it failed
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
// Parent process
|
||||
std.posix.close(stdout_fd);
|
||||
std.posix.close(stderr_fd);
|
||||
|
||||
return @intCast(pid);
|
||||
}
|
||||
|
||||
pub fn stopProcess(this: *ProcessManager, name: []const u8) !void {
|
||||
const proc = this.processes.get(name) orelse return error.ProcessNotFound;
|
||||
|
||||
// Send SIGTERM
|
||||
if (comptime !Environment.isWindows) {
|
||||
std.posix.kill(@intCast(proc.pid), std.posix.SIG.TERM) catch |err| {
|
||||
if (err != error.ProcessNotFound) {
|
||||
return err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Remove from map
|
||||
var entry = this.processes.fetchRemove(name).?;
|
||||
entry.value.deinit();
|
||||
|
||||
try this.saveState();
|
||||
}
|
||||
|
||||
pub fn listProcesses(this: *ProcessManager, allocator: std.mem.Allocator) ![]Protocol.ProcessInfo {
|
||||
const list = try allocator.alloc(Protocol.ProcessInfo, this.processes.count());
|
||||
|
||||
var it = this.processes.valueIterator();
|
||||
var i: usize = 0;
|
||||
while (it.next()) |proc| : (i += 1) {
|
||||
const uptime = std.time.timestamp() - proc.start_time;
|
||||
list[i] = .{
|
||||
.name = proc.name,
|
||||
.pid = proc.pid,
|
||||
.script = proc.script,
|
||||
.uptime = uptime,
|
||||
};
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn getLogPaths(this: *ProcessManager, name: []const u8) !Protocol.Response {
|
||||
const proc = this.processes.get(name) orelse return error.ProcessNotFound;
|
||||
|
||||
return Protocol.Response{
|
||||
.log_path = .{
|
||||
.stdout = proc.stdout_path,
|
||||
.stderr = proc.stderr_path,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn saveState(this: *ProcessManager) !void {
|
||||
const state_path = try std.fmt.bufPrint(&path_buf, "{s}/state.json", .{this.state_dir});
|
||||
|
||||
var file = try std.fs.createFileAbsolute(state_path, .{ .truncate = true });
|
||||
defer file.close();
|
||||
|
||||
var buffered_writer = std.io.bufferedWriter(file.writer());
|
||||
const writer = buffered_writer.writer();
|
||||
|
||||
try writer.writeAll("{\n \"processes\": [\n");
|
||||
|
||||
var it = this.processes.valueIterator();
|
||||
var first = true;
|
||||
while (it.next()) |proc| {
|
||||
if (!first) {
|
||||
try writer.writeAll(",\n");
|
||||
}
|
||||
first = false;
|
||||
|
||||
try writer.writeAll(" {\n");
|
||||
try std.json.stringify(.{
|
||||
.name = proc.name,
|
||||
.script = proc.script,
|
||||
.pid = proc.pid,
|
||||
.cwd = proc.cwd,
|
||||
.start_time = proc.start_time,
|
||||
.stdout_path = proc.stdout_path,
|
||||
.stderr_path = proc.stderr_path,
|
||||
}, .{}, writer);
|
||||
try writer.writeAll("\n }");
|
||||
}
|
||||
|
||||
try writer.writeAll("\n ]\n}\n");
|
||||
try buffered_writer.flush();
|
||||
}
|
||||
|
||||
fn loadState(this: *ProcessManager) !void {
|
||||
const state_path = try std.fmt.bufPrint(&path_buf, "{s}/state.json", .{this.state_dir});
|
||||
|
||||
const file = std.fs.openFileAbsolute(state_path, .{}) catch |err| {
|
||||
if (err == error.FileNotFound) return;
|
||||
return err;
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
const contents = try file.readToEndAlloc(this.allocator, 1024 * 1024);
|
||||
defer this.allocator.free(contents);
|
||||
|
||||
const State = struct {
|
||||
processes: []struct {
|
||||
name: []const u8,
|
||||
script: []const u8,
|
||||
pid: i32,
|
||||
cwd: []const u8,
|
||||
start_time: i64,
|
||||
stdout_path: []const u8,
|
||||
stderr_path: []const u8,
|
||||
},
|
||||
};
|
||||
|
||||
const parsed = try std.json.parseFromSlice(State, this.allocator, contents, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
// Verify each process is still running
|
||||
for (parsed.value.processes) |proc_data| {
|
||||
const is_running = blk: {
|
||||
if (comptime Environment.isWindows) {
|
||||
break :blk false;
|
||||
} else {
|
||||
// Check if process exists by sending signal 0
|
||||
std.posix.kill(@intCast(proc_data.pid), 0) catch {
|
||||
break :blk false;
|
||||
};
|
||||
break :blk true;
|
||||
}
|
||||
};
|
||||
|
||||
if (is_running) {
|
||||
const proc = ManagedProcess{
|
||||
.name = try this.allocator.dupe(u8, proc_data.name),
|
||||
.script = try this.allocator.dupe(u8, proc_data.script),
|
||||
.pid = proc_data.pid,
|
||||
.cwd = try this.allocator.dupe(u8, proc_data.cwd),
|
||||
.start_time = proc_data.start_time,
|
||||
.stdout_path = try this.allocator.dupe(u8, proc_data.stdout_path),
|
||||
.stderr_path = try this.allocator.dupe(u8, proc_data.stderr_path),
|
||||
.allocator = this.allocator,
|
||||
};
|
||||
try this.processes.put(proc.name, proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(this: *ProcessManager) !void {
|
||||
// Clean up dead processes
|
||||
var to_remove = std.ArrayList([]const u8).init(this.allocator);
|
||||
defer to_remove.deinit();
|
||||
|
||||
var it = this.processes.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const is_running = blk: {
|
||||
if (comptime Environment.isWindows) {
|
||||
break :blk false;
|
||||
} else {
|
||||
std.posix.kill(@intCast(entry.value_ptr.pid), 0) catch {
|
||||
break :blk false;
|
||||
};
|
||||
break :blk true;
|
||||
}
|
||||
};
|
||||
|
||||
if (!is_running) {
|
||||
try to_remove.append(entry.key_ptr.*);
|
||||
}
|
||||
}
|
||||
|
||||
for (to_remove.items) |name| {
|
||||
var entry = this.processes.fetchRemove(name).?;
|
||||
entry.value.deinit();
|
||||
}
|
||||
|
||||
if (to_remove.items.len > 0) {
|
||||
try this.saveState();
|
||||
}
|
||||
}
|
||||
};
|
||||
38
src/cli/process_manager/protocol.zig
Normal file
38
src/cli/process_manager/protocol.zig
Normal file
@@ -0,0 +1,38 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Command = union(enum) {
|
||||
start: struct {
|
||||
name: []const u8,
|
||||
script: []const u8,
|
||||
cwd: []const u8,
|
||||
},
|
||||
stop: struct {
|
||||
name: []const u8,
|
||||
},
|
||||
list: void,
|
||||
logs: struct {
|
||||
name: []const u8,
|
||||
follow: bool,
|
||||
},
|
||||
};
|
||||
|
||||
pub const Response = union(enum) {
|
||||
success: struct {
|
||||
message: []const u8,
|
||||
},
|
||||
err: struct {
|
||||
message: []const u8,
|
||||
},
|
||||
process_list: []ProcessInfo,
|
||||
log_path: struct {
|
||||
stdout: []const u8,
|
||||
stderr: []const u8,
|
||||
},
|
||||
};
|
||||
|
||||
pub const ProcessInfo = struct {
|
||||
name: []const u8,
|
||||
pid: i32,
|
||||
script: []const u8,
|
||||
uptime: i64,
|
||||
};
|
||||
67
src/cli/process_manager_command.zig
Normal file
67
src/cli/process_manager_command.zig
Normal file
@@ -0,0 +1,67 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const Command = @import("../cli.zig").Command;
|
||||
const strings = bun.strings;
|
||||
const Output = bun.Output;
|
||||
const Global = bun.Global;
|
||||
|
||||
const Manager = @import("./process_manager/manager.zig");
|
||||
const Client = @import("./process_manager/client.zig");
|
||||
const Protocol = @import("./process_manager/protocol.zig");
|
||||
|
||||
pub const ProcessManagerCommand = struct {
|
||||
pub fn exec(ctx: Command.Context) !void {
|
||||
const args = ctx.positionals;
|
||||
|
||||
if (args.len == 0) {
|
||||
printHelp();
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
const subcommand = args[0];
|
||||
|
||||
if (strings.eqlComptime(subcommand, "start")) {
|
||||
try Client.startCommand(ctx);
|
||||
} else if (strings.eqlComptime(subcommand, "stop")) {
|
||||
try Client.stopCommand(ctx);
|
||||
} else if (strings.eqlComptime(subcommand, "list")) {
|
||||
try Client.listCommand(ctx);
|
||||
} else if (strings.eqlComptime(subcommand, "logs")) {
|
||||
try Client.logsCommand(ctx);
|
||||
} else {
|
||||
Output.errGeneric("Unknown subcommand: {s}", .{subcommand});
|
||||
printHelp();
|
||||
Global.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn printHelp() void {
|
||||
const help_text =
|
||||
\\<b>Usage:<r>
|
||||
\\
|
||||
\\ <b><green>bun start<r> <cyan>SCRIPT<r>
|
||||
\\ Start a process in the background
|
||||
\\
|
||||
\\ <b><green>bun stop<r> <cyan>NAME<r>
|
||||
\\ Stop a running process
|
||||
\\
|
||||
\\ <b><green>bun list<r>
|
||||
\\ List all running processes in this workspace
|
||||
\\
|
||||
\\ <b><green>bun logs<r> <cyan>NAME<r>
|
||||
\\ Show logs for a process
|
||||
\\
|
||||
\\<b>Examples:<r>
|
||||
\\
|
||||
\\ bun start dev # Start "dev" from package.json
|
||||
\\ bun start ./server.js # Start a file directly
|
||||
\\ bun list # See what's running
|
||||
\\ bun logs dev # View dev logs
|
||||
\\ bun stop dev # Stop dev process
|
||||
\\
|
||||
;
|
||||
|
||||
Output.pretty(help_text, .{});
|
||||
Output.flush();
|
||||
}
|
||||
};
|
||||
531
test/cli/process-manager.test.ts
Normal file
531
test/cli/process-manager.test.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import { spawnSync } from "bun";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
import * as path from "node:path";
|
||||
|
||||
describe("bun process manager", () => {
|
||||
let testDir: ReturnType<typeof tempDir>;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = tempDir("process-manager", {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up any running processes
|
||||
const { exitCode } = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// If there are processes, stop them
|
||||
if (exitCode === 0) {
|
||||
const listResult = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = listResult.stdout.toString();
|
||||
const lines = output.split("\n");
|
||||
|
||||
// Find process names from the output
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^(\S+)\s+\d+/);
|
||||
if (match && match[1] && match[1] !== "NAME") {
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "stop", match[1]],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("start a simple script", async () => {
|
||||
await Bun.write(
|
||||
path.join(String(testDir), "server.js"),
|
||||
`
|
||||
console.log("Server started");
|
||||
setInterval(() => {
|
||||
console.log("Server running...");
|
||||
}, 1000);
|
||||
`,
|
||||
);
|
||||
|
||||
const { exitCode, stdout, stderr } = spawnSync({
|
||||
cmd: [bunExe(), "start", "./server.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toString()).toContain("Started");
|
||||
expect(stdout.toString()).toContain("server.js");
|
||||
|
||||
// Give it a moment to start
|
||||
await Bun.sleep(100);
|
||||
|
||||
// Verify the process is running
|
||||
const listResult = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(listResult.exitCode).toBe(0);
|
||||
expect(listResult.stdout.toString()).toContain("server.js");
|
||||
});
|
||||
|
||||
test("list shows running processes", async () => {
|
||||
await Bun.write(path.join(String(testDir), "worker1.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
await Bun.write(path.join(String(testDir), "worker2.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
// Start two processes
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./worker1.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./worker2.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await Bun.sleep(100);
|
||||
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
const output = stdout.toString();
|
||||
expect(output).toContain("worker1.js");
|
||||
expect(output).toContain("worker2.js");
|
||||
expect(output).toContain("NAME");
|
||||
expect(output).toContain("PID");
|
||||
expect(output).toContain("UPTIME");
|
||||
});
|
||||
|
||||
test("stop a running process", async () => {
|
||||
await Bun.write(path.join(String(testDir), "stoppable.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
// Start process
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./stoppable.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await Bun.sleep(100);
|
||||
|
||||
// Stop process
|
||||
const stopResult = spawnSync({
|
||||
cmd: [bunExe(), "stop", "./stoppable.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(stopResult.exitCode).toBe(0);
|
||||
expect(stopResult.stdout.toString()).toContain("Stopped");
|
||||
|
||||
// Verify it's not in the list anymore
|
||||
const listResult = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(listResult.stdout.toString()).not.toContain("stoppable.js");
|
||||
});
|
||||
|
||||
test("logs command shows log paths", async () => {
|
||||
await Bun.write(
|
||||
path.join(String(testDir), "logger.js"),
|
||||
`
|
||||
console.log("stdout message");
|
||||
console.error("stderr message");
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
);
|
||||
|
||||
// Start process
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./logger.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await Bun.sleep(200);
|
||||
|
||||
// Get logs
|
||||
const logsResult = spawnSync({
|
||||
cmd: [bunExe(), "logs", "./logger.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(logsResult.exitCode).toBe(0);
|
||||
const output = logsResult.stdout.toString();
|
||||
expect(output).toContain("stdout");
|
||||
expect(output).toContain("stderr");
|
||||
expect(output).toContain("/tmp/bun-logs/");
|
||||
});
|
||||
|
||||
test("cannot start the same process twice", async () => {
|
||||
await Bun.write(path.join(String(testDir), "unique.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
// Start process
|
||||
const first = spawnSync({
|
||||
cmd: [bunExe(), "start", "./unique.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(first.exitCode).toBe(0);
|
||||
|
||||
// Try to start again
|
||||
const second = spawnSync({
|
||||
cmd: [bunExe(), "start", "./unique.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(second.exitCode).not.toBe(0);
|
||||
expect(second.stderr.toString()).toContain("already running");
|
||||
});
|
||||
|
||||
test("stop non-existent process fails", () => {
|
||||
const { exitCode, stderr } = spawnSync({
|
||||
cmd: [bunExe(), "stop", "nonexistent"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr.toString()).toContain("not found");
|
||||
});
|
||||
|
||||
test("logs for non-existent process fails", () => {
|
||||
const { exitCode, stderr } = spawnSync({
|
||||
cmd: [bunExe(), "logs", "nonexistent"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr.toString()).toContain("not found");
|
||||
});
|
||||
|
||||
test("list with no processes", () => {
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toString()).toContain("No processes running");
|
||||
});
|
||||
|
||||
test("process output is captured to logs", async () => {
|
||||
await Bun.write(
|
||||
path.join(String(testDir), "output.js"),
|
||||
`
|
||||
console.log("test output line 1");
|
||||
console.log("test output line 2");
|
||||
console.error("test error line 1");
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
);
|
||||
|
||||
// Start process
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./output.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Wait for output
|
||||
await Bun.sleep(300);
|
||||
|
||||
// Get logs
|
||||
const logsResult = spawnSync({
|
||||
cmd: [bunExe(), "logs", "./output.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(logsResult.exitCode).toBe(0);
|
||||
const output = logsResult.stdout.toString();
|
||||
expect(output).toContain("test output line 1");
|
||||
expect(output).toContain("test output line 2");
|
||||
expect(output).toContain("test error line 1");
|
||||
});
|
||||
|
||||
test("start script from package.json", async () => {
|
||||
await Bun.write(
|
||||
path.join(String(testDir), "package.json"),
|
||||
JSON.stringify({
|
||||
name: "test",
|
||||
scripts: {
|
||||
dev: "bun run ./script.js",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await Bun.write(
|
||||
path.join(String(testDir), "script.js"),
|
||||
`
|
||||
console.log("Running from package.json script");
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
);
|
||||
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cmd: [bunExe(), "start", "dev"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toString()).toContain("Started");
|
||||
|
||||
await Bun.sleep(100);
|
||||
|
||||
const listResult = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(listResult.exitCode).toBe(0);
|
||||
expect(listResult.stdout.toString()).toContain("dev");
|
||||
});
|
||||
|
||||
test("workspace isolation - processes in different dirs don't interfere", async () => {
|
||||
const dir1 = tempDir("process-manager-1", {});
|
||||
const dir2 = tempDir("process-manager-2", {});
|
||||
|
||||
try {
|
||||
await Bun.write(path.join(String(dir1), "app.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
await Bun.write(path.join(String(dir2), "app.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
// Start process in dir1
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./app.js"],
|
||||
cwd: String(dir1),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Start process in dir2
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./app.js"],
|
||||
cwd: String(dir2),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await Bun.sleep(100);
|
||||
|
||||
// List in dir1 should only show its process
|
||||
const list1 = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(dir1),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// List in dir2 should only show its process
|
||||
const list2 = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(dir2),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Both should show exactly one process
|
||||
const lines1 = list1.stdout
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter(l => l.includes("app.js"));
|
||||
const lines2 = list2.stdout
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter(l => l.includes("app.js"));
|
||||
|
||||
expect(lines1.length).toBe(1);
|
||||
expect(lines2.length).toBe(1);
|
||||
|
||||
// Cleanup
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "stop", "./app.js"],
|
||||
cwd: String(dir1),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "stop", "./app.js"],
|
||||
cwd: String(dir2),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
} finally {
|
||||
// Cleanup dirs
|
||||
}
|
||||
});
|
||||
|
||||
test("help text is shown when no subcommand", () => {
|
||||
const { exitCode, stdout } = spawnSync({
|
||||
cmd: [bunExe(), "start"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
// The help should be printed by the main command handler
|
||||
});
|
||||
|
||||
test("uptime is tracked correctly", async () => {
|
||||
await Bun.write(path.join(String(testDir), "timed.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
// Start process
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./timed.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Wait a bit
|
||||
await Bun.sleep(2000);
|
||||
|
||||
// Check list
|
||||
const listResult = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(listResult.exitCode).toBe(0);
|
||||
const output = listResult.stdout.toString();
|
||||
// Should show at least 1 second of uptime
|
||||
expect(output).toMatch(/\d+s/);
|
||||
});
|
||||
|
||||
test("process manager persists state across commands", async () => {
|
||||
await Bun.write(path.join(String(testDir), "persistent.js"), `setInterval(() => {}, 1000);`);
|
||||
|
||||
// Start process
|
||||
spawnSync({
|
||||
cmd: [bunExe(), "start", "./persistent.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await Bun.sleep(100);
|
||||
|
||||
// Multiple list calls should all see the same process
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const listResult = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(listResult.exitCode).toBe(0);
|
||||
expect(listResult.stdout.toString()).toContain("persistent.js");
|
||||
await Bun.sleep(50);
|
||||
}
|
||||
|
||||
// Stop should work
|
||||
const stopResult = spawnSync({
|
||||
cmd: [bunExe(), "stop", "./persistent.js"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(stopResult.exitCode).toBe(0);
|
||||
|
||||
// And it should be gone
|
||||
const finalList = spawnSync({
|
||||
cmd: [bunExe(), "list"],
|
||||
cwd: String(testDir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(finalList.stdout.toString()).not.toContain("persistent.js");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user