bm2 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +1591 -0
- package/package.json +68 -0
- package/src/cluster-manager.ts +117 -0
- package/src/constants.ts +44 -0
- package/src/cron-manager.ts +74 -0
- package/src/daemon.ts +233 -0
- package/src/dashboard-ui.ts +285 -0
- package/src/dashboard.ts +203 -0
- package/src/deploy.ts +188 -0
- package/src/env-manager.ts +77 -0
- package/src/graceful-reload.ts +71 -0
- package/src/health-checker.ts +112 -0
- package/src/index.ts +15 -0
- package/src/log-manager.ts +201 -0
- package/src/module-manager.ts +119 -0
- package/src/monitor.ts +185 -0
- package/src/process-container.ts +508 -0
- package/src/process-manager.ts +403 -0
- package/src/startup.ts +158 -0
- package/src/types.ts +230 -0
- package/src/utils.ts +171 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bm2",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A blazing-fast, full-featured process manager built entirely on Bun native APIs. The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"bm2": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "bun run src/index.ts",
|
|
18
|
+
"dev": "bun run --watch src/index.ts",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"lint": "bun x tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "bun test"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"bun",
|
|
25
|
+
"process-manager",
|
|
26
|
+
"pm2",
|
|
27
|
+
"pm2-alternative",
|
|
28
|
+
"cluster",
|
|
29
|
+
"daemon",
|
|
30
|
+
"production",
|
|
31
|
+
"deployment",
|
|
32
|
+
"monitoring",
|
|
33
|
+
"prometheus",
|
|
34
|
+
"zero-downtime",
|
|
35
|
+
"reload",
|
|
36
|
+
"log-management",
|
|
37
|
+
"health-check",
|
|
38
|
+
"bm2"
|
|
39
|
+
],
|
|
40
|
+
"author": {
|
|
41
|
+
"name": "MaxxPainn",
|
|
42
|
+
"email": "hello@maxxpainn.com",
|
|
43
|
+
"url": "https://maxxpainn.com"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/bun-bm2/bm2.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/bun-bm2/bm2/issues",
|
|
52
|
+
"email": "hello@maxxpainn.com"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/bun-bm2/bm2#readme",
|
|
55
|
+
"engines": {
|
|
56
|
+
"bun": ">=1.0.0"
|
|
57
|
+
},
|
|
58
|
+
"os": [
|
|
59
|
+
"linux",
|
|
60
|
+
"darwin"
|
|
61
|
+
],
|
|
62
|
+
"dependencies": {},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/bun": "^1.3.9",
|
|
65
|
+
"bun-types": "latest",
|
|
66
|
+
"typescript": "^5.9.3"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* A production-grade process manager for Bun.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fork & cluster execution modes
|
|
7
|
+
* - Auto-restart & crash recovery
|
|
8
|
+
* - Health checks & monitoring
|
|
9
|
+
* - Log management & rotation
|
|
10
|
+
* - Deployment support
|
|
11
|
+
*
|
|
12
|
+
* https://github.com/your-org/bm2
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
import type { Subprocess } from "bun";
|
|
17
|
+
import type { ProcessDescription } from "./types";
|
|
18
|
+
import { getCpuCount } from "./utils";
|
|
19
|
+
|
|
20
|
+
export class ClusterManager {
|
|
21
|
+
private workers: Map<number, Map<number, Subprocess>> = new Map();
|
|
22
|
+
|
|
23
|
+
resolveInstances(instances: number | string | undefined): number {
|
|
24
|
+
if (instances === undefined || instances === 0) return 1;
|
|
25
|
+
if (typeof instances === "string") {
|
|
26
|
+
if (instances === "max" || instances === "-1") return getCpuCount();
|
|
27
|
+
return parseInt(instances) || 1;
|
|
28
|
+
}
|
|
29
|
+
if (instances === -1) return getCpuCount();
|
|
30
|
+
return instances;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
createWorkerEnv(
|
|
34
|
+
baseEnv: Record<string, string>,
|
|
35
|
+
workerId: number,
|
|
36
|
+
totalWorkers: number,
|
|
37
|
+
basePort?: number
|
|
38
|
+
): Record<string, string> {
|
|
39
|
+
return {
|
|
40
|
+
...baseEnv,
|
|
41
|
+
BM2_CLUSTER: "true",
|
|
42
|
+
BM2_WORKER_ID: String(workerId),
|
|
43
|
+
BM2_INSTANCES: String(totalWorkers),
|
|
44
|
+
NODE_APP_INSTANCE: String(workerId),
|
|
45
|
+
...(basePort ? { PORT: String(basePort + workerId) } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildWorkerCommand(config: ProcessDescription): string[] {
|
|
50
|
+
const cmd: string[] = [];
|
|
51
|
+
|
|
52
|
+
if (config.interpreter) {
|
|
53
|
+
cmd.push(config.interpreter);
|
|
54
|
+
if (config.interpreterArgs) cmd.push(...config.interpreterArgs);
|
|
55
|
+
} else {
|
|
56
|
+
const ext = config.script.split(".").pop()?.toLowerCase();
|
|
57
|
+
if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx" || ext === "mjs") {
|
|
58
|
+
cmd.push("bun", "run");
|
|
59
|
+
} else if (ext === "py") {
|
|
60
|
+
cmd.push("python3");
|
|
61
|
+
} else {
|
|
62
|
+
cmd.push("bun", "run");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (config.nodeArgs?.length) {
|
|
67
|
+
cmd.push(...config.nodeArgs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
cmd.push(config.script);
|
|
71
|
+
if (config.args?.length) cmd.push(...config.args);
|
|
72
|
+
|
|
73
|
+
return cmd;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
spawnWorker(
|
|
77
|
+
config: ProcessDescription,
|
|
78
|
+
workerId: number,
|
|
79
|
+
totalWorkers: number,
|
|
80
|
+
logStreams: { stdout: "pipe" | "inherit"; stderr: "pipe" | "inherit" }
|
|
81
|
+
): Subprocess {
|
|
82
|
+
const cmd = this.buildWorkerCommand(config);
|
|
83
|
+
const env = this.createWorkerEnv(
|
|
84
|
+
{ ...process.env as Record<string, string>, ...config.env },
|
|
85
|
+
workerId,
|
|
86
|
+
totalWorkers,
|
|
87
|
+
config.port
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const proc = Bun.spawn(cmd, {
|
|
91
|
+
cwd: config.cwd || process.cwd(),
|
|
92
|
+
env,
|
|
93
|
+
stdout: logStreams.stdout,
|
|
94
|
+
stderr: logStreams.stderr,
|
|
95
|
+
stdin: "ignore",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!this.workers.has(config.id)) {
|
|
99
|
+
this.workers.set(config.id, new Map());
|
|
100
|
+
}
|
|
101
|
+
this.workers.get(config.id)!.set(workerId, proc);
|
|
102
|
+
|
|
103
|
+
return proc;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getWorkers(processId: number): Map<number, Subprocess> | undefined {
|
|
107
|
+
return this.workers.get(processId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
removeWorker(processId: number, workerId: number) {
|
|
111
|
+
this.workers.get(processId)?.delete(workerId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
removeAllWorkers(processId: number) {
|
|
115
|
+
this.workers.delete(processId);
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* A production-grade process manager for Bun.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fork & cluster execution modes
|
|
7
|
+
* - Auto-restart & crash recovery
|
|
8
|
+
* - Health checks & monitoring
|
|
9
|
+
* - Log management & rotation
|
|
10
|
+
* - Deployment support
|
|
11
|
+
*
|
|
12
|
+
* https://github.com/your-org/bm2
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
|
|
20
|
+
export const APP_NAME = "bm2";
|
|
21
|
+
export const VERSION = "1.0.0";
|
|
22
|
+
|
|
23
|
+
export const BM2_HOME = join(homedir(), ".bm2");
|
|
24
|
+
export const DAEMON_SOCKET = join(BM2_HOME, "daemon.sock");
|
|
25
|
+
export const DAEMON_PID_FILE = join(BM2_HOME, "daemon.pid");
|
|
26
|
+
export const LOG_DIR = join(BM2_HOME, "logs");
|
|
27
|
+
export const PID_DIR = join(BM2_HOME, "pids");
|
|
28
|
+
export const DUMP_FILE = join(BM2_HOME, "dump.json");
|
|
29
|
+
export const METRICS_DIR = join(BM2_HOME, "metrics");
|
|
30
|
+
export const MODULE_DIR = join(BM2_HOME, "modules");
|
|
31
|
+
export const CONFIG_FILE = join(BM2_HOME, "config.json");
|
|
32
|
+
export const DASHBOARD_PORT = 9615;
|
|
33
|
+
export const METRICS_PORT = 9616;
|
|
34
|
+
|
|
35
|
+
export const ALL_DIRS = [BM2_HOME, LOG_DIR, PID_DIR, METRICS_DIR, MODULE_DIR];
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_KILL_TIMEOUT = 5000;
|
|
38
|
+
export const DEFAULT_MIN_UPTIME = 1000;
|
|
39
|
+
export const DEFAULT_MAX_RESTARTS = 16;
|
|
40
|
+
export const DEFAULT_RESTART_DELAY = 0;
|
|
41
|
+
export const DEFAULT_LOG_MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
|
42
|
+
export const DEFAULT_LOG_RETAIN = 5;
|
|
43
|
+
export const MONITOR_INTERVAL = 1000;
|
|
44
|
+
export const HEALTH_CHECK_INTERVAL = 30000;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* A production-grade process manager for Bun.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fork & cluster execution modes
|
|
7
|
+
* - Auto-restart & crash recovery
|
|
8
|
+
* - Health checks & monitoring
|
|
9
|
+
* - Log management & rotation
|
|
10
|
+
* - Deployment support
|
|
11
|
+
*
|
|
12
|
+
* https://github.com/your-org/bm2
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
import { parseCron } from "./utils";
|
|
17
|
+
|
|
18
|
+
export class CronManager {
|
|
19
|
+
private jobs: Map<number, {
|
|
20
|
+
expression: string;
|
|
21
|
+
timer: ReturnType<typeof setTimeout>;
|
|
22
|
+
}> = new Map();
|
|
23
|
+
|
|
24
|
+
schedule(processId: number, expression: string, callback: () => void) {
|
|
25
|
+
this.cancel(processId);
|
|
26
|
+
|
|
27
|
+
const scheduleNext = () => {
|
|
28
|
+
try {
|
|
29
|
+
const cron = parseCron(expression);
|
|
30
|
+
const nextDate = cron.next();
|
|
31
|
+
const delay = nextDate.getTime() - Date.now();
|
|
32
|
+
|
|
33
|
+
if (delay < 0) {
|
|
34
|
+
// Schedule for next minute at least
|
|
35
|
+
setTimeout(scheduleNext, 60000);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
callback();
|
|
41
|
+
scheduleNext(); // Schedule the next occurrence
|
|
42
|
+
}, delay);
|
|
43
|
+
|
|
44
|
+
this.jobs.set(processId, { expression, timer });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`[bm2] Cron schedule error for process ${processId}:`, err);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
scheduleNext();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cancel(processId: number) {
|
|
54
|
+
const job = this.jobs.get(processId);
|
|
55
|
+
if (job) {
|
|
56
|
+
clearTimeout(job.timer);
|
|
57
|
+
this.jobs.delete(processId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cancelAll() {
|
|
62
|
+
for (const [id] of this.jobs) {
|
|
63
|
+
this.cancel(id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
listJobs() {
|
|
68
|
+
const result: Array<{ processId: number; expression: string }> = [];
|
|
69
|
+
for (const [id, job] of this.jobs) {
|
|
70
|
+
result.push({ processId: id, expression: job.expression });
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* A production-grade process manager for Bun.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fork & cluster execution modes
|
|
7
|
+
* - Auto-restart & crash recovery
|
|
8
|
+
* - Health checks & monitoring
|
|
9
|
+
* - Log management & rotation
|
|
10
|
+
* - Deployment support
|
|
11
|
+
*
|
|
12
|
+
* https://github.com/your-org/bm2
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ProcessManager } from "./process-manager";
|
|
18
|
+
import { Dashboard } from "./dashboard";
|
|
19
|
+
import { ModuleManager } from "./module-manager";
|
|
20
|
+
import {
|
|
21
|
+
DAEMON_SOCKET,
|
|
22
|
+
DAEMON_PID_FILE,
|
|
23
|
+
DASHBOARD_PORT,
|
|
24
|
+
METRICS_PORT,
|
|
25
|
+
} from "./constants";
|
|
26
|
+
import { ensureDirs } from "./utils";
|
|
27
|
+
import { unlinkSync, existsSync } from "fs";
|
|
28
|
+
import type { DaemonMessage, DaemonResponse } from "./types";
|
|
29
|
+
import type { ServerWebSocket } from "bun";
|
|
30
|
+
|
|
31
|
+
ensureDirs();
|
|
32
|
+
|
|
33
|
+
const pm = new ProcessManager();
|
|
34
|
+
const dashboard = new Dashboard(pm);
|
|
35
|
+
const moduleManager = new ModuleManager(pm);
|
|
36
|
+
|
|
37
|
+
// Clean up existing socket
|
|
38
|
+
if (existsSync(DAEMON_SOCKET)) {
|
|
39
|
+
try { unlinkSync(DAEMON_SOCKET); } catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Write PID file
|
|
43
|
+
await Bun.write(DAEMON_PID_FILE, String(process.pid));
|
|
44
|
+
|
|
45
|
+
// Load modules
|
|
46
|
+
await moduleManager.loadAll();
|
|
47
|
+
|
|
48
|
+
// Start metric collection
|
|
49
|
+
const metricsInterval = setInterval(() => {
|
|
50
|
+
pm.getMetrics();
|
|
51
|
+
}, 2000);
|
|
52
|
+
|
|
53
|
+
async function handleMessage(msg: DaemonMessage): Promise<DaemonResponse> {
|
|
54
|
+
try {
|
|
55
|
+
switch (msg.type) {
|
|
56
|
+
case "start": {
|
|
57
|
+
const states = await pm.start(msg.data);
|
|
58
|
+
return { type: "start", data: states, success: true, id: msg.id };
|
|
59
|
+
}
|
|
60
|
+
case "stop": {
|
|
61
|
+
const states = await pm.stop(msg.data.target);
|
|
62
|
+
return { type: "stop", data: states, success: true, id: msg.id };
|
|
63
|
+
}
|
|
64
|
+
case "restart": {
|
|
65
|
+
const states = await pm.restart(msg.data.target);
|
|
66
|
+
return { type: "restart", data: states, success: true, id: msg.id };
|
|
67
|
+
}
|
|
68
|
+
case "reload": {
|
|
69
|
+
const states = await pm.reload(msg.data.target);
|
|
70
|
+
return { type: "reload", data: states, success: true, id: msg.id };
|
|
71
|
+
}
|
|
72
|
+
case "delete": {
|
|
73
|
+
const states = await pm.del(msg.data.target);
|
|
74
|
+
return { type: "delete", data: states, success: true, id: msg.id };
|
|
75
|
+
}
|
|
76
|
+
case "scale": {
|
|
77
|
+
const states = await pm.scale(msg.data.target, msg.data.count);
|
|
78
|
+
return { type: "scale", data: states, success: true, id: msg.id };
|
|
79
|
+
}
|
|
80
|
+
case "stopAll": {
|
|
81
|
+
const states = await pm.stopAll();
|
|
82
|
+
return { type: "stopAll", data: states, success: true, id: msg.id };
|
|
83
|
+
}
|
|
84
|
+
case "restartAll": {
|
|
85
|
+
const states = await pm.restartAll();
|
|
86
|
+
return { type: "restartAll", data: states, success: true, id: msg.id };
|
|
87
|
+
}
|
|
88
|
+
case "reloadAll": {
|
|
89
|
+
const states = await pm.reloadAll();
|
|
90
|
+
return { type: "reloadAll", data: states, success: true, id: msg.id };
|
|
91
|
+
}
|
|
92
|
+
case "deleteAll": {
|
|
93
|
+
const states = await pm.deleteAll();
|
|
94
|
+
return { type: "deleteAll", data: states, success: true, id: msg.id };
|
|
95
|
+
}
|
|
96
|
+
case "list": {
|
|
97
|
+
return { type: "list", data: pm.list(), success: true, id: msg.id };
|
|
98
|
+
}
|
|
99
|
+
case "describe": {
|
|
100
|
+
return { type: "describe", data: pm.describe(msg.data.target), success: true, id: msg.id };
|
|
101
|
+
}
|
|
102
|
+
case "logs": {
|
|
103
|
+
const logs = await pm.getLogs(msg.data.target, msg.data.lines);
|
|
104
|
+
return { type: "logs", data: logs, success: true, id: msg.id };
|
|
105
|
+
}
|
|
106
|
+
case "flush": {
|
|
107
|
+
await pm.flushLogs(msg.data?.target);
|
|
108
|
+
return { type: "flush", success: true, id: msg.id };
|
|
109
|
+
}
|
|
110
|
+
case "save": {
|
|
111
|
+
await pm.save();
|
|
112
|
+
return { type: "save", success: true, id: msg.id };
|
|
113
|
+
}
|
|
114
|
+
case "resurrect": {
|
|
115
|
+
const states = await pm.resurrect();
|
|
116
|
+
return { type: "resurrect", data: states, success: true, id: msg.id };
|
|
117
|
+
}
|
|
118
|
+
case "ecosystem": {
|
|
119
|
+
const states = await pm.startEcosystem(msg.data);
|
|
120
|
+
return { type: "ecosystem", data: states, success: true, id: msg.id };
|
|
121
|
+
}
|
|
122
|
+
case "signal": {
|
|
123
|
+
await pm.sendSignal(msg.data.target, msg.data.signal);
|
|
124
|
+
return { type: "signal", success: true, id: msg.id };
|
|
125
|
+
}
|
|
126
|
+
case "reset": {
|
|
127
|
+
const states = await pm.reset(msg.data.target);
|
|
128
|
+
return { type: "reset", data: states, success: true, id: msg.id };
|
|
129
|
+
}
|
|
130
|
+
case "metrics": {
|
|
131
|
+
const metrics = await pm.getMetrics();
|
|
132
|
+
return { type: "metrics", data: metrics, success: true, id: msg.id };
|
|
133
|
+
}
|
|
134
|
+
case "metricsHistory": {
|
|
135
|
+
const history = pm.getMetricsHistory(msg.data?.seconds || 300);
|
|
136
|
+
return { type: "metricsHistory", data: history, success: true, id: msg.id };
|
|
137
|
+
}
|
|
138
|
+
case "prometheus": {
|
|
139
|
+
const prom = pm.getPrometheusMetrics();
|
|
140
|
+
return { type: "prometheus", data: prom, success: true, id: msg.id };
|
|
141
|
+
}
|
|
142
|
+
case "dashboard": {
|
|
143
|
+
const port = msg.data?.port || DASHBOARD_PORT;
|
|
144
|
+
const metricsPort = msg.data?.metricsPort || METRICS_PORT;
|
|
145
|
+
dashboard.start(port, metricsPort);
|
|
146
|
+
return { type: "dashboard", data: { port, metricsPort }, success: true, id: msg.id };
|
|
147
|
+
}
|
|
148
|
+
case "dashboardStop": {
|
|
149
|
+
dashboard.stop();
|
|
150
|
+
return { type: "dashboardStop", success: true, id: msg.id };
|
|
151
|
+
}
|
|
152
|
+
case "moduleInstall": {
|
|
153
|
+
const path = await moduleManager.install(msg.data.module);
|
|
154
|
+
return { type: "moduleInstall", data: { path }, success: true, id: msg.id };
|
|
155
|
+
}
|
|
156
|
+
case "moduleUninstall": {
|
|
157
|
+
await moduleManager.uninstall(msg.data.module);
|
|
158
|
+
return { type: "moduleUninstall", success: true, id: msg.id };
|
|
159
|
+
}
|
|
160
|
+
case "moduleList": {
|
|
161
|
+
return { type: "moduleList", data: moduleManager.list(), success: true, id: msg.id };
|
|
162
|
+
}
|
|
163
|
+
case "ping": {
|
|
164
|
+
return {
|
|
165
|
+
type: "pong",
|
|
166
|
+
data: { pid: process.pid, uptime: process.uptime() },
|
|
167
|
+
success: true,
|
|
168
|
+
id: msg.id,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case "kill": {
|
|
172
|
+
await pm.stopAll();
|
|
173
|
+
dashboard.stop();
|
|
174
|
+
clearInterval(metricsInterval);
|
|
175
|
+
setTimeout(() => process.exit(0), 200);
|
|
176
|
+
return { type: "kill", success: true, id: msg.id };
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
return { type: "error", error: `Unknown command: ${msg.type}`, success: false, id: msg.id };
|
|
180
|
+
}
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
return { type: "error", error: err.message, success: false, id: msg.id };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Unix socket server
|
|
187
|
+
const server = Bun.serve({
|
|
188
|
+
unix: DAEMON_SOCKET,
|
|
189
|
+
fetch(req, server) {
|
|
190
|
+
if (server.upgrade(req)) return;
|
|
191
|
+
return new Response("bm2 daemon");
|
|
192
|
+
},
|
|
193
|
+
websocket: {
|
|
194
|
+
async message(ws: ServerWebSocket<unknown>, message) {
|
|
195
|
+
try {
|
|
196
|
+
const msg: DaemonMessage = JSON.parse(String(message));
|
|
197
|
+
const response = await handleMessage(msg);
|
|
198
|
+
ws.send(JSON.stringify(response));
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
ws.send(JSON.stringify({ type: "error", error: err.message, success: false }));
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
open(ws) {},
|
|
204
|
+
close(ws) {},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Signal handlers
|
|
209
|
+
const shutdown = async () => {
|
|
210
|
+
console.log("\n[bm2] Shutting down daemon...");
|
|
211
|
+
await pm.stopAll();
|
|
212
|
+
dashboard.stop();
|
|
213
|
+
clearInterval(metricsInterval);
|
|
214
|
+
try { unlinkSync(DAEMON_SOCKET); } catch {}
|
|
215
|
+
try { unlinkSync(DAEMON_PID_FILE); } catch {}
|
|
216
|
+
process.exit(0);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
process.on("SIGTERM", shutdown);
|
|
220
|
+
process.on("SIGINT", shutdown);
|
|
221
|
+
process.on("SIGHUP", shutdown);
|
|
222
|
+
|
|
223
|
+
// Handle uncaught errors to keep daemon alive
|
|
224
|
+
process.on("uncaughtException", (err) => {
|
|
225
|
+
console.error("[bm2] Uncaught exception:", err);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
process.on("unhandledRejection", (err) => {
|
|
229
|
+
console.error("[bm2] Unhandled rejection:", err);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log(`[bm2] Daemon running (PID: ${process.pid})`);
|
|
233
|
+
console.log(`[bm2] Socket: ${DAEMON_SOCKET}`);
|