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/types.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
export type ProcessStatus =
|
|
18
|
+
| "online"
|
|
19
|
+
| "stopping"
|
|
20
|
+
| "stopped"
|
|
21
|
+
| "errored"
|
|
22
|
+
| "launching"
|
|
23
|
+
| "waiting-restart"
|
|
24
|
+
| "one-launch-status";
|
|
25
|
+
|
|
26
|
+
export type ExecMode = "fork" | "cluster";
|
|
27
|
+
|
|
28
|
+
export interface ProcessDescription {
|
|
29
|
+
id: number;
|
|
30
|
+
name: string;
|
|
31
|
+
script: string;
|
|
32
|
+
args: string[];
|
|
33
|
+
cwd: string;
|
|
34
|
+
env: Record<string, string>;
|
|
35
|
+
instances: number;
|
|
36
|
+
execMode: ExecMode;
|
|
37
|
+
autorestart: boolean;
|
|
38
|
+
maxRestarts: number;
|
|
39
|
+
minUptime: number;
|
|
40
|
+
maxMemoryRestart?: number;
|
|
41
|
+
watch: boolean;
|
|
42
|
+
watchPaths?: string[];
|
|
43
|
+
ignoreWatch?: string[];
|
|
44
|
+
cronRestart?: string;
|
|
45
|
+
interpreter?: string;
|
|
46
|
+
interpreterArgs?: string[];
|
|
47
|
+
mergeLogs: boolean;
|
|
48
|
+
logDateFormat?: string;
|
|
49
|
+
errorFile?: string;
|
|
50
|
+
outFile?: string;
|
|
51
|
+
pidFile?: string;
|
|
52
|
+
killTimeout: number;
|
|
53
|
+
restartDelay: number;
|
|
54
|
+
listenTimeout?: number;
|
|
55
|
+
shutdownWithMessage?: boolean;
|
|
56
|
+
treekill?: boolean;
|
|
57
|
+
port?: number;
|
|
58
|
+
// Cluster specific
|
|
59
|
+
clusterMode?: boolean;
|
|
60
|
+
reusePort?: boolean;
|
|
61
|
+
// Health check
|
|
62
|
+
healthCheckUrl?: string;
|
|
63
|
+
healthCheckInterval?: number;
|
|
64
|
+
healthCheckTimeout?: number;
|
|
65
|
+
healthCheckMaxFails?: number;
|
|
66
|
+
// Log rotation
|
|
67
|
+
logMaxSize?: number;
|
|
68
|
+
logRetain?: number;
|
|
69
|
+
logCompress?: boolean;
|
|
70
|
+
// Graceful
|
|
71
|
+
gracefulListenTimeout?: number;
|
|
72
|
+
waitReady?: boolean;
|
|
73
|
+
// Deploy
|
|
74
|
+
deployConfig?: DeployConfig;
|
|
75
|
+
// Source map
|
|
76
|
+
sourceMapSupport?: boolean;
|
|
77
|
+
// Node args compatibility
|
|
78
|
+
nodeArgs?: string[];
|
|
79
|
+
// Namespace
|
|
80
|
+
namespace?: string;
|
|
81
|
+
// Version tracking
|
|
82
|
+
version?: string;
|
|
83
|
+
versioningConfig?: VersioningConfig;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface VersioningConfig {
|
|
87
|
+
currentVersion?: string;
|
|
88
|
+
previousVersions?: string[];
|
|
89
|
+
maxVersions?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ProcessState {
|
|
93
|
+
id: number;
|
|
94
|
+
name: string;
|
|
95
|
+
namespace?: string;
|
|
96
|
+
status: ProcessStatus;
|
|
97
|
+
pid?: number;
|
|
98
|
+
pm_id: number;
|
|
99
|
+
monit: {
|
|
100
|
+
memory: number;
|
|
101
|
+
cpu: number;
|
|
102
|
+
handles?: number;
|
|
103
|
+
eventLoopLatency?: number;
|
|
104
|
+
};
|
|
105
|
+
pm2_env: ProcessDescription & {
|
|
106
|
+
status: ProcessStatus;
|
|
107
|
+
pm_uptime: number;
|
|
108
|
+
restart_time: number;
|
|
109
|
+
unstable_restarts: number;
|
|
110
|
+
created_at: number;
|
|
111
|
+
pm_id: number;
|
|
112
|
+
version?: string;
|
|
113
|
+
axm_monitor?: Record<string, any>;
|
|
114
|
+
axm_actions?: any[];
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface StartOptions {
|
|
119
|
+
name?: string;
|
|
120
|
+
script: string;
|
|
121
|
+
args?: string[];
|
|
122
|
+
cwd?: string;
|
|
123
|
+
env?: Record<string, string>;
|
|
124
|
+
instances?: number;
|
|
125
|
+
execMode?: ExecMode;
|
|
126
|
+
autorestart?: boolean;
|
|
127
|
+
maxRestarts?: number;
|
|
128
|
+
minUptime?: number;
|
|
129
|
+
maxMemoryRestart?: string | number;
|
|
130
|
+
watch?: boolean | string[];
|
|
131
|
+
ignoreWatch?: string[];
|
|
132
|
+
interpreter?: string;
|
|
133
|
+
interpreterArgs?: string[];
|
|
134
|
+
mergeLogs?: boolean;
|
|
135
|
+
logDateFormat?: string;
|
|
136
|
+
errorFile?: string;
|
|
137
|
+
outFile?: string;
|
|
138
|
+
killTimeout?: number;
|
|
139
|
+
restartDelay?: number;
|
|
140
|
+
cron?: string;
|
|
141
|
+
port?: number;
|
|
142
|
+
healthCheckUrl?: string;
|
|
143
|
+
healthCheckInterval?: number;
|
|
144
|
+
healthCheckTimeout?: number;
|
|
145
|
+
healthCheckMaxFails?: number;
|
|
146
|
+
logMaxSize?: string | number;
|
|
147
|
+
logRetain?: number;
|
|
148
|
+
logCompress?: boolean;
|
|
149
|
+
waitReady?: boolean;
|
|
150
|
+
listenTimeout?: number;
|
|
151
|
+
namespace?: string;
|
|
152
|
+
nodeArgs?: string[];
|
|
153
|
+
sourceMapSupport?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface EcosystemConfig {
|
|
157
|
+
apps: StartOptions[];
|
|
158
|
+
deploy?: Record<string, DeployConfig>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface DeployConfig {
|
|
162
|
+
user: string;
|
|
163
|
+
host: string | string[];
|
|
164
|
+
ref: string;
|
|
165
|
+
repo: string;
|
|
166
|
+
path: string;
|
|
167
|
+
preDeploy?: string;
|
|
168
|
+
postDeploy?: string;
|
|
169
|
+
preSetup?: string;
|
|
170
|
+
postSetup?: string;
|
|
171
|
+
ssh_options?: string;
|
|
172
|
+
env?: Record<string, string>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface DaemonMessage {
|
|
176
|
+
type: string;
|
|
177
|
+
data?: any;
|
|
178
|
+
id?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface DaemonResponse {
|
|
182
|
+
type: string;
|
|
183
|
+
data?: any;
|
|
184
|
+
success: boolean;
|
|
185
|
+
error?: string;
|
|
186
|
+
id?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface MetricSnapshot {
|
|
190
|
+
timestamp: number;
|
|
191
|
+
processes: Array<{
|
|
192
|
+
id: number;
|
|
193
|
+
name: string;
|
|
194
|
+
pid?: number;
|
|
195
|
+
cpu: number;
|
|
196
|
+
memory: number;
|
|
197
|
+
eventLoopLatency?: number;
|
|
198
|
+
handles?: number;
|
|
199
|
+
status: ProcessStatus;
|
|
200
|
+
restarts: number;
|
|
201
|
+
uptime: number;
|
|
202
|
+
}>;
|
|
203
|
+
system: {
|
|
204
|
+
totalMemory: number;
|
|
205
|
+
freeMemory: number;
|
|
206
|
+
cpuCount: number;
|
|
207
|
+
loadAvg: number[];
|
|
208
|
+
platform: string;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface LogRotateOptions {
|
|
213
|
+
maxSize: number;
|
|
214
|
+
retain: number;
|
|
215
|
+
compress: boolean;
|
|
216
|
+
dateFormat?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface HealthCheckConfig {
|
|
220
|
+
url: string;
|
|
221
|
+
interval: number;
|
|
222
|
+
timeout: number;
|
|
223
|
+
maxFails: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface DashboardState {
|
|
227
|
+
processes: ProcessState[];
|
|
228
|
+
metrics: MetricSnapshot;
|
|
229
|
+
logs: Record<string, { out: string; err: string }>;
|
|
230
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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 { mkdirSync, existsSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { ALL_DIRS, BM2_HOME } from "./constants";
|
|
20
|
+
|
|
21
|
+
export const DUMP_FILE = join(BM2_HOME, "dump.json");
|
|
22
|
+
|
|
23
|
+
export function ensureDirs() {
|
|
24
|
+
for (const dir of ALL_DIRS) {
|
|
25
|
+
if (!existsSync(dir)) {
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseMemory(value: string | number): number {
|
|
32
|
+
if (typeof value === "number") return value;
|
|
33
|
+
const match = value.match(/^(\d+(?:\.\d+)?)\s*(K|M|G|T)?B?$/i);
|
|
34
|
+
if (!match) throw new Error(`Invalid memory value: ${value}`);
|
|
35
|
+
const num = parseFloat(match[1]!);
|
|
36
|
+
const unit = (match[2] || "").toUpperCase();
|
|
37
|
+
const multipliers: Record<string, number> = {
|
|
38
|
+
"": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4,
|
|
39
|
+
};
|
|
40
|
+
return num * (multipliers[unit] || 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatBytes(bytes: number): string {
|
|
44
|
+
if (bytes === 0) return "0 B";
|
|
45
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
46
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
47
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatUptime(ms: number): string {
|
|
51
|
+
const s = Math.floor(ms / 1000);
|
|
52
|
+
const m = Math.floor(s / 60);
|
|
53
|
+
const h = Math.floor(m / 60);
|
|
54
|
+
const d = Math.floor(h / 24);
|
|
55
|
+
if (d > 0) return `${d}d ${h % 24}h`;
|
|
56
|
+
if (h > 0) return `${h}h ${m % 60}m`;
|
|
57
|
+
if (m > 0) return `${m}m ${s % 60}s`;
|
|
58
|
+
return `${s}s`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function generateId(): string {
|
|
62
|
+
return crypto.randomUUID().replace(/-/g, "").substring(0, 12);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function colorize(text: string, color: string): string {
|
|
66
|
+
const colors: Record<string, string> = {
|
|
67
|
+
red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m",
|
|
68
|
+
blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m",
|
|
69
|
+
white: "\x1b[37m", gray: "\x1b[90m", bold: "\x1b[1m",
|
|
70
|
+
dim: "\x1b[2m", reset: "\x1b[0m",
|
|
71
|
+
};
|
|
72
|
+
return `${colors[color] || ""}${text}\x1b[0m`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function padRight(str: string, len: number): string {
|
|
76
|
+
return str.length >= len ? str.substring(0, len) : str + " ".repeat(len - str.length);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getCpuCount(): number {
|
|
80
|
+
const cpus = require("os").cpus();
|
|
81
|
+
return cpus.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getSystemInfo() {
|
|
85
|
+
const os = require("os");
|
|
86
|
+
return {
|
|
87
|
+
totalMemory: os.totalmem(),
|
|
88
|
+
freeMemory: os.freemem(),
|
|
89
|
+
cpuCount: os.cpus().length,
|
|
90
|
+
loadAvg: os.loadavg(),
|
|
91
|
+
platform: os.platform(),
|
|
92
|
+
hostname: os.hostname(),
|
|
93
|
+
uptime: os.uptime(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function treeKill(pid: number, signal: string = "SIGTERM"): Promise<void> {
|
|
98
|
+
return new Promise(async (resolve) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = Bun.spawn(["pgrep", "-P", String(pid)], { stdout: "pipe" });
|
|
101
|
+
const output = await new Response(result.stdout).text();
|
|
102
|
+
const childPids = output.trim().split("\n").filter(Boolean).map(Number);
|
|
103
|
+
|
|
104
|
+
for (const childPid of childPids) {
|
|
105
|
+
await treeKill(childPid, signal);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
process.kill(pid, signal as any);
|
|
110
|
+
} catch {}
|
|
111
|
+
} catch {}
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseCron(expression: string): { next: () => Date } {
|
|
117
|
+
const parts = expression.trim().split(/\s+/);
|
|
118
|
+
if (parts.length !== 5) throw new Error(`Invalid cron: ${expression}`);
|
|
119
|
+
|
|
120
|
+
const [minExpr, hourExpr, domExpr, monExpr, dowExpr] = parts as [string, string, string, string, string];
|
|
121
|
+
|
|
122
|
+
function matchField(value: number, expr: string, _max: number): boolean {
|
|
123
|
+
if (expr === "*") return true;
|
|
124
|
+
|
|
125
|
+
for (const part of expr.split(",")) {
|
|
126
|
+
if (part.includes("/")) {
|
|
127
|
+
const [range, step] = part.split("/");
|
|
128
|
+
const stepNum = parseInt(step!);
|
|
129
|
+
const start = range === "*" ? 0 : parseInt(range!);
|
|
130
|
+
if ((value - start) % stepNum === 0 && value >= start) return true;
|
|
131
|
+
} else if (part.includes("-")) {
|
|
132
|
+
const [lo, hi] = part.split("-").map(Number);
|
|
133
|
+
if (value >= lo! && value <= hi!) return true;
|
|
134
|
+
} else {
|
|
135
|
+
if (value === parseInt(part)) return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
next(): Date {
|
|
143
|
+
const now = new Date();
|
|
144
|
+
const candidate = new Date(now);
|
|
145
|
+
candidate.setSeconds(0, 0);
|
|
146
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < 525600; i++) {
|
|
149
|
+
const min = candidate.getMinutes();
|
|
150
|
+
const hour = candidate.getHours();
|
|
151
|
+
const dom = candidate.getDate();
|
|
152
|
+
const mon = candidate.getMonth() + 1;
|
|
153
|
+
const dow = candidate.getDay();
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
matchField(min, minExpr, 59) &&
|
|
157
|
+
matchField(hour, hourExpr, 23) &&
|
|
158
|
+
matchField(dom, domExpr, 31) &&
|
|
159
|
+
matchField(mon, monExpr, 12) &&
|
|
160
|
+
matchField(dow, dowExpr, 6)
|
|
161
|
+
) {
|
|
162
|
+
return candidate;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw new Error("Could not find next cron time");
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|