bm2 1.0.33 → 1.0.35
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/README.md +29 -6
- package/package.json +1 -1
- package/src/daemon.ts +12 -2
- package/src/index.ts +59 -19
- package/src/process-manager.ts +11 -6
package/README.md
CHANGED
|
@@ -451,43 +451,66 @@ bm2 reset all
|
|
|
451
451
|
|
|
452
452
|
Cluster mode spawns multiple instances of your application, each running in its own process. This is ideal for CPU-bound workloads and for taking full advantage of multi-core servers.
|
|
453
453
|
|
|
454
|
-
```
|
|
454
|
+
```bash
|
|
455
455
|
bm2 start server.ts --name api --instances max
|
|
456
|
-
```
|
|
457
456
|
|
|
458
457
|
```
|
|
458
|
+
|
|
459
|
+
```bash
|
|
459
460
|
bm2 start server.ts --name api --instances 4
|
|
460
|
-
```
|
|
461
461
|
|
|
462
462
|
```
|
|
463
|
+
|
|
464
|
+
```bash
|
|
463
465
|
bm2 start server.ts --name api --instances 4 --port 3000
|
|
466
|
+
|
|
464
467
|
```
|
|
465
468
|
|
|
469
|
+
#### ⚠️ Current Status & Limitations
|
|
470
|
+
|
|
471
|
+
While `bm2` provides the orchestration for clustering, please note that **Bun’s native cluster implementation is currently limited by the underlying OS:**
|
|
472
|
+
|
|
473
|
+
* **Linux Only:** Port sharing via `reusePort` is only fully supported on **Linux**.
|
|
474
|
+
* **macOS & Windows:** Due to OS-level limitations with `SO_REUSEPORT`, these platforms ignore the `reusePort` option. On these systems, clustering may result in "Address already in use" errors if attempting to bind multiple workers to the same port.
|
|
475
|
+
|
|
476
|
+
`bm2` leverages the native [Bun.serve cluster logic](https://www.google.com/search?q=https://bun.sh/docs/api/http%23cluster) to ensure maximum performance, but it remains subject to the runtime's maturity.
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
#### Environment Variables
|
|
480
|
+
|
|
466
481
|
Each cluster worker receives the following environment variables:
|
|
467
482
|
|
|
468
483
|
| Variable | Description |
|
|
469
|
-
|
|
484
|
+
| --- | --- |
|
|
470
485
|
| `BM2_CLUSTER` | Set to `"true"` in cluster mode |
|
|
471
486
|
| `BM2_WORKER_ID` | Zero-indexed worker ID |
|
|
472
487
|
| `BM2_INSTANCES` | Total number of instances |
|
|
473
488
|
| `NODE_APP_INSTANCE` | Same as `BM2_WORKER_ID` (PM2 compatibility) |
|
|
474
489
|
| `PORT` | `basePort + workerIndex` (if `--port` is specified) |
|
|
475
490
|
|
|
476
|
-
|
|
491
|
+
---
|
|
477
492
|
|
|
478
|
-
|
|
493
|
+
#### Example: Cluster-Aware Port Binding
|
|
494
|
+
|
|
495
|
+
To enable clustering in Bun, you must explicitly set `reusePort: true`. This allows multiple processes to listen on the same port (on supported OSs).
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
479
498
|
// server.ts
|
|
480
499
|
const workerId = parseInt(process.env.BM2_WORKER_ID || "0");
|
|
481
500
|
const port = parseInt(process.env.PORT || "3000");
|
|
482
501
|
|
|
483
502
|
Bun.serve({
|
|
484
503
|
port,
|
|
504
|
+
// Share the same port across multiple processes
|
|
505
|
+
// This is the important part!
|
|
506
|
+
reusePort: true,
|
|
485
507
|
fetch(req) {
|
|
486
508
|
return new Response(`Hello from worker ${workerId} on port ${port}`);
|
|
487
509
|
},
|
|
488
510
|
});
|
|
489
511
|
|
|
490
512
|
console.log(`Worker ${workerId} listening on :${port}`);
|
|
513
|
+
|
|
491
514
|
```
|
|
492
515
|
|
|
493
516
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bm2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
4
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
5
|
"main": "src/api.ts",
|
|
6
6
|
"module": "src/api.ts",
|
package/src/daemon.ts
CHANGED
|
@@ -35,6 +35,11 @@ const pm = new ProcessManager();
|
|
|
35
35
|
const dashboard = new Dashboard(pm);
|
|
36
36
|
const moduleManager = new ModuleManager(pm);
|
|
37
37
|
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
|
|
40
|
+
// Checks if '--debug' exists anywhere in the arguments
|
|
41
|
+
const debugMode = args.includes('--debug');
|
|
42
|
+
|
|
38
43
|
// Clean up existing socket
|
|
39
44
|
if (existsSync(DAEMON_SOCKET)) {
|
|
40
45
|
try { unlinkSync(DAEMON_SOCKET); } catch {}
|
|
@@ -223,8 +228,13 @@ async function handleMessage(msg: DaemonMessage): Promise<DaemonResponse> {
|
|
|
223
228
|
default:
|
|
224
229
|
return { type: "error", error: `Unknown command: ${msg.type}`, success: false, id: msg.id };
|
|
225
230
|
}
|
|
226
|
-
} catch (err: any) {
|
|
227
|
-
|
|
231
|
+
} catch (err: Error | any) {
|
|
232
|
+
let error = err.message;
|
|
233
|
+
if (debugMode) {
|
|
234
|
+
error = `Message: ${err.message}\nStack: ${err.stack}`
|
|
235
|
+
console.error(err, err.stack)
|
|
236
|
+
}
|
|
237
|
+
return { type: "error", error, success: false, id: msg.id };
|
|
228
238
|
}
|
|
229
239
|
}
|
|
230
240
|
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
} from "./types";
|
|
42
42
|
import { statusColor } from "./colors";
|
|
43
43
|
import { liveWatchProcess, printProcessTable } from "./process-table";
|
|
44
|
+
import { exists } from "fs/promises";
|
|
44
45
|
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
46
47
|
// Ensure directory structure exists
|
|
@@ -51,6 +52,7 @@ ensureDirs();
|
|
|
51
52
|
// Daemon communication helpers
|
|
52
53
|
// ---------------------------------------------------------------------------
|
|
53
54
|
|
|
55
|
+
|
|
54
56
|
function isDaemonRunning(): boolean {
|
|
55
57
|
if (!existsSync(DAEMON_PID_FILE)) return false;
|
|
56
58
|
try {
|
|
@@ -98,6 +100,25 @@ async function startDaemon(): Promise<void> {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
async function stopDaemon(): Promise<void> {
|
|
104
|
+
try {
|
|
105
|
+
|
|
106
|
+
if (!isDaemonRunning()) return;
|
|
107
|
+
|
|
108
|
+
const pidText = await Bun.file(DAEMON_PID_FILE).text();
|
|
109
|
+
const pid = Number(pidText);
|
|
110
|
+
|
|
111
|
+
process.kill(pid, "SIGTERM"); // graceful stop
|
|
112
|
+
|
|
113
|
+
console.error("Daemon stopped");
|
|
114
|
+
|
|
115
|
+
// cleanup
|
|
116
|
+
await Bun.write(DAEMON_PID_FILE, "");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("Failed to stop daemon:", err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
|
|
102
123
|
|
|
103
124
|
//start the daemon
|
|
@@ -143,8 +164,10 @@ async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
|
|
|
143
164
|
async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
|
|
144
165
|
|
|
145
166
|
const abs = resolve(filePath);
|
|
167
|
+
|
|
168
|
+
const file = Bun.file(abs);
|
|
146
169
|
|
|
147
|
-
if (!
|
|
170
|
+
if (!(await file.exists())) {
|
|
148
171
|
throw new Error(`Ecosystem file not found: ${abs}`);
|
|
149
172
|
}
|
|
150
173
|
|
|
@@ -153,7 +176,7 @@ async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
|
|
|
153
176
|
let config;
|
|
154
177
|
|
|
155
178
|
if (ext === ".json") {
|
|
156
|
-
config = (await
|
|
179
|
+
config = (await file.json()) as EcosystemConfig;
|
|
157
180
|
} else {
|
|
158
181
|
// .ts, .js, .mjs — dynamic import
|
|
159
182
|
const mod = await import(abs);
|
|
@@ -362,8 +385,11 @@ async function cmdStart(args: string[]) {
|
|
|
362
385
|
opts.script = resolve(scriptOrConfig);
|
|
363
386
|
|
|
364
387
|
const cwd = path.dirname(opts.script);
|
|
365
|
-
|
|
366
|
-
|
|
388
|
+
|
|
389
|
+
opts.cwd = cwd;
|
|
390
|
+
|
|
391
|
+
const res = await sendToDaemon({ type: "start", data: opts });
|
|
392
|
+
|
|
367
393
|
if (!res.success) {
|
|
368
394
|
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
369
395
|
process.exit(1);
|
|
@@ -697,7 +723,8 @@ async function cmdDeploy(args: string[]) {
|
|
|
697
723
|
process.exit(1);
|
|
698
724
|
}
|
|
699
725
|
|
|
700
|
-
const
|
|
726
|
+
const config = await loadEcosystemConfig(configFile);
|
|
727
|
+
|
|
701
728
|
if (!config.deploy || !config.deploy[environment]) {
|
|
702
729
|
console.error(colorize(`Deploy environment "${environment}" not found in config`, "red"));
|
|
703
730
|
process.exit(1);
|
|
@@ -846,29 +873,42 @@ async function cmdModule(args: string[]) {
|
|
|
846
873
|
}
|
|
847
874
|
|
|
848
875
|
async function cmdDaemon(args: string[]) {
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
const daemonStatus = () => {
|
|
879
|
+
if (isDaemonRunning()) {
|
|
880
|
+
console.log(colorize("running", "green"));
|
|
881
|
+
} else {
|
|
882
|
+
console.error(colorize("stopped", "red"));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
|
|
849
888
|
const subCmd = args[0];
|
|
850
|
-
let type;
|
|
851
889
|
|
|
852
890
|
switch (subCmd) {
|
|
891
|
+
case "status":
|
|
892
|
+
daemonStatus();
|
|
893
|
+
break;
|
|
894
|
+
case "start":
|
|
895
|
+
await startDaemon();
|
|
896
|
+
process.exit(1);
|
|
897
|
+
break
|
|
898
|
+
case "stop":
|
|
899
|
+
await stopDaemon();
|
|
900
|
+
process.exit(1);
|
|
901
|
+
break;
|
|
853
902
|
case "reload":
|
|
854
|
-
|
|
903
|
+
await stopDaemon();
|
|
904
|
+
await startDaemon();
|
|
905
|
+
process.exit(1);
|
|
855
906
|
break;
|
|
856
907
|
default:
|
|
857
|
-
console.error(colorize("Usage: bm2 daemon <reload>", "red"));
|
|
908
|
+
console.error(colorize("Usage: bm2 daemon <status|start|stop|reload>", "red"));
|
|
858
909
|
process.exit(1);
|
|
859
910
|
}
|
|
860
|
-
|
|
861
|
-
const res = await sendToDaemon({ type });
|
|
862
911
|
|
|
863
|
-
if (res?.error) {
|
|
864
|
-
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
865
|
-
process.exit(1);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
console.log(colorize(res.data, "green"));
|
|
869
|
-
|
|
870
|
-
process.exit(1);
|
|
871
|
-
|
|
872
912
|
}
|
|
873
913
|
|
|
874
914
|
async function cmdPrometheus() {
|
package/src/process-manager.ts
CHANGED
|
@@ -66,11 +66,12 @@ import path from "path";
|
|
|
66
66
|
options.script = path.isAbsolute(options.script)
|
|
67
67
|
? options.script
|
|
68
68
|
: path.join(options.cwd!, options.script);
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
|
|
70
71
|
if (!(await Bun.file(options.script).exists())) {
|
|
71
72
|
throw new Error(`Script not found: ${options.script}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
74
75
|
if (isCluster) {
|
|
75
76
|
// In cluster mode, each instance is a separate container
|
|
76
77
|
for (let i = 0; i < resolvedInstances; i++) {
|
|
@@ -94,6 +95,7 @@ import path from "path";
|
|
|
94
95
|
await container.start();
|
|
95
96
|
states.push(container.getState());
|
|
96
97
|
}
|
|
98
|
+
|
|
97
99
|
} else {
|
|
98
100
|
const id = this.nextId++;
|
|
99
101
|
const name =
|
|
@@ -103,8 +105,11 @@ import path from "path";
|
|
|
103
105
|
|
|
104
106
|
const config = this.buildConfig(id, name, options, 1, 0);
|
|
105
107
|
const container = new ProcessContainer(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
id, config,
|
|
109
|
+
this.logManager,
|
|
110
|
+
this.clusterManager,
|
|
111
|
+
this.healthChecker,
|
|
112
|
+
this.cronManager
|
|
108
113
|
);
|
|
109
114
|
|
|
110
115
|
this.processes.set(id, container);
|
|
@@ -113,7 +118,7 @@ import path from "path";
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
return states;
|
|
116
|
-
|
|
121
|
+
}
|
|
117
122
|
|
|
118
123
|
private buildConfig(
|
|
119
124
|
id: number,
|