mirror of
https://github.com/oven-sh/bun
synced 2026-02-12 03:48:56 +00:00
Implements a lightweight process manager for managing background processes
within a workspace. Each workspace (cwd) gets its own daemon that manages
processes via Unix socket IPC.
Commands:
- bun start [script] - Start a process in the background
- bun stop [name] - Stop a running process
- bun list - List all running processes in this workspace
- bun logs [name] [-f] - View process logs (with optional follow mode)
Implementation details:
- Uses uSockets for efficient Unix socket IPC between client and daemon
- Daemon auto-spawns on first command and runs in the background
- Processes are workspace-isolated based on cwd hash
- Logs are captured to /tmp/bun-logs/{workspace-hash}/
- Simple process supervision with pid tracking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
296 lines
7.6 KiB
TypeScript
296 lines
7.6 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
|
|
|
|
describe("bun process manager", () => {
|
|
test("bun start - starts a process", async () => {
|
|
using dir = tempDir("process-manager-start", {
|
|
"server.js": `
|
|
console.log("Server started");
|
|
setInterval(() => {}, 1000); // Keep alive
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "start", "server.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`"✓ Started: server.js"`);
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Clean up - stop the process
|
|
const stopProc = Bun.spawn({
|
|
cmd: [bunExe(), "stop", "server.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await stopProc.exited;
|
|
});
|
|
|
|
test("bun list - lists running processes", async () => {
|
|
using dir = tempDir("process-manager-list", {
|
|
"worker.js": `
|
|
console.log("Worker started");
|
|
setInterval(() => {}, 1000);
|
|
`,
|
|
});
|
|
|
|
// Start a process first
|
|
const startProc = Bun.spawn({
|
|
cmd: [bunExe(), "start", "worker.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await startProc.exited;
|
|
|
|
// Now list processes
|
|
await using listProc = Bun.spawn({
|
|
cmd: [bunExe(), "list"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
listProc.stdout.text(),
|
|
listProc.stderr.text(),
|
|
listProc.exited,
|
|
]);
|
|
|
|
// Should show the worker process
|
|
expect(stdout).toContain("worker.js");
|
|
expect(stdout).toContain("NAME");
|
|
expect(stdout).toContain("PID");
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Clean up
|
|
const stopProc = Bun.spawn({
|
|
cmd: [bunExe(), "stop", "worker.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await stopProc.exited;
|
|
});
|
|
|
|
test("bun stop - stops a running process", async () => {
|
|
using dir = tempDir("process-manager-stop", {
|
|
"service.js": `
|
|
console.log("Service running");
|
|
setInterval(() => {}, 1000);
|
|
`,
|
|
});
|
|
|
|
// Start a process
|
|
const startProc = Bun.spawn({
|
|
cmd: [bunExe(), "start", "service.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await startProc.exited;
|
|
|
|
// Stop the process
|
|
await using stopProc = Bun.spawn({
|
|
cmd: [bunExe(), "stop", "service.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
stopProc.stdout.text(),
|
|
stopProc.stderr.text(),
|
|
stopProc.exited,
|
|
]);
|
|
|
|
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`"✓ Stopped: service.js"`);
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Verify it's not in the list anymore
|
|
const listProc = Bun.spawn({
|
|
cmd: [bunExe(), "list"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const listOutput = await listProc.stdout.text();
|
|
await listProc.exited;
|
|
|
|
// Should either show no processes or not include service.js
|
|
if (!listOutput.includes("No processes")) {
|
|
expect(listOutput).not.toContain("service.js");
|
|
}
|
|
});
|
|
|
|
test("bun logs - shows process logs", async () => {
|
|
using dir = tempDir("process-manager-logs", {
|
|
"logger.js": `
|
|
console.log("Log message 1");
|
|
console.error("Error message 1");
|
|
console.log("Log message 2");
|
|
`,
|
|
});
|
|
|
|
// Start and let it finish
|
|
const startProc = Bun.spawn({
|
|
cmd: [bunExe(), "start", "logger.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await startProc.exited;
|
|
|
|
// Wait a bit for logs to be written
|
|
await Bun.sleep(100);
|
|
|
|
// Check logs
|
|
await using logsProc = Bun.spawn({
|
|
cmd: [bunExe(), "logs", "logger.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
logsProc.stdout.text(),
|
|
logsProc.stderr.text(),
|
|
logsProc.exited,
|
|
]);
|
|
|
|
expect(stdout).toContain("Log message 1");
|
|
expect(stdout).toContain("Log message 2");
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Clean up
|
|
const stopProc = Bun.spawn({
|
|
cmd: [bunExe(), "stop", "logger.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await stopProc.exited;
|
|
});
|
|
|
|
test("bun start - prevents duplicate process names", async () => {
|
|
using dir = tempDir("process-manager-duplicate", {
|
|
"app.js": `
|
|
console.log("App started");
|
|
setInterval(() => {}, 1000);
|
|
`,
|
|
});
|
|
|
|
// Start first process
|
|
const start1 = Bun.spawn({
|
|
cmd: [bunExe(), "start", "app.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await start1.exited;
|
|
|
|
// Try to start again with same name
|
|
await using start2 = Bun.spawn({
|
|
cmd: [bunExe(), "start", "app.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([start2.stdout.text(), start2.stderr.text(), start2.exited]);
|
|
|
|
expect(exitCode).not.toBe(0);
|
|
expect(stderr.toLowerCase()).toMatch(/already|exists/);
|
|
|
|
// Clean up
|
|
const stopProc = Bun.spawn({
|
|
cmd: [bunExe(), "stop", "app.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
await stopProc.exited;
|
|
});
|
|
|
|
test("bun list - shows empty list when no processes running", async () => {
|
|
using dir = tempDir("process-manager-empty");
|
|
|
|
await using listProc = Bun.spawn({
|
|
cmd: [bunExe(), "list"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
listProc.stdout.text(),
|
|
listProc.stderr.text(),
|
|
listProc.exited,
|
|
]);
|
|
|
|
expect(stdout.toLowerCase()).toMatch(/no processes|not running/);
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("workspace isolation - processes in different directories are separate", async () => {
|
|
using dir1 = tempDir("process-manager-ws1", {
|
|
"proc.js": `setInterval(() => {}, 1000);`,
|
|
});
|
|
|
|
using dir2 = tempDir("process-manager-ws2", {
|
|
"proc.js": `setInterval(() => {}, 1000);`,
|
|
});
|
|
|
|
// Start process in dir1
|
|
const start1 = Bun.spawn({
|
|
cmd: [bunExe(), "start", "proc.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir1),
|
|
});
|
|
await start1.exited;
|
|
|
|
// Start process in dir2
|
|
const start2 = Bun.spawn({
|
|
cmd: [bunExe(), "start", "proc.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir2),
|
|
});
|
|
await start2.exited;
|
|
|
|
// List in dir1 should only show dir1's process
|
|
const list1 = Bun.spawn({
|
|
cmd: [bunExe(), "list"],
|
|
env: bunEnv,
|
|
cwd: String(dir1),
|
|
stdout: "pipe",
|
|
});
|
|
const out1 = await list1.stdout.text();
|
|
await list1.exited;
|
|
|
|
// List in dir2 should only show dir2's process
|
|
const list2 = Bun.spawn({
|
|
cmd: [bunExe(), "list"],
|
|
env: bunEnv,
|
|
cwd: String(dir2),
|
|
stdout: "pipe",
|
|
});
|
|
const out2 = await list2.stdout.text();
|
|
await list2.exited;
|
|
|
|
// Both should show exactly one process
|
|
const count1 = (out1.match(/proc\.js/g) || []).length;
|
|
const count2 = (out2.match(/proc\.js/g) || []).length;
|
|
expect(count1).toBe(1);
|
|
expect(count2).toBe(1);
|
|
|
|
// Clean up
|
|
Bun.spawn({ cmd: [bunExe(), "stop", "proc.js"], env: bunEnv, cwd: String(dir1) });
|
|
Bun.spawn({ cmd: [bunExe(), "stop", "proc.js"], env: bunEnv, cwd: String(dir2) });
|
|
});
|
|
});
|