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/src/monitor.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
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 type { MetricSnapshot, ProcessState } from "./types";
|
|
18
|
+
import { getSystemInfo } from "./utils";
|
|
19
|
+
import { METRICS_DIR } from "./constants";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
|
|
22
|
+
export class Monitor {
|
|
23
|
+
private history: MetricSnapshot[] = [];
|
|
24
|
+
private maxHistory = 3600; // 1 hour at 1s intervals
|
|
25
|
+
|
|
26
|
+
async collectProcessMetrics(
|
|
27
|
+
pid: number
|
|
28
|
+
): Promise<{ memory: number; cpu: number; handles?: number }> {
|
|
29
|
+
try {
|
|
30
|
+
if (process.platform === "linux") {
|
|
31
|
+
const statusFile = Bun.file(`/proc/${pid}/status`);
|
|
32
|
+
const statFile = Bun.file(`/proc/${pid}/stat`);
|
|
33
|
+
|
|
34
|
+
let memory = 0;
|
|
35
|
+
let cpu = 0;
|
|
36
|
+
let handles: number | undefined;
|
|
37
|
+
|
|
38
|
+
if (await statusFile.exists()) {
|
|
39
|
+
const content = await statusFile.text();
|
|
40
|
+
const vmRss = content.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
41
|
+
|
|
42
|
+
if (vmRss) memory = parseInt(vmRss[1]!) * 1024;
|
|
43
|
+
|
|
44
|
+
// Count file descriptors
|
|
45
|
+
try {
|
|
46
|
+
const { readdirSync } = require("fs");
|
|
47
|
+
const fds = readdirSync(`/proc/${pid}/fd`);
|
|
48
|
+
handles = fds.length;
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (await statFile.exists()) {
|
|
53
|
+
const stat = await statFile.text();
|
|
54
|
+
const parts = stat.split(" ");
|
|
55
|
+
|
|
56
|
+
const utime = parseInt(parts[13]!) || 0;
|
|
57
|
+
const stime = parseInt(parts[14]!) || 0;
|
|
58
|
+
|
|
59
|
+
// Simplified CPU calculation
|
|
60
|
+
cpu = (utime + stime) / 100;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { memory, cpu, handles };
|
|
64
|
+
} else {
|
|
65
|
+
// macOS / fallback
|
|
66
|
+
const ps = Bun.spawn(
|
|
67
|
+
["ps", "-o", "rss=,pcpu=", "-p", String(pid)],
|
|
68
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
69
|
+
);
|
|
70
|
+
const output = await new Response(ps.stdout).text();
|
|
71
|
+
const parts = output.trim().split(/\s+/);
|
|
72
|
+
|
|
73
|
+
if (parts.length >= 2) {
|
|
74
|
+
return {
|
|
75
|
+
memory: parseInt(parts[0]!) * 1024,
|
|
76
|
+
cpu: parseFloat(parts[1]!),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
return { memory: 0, cpu: 0 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async takeSnapshot(processes: ProcessState[]): Promise<MetricSnapshot> {
|
|
86
|
+
const system = getSystemInfo();
|
|
87
|
+
const snapshot: MetricSnapshot = {
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
processes: processes.map((p) => ({
|
|
90
|
+
id: p.id,
|
|
91
|
+
name: p.name,
|
|
92
|
+
pid: p.pid,
|
|
93
|
+
cpu: p.monit.cpu,
|
|
94
|
+
memory: p.monit.memory,
|
|
95
|
+
eventLoopLatency: p.monit.eventLoopLatency,
|
|
96
|
+
handles: p.monit.handles,
|
|
97
|
+
status: p.status,
|
|
98
|
+
restarts: p.pm2_env.restart_time,
|
|
99
|
+
uptime: p.pm2_env.status === "online" ? Date.now() - p.pm2_env.pm_uptime : 0,
|
|
100
|
+
})),
|
|
101
|
+
system: {
|
|
102
|
+
totalMemory: system.totalMemory,
|
|
103
|
+
freeMemory: system.freeMemory,
|
|
104
|
+
cpuCount: system.cpuCount,
|
|
105
|
+
loadAvg: system.loadAvg,
|
|
106
|
+
platform: system.platform,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.history.push(snapshot);
|
|
111
|
+
if (this.history.length > this.maxHistory) {
|
|
112
|
+
this.history = this.history.slice(-this.maxHistory);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return snapshot;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getHistory(seconds: number = 300): MetricSnapshot[] {
|
|
119
|
+
const cutoff = Date.now() - seconds * 1000;
|
|
120
|
+
return this.history.filter((s) => s.timestamp >= cutoff);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getLatest(): MetricSnapshot | null {
|
|
124
|
+
return this.history.length > 0 ? this.history[this.history.length - 1]! : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async saveMetrics(): Promise<void> {
|
|
128
|
+
const filePath = join(METRICS_DIR, `metrics-${Date.now()}.json`);
|
|
129
|
+
await Bun.write(filePath, JSON.stringify(this.history.slice(-300)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
generatePrometheusMetrics(processes: ProcessState[]): string {
|
|
133
|
+
const lines: string[] = [];
|
|
134
|
+
|
|
135
|
+
lines.push("# HELP bm2_process_cpu CPU usage percentage");
|
|
136
|
+
lines.push("# TYPE bm2_process_cpu gauge");
|
|
137
|
+
for (const p of processes) {
|
|
138
|
+
lines.push(`bm2_process_cpu{name="${p.name}",id="${p.pm_id}"} ${p.monit.cpu}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push("# HELP bm2_process_memory_bytes Memory usage in bytes");
|
|
142
|
+
lines.push("# TYPE bm2_process_memory_bytes gauge");
|
|
143
|
+
for (const p of processes) {
|
|
144
|
+
lines.push(`bm2_process_memory_bytes{name="${p.name}",id="${p.pm_id}"} ${p.monit.memory}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines.push("# HELP bm2_process_restarts_total Total restart count");
|
|
148
|
+
lines.push("# TYPE bm2_process_restarts_total counter");
|
|
149
|
+
for (const p of processes) {
|
|
150
|
+
lines.push(`bm2_process_restarts_total{name="${p.name}",id="${p.pm_id}"} ${p.pm2_env.restart_time}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push("# HELP bm2_process_uptime_seconds Process uptime in seconds");
|
|
154
|
+
lines.push("# TYPE bm2_process_uptime_seconds gauge");
|
|
155
|
+
for (const p of processes) {
|
|
156
|
+
const uptime = p.pm2_env.status === "online"
|
|
157
|
+
? (Date.now() - p.pm2_env.pm_uptime) / 1000
|
|
158
|
+
: 0;
|
|
159
|
+
lines.push(`bm2_process_uptime_seconds{name="${p.name}",id="${p.pm_id}"} ${uptime.toFixed(0)}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lines.push("# HELP bm2_process_status Process status (1=online)");
|
|
163
|
+
lines.push("# TYPE bm2_process_status gauge");
|
|
164
|
+
for (const p of processes) {
|
|
165
|
+
lines.push(`bm2_process_status{name="${p.name}",id="${p.pm_id}",status="${p.status}"} ${p.status === "online" ? 1 : 0}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sys = getSystemInfo();
|
|
169
|
+
lines.push("# HELP bm2_system_memory_total_bytes Total system memory");
|
|
170
|
+
lines.push("# TYPE bm2_system_memory_total_bytes gauge");
|
|
171
|
+
lines.push(`bm2_system_memory_total_bytes ${sys.totalMemory}`);
|
|
172
|
+
|
|
173
|
+
lines.push("# HELP bm2_system_memory_free_bytes Free system memory");
|
|
174
|
+
lines.push("# TYPE bm2_system_memory_free_bytes gauge");
|
|
175
|
+
lines.push(`bm2_system_memory_free_bytes ${sys.freeMemory}`);
|
|
176
|
+
|
|
177
|
+
lines.push("# HELP bm2_system_load_average System load average");
|
|
178
|
+
lines.push("# TYPE bm2_system_load_average gauge");
|
|
179
|
+
lines.push(`bm2_system_load_average{period="1m"} ${sys.loadAvg[0]}`);
|
|
180
|
+
lines.push(`bm2_system_load_average{period="5m"} ${sys.loadAvg[1]}`);
|
|
181
|
+
lines.push(`bm2_system_load_average{period="15m"} ${sys.loadAvg[2]}`);
|
|
182
|
+
|
|
183
|
+
return lines.join("\n") + "\n";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,508 @@
|
|
|
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 {
|
|
18
|
+
ProcessDescription,
|
|
19
|
+
ProcessState,
|
|
20
|
+
ProcessStatus,
|
|
21
|
+
LogRotateOptions,
|
|
22
|
+
} from "./types";
|
|
23
|
+
import { LogManager } from "./log-manager";
|
|
24
|
+
import { ClusterManager } from "./cluster-manager";
|
|
25
|
+
import { HealthChecker } from "./health-checker";
|
|
26
|
+
import { CronManager } from "./cron-manager";
|
|
27
|
+
import { treeKill } from "./utils";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
import {
|
|
30
|
+
PID_DIR,
|
|
31
|
+
MONITOR_INTERVAL,
|
|
32
|
+
DEFAULT_LOG_MAX_SIZE,
|
|
33
|
+
DEFAULT_LOG_RETAIN,
|
|
34
|
+
} from "./constants";
|
|
35
|
+
|
|
36
|
+
export class ProcessContainer {
|
|
37
|
+
public id: number;
|
|
38
|
+
public name: string;
|
|
39
|
+
public config: ProcessDescription;
|
|
40
|
+
public status: ProcessStatus = "stopped";
|
|
41
|
+
public process: Subprocess | null = null;
|
|
42
|
+
public pid: number | undefined;
|
|
43
|
+
public restartCount: number = 0;
|
|
44
|
+
public unstableRestarts: number = 0;
|
|
45
|
+
public createdAt: number;
|
|
46
|
+
public startedAt: number = 0;
|
|
47
|
+
public memory: number = 0;
|
|
48
|
+
public cpu: number = 0;
|
|
49
|
+
public handles: number = 0;
|
|
50
|
+
public eventLoopLatency: number = 0;
|
|
51
|
+
public axmMonitor: Record<string, any> = {};
|
|
52
|
+
|
|
53
|
+
private logManager: LogManager;
|
|
54
|
+
private clusterManager: ClusterManager;
|
|
55
|
+
private healthChecker: HealthChecker;
|
|
56
|
+
private cronManager: CronManager;
|
|
57
|
+
private restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
58
|
+
private watcher: ReturnType<typeof import("fs").watch> | null = null;
|
|
59
|
+
private monitorInterval: ReturnType<typeof setInterval> | null = null;
|
|
60
|
+
private logRotateInterval: ReturnType<typeof setInterval> | null = null;
|
|
61
|
+
private isRestarting: boolean = false;
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
id: number,
|
|
65
|
+
config: ProcessDescription,
|
|
66
|
+
logManager: LogManager,
|
|
67
|
+
clusterManager: ClusterManager,
|
|
68
|
+
healthChecker: HealthChecker,
|
|
69
|
+
cronManager: CronManager
|
|
70
|
+
) {
|
|
71
|
+
this.id = id;
|
|
72
|
+
this.name = config.name;
|
|
73
|
+
this.config = config;
|
|
74
|
+
this.logManager = logManager;
|
|
75
|
+
this.clusterManager = clusterManager;
|
|
76
|
+
this.healthChecker = healthChecker;
|
|
77
|
+
this.cronManager = cronManager;
|
|
78
|
+
this.createdAt = Date.now();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async start(): Promise<void> {
|
|
82
|
+
if (this.status === "online") return;
|
|
83
|
+
|
|
84
|
+
this.status = "launching";
|
|
85
|
+
const logPaths = this.logManager.getLogPaths(
|
|
86
|
+
this.name,
|
|
87
|
+
this.id,
|
|
88
|
+
this.config.outFile,
|
|
89
|
+
this.config.errorFile
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Ensure log files exist
|
|
94
|
+
for (const f of [logPaths.outFile, logPaths.errFile]) {
|
|
95
|
+
const file = Bun.file(f);
|
|
96
|
+
if (!(await file.exists())) await Bun.write(f, "");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.config.execMode === "cluster" && this.config.instances > 1) {
|
|
100
|
+
await this.startCluster(logPaths);
|
|
101
|
+
} else {
|
|
102
|
+
await this.startFork(logPaths);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.startedAt = Date.now();
|
|
106
|
+
this.status = "online";
|
|
107
|
+
|
|
108
|
+
// Write PID file
|
|
109
|
+
if (this.pid) {
|
|
110
|
+
await Bun.write(
|
|
111
|
+
join(PID_DIR, `${this.name}-${this.id}.pid`),
|
|
112
|
+
String(this.pid)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Start monitoring
|
|
117
|
+
this.startMonitoring();
|
|
118
|
+
|
|
119
|
+
// Start log rotation
|
|
120
|
+
this.startLogRotation(logPaths);
|
|
121
|
+
|
|
122
|
+
// Setup watch mode
|
|
123
|
+
if (this.config.watch) {
|
|
124
|
+
this.setupWatch();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Setup health checks
|
|
128
|
+
if (this.config.healthCheckUrl) {
|
|
129
|
+
this.healthChecker.startCheck(
|
|
130
|
+
this.id,
|
|
131
|
+
{
|
|
132
|
+
url: this.config.healthCheckUrl,
|
|
133
|
+
interval: this.config.healthCheckInterval || 30000,
|
|
134
|
+
timeout: this.config.healthCheckTimeout || 5000,
|
|
135
|
+
maxFails: this.config.healthCheckMaxFails || 3,
|
|
136
|
+
},
|
|
137
|
+
(_id, reason) => {
|
|
138
|
+
console.log(`[bm2] Health check failed for ${this.name}: ${reason}`);
|
|
139
|
+
this.restart();
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Setup cron restart
|
|
145
|
+
if (this.config.cronRestart) {
|
|
146
|
+
this.cronManager.schedule(this.id, this.config.cronRestart, () => {
|
|
147
|
+
console.log(`[bm2] Cron restart triggered for ${this.name}`);
|
|
148
|
+
this.restart();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
this.status = "errored";
|
|
153
|
+
const timestamp = new Date().toISOString();
|
|
154
|
+
await this.logManager.appendLog(
|
|
155
|
+
logPaths.errFile,
|
|
156
|
+
`[${timestamp}] [bm2] Failed to start: ${err.message}\n`
|
|
157
|
+
);
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async startFork(logPaths: { outFile: string; errFile: string }) {
|
|
163
|
+
const cmd = this.clusterManager.buildWorkerCommand(this.config);
|
|
164
|
+
const env: Record<string, string> = {
|
|
165
|
+
...(process.env as Record<string, string>),
|
|
166
|
+
...this.config.env,
|
|
167
|
+
BM2_ID: String(this.id),
|
|
168
|
+
BM2_NAME: this.name,
|
|
169
|
+
BM2_EXEC_MODE: "fork",
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
this.process = Bun.spawn(cmd, {
|
|
173
|
+
cwd: this.config.cwd || process.cwd(),
|
|
174
|
+
env,
|
|
175
|
+
stdout: "pipe",
|
|
176
|
+
stderr: "pipe",
|
|
177
|
+
stdin: "ignore",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.pid = this.process.pid;
|
|
181
|
+
this.pipeOutput(logPaths);
|
|
182
|
+
|
|
183
|
+
this.process.exited.then((code) => {
|
|
184
|
+
if (!this.isRestarting) {
|
|
185
|
+
this.handleExit(code);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async startCluster(logPaths: { outFile: string; errFile: string }) {
|
|
191
|
+
const proc = this.clusterManager.spawnWorker(
|
|
192
|
+
this.config,
|
|
193
|
+
0,
|
|
194
|
+
this.config.instances,
|
|
195
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
this.process = proc;
|
|
199
|
+
this.pid = proc.pid;
|
|
200
|
+
|
|
201
|
+
if (proc.stdout && typeof proc.stdout !== "number") {
|
|
202
|
+
this.pipeStream(proc.stdout, logPaths.outFile);
|
|
203
|
+
}
|
|
204
|
+
if (proc.stderr && typeof proc.stderr !== "number") {
|
|
205
|
+
this.pipeStream(proc.stderr, logPaths.errFile);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
proc.exited.then((code) => {
|
|
209
|
+
if (!this.isRestarting) {
|
|
210
|
+
this.handleExit(code);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private pipeOutput(logPaths: { outFile: string; errFile: string }) {
|
|
216
|
+
if (!this.process) return;
|
|
217
|
+
if (this.process.stdout && typeof this.process.stdout !== "number") {
|
|
218
|
+
this.pipeStream(this.process.stdout, logPaths.outFile);
|
|
219
|
+
}
|
|
220
|
+
if (this.process.stderr && typeof this.process.stderr !== "number") {
|
|
221
|
+
this.pipeStream(this.process.stderr, logPaths.errFile);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async pipeStream(stream: ReadableStream<Uint8Array>, filePath: string) {
|
|
226
|
+
const reader = stream.getReader();
|
|
227
|
+
try {
|
|
228
|
+
while (true) {
|
|
229
|
+
const { done, value } = await reader.read();
|
|
230
|
+
if (done) break;
|
|
231
|
+
const text = new TextDecoder().decode(value);
|
|
232
|
+
const timestamp = new Date().toISOString();
|
|
233
|
+
const lines = text.split("\n").filter(Boolean);
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
await this.logManager.appendLog(filePath, `[${timestamp}] ${line}\n`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private startMonitoring() {
|
|
242
|
+
this.monitorInterval = setInterval(async () => {
|
|
243
|
+
if (!this.pid || this.status !== "online") return;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
if (process.platform === "linux") {
|
|
247
|
+
const statusFile = Bun.file(`/proc/${this.pid}/status`);
|
|
248
|
+
if (await statusFile.exists()) {
|
|
249
|
+
const content = await statusFile.text();
|
|
250
|
+
const vmRss = content.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
251
|
+
if (vmRss) this.memory = parseInt(vmRss[1]!) * 1024;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const { readdirSync } = require("fs");
|
|
256
|
+
this.handles = readdirSync(`/proc/${this.pid}/fd`).length;
|
|
257
|
+
} catch {}
|
|
258
|
+
} else {
|
|
259
|
+
const ps = Bun.spawn(["ps", "-o", "rss=,pcpu=", "-p", String(this.pid)], {
|
|
260
|
+
stdout: "pipe", stderr: "pipe",
|
|
261
|
+
});
|
|
262
|
+
const output = await new Response(ps.stdout).text();
|
|
263
|
+
const parts = output.trim().split(/\s+/);
|
|
264
|
+
if (parts.length >= 2) {
|
|
265
|
+
this.memory = parseInt(parts[0]!) * 1024;
|
|
266
|
+
this.cpu = parseFloat(parts[1]!);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Max memory restart
|
|
271
|
+
if (this.config.maxMemoryRestart && this.memory > this.config.maxMemoryRestart) {
|
|
272
|
+
console.log(`[bm2] ${this.name} exceeded memory limit (${this.memory} > ${this.config.maxMemoryRestart}), restarting...`);
|
|
273
|
+
await this.restart();
|
|
274
|
+
}
|
|
275
|
+
} catch {}
|
|
276
|
+
}, MONITOR_INTERVAL);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private startLogRotation(logPaths: { outFile: string; errFile: string }) {
|
|
280
|
+
const rotateOpts: LogRotateOptions = {
|
|
281
|
+
maxSize: this.config.logMaxSize || DEFAULT_LOG_MAX_SIZE,
|
|
282
|
+
retain: this.config.logRetain || DEFAULT_LOG_RETAIN,
|
|
283
|
+
compress: this.config.logCompress || false,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
this.logRotateInterval = setInterval(() => {
|
|
287
|
+
this.logManager.checkRotation(
|
|
288
|
+
this.name,
|
|
289
|
+
this.id,
|
|
290
|
+
rotateOpts,
|
|
291
|
+
this.config.outFile,
|
|
292
|
+
this.config.errorFile
|
|
293
|
+
);
|
|
294
|
+
}, 60000);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private setupWatch() {
|
|
298
|
+
const { watch } = require("fs");
|
|
299
|
+
const paths = this.config.watchPaths || [this.config.cwd || process.cwd()];
|
|
300
|
+
const ignorePatterns = this.config.ignoreWatch || ["node_modules", ".git", ".bm2"];
|
|
301
|
+
|
|
302
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
303
|
+
|
|
304
|
+
for (const watchPath of paths) {
|
|
305
|
+
try {
|
|
306
|
+
this.watcher = watch(
|
|
307
|
+
watchPath,
|
|
308
|
+
{ recursive: true },
|
|
309
|
+
(_event: string, filename: string | null) => {
|
|
310
|
+
if (!filename) return;
|
|
311
|
+
if (ignorePatterns.some((p) => filename.includes(p))) return;
|
|
312
|
+
|
|
313
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
314
|
+
debounceTimer = setTimeout(() => {
|
|
315
|
+
console.log(`[bm2] ${filename} changed, restarting ${this.name}...`);
|
|
316
|
+
this.restart();
|
|
317
|
+
}, 1000);
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private handleExit(code: number | null) {
|
|
325
|
+
const wasOnline = this.status === "online";
|
|
326
|
+
this.status = code === 0 ? "stopped" : "errored";
|
|
327
|
+
this.pid = undefined;
|
|
328
|
+
this.process = null;
|
|
329
|
+
|
|
330
|
+
this.cleanupTimers();
|
|
331
|
+
|
|
332
|
+
const uptime = Date.now() - this.startedAt;
|
|
333
|
+
|
|
334
|
+
if (wasOnline && this.config.autorestart && this.restartCount < this.config.maxRestarts) {
|
|
335
|
+
if (uptime < this.config.minUptime) {
|
|
336
|
+
this.unstableRestarts++;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.status = "waiting-restart";
|
|
340
|
+
const delay = this.config.restartDelay || 0;
|
|
341
|
+
|
|
342
|
+
this.restartTimer = setTimeout(() => {
|
|
343
|
+
this.restartCount++;
|
|
344
|
+
console.log(`[bm2] Restarting ${this.name} (attempt ${this.restartCount}/${this.config.maxRestarts})`);
|
|
345
|
+
this.start().catch((err) => {
|
|
346
|
+
console.error(`[bm2] Failed to restart ${this.name}:`, err);
|
|
347
|
+
});
|
|
348
|
+
}, delay);
|
|
349
|
+
} else if (this.restartCount >= this.config.maxRestarts) {
|
|
350
|
+
console.log(`[bm2] ${this.name} reached max restarts (${this.config.maxRestarts}), not restarting`);
|
|
351
|
+
this.status = "errored";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private cleanupTimers() {
|
|
356
|
+
if (this.monitorInterval) {
|
|
357
|
+
clearInterval(this.monitorInterval);
|
|
358
|
+
this.monitorInterval = null;
|
|
359
|
+
}
|
|
360
|
+
if (this.logRotateInterval) {
|
|
361
|
+
clearInterval(this.logRotateInterval);
|
|
362
|
+
this.logRotateInterval = null;
|
|
363
|
+
}
|
|
364
|
+
this.healthChecker.stopCheck(this.id);
|
|
365
|
+
this.cronManager.cancel(this.id);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async stop(force: boolean = false): Promise<void> {
|
|
369
|
+
if (this.status !== "online" && this.status !== "launching" && this.status !== "waiting-restart") {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.isRestarting = false;
|
|
374
|
+
this.status = "stopping";
|
|
375
|
+
this.config.autorestart = false;
|
|
376
|
+
|
|
377
|
+
if (this.restartTimer) {
|
|
378
|
+
clearTimeout(this.restartTimer);
|
|
379
|
+
this.restartTimer = null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.cleanupTimers();
|
|
383
|
+
|
|
384
|
+
if (this.watcher) {
|
|
385
|
+
this.watcher.close();
|
|
386
|
+
this.watcher = null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (this.process && this.pid) {
|
|
390
|
+
if (this.config.treekill !== false) {
|
|
391
|
+
await treeKill(this.pid, "SIGTERM");
|
|
392
|
+
} else {
|
|
393
|
+
this.process.kill("SIGTERM" as any);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!force) {
|
|
397
|
+
const timeout = this.config.killTimeout || 5000;
|
|
398
|
+
const exited = await Promise.race([
|
|
399
|
+
this.process.exited.then(() => true),
|
|
400
|
+
new Promise<boolean>((r) => setTimeout(() => r(false), timeout)),
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
if (!exited && this.process) {
|
|
404
|
+
if (this.config.treekill !== false && this.pid) {
|
|
405
|
+
await treeKill(this.pid, "SIGKILL");
|
|
406
|
+
} else {
|
|
407
|
+
this.process.kill("SIGKILL" as any);
|
|
408
|
+
}
|
|
409
|
+
await this.process.exited;
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
if (this.config.treekill !== false && this.pid) {
|
|
413
|
+
await treeKill(this.pid, "SIGKILL");
|
|
414
|
+
} else {
|
|
415
|
+
this.process.kill("SIGKILL" as any);
|
|
416
|
+
}
|
|
417
|
+
await this.process.exited;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Clean up cluster workers
|
|
422
|
+
this.clusterManager.removeAllWorkers(this.id);
|
|
423
|
+
|
|
424
|
+
this.status = "stopped";
|
|
425
|
+
this.pid = undefined;
|
|
426
|
+
this.process = null;
|
|
427
|
+
this.memory = 0;
|
|
428
|
+
this.cpu = 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async restart(): Promise<void> {
|
|
432
|
+
this.isRestarting = true;
|
|
433
|
+
const wasAutoRestart = this.config.autorestart;
|
|
434
|
+
await this.stop();
|
|
435
|
+
this.config.autorestart = wasAutoRestart;
|
|
436
|
+
this.isRestarting = false;
|
|
437
|
+
await this.start();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async reload(): Promise<void> {
|
|
441
|
+
const oldPid = this.pid;
|
|
442
|
+
const oldProcess = this.process;
|
|
443
|
+
|
|
444
|
+
this.isRestarting = true;
|
|
445
|
+
this.process = null;
|
|
446
|
+
this.pid = undefined;
|
|
447
|
+
|
|
448
|
+
await this.start();
|
|
449
|
+
|
|
450
|
+
// Wait for new process to be stable
|
|
451
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
452
|
+
|
|
453
|
+
// Kill old process
|
|
454
|
+
if (oldProcess && oldPid) {
|
|
455
|
+
try {
|
|
456
|
+
if (this.config.treekill !== false) {
|
|
457
|
+
await treeKill(oldPid, "SIGTERM");
|
|
458
|
+
} else {
|
|
459
|
+
oldProcess.kill("SIGTERM" as any);
|
|
460
|
+
}
|
|
461
|
+
} catch {}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.isRestarting = false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async sendSignal(signal: string): Promise<void> {
|
|
468
|
+
if (this.pid) {
|
|
469
|
+
process.kill(this.pid, signal as any);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
getState(): ProcessState {
|
|
474
|
+
return {
|
|
475
|
+
id: this.id,
|
|
476
|
+
name: this.name,
|
|
477
|
+
namespace: this.config.namespace,
|
|
478
|
+
status: this.status,
|
|
479
|
+
pid: this.pid,
|
|
480
|
+
pm_id: this.id,
|
|
481
|
+
monit: {
|
|
482
|
+
memory: this.memory,
|
|
483
|
+
cpu: this.cpu,
|
|
484
|
+
handles: this.handles,
|
|
485
|
+
eventLoopLatency: this.eventLoopLatency,
|
|
486
|
+
},
|
|
487
|
+
pm2_env: {
|
|
488
|
+
...this.config,
|
|
489
|
+
status: this.status,
|
|
490
|
+
pm_uptime: this.startedAt,
|
|
491
|
+
restart_time: this.restartCount,
|
|
492
|
+
unstable_restarts: this.unstableRestarts,
|
|
493
|
+
created_at: this.createdAt,
|
|
494
|
+
pm_id: this.id,
|
|
495
|
+
axm_monitor: this.axmMonitor,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
toJSON() {
|
|
501
|
+
return {
|
|
502
|
+
id: this.id,
|
|
503
|
+
name: this.name,
|
|
504
|
+
config: this.config,
|
|
505
|
+
restartCount: this.restartCount,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|