bm2 1.0.19 → 1.0.22
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 +542 -2
- package/package.json +9 -5
- package/src/api.ts +598 -0
- package/src/index.ts +0 -1
- package/src/process-table.ts +2 -1
- package/src/startup-manager.ts +37 -35
- package/src/hello.js +0 -7
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
# ⚡ BM2
|
|
2
3
|
|
|
3
4
|
**A blazing-fast, full-featured process manager built entirely on Bun native APIs.**
|
|
@@ -47,6 +48,20 @@ The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.
|
|
|
47
48
|
- [WebSocket API](#websocket-api)
|
|
48
49
|
- [Prometheus and Grafana Integration](#prometheus-and-grafana-integration)
|
|
49
50
|
- [Programmatic API](#programmatic-api)
|
|
51
|
+
- [Quick Start](#programmatic-quick-start)
|
|
52
|
+
- [Connection Lifecycle](#connection-lifecycle)
|
|
53
|
+
- [Process Management](#programmatic-process-management)
|
|
54
|
+
- [Introspection](#introspection)
|
|
55
|
+
- [Logs](#logs)
|
|
56
|
+
- [Monitoring and Metrics](#programmatic-monitoring-and-metrics)
|
|
57
|
+
- [Persistence](#persistence)
|
|
58
|
+
- [Dashboard Control](#dashboard-control)
|
|
59
|
+
- [Module Management](#module-management)
|
|
60
|
+
- [Daemon Lifecycle](#daemon-lifecycle)
|
|
61
|
+
- [Low-Level Transport](#low-level-transport)
|
|
62
|
+
- [Events](#events)
|
|
63
|
+
- [Error Handling](#error-handling)
|
|
64
|
+
- [Direct ProcessManager Usage](#direct-processmanager-usage)
|
|
50
65
|
- [Architecture](#architecture)
|
|
51
66
|
- [Comparison with PM2](#comparison-with-pm2)
|
|
52
67
|
- [Recipes and Examples](#recipes-and-examples)
|
|
@@ -1258,10 +1273,475 @@ groups:
|
|
|
1258
1273
|
|
|
1259
1274
|
## Programmatic API
|
|
1260
1275
|
|
|
1261
|
-
BM2
|
|
1276
|
+
BM2 exposes two levels of programmatic access. The `BM2` client class communicates with the daemon over its Unix socket, giving you the same capabilities as the CLI from within any Bun application. For in-process usage without a daemon, you can use the `ProcessManager` class directly.
|
|
1277
|
+
|
|
1278
|
+
### Programmatic Quick Start
|
|
1279
|
+
|
|
1280
|
+
```ts
|
|
1281
|
+
import BM2 from "bm2";
|
|
1282
|
+
|
|
1283
|
+
const bm2 = new BM2();
|
|
1284
|
+
await bm2.connect();
|
|
1285
|
+
|
|
1286
|
+
// Start a clustered application
|
|
1287
|
+
await bm2.start({
|
|
1288
|
+
script: "./server.ts",
|
|
1289
|
+
name: "api",
|
|
1290
|
+
instances: 4,
|
|
1291
|
+
execMode: "cluster",
|
|
1292
|
+
port: 3000,
|
|
1293
|
+
env: { NODE_ENV: "production" },
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// List all processes
|
|
1297
|
+
const processes = await bm2.list();
|
|
1298
|
+
console.log(processes);
|
|
1299
|
+
|
|
1300
|
+
// Stream metrics every 2 seconds
|
|
1301
|
+
bm2.on("metrics", (snapshot) => {
|
|
1302
|
+
console.log(`CPU: ${snapshot.system.cpu}% Memory: ${snapshot.system.memory}%`);
|
|
1303
|
+
});
|
|
1304
|
+
bm2.startPolling(2000);
|
|
1305
|
+
|
|
1306
|
+
// Graceful shutdown
|
|
1307
|
+
bm2.stopPolling();
|
|
1308
|
+
await bm2.disconnect();
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
---
|
|
1312
|
+
|
|
1313
|
+
### Connection Lifecycle
|
|
1314
|
+
|
|
1315
|
+
#### `bm2.connect(): Promise<BM2>`
|
|
1316
|
+
|
|
1317
|
+
Connect to the BM2 daemon. If the daemon is not running, it is spawned automatically and the method waits up to 5 seconds for it to become responsive. Returns the `BM2` instance for chaining.
|
|
1318
|
+
|
|
1319
|
+
```ts
|
|
1320
|
+
const bm2 = new BM2();
|
|
1321
|
+
await bm2.connect();
|
|
1322
|
+
console.log(`Connected to daemon PID ${bm2.daemonPid}`);
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
#### `bm2.disconnect(): Promise<void>`
|
|
1326
|
+
|
|
1327
|
+
Disconnect from the daemon. This stops any internal polling timers but does not kill the daemon — all managed processes continue running.
|
|
1328
|
+
|
|
1329
|
+
```ts
|
|
1330
|
+
await bm2.disconnect();
|
|
1331
|
+
console.log(bm2.connected); // false
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
#### `bm2.connected: boolean`
|
|
1335
|
+
|
|
1336
|
+
Read-only property indicating whether the client believes the daemon is reachable.
|
|
1337
|
+
|
|
1338
|
+
#### `bm2.daemonPid: number | null`
|
|
1339
|
+
|
|
1340
|
+
Read-only property containing the PID of the daemon process, or `null` if unknown.
|
|
1341
|
+
|
|
1342
|
+
---
|
|
1343
|
+
|
|
1344
|
+
### Programmatic Process Management
|
|
1345
|
+
|
|
1346
|
+
#### `bm2.start(options: StartOptions): Promise<ProcessState[]>`
|
|
1347
|
+
|
|
1348
|
+
Start a new process or process group. The `script` path is automatically resolved to an absolute path. Returns the array of `ProcessState` objects for the started instances.
|
|
1349
|
+
|
|
1350
|
+
```ts
|
|
1351
|
+
const procs = await bm2.start({
|
|
1352
|
+
script: "./worker.ts",
|
|
1353
|
+
name: "worker",
|
|
1354
|
+
instances: 2,
|
|
1355
|
+
env: { QUEUE: "emails" },
|
|
1356
|
+
maxMemoryRestart: "256M",
|
|
1357
|
+
});
|
|
1358
|
+
console.log(`Started ${procs.length} instances`);
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
The `StartOptions` object accepts all the same fields documented in the [Process Options](#process-options) configuration reference.
|
|
1362
|
+
|
|
1363
|
+
#### `bm2.startEcosystem(config: EcosystemConfig): Promise<ProcessState[]>`
|
|
1364
|
+
|
|
1365
|
+
Start an entire ecosystem configuration. All script paths within the config are resolved to absolute paths before being sent to the daemon.
|
|
1366
|
+
|
|
1367
|
+
```ts
|
|
1368
|
+
const procs = await bm2.startEcosystem({
|
|
1369
|
+
apps: [
|
|
1370
|
+
{ script: "./api.ts", name: "api", instances: 4, port: 3000 },
|
|
1371
|
+
{ script: "./worker.ts", name: "worker", instances: 2 },
|
|
1372
|
+
],
|
|
1373
|
+
});
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
#### `bm2.stop(target?: string | number): Promise<ProcessState[]>`
|
|
1377
|
+
|
|
1378
|
+
Stop one or more processes. The `target` can be a process name, numeric ID, namespace, or `"all"`. Defaults to `"all"` when omitted.
|
|
1379
|
+
|
|
1380
|
+
```ts
|
|
1381
|
+
await bm2.stop("api"); // Stop by name
|
|
1382
|
+
await bm2.stop(0); // Stop by ID
|
|
1383
|
+
await bm2.stop(); // Stop all
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
#### `bm2.restart(target?: string | number): Promise<ProcessState[]>`
|
|
1387
|
+
|
|
1388
|
+
Hard restart one or more processes. The process is fully stopped and then re-spawned.
|
|
1389
|
+
|
|
1390
|
+
```ts
|
|
1391
|
+
await bm2.restart("api");
|
|
1392
|
+
await bm2.restart(); // Restart all
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
#### `bm2.reload(target?: string | number): Promise<ProcessState[]>`
|
|
1396
|
+
|
|
1397
|
+
Graceful zero-downtime reload. New instances are started before old ones are stopped, ensuring no dropped requests. Ideal for deploying new code.
|
|
1398
|
+
|
|
1399
|
+
```ts
|
|
1400
|
+
await bm2.reload("api");
|
|
1401
|
+
await bm2.reload(); // Reload all
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
#### `bm2.delete(target?: string | number): Promise<ProcessState[]>`
|
|
1405
|
+
|
|
1406
|
+
Stop and remove one or more processes from BM2's management entirely.
|
|
1407
|
+
|
|
1408
|
+
```ts
|
|
1409
|
+
await bm2.delete("api");
|
|
1410
|
+
await bm2.delete(); // Delete all
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
#### `bm2.scale(target: string | number, count: number): Promise<ProcessState[]>`
|
|
1414
|
+
|
|
1415
|
+
Scale a process group to the specified number of instances. When scaling up, new instances inherit the configuration of existing ones. When scaling down, the highest-numbered instances are removed first.
|
|
1416
|
+
|
|
1417
|
+
```ts
|
|
1418
|
+
await bm2.scale("api", 8); // Scale up to 8 instances
|
|
1419
|
+
await bm2.scale("api", 2); // Scale down to 2 instances
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
#### `bm2.sendSignal(target: string | number, signal: string): Promise<void>`
|
|
1423
|
+
|
|
1424
|
+
Send an OS signal to a managed process.
|
|
1425
|
+
|
|
1426
|
+
```ts
|
|
1427
|
+
await bm2.sendSignal("api", "SIGUSR2");
|
|
1428
|
+
await bm2.sendSignal(0, "SIGHUP");
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
#### `bm2.reset(target?: string | number): Promise<ProcessState[]>`
|
|
1432
|
+
|
|
1433
|
+
Reset the restart counter for one or more processes. Defaults to `"all"`.
|
|
1434
|
+
|
|
1435
|
+
```ts
|
|
1436
|
+
await bm2.reset("api");
|
|
1437
|
+
await bm2.reset(); // Reset all
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
---
|
|
1441
|
+
|
|
1442
|
+
### Introspection
|
|
1443
|
+
|
|
1444
|
+
#### `bm2.list(): Promise<ProcessState[]>`
|
|
1445
|
+
|
|
1446
|
+
List all managed processes with their current state.
|
|
1447
|
+
|
|
1448
|
+
```ts
|
|
1449
|
+
const processes = await bm2.list();
|
|
1450
|
+
for (const proc of processes) {
|
|
1451
|
+
console.log(`${proc.name} [${proc.status}] PID=${proc.pid} CPU=${proc.cpu}%`);
|
|
1452
|
+
}
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
#### `bm2.describe(target: string | number): Promise<ProcessState[]>`
|
|
1456
|
+
|
|
1457
|
+
Get detailed information about a specific process or process group.
|
|
1458
|
+
|
|
1459
|
+
```ts
|
|
1460
|
+
const details = await bm2.describe("api");
|
|
1461
|
+
console.log(details[0]);
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
---
|
|
1262
1465
|
|
|
1466
|
+
### Logs
|
|
1467
|
+
|
|
1468
|
+
#### `bm2.logs(target?: string | number, lines?: number): Promise<Array<{ name: string; id: number; out: string; err: string }>>`
|
|
1469
|
+
|
|
1470
|
+
Retrieve recent log lines for one or all processes. Defaults to `"all"` with `20` lines.
|
|
1471
|
+
|
|
1472
|
+
```ts
|
|
1473
|
+
const logs = await bm2.logs("api", 100);
|
|
1474
|
+
for (const entry of logs) {
|
|
1475
|
+
console.log(`[${entry.name}] stdout:\n${entry.out}`);
|
|
1476
|
+
if (entry.err) console.error(`[${entry.name}] stderr:\n${entry.err}`);
|
|
1477
|
+
}
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
#### `bm2.flush(target?: string | number): Promise<void>`
|
|
1481
|
+
|
|
1482
|
+
Truncate log files for one or all processes.
|
|
1483
|
+
|
|
1484
|
+
```ts
|
|
1485
|
+
await bm2.flush("api"); // Flush logs for "api"
|
|
1486
|
+
await bm2.flush(); // Flush all logs
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
---
|
|
1490
|
+
|
|
1491
|
+
### Programmatic Monitoring and Metrics
|
|
1492
|
+
|
|
1493
|
+
#### `bm2.metrics(): Promise<MetricSnapshot>`
|
|
1494
|
+
|
|
1495
|
+
Take a single metrics snapshot containing process-level and system-level telemetry.
|
|
1496
|
+
|
|
1497
|
+
```ts
|
|
1498
|
+
const snapshot = await bm2.metrics();
|
|
1499
|
+
console.log(`System CPU: ${snapshot.system.cpu}%`);
|
|
1500
|
+
for (const proc of snapshot.processes) {
|
|
1501
|
+
console.log(` ${proc.name}: ${proc.memory} bytes, ${proc.cpu}% CPU`);
|
|
1502
|
+
}
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
#### `bm2.metricsHistory(seconds?: number): Promise<MetricSnapshot[]>`
|
|
1506
|
+
|
|
1507
|
+
Retrieve historical metric snapshots from the daemon's in-memory ring buffer. The `seconds` parameter controls the look-back window and defaults to `300` (5 minutes). The daemon retains up to 1 hour of per-second snapshots.
|
|
1508
|
+
|
|
1509
|
+
```ts
|
|
1510
|
+
const history = await bm2.metricsHistory(600); // Last 10 minutes
|
|
1511
|
+
console.log(`Got ${history.length} snapshots`);
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
#### `bm2.prometheus(): Promise<string>`
|
|
1515
|
+
|
|
1516
|
+
Get the current metrics formatted as a Prometheus exposition text string.
|
|
1517
|
+
|
|
1518
|
+
```ts
|
|
1519
|
+
const text = await bm2.prometheus();
|
|
1520
|
+
console.log(text);
|
|
1521
|
+
// # HELP bm2_process_cpu CPU usage percentage
|
|
1522
|
+
// # TYPE bm2_process_cpu gauge
|
|
1523
|
+
// bm2_process_cpu{name="api-0",id="0"} 1.2
|
|
1524
|
+
// ...
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
#### `bm2.startPolling(intervalMs?: number): void`
|
|
1528
|
+
|
|
1529
|
+
Start polling the daemon for metrics at a fixed interval and emitting `"metrics"` events. Defaults to `2000` ms. Calling this again replaces the existing polling timer.
|
|
1530
|
+
|
|
1531
|
+
```ts
|
|
1532
|
+
bm2.on("metrics", (snapshot) => {
|
|
1533
|
+
console.log(`${snapshot.processes.length} processes running`);
|
|
1534
|
+
});
|
|
1535
|
+
bm2.startPolling(1000); // Poll every second
|
|
1263
1536
|
```
|
|
1264
|
-
|
|
1537
|
+
|
|
1538
|
+
#### `bm2.stopPolling(): void`
|
|
1539
|
+
|
|
1540
|
+
Stop the metrics polling loop.
|
|
1541
|
+
|
|
1542
|
+
```ts
|
|
1543
|
+
bm2.stopPolling();
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
---
|
|
1547
|
+
|
|
1548
|
+
### Persistence
|
|
1549
|
+
|
|
1550
|
+
#### `bm2.save(): Promise<void>`
|
|
1551
|
+
|
|
1552
|
+
Persist the current process list to `~/.bm2/dump.json` so it can be restored later.
|
|
1553
|
+
|
|
1554
|
+
```ts
|
|
1555
|
+
await bm2.save();
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
#### `bm2.resurrect(): Promise<ProcessState[]>`
|
|
1559
|
+
|
|
1560
|
+
Restore previously saved processes from `~/.bm2/dump.json`.
|
|
1561
|
+
|
|
1562
|
+
```ts
|
|
1563
|
+
const restored = await bm2.resurrect();
|
|
1564
|
+
console.log(`Restored ${restored.length} processes`);
|
|
1565
|
+
```
|
|
1566
|
+
|
|
1567
|
+
---
|
|
1568
|
+
|
|
1569
|
+
### Dashboard Control
|
|
1570
|
+
|
|
1571
|
+
#### `bm2.dashboard(port?: number, metricsPort?: number): Promise<{ port: number; metricsPort: number }>`
|
|
1572
|
+
|
|
1573
|
+
Start the web dashboard. Defaults to port `9615` for the dashboard and `9616` for the Prometheus metrics endpoint.
|
|
1574
|
+
|
|
1575
|
+
```ts
|
|
1576
|
+
const { port, metricsPort } = await bm2.dashboard(8080, 8081);
|
|
1577
|
+
console.log(`Dashboard: http://localhost:${port}`);
|
|
1578
|
+
console.log(`Metrics: http://localhost:${metricsPort}/metrics`);
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
#### `bm2.dashboardStop(): Promise<void>`
|
|
1582
|
+
|
|
1583
|
+
Stop the web dashboard.
|
|
1584
|
+
|
|
1585
|
+
```ts
|
|
1586
|
+
await bm2.dashboardStop();
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
---
|
|
1590
|
+
|
|
1591
|
+
### Module Management
|
|
1592
|
+
|
|
1593
|
+
#### `bm2.moduleInstall(nameOrPath: string): Promise<{ path: string }>`
|
|
1594
|
+
|
|
1595
|
+
Install a BM2 module from a git URL, local path, or npm package name.
|
|
1596
|
+
|
|
1597
|
+
```ts
|
|
1598
|
+
const result = await bm2.moduleInstall("bm2-prometheus-pushgateway");
|
|
1599
|
+
console.log(`Installed to ${result.path}`);
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
#### `bm2.moduleUninstall(name: string): Promise<void>`
|
|
1603
|
+
|
|
1604
|
+
Uninstall a BM2 module.
|
|
1605
|
+
|
|
1606
|
+
```ts
|
|
1607
|
+
await bm2.moduleUninstall("bm2-prometheus-pushgateway");
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
#### `bm2.moduleList(): Promise<Array<{ name: string; version: string }>>`
|
|
1611
|
+
|
|
1612
|
+
List all installed modules.
|
|
1613
|
+
|
|
1614
|
+
```ts
|
|
1615
|
+
const modules = await bm2.moduleList();
|
|
1616
|
+
for (const mod of modules) {
|
|
1617
|
+
console.log(`${mod.name}@${mod.version}`);
|
|
1618
|
+
}
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
---
|
|
1622
|
+
|
|
1623
|
+
### Daemon Lifecycle
|
|
1624
|
+
|
|
1625
|
+
#### `bm2.ping(): Promise<{ pid: number; uptime: number }>`
|
|
1626
|
+
|
|
1627
|
+
Ping the daemon and return its PID and uptime in milliseconds.
|
|
1628
|
+
|
|
1629
|
+
```ts
|
|
1630
|
+
const info = await bm2.ping();
|
|
1631
|
+
console.log(`Daemon PID ${info.pid}, up for ${Math.round(info.uptime / 1000)}s`);
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
#### `bm2.kill(): Promise<void>`
|
|
1635
|
+
|
|
1636
|
+
Kill the daemon and all managed processes. Cleans up the socket and PID files. The daemon connection will not respond after this call, which is expected.
|
|
1637
|
+
|
|
1638
|
+
```ts
|
|
1639
|
+
await bm2.kill();
|
|
1640
|
+
console.log(bm2.connected); // false
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
#### `bm2.daemonReload(): Promise<string>`
|
|
1644
|
+
|
|
1645
|
+
Reload the daemon server itself without killing managed processes.
|
|
1646
|
+
|
|
1647
|
+
```ts
|
|
1648
|
+
const result = await bm2.daemonReload();
|
|
1649
|
+
console.log(result);
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
---
|
|
1653
|
+
|
|
1654
|
+
### Low-Level Transport
|
|
1655
|
+
|
|
1656
|
+
#### `bm2.send(message: DaemonMessage): Promise<DaemonResponse>`
|
|
1657
|
+
|
|
1658
|
+
Send an arbitrary message to the daemon over the Unix socket and return the raw response. This is useful for custom command types, future extensions, or direct daemon interaction.
|
|
1659
|
+
|
|
1660
|
+
```ts
|
|
1661
|
+
const response = await bm2.send({ type: "ping" });
|
|
1662
|
+
console.log(response);
|
|
1663
|
+
// { success: true, data: { pid: 12345, uptime: 60000 }, id: "abc123" }
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
Messages are JSON objects with a `type` field for routing and an optional `id` field for request-response correlation (auto-generated if omitted). The `data` field carries command-specific payload.
|
|
1667
|
+
|
|
1668
|
+
---
|
|
1669
|
+
|
|
1670
|
+
### Events
|
|
1671
|
+
|
|
1672
|
+
The `BM2` class extends `EventEmitter` and emits the following typed events:
|
|
1673
|
+
|
|
1674
|
+
| Event | Payload | Description |
|
|
1675
|
+
|---|---|---|
|
|
1676
|
+
| `daemon:connected` | — | Daemon connection established |
|
|
1677
|
+
| `daemon:disconnected` | — | Client disconnected from daemon |
|
|
1678
|
+
| `daemon:launched` | `pid: number` | Daemon was spawned by this client |
|
|
1679
|
+
| `daemon:killed` | — | Daemon was killed via `kill()` |
|
|
1680
|
+
| `error` | `error: Error` | Transport or polling error |
|
|
1681
|
+
| `process:start` | `processes: ProcessState[]` | Process(es) started |
|
|
1682
|
+
| `process:stop` | `processes: ProcessState[]` | Process(es) stopped |
|
|
1683
|
+
| `process:restart` | `processes: ProcessState[]` | Process(es) restarted |
|
|
1684
|
+
| `process:reload` | `processes: ProcessState[]` | Process(es) reloaded |
|
|
1685
|
+
| `process:delete` | `processes: ProcessState[]` | Process(es) deleted |
|
|
1686
|
+
| `process:scale` | `processes: ProcessState[]` | Process group scaled |
|
|
1687
|
+
| `metrics` | `snapshot: MetricSnapshot` | Metrics snapshot received |
|
|
1688
|
+
| `log:data` | `logs: Array<{ name, id, out, err }>` | Log data retrieved |
|
|
1689
|
+
|
|
1690
|
+
```ts
|
|
1691
|
+
const bm2 = new BM2();
|
|
1692
|
+
|
|
1693
|
+
bm2.on("daemon:connected", () => console.log("Connected!"));
|
|
1694
|
+
bm2.on("daemon:disconnected", () => console.log("Disconnected"));
|
|
1695
|
+
bm2.on("process:start", (procs) => {
|
|
1696
|
+
console.log("Started:", procs.map((p) => p.name).join(", "));
|
|
1697
|
+
});
|
|
1698
|
+
bm2.on("process:stop", (procs) => {
|
|
1699
|
+
console.log("Stopped:", procs.map((p) => p.name).join(", "));
|
|
1700
|
+
});
|
|
1701
|
+
bm2.on("error", (err) => console.error("BM2 error:", err.message));
|
|
1702
|
+
bm2.on("metrics", (snapshot) => {
|
|
1703
|
+
console.log(`${snapshot.processes.length} processes, system CPU ${snapshot.system.cpu}%`);
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
await bm2.connect();
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
---
|
|
1710
|
+
|
|
1711
|
+
### Error Handling
|
|
1712
|
+
|
|
1713
|
+
All methods that communicate with the daemon throw a `BM2Error` when the daemon returns a failure response. The error includes the command that failed and the full daemon response for inspection.
|
|
1714
|
+
|
|
1715
|
+
```ts
|
|
1716
|
+
import { BM2Error } from "bm2";
|
|
1717
|
+
|
|
1718
|
+
try {
|
|
1719
|
+
await bm2.describe("nonexistent");
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
if (err instanceof BM2Error) {
|
|
1722
|
+
console.error(`Command "${err.command}" failed: ${err.message}`);
|
|
1723
|
+
console.error("Full response:", err.response);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
```
|
|
1727
|
+
|
|
1728
|
+
Transport-level errors (daemon unreachable, socket closed) throw standard `Error` instances.
|
|
1729
|
+
|
|
1730
|
+
#### `BM2Error` Properties
|
|
1731
|
+
|
|
1732
|
+
| Property | Type | Description |
|
|
1733
|
+
|---|---|---|
|
|
1734
|
+
| `message` | `string` | Human-readable error message |
|
|
1735
|
+
| `command` | `string` | The daemon command type that failed |
|
|
1736
|
+
| `response` | `DaemonResponse` | The full response object from the daemon |
|
|
1737
|
+
|
|
1738
|
+
---
|
|
1739
|
+
|
|
1740
|
+
### Direct ProcessManager Usage
|
|
1741
|
+
|
|
1742
|
+
For in-process usage without a running daemon, you can use the `ProcessManager` class directly. This is useful for embedding BM2 into your own application or for custom tooling.
|
|
1743
|
+
|
|
1744
|
+
```ts
|
|
1265
1745
|
import { ProcessManager } from "bm2/process-manager";
|
|
1266
1746
|
import { Dashboard } from "bm2/dashboard";
|
|
1267
1747
|
|
|
@@ -1308,6 +1788,8 @@ await pm.resurrect();
|
|
|
1308
1788
|
await pm.stopAll();
|
|
1309
1789
|
```
|
|
1310
1790
|
|
|
1791
|
+
The `ProcessManager` provides the same process management capabilities but runs in-process rather than communicating with a daemon. Use the `BM2` client class for the standard daemon-based workflow, and `ProcessManager` when you need direct, embedded control.
|
|
1792
|
+
|
|
1311
1793
|
---
|
|
1312
1794
|
|
|
1313
1795
|
## Architecture
|
|
@@ -1476,6 +1958,64 @@ bun install
|
|
|
1476
1958
|
bm2 reload all
|
|
1477
1959
|
```
|
|
1478
1960
|
|
|
1961
|
+
### Programmatic Monitoring Service
|
|
1962
|
+
|
|
1963
|
+
```ts
|
|
1964
|
+
import BM2 from "bm2";
|
|
1965
|
+
|
|
1966
|
+
const bm2 = new BM2();
|
|
1967
|
+
await bm2.connect();
|
|
1968
|
+
|
|
1969
|
+
// Alert when any process uses more than 512 MB
|
|
1970
|
+
bm2.on("metrics", (snapshot) => {
|
|
1971
|
+
for (const proc of snapshot.processes) {
|
|
1972
|
+
if (proc.memory > 512 * 1024 * 1024) {
|
|
1973
|
+
console.warn(`⚠️ ${proc.name} using ${Math.round(proc.memory / 1024 / 1024)} MB`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
bm2.startPolling(5000);
|
|
1979
|
+
|
|
1980
|
+
// Keep running
|
|
1981
|
+
process.on("SIGINT", async () => {
|
|
1982
|
+
bm2.stopPolling();
|
|
1983
|
+
await bm2.disconnect();
|
|
1984
|
+
process.exit(0);
|
|
1985
|
+
});
|
|
1986
|
+
```
|
|
1987
|
+
|
|
1988
|
+
### Programmatic Deploy Pipeline
|
|
1989
|
+
|
|
1990
|
+
```ts
|
|
1991
|
+
import BM2 from "bm2";
|
|
1992
|
+
|
|
1993
|
+
const bm2 = new BM2();
|
|
1994
|
+
await bm2.connect();
|
|
1995
|
+
|
|
1996
|
+
// Deploy new code, then reload
|
|
1997
|
+
console.log("Reloading all processes...");
|
|
1998
|
+
const reloaded = await bm2.reload("all");
|
|
1999
|
+
console.log(`Reloaded ${reloaded.length} processes`);
|
|
2000
|
+
|
|
2001
|
+
// Verify everything is healthy
|
|
2002
|
+
const processes = await bm2.list();
|
|
2003
|
+
const allOnline = processes.every((p) => p.status === "online");
|
|
2004
|
+
|
|
2005
|
+
if (allOnline) {
|
|
2006
|
+
console.log("✅ All processes online");
|
|
2007
|
+
await bm2.save();
|
|
2008
|
+
} else {
|
|
2009
|
+
console.error("❌ Some processes failed to come online");
|
|
2010
|
+
const failed = processes.filter((p) => p.status !== "online");
|
|
2011
|
+
for (const p of failed) {
|
|
2012
|
+
console.error(` ${p.name}: ${p.status}`);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
await bm2.disconnect();
|
|
2017
|
+
```
|
|
2018
|
+
|
|
1479
2019
|
---
|
|
1480
2020
|
|
|
1481
2021
|
## Troubleshooting
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bm2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
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
|
-
"main": "src/
|
|
6
|
-
"module": "src/
|
|
7
|
-
"types": "src/
|
|
5
|
+
"main": "src/api.ts",
|
|
6
|
+
"module": "src/api.ts",
|
|
7
|
+
"types": "src/api.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/api.ts",
|
|
10
|
+
"./cli": "./src/index.ts"
|
|
11
|
+
},
|
|
8
12
|
"bin": {
|
|
9
13
|
"bm2": "./src/index.ts"
|
|
10
14
|
},
|
|
@@ -42,7 +46,7 @@
|
|
|
42
46
|
"email": "hello@maxxpainn.com",
|
|
43
47
|
"url": "https://maxxpainn.com"
|
|
44
48
|
},
|
|
45
|
-
"license": "
|
|
49
|
+
"license": "GPL-3.0",
|
|
46
50
|
"repository": {
|
|
47
51
|
"type": "git",
|
|
48
52
|
"url": "git+https://github.com/bun-bm2/bm2.git"
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM2 — Bun Process Manager
|
|
3
|
+
* Programmatic API
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import BM2 from "bm2";
|
|
7
|
+
* const bm2 = new BM2();
|
|
8
|
+
* await bm2.connect();
|
|
9
|
+
* const list = await bm2.list();
|
|
10
|
+
* await bm2.start({ script: "./app.ts", name: "my-app" });
|
|
11
|
+
* await bm2.disconnect();
|
|
12
|
+
*
|
|
13
|
+
* License: GPL-3.0-only
|
|
14
|
+
* Author: Zak <zak@maxxpainn.com>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { EventEmitter } from "events";
|
|
18
|
+
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { resolve } from "path";
|
|
21
|
+
import {
|
|
22
|
+
DAEMON_SOCKET,
|
|
23
|
+
DAEMON_PID_FILE,
|
|
24
|
+
BM2_HOME,
|
|
25
|
+
DASHBOARD_PORT,
|
|
26
|
+
METRICS_PORT,
|
|
27
|
+
DAEMON_OUT_LOG_FILE,
|
|
28
|
+
DAEMON_ERR_LOG_FILE,
|
|
29
|
+
} from "./constants";
|
|
30
|
+
import { ensureDirs, generateId } from "./utils";
|
|
31
|
+
import type {
|
|
32
|
+
DaemonMessage,
|
|
33
|
+
DaemonResponse,
|
|
34
|
+
StartOptions,
|
|
35
|
+
EcosystemConfig,
|
|
36
|
+
ProcessState,
|
|
37
|
+
MetricSnapshot,
|
|
38
|
+
ProcessStatus,
|
|
39
|
+
} from "./types";
|
|
40
|
+
|
|
41
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Bus event types emitted by BM2
|
|
43
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export interface BM2Events {
|
|
46
|
+
/** Daemon successfully connected */
|
|
47
|
+
"daemon:connected": [];
|
|
48
|
+
/** Daemon connection lost */
|
|
49
|
+
"daemon:disconnected": [];
|
|
50
|
+
/** Daemon launched by this client */
|
|
51
|
+
"daemon:launched": [pid: number];
|
|
52
|
+
/** Daemon killed */
|
|
53
|
+
"daemon:killed": [];
|
|
54
|
+
/** Error on the transport layer */
|
|
55
|
+
"error": [error: Error];
|
|
56
|
+
/** Process started */
|
|
57
|
+
"process:start": [processes: ProcessState[]];
|
|
58
|
+
/** Process stopped */
|
|
59
|
+
"process:stop": [processes: ProcessState[]];
|
|
60
|
+
/** Process restarted */
|
|
61
|
+
"process:restart": [processes: ProcessState[]];
|
|
62
|
+
/** Process reloaded */
|
|
63
|
+
"process:reload": [processes: ProcessState[]];
|
|
64
|
+
/** Process deleted */
|
|
65
|
+
"process:delete": [processes: ProcessState[]];
|
|
66
|
+
/** Process scaled */
|
|
67
|
+
"process:scale": [processes: ProcessState[]];
|
|
68
|
+
/** Metrics snapshot received */
|
|
69
|
+
"metrics": [snapshot: MetricSnapshot];
|
|
70
|
+
/** Log data received */
|
|
71
|
+
"log:data": [logs: Array<{ name: string; id: number; out: string; err: string }>];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// Main API class
|
|
76
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export class BM2 extends EventEmitter<BM2Events> {
|
|
79
|
+
private _connected: boolean = false;
|
|
80
|
+
private _daemonPid: number | null = null;
|
|
81
|
+
private _pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
82
|
+
|
|
83
|
+
/** Whether the client believes the daemon is reachable. */
|
|
84
|
+
get connected(): boolean {
|
|
85
|
+
return this._connected;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** PID of the daemon process (if known). */
|
|
89
|
+
get daemonPid(): number | null {
|
|
90
|
+
return this._daemonPid;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ──────────────────────────── lifecycle ────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Connect to the BM2 daemon.
|
|
97
|
+
* If the daemon is not running it will be spawned automatically
|
|
98
|
+
* (same behaviour as the CLI).
|
|
99
|
+
*/
|
|
100
|
+
async connect(): Promise<this> {
|
|
101
|
+
ensureDirs();
|
|
102
|
+
|
|
103
|
+
if (!(await this.isDaemonAlive())) {
|
|
104
|
+
await this.launchDaemon();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Verify connectivity
|
|
108
|
+
const pong = await this.send({ type: "ping" });
|
|
109
|
+
if (!pong.success) {
|
|
110
|
+
throw new Error("Failed to connect to BM2 daemon");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this._connected = true;
|
|
114
|
+
this._daemonPid = pong.data?.pid ?? null;
|
|
115
|
+
this.emit("daemon:connected");
|
|
116
|
+
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Disconnect from the daemon. Stops any internal polling but does **not**
|
|
122
|
+
* kill the daemon — processes keep running.
|
|
123
|
+
*/
|
|
124
|
+
async disconnect(): Promise<void> {
|
|
125
|
+
this.stopPolling();
|
|
126
|
+
this._connected = false;
|
|
127
|
+
this.emit("daemon:disconnected");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─────────────────────── process management ───────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Start a new process (or ecosystem).
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* await bm2.start({ script: "./server.ts", name: "api", instances: 4 });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
async start(options: StartOptions): Promise<ProcessState[]> {
|
|
140
|
+
if (options.script) {
|
|
141
|
+
options.script = resolve(options.script);
|
|
142
|
+
}
|
|
143
|
+
const res = await this.sendOrThrow({ type: "start", data: options });
|
|
144
|
+
this.emit("process:start", res.data);
|
|
145
|
+
return res.data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Start an ecosystem configuration object.
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* await bm2.startEcosystem({ apps: [{ script: "./a.ts" }, { script: "./b.ts" }] });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
async startEcosystem(config: EcosystemConfig): Promise<ProcessState[]> {
|
|
156
|
+
// Resolve scripts to absolute paths
|
|
157
|
+
for (const app of config.apps) {
|
|
158
|
+
if (app.script) app.script = resolve(app.script);
|
|
159
|
+
}
|
|
160
|
+
const res = await this.sendOrThrow({ type: "ecosystem", data: config });
|
|
161
|
+
this.emit("process:start", res.data);
|
|
162
|
+
return res.data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Stop one or more processes.
|
|
167
|
+
* @param target Process id, name, namespace, or `"all"`.
|
|
168
|
+
*/
|
|
169
|
+
async stop(target: string | number = "all"): Promise<ProcessState[]> {
|
|
170
|
+
const type = target === "all" ? "stopAll" : "stop";
|
|
171
|
+
const data = target === "all" ? undefined : { target: String(target) };
|
|
172
|
+
const res = await this.sendOrThrow({ type, data });
|
|
173
|
+
this.emit("process:stop", res.data);
|
|
174
|
+
return res.data;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Restart one or more processes (hard restart).
|
|
179
|
+
*/
|
|
180
|
+
async restart(target: string | number = "all"): Promise<ProcessState[]> {
|
|
181
|
+
const type = target === "all" ? "restartAll" : "restart";
|
|
182
|
+
const data = target === "all" ? undefined : { target: String(target) };
|
|
183
|
+
const res = await this.sendOrThrow({ type, data });
|
|
184
|
+
this.emit("process:restart", res.data);
|
|
185
|
+
return res.data;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Graceful zero-downtime reload.
|
|
190
|
+
*/
|
|
191
|
+
async reload(target: string | number = "all"): Promise<ProcessState[]> {
|
|
192
|
+
const type = target === "all" ? "reloadAll" : "reload";
|
|
193
|
+
const data = target === "all" ? undefined : { target: String(target) };
|
|
194
|
+
const res = await this.sendOrThrow({ type, data });
|
|
195
|
+
this.emit("process:reload", res.data);
|
|
196
|
+
return res.data;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Stop and remove one or more processes from BM2's list.
|
|
201
|
+
*/
|
|
202
|
+
async delete(target: string | number = "all"): Promise<ProcessState[]> {
|
|
203
|
+
const type = target === "all" ? "deleteAll" : "delete";
|
|
204
|
+
const data = target === "all" ? undefined : { target: String(target) };
|
|
205
|
+
const res = await this.sendOrThrow({ type, data });
|
|
206
|
+
this.emit("process:delete", res.data);
|
|
207
|
+
return res.data;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Scale a process group to `count` instances.
|
|
212
|
+
*/
|
|
213
|
+
async scale(target: string | number, count: number): Promise<ProcessState[]> {
|
|
214
|
+
const res = await this.sendOrThrow({
|
|
215
|
+
type: "scale",
|
|
216
|
+
data: { target: String(target), count },
|
|
217
|
+
});
|
|
218
|
+
this.emit("process:scale", res.data);
|
|
219
|
+
return res.data;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Send an OS signal to a process.
|
|
224
|
+
*/
|
|
225
|
+
async sendSignal(target: string | number, signal: string): Promise<void> {
|
|
226
|
+
await this.sendOrThrow({
|
|
227
|
+
type: "signal",
|
|
228
|
+
data: { target: String(target), signal },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Reset restart counters for one or more processes.
|
|
234
|
+
*/
|
|
235
|
+
async reset(target: string | number = "all"): Promise<ProcessState[]> {
|
|
236
|
+
const res = await this.sendOrThrow({
|
|
237
|
+
type: "reset",
|
|
238
|
+
data: { target: String(target) },
|
|
239
|
+
});
|
|
240
|
+
return res.data;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ───────────────────────── introspection ──────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* List all managed processes.
|
|
247
|
+
*/
|
|
248
|
+
async list(): Promise<ProcessState[]> {
|
|
249
|
+
const res = await this.sendOrThrow({ type: "list" });
|
|
250
|
+
return res.data;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get detailed description(s) of a process.
|
|
255
|
+
*/
|
|
256
|
+
async describe(target: string | number): Promise<ProcessState[]> {
|
|
257
|
+
const res = await this.sendOrThrow({
|
|
258
|
+
type: "describe",
|
|
259
|
+
data: { target: String(target) },
|
|
260
|
+
});
|
|
261
|
+
return res.data;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ────────────────────────── logs ───────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Retrieve recent log lines.
|
|
268
|
+
*/
|
|
269
|
+
async logs(
|
|
270
|
+
target: string | number = "all",
|
|
271
|
+
lines: number = 20
|
|
272
|
+
): Promise<Array<{ name: string; id: number; out: string; err: string }>> {
|
|
273
|
+
const res = await this.sendOrThrow({
|
|
274
|
+
type: "logs",
|
|
275
|
+
data: { target: String(target), lines },
|
|
276
|
+
});
|
|
277
|
+
this.emit("log:data", res.data);
|
|
278
|
+
return res.data;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Flush (truncate) log files for one or all processes.
|
|
283
|
+
*/
|
|
284
|
+
async flush(target?: string | number): Promise<void> {
|
|
285
|
+
await this.sendOrThrow({
|
|
286
|
+
type: "flush",
|
|
287
|
+
data: target !== undefined ? { target: String(target) } : undefined,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ──────────────────────── monitoring ───────────────────────────────────
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Take a single metrics snapshot.
|
|
295
|
+
*/
|
|
296
|
+
async metrics(): Promise<MetricSnapshot> {
|
|
297
|
+
const res = await this.sendOrThrow({ type: "metrics" });
|
|
298
|
+
this.emit("metrics", res.data);
|
|
299
|
+
return res.data;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get historical metric snapshots.
|
|
304
|
+
* @param seconds Look-back window (default 300 = 5 min).
|
|
305
|
+
*/
|
|
306
|
+
async metricsHistory(seconds: number = 300): Promise<MetricSnapshot[]> {
|
|
307
|
+
const res = await this.sendOrThrow({
|
|
308
|
+
type: "metricsHistory",
|
|
309
|
+
data: { seconds },
|
|
310
|
+
});
|
|
311
|
+
return res.data;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get Prometheus-formatted metrics string.
|
|
316
|
+
*/
|
|
317
|
+
async prometheus(): Promise<string> {
|
|
318
|
+
const res = await this.sendOrThrow({ type: "prometheus" });
|
|
319
|
+
return res.data;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Start polling metrics at a fixed interval and emitting `"metrics"` events.
|
|
324
|
+
*
|
|
325
|
+
* ```ts
|
|
326
|
+
* bm2.on("metrics", (snapshot) => console.log(snapshot));
|
|
327
|
+
* bm2.startPolling(2000);
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
startPolling(intervalMs: number = 2000): void {
|
|
331
|
+
this.stopPolling();
|
|
332
|
+
this._pollTimer = setInterval(async () => {
|
|
333
|
+
try {
|
|
334
|
+
await this.metrics();
|
|
335
|
+
} catch (err) {
|
|
336
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
337
|
+
}
|
|
338
|
+
}, intervalMs);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Stop the metrics polling loop started by `startPolling()`. */
|
|
342
|
+
stopPolling(): void {
|
|
343
|
+
if (this._pollTimer) {
|
|
344
|
+
clearInterval(this._pollTimer);
|
|
345
|
+
this._pollTimer = null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ────────────────────── persistence ───────────────────────────────────
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Persist the current process list to disk so it can be restored later.
|
|
353
|
+
*/
|
|
354
|
+
async save(): Promise<void> {
|
|
355
|
+
await this.sendOrThrow({ type: "save" });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Restore previously saved processes.
|
|
360
|
+
*/
|
|
361
|
+
async resurrect(): Promise<ProcessState[]> {
|
|
362
|
+
const res = await this.sendOrThrow({ type: "resurrect" });
|
|
363
|
+
return res.data;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ────────────────────── dashboard ─────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Start the web dashboard.
|
|
370
|
+
*/
|
|
371
|
+
async dashboard(
|
|
372
|
+
port: number = DASHBOARD_PORT,
|
|
373
|
+
metricsPort: number = METRICS_PORT
|
|
374
|
+
): Promise<{ port: number; metricsPort: number }> {
|
|
375
|
+
const res = await this.sendOrThrow({
|
|
376
|
+
type: "dashboard",
|
|
377
|
+
data: { port, metricsPort },
|
|
378
|
+
});
|
|
379
|
+
return res.data;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Stop the web dashboard.
|
|
384
|
+
*/
|
|
385
|
+
async dashboardStop(): Promise<void> {
|
|
386
|
+
await this.sendOrThrow({ type: "dashboardStop" });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ────────────────────── modules ───────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Install a BM2 module.
|
|
393
|
+
*/
|
|
394
|
+
async moduleInstall(nameOrPath: string): Promise<{ path: string }> {
|
|
395
|
+
const res = await this.sendOrThrow({
|
|
396
|
+
type: "moduleInstall",
|
|
397
|
+
data: { module: nameOrPath },
|
|
398
|
+
});
|
|
399
|
+
return res.data;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Uninstall a BM2 module.
|
|
404
|
+
*/
|
|
405
|
+
async moduleUninstall(name: string): Promise<void> {
|
|
406
|
+
await this.sendOrThrow({
|
|
407
|
+
type: "moduleUninstall",
|
|
408
|
+
data: { module: name },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* List installed modules.
|
|
414
|
+
*/
|
|
415
|
+
async moduleList(): Promise<Array<{ name: string; version: string }>> {
|
|
416
|
+
const res = await this.sendOrThrow({ type: "moduleList" });
|
|
417
|
+
return res.data;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ────────────────────── daemon lifecycle ──────────────────────────────
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Ping the daemon. Returns daemon PID and uptime.
|
|
424
|
+
*/
|
|
425
|
+
async ping(): Promise<{ pid: number; uptime: number }> {
|
|
426
|
+
const res = await this.sendOrThrow({ type: "ping" });
|
|
427
|
+
return res.data;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Kill the daemon and all managed processes.
|
|
432
|
+
*/
|
|
433
|
+
async kill(): Promise<void> {
|
|
434
|
+
try {
|
|
435
|
+
await this.send({ type: "kill" });
|
|
436
|
+
} catch {
|
|
437
|
+
// Expected — daemon exits before responding
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Clean up leftover files
|
|
441
|
+
try { if (existsSync(DAEMON_SOCKET)) unlinkSync(DAEMON_SOCKET); } catch {}
|
|
442
|
+
try { if (existsSync(DAEMON_PID_FILE)) unlinkSync(DAEMON_PID_FILE); } catch {}
|
|
443
|
+
|
|
444
|
+
this._connected = false;
|
|
445
|
+
this._daemonPid = null;
|
|
446
|
+
this.stopPolling();
|
|
447
|
+
this.emit("daemon:killed");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Reload the daemon server itself.
|
|
452
|
+
*/
|
|
453
|
+
async daemonReload(): Promise<string> {
|
|
454
|
+
const res = await this.sendOrThrow({ type: "daemonReload" });
|
|
455
|
+
return res.data;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ────────────────────── internal transport ────────────────────────────
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Low-level: send an arbitrary message to the daemon and return the
|
|
462
|
+
* raw response. Useful for custom or future command types.
|
|
463
|
+
*/
|
|
464
|
+
async send(message: DaemonMessage): Promise<DaemonResponse> {
|
|
465
|
+
|
|
466
|
+
if (!message.id) {
|
|
467
|
+
message.id = generateId();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const body = JSON.stringify(message);
|
|
471
|
+
|
|
472
|
+
// Bun supports fetching over Unix sockets with the `unix` option
|
|
473
|
+
const response = await fetch(`http://localhost/`, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: { "Content-Type": "application/json" },
|
|
476
|
+
body,
|
|
477
|
+
unix: DAEMON_SOCKET,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
const text = await response.text();
|
|
482
|
+
throw new Error(`Daemon HTTP ${response.status}: ${text}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (await response.json()) as DaemonResponse;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ────────────────────── private helpers ───────────────────────────────
|
|
489
|
+
|
|
490
|
+
/** Send and throw a friendly error if `success` is false. */
|
|
491
|
+
private async sendOrThrow(message: DaemonMessage): Promise<DaemonResponse> {
|
|
492
|
+
const res = await this.send(message);
|
|
493
|
+
if (!res.success) {
|
|
494
|
+
throw new BM2Error(
|
|
495
|
+
res.error || `Command "${message.type}" failed`,
|
|
496
|
+
message.type,
|
|
497
|
+
res
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return res;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Check whether the daemon is running and reachable. */
|
|
504
|
+
private async isDaemonAlive(): Promise<boolean> {
|
|
505
|
+
// Quick PID-file check first
|
|
506
|
+
if (existsSync(DAEMON_PID_FILE)) {
|
|
507
|
+
try {
|
|
508
|
+
const pid = parseInt(readFileSync(DAEMON_PID_FILE, "utf-8").trim());
|
|
509
|
+
process.kill(pid, 0); // throws if process doesn't exist
|
|
510
|
+
} catch {
|
|
511
|
+
// Stale PID file
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Verify the socket is responsive
|
|
519
|
+
if (!existsSync(DAEMON_SOCKET)) return false;
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const res = await this.send({ type: "ping" });
|
|
523
|
+
return res.success;
|
|
524
|
+
} catch {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Launch the daemon as a detached background process.
|
|
531
|
+
* Waits up to 5 seconds for it to become responsive.
|
|
532
|
+
*/
|
|
533
|
+
private async launchDaemon(): Promise<void> {
|
|
534
|
+
const daemonScript = join(import.meta.dir, "daemon.ts");
|
|
535
|
+
const bunPath = Bun.which("bun") || "bun";
|
|
536
|
+
|
|
537
|
+
// Open log files for daemon stdout/stderr
|
|
538
|
+
const outLog = Bun.file(DAEMON_OUT_LOG_FILE);
|
|
539
|
+
const errLog = Bun.file(DAEMON_ERR_LOG_FILE);
|
|
540
|
+
|
|
541
|
+
const proc = Bun.spawn([bunPath, "run", daemonScript], {
|
|
542
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
543
|
+
env: { ...process.env as Record<string, string> },
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Detach — we don't want to keep a handle
|
|
547
|
+
proc.unref();
|
|
548
|
+
|
|
549
|
+
// Poll until daemon is responsive (up to 5 s)
|
|
550
|
+
const deadline = Date.now() + 5000;
|
|
551
|
+
let alive = false;
|
|
552
|
+
|
|
553
|
+
while (Date.now() < deadline) {
|
|
554
|
+
await Bun.sleep(200);
|
|
555
|
+
try {
|
|
556
|
+
if (existsSync(DAEMON_SOCKET)) {
|
|
557
|
+
const res = await this.send({ type: "ping" });
|
|
558
|
+
if (res.success) {
|
|
559
|
+
alive = true;
|
|
560
|
+
this._daemonPid = res.data?.pid ?? proc.pid;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
// Not ready yet — keep waiting
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!alive) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
"Timed out waiting for BM2 daemon to start. " +
|
|
572
|
+
`Check ${DAEMON_ERR_LOG_FILE} for details.`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
this.emit("daemon:launched", this._daemonPid!);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ────────────────────────── error class ─────────────────────────────────
|
|
581
|
+
|
|
582
|
+
export class BM2Error extends Error {
|
|
583
|
+
/** The daemon command type that failed. */
|
|
584
|
+
public readonly command: string;
|
|
585
|
+
/** The full daemon response. */
|
|
586
|
+
public readonly response: DaemonResponse;
|
|
587
|
+
|
|
588
|
+
constructor(message: string, command: string, response: DaemonResponse) {
|
|
589
|
+
super(message);
|
|
590
|
+
this.name = "BM2Error";
|
|
591
|
+
this.command = command;
|
|
592
|
+
this.response = response;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ────────────────────────── default export ──────────────────────────────
|
|
597
|
+
|
|
598
|
+
export default BM2;
|
package/src/index.ts
CHANGED
package/src/process-table.ts
CHANGED
|
@@ -84,6 +84,7 @@ function minimalBorders() {
|
|
|
84
84
|
// ---------- Table Printer ----------
|
|
85
85
|
|
|
86
86
|
export function printProcessTable(processes: ProcessState[]) {
|
|
87
|
+
|
|
87
88
|
console.log("");
|
|
88
89
|
console.log(color("BM2 — Bun Process Manager", "bold"));
|
|
89
90
|
console.log(color("─────────────────────────────────────────────", "dim"));
|
|
@@ -133,7 +134,7 @@ export function printProcessTable(processes: ProcessState[]) {
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
|
|
136
|
-
export function liveWatchProcess(processes: ProcessState[], interval =
|
|
137
|
+
export function liveWatchProcess(processes: ProcessState[], interval = 5_000) {
|
|
137
138
|
let sortBy: "cpu" | "mem" | "uptime" | "default" = "default";
|
|
138
139
|
|
|
139
140
|
// Clear console helper
|
package/src/startup-manager.ts
CHANGED
|
@@ -34,39 +34,41 @@
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
private generateSystemd(bunPath: string, bm2Path: string, daemonPath: string): string {
|
|
37
|
+
|
|
37
38
|
const unit = `[Unit]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
Description=BM2 Process Manager
|
|
40
|
+
Documentation=https://github.com/bm2
|
|
41
|
+
After=network.target
|
|
42
|
+
|
|
43
|
+
[Service]
|
|
44
|
+
Type=simple
|
|
45
|
+
User=${process.env.USER || "root"}
|
|
46
|
+
LimitNOFILE=infinity
|
|
47
|
+
LimitNPROC=infinity
|
|
48
|
+
LimitCORE=infinity
|
|
49
|
+
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${join(bunPath, "..")}
|
|
50
|
+
Environment=BM2_HOME=${join(process.env.HOME || "/root", ".bm2")}
|
|
51
|
+
Restart=on-failure
|
|
52
|
+
|
|
53
|
+
ExecStart=${bunPath} run ${daemonPath}
|
|
54
|
+
ExecStartPost=${bunPath} run ${bm2Path} resurrect
|
|
55
|
+
ExecReload=${bunPath} run ${bm2Path} reload all
|
|
56
|
+
ExecStop=${bunPath} run ${bm2Path} kill
|
|
57
|
+
|
|
58
|
+
[Install]
|
|
59
|
+
WantedBy=multi-user.target
|
|
60
|
+
`;
|
|
61
|
+
|
|
60
62
|
const servicePath = "/etc/systemd/system/bm2.service";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
return `# BM2 Systemd Service
|
|
64
|
+
# Save to: ${servicePath}
|
|
65
|
+
# Then run:
|
|
66
|
+
# sudo systemctl daemon-reload
|
|
67
|
+
# sudo systemctl enable bm2
|
|
68
|
+
# sudo systemctl start bm2
|
|
69
|
+
|
|
70
|
+
${unit}`;
|
|
71
|
+
}
|
|
70
72
|
|
|
71
73
|
private generateLaunchd(bunPath: string, bm2Path: string, daemonPath: string): string {
|
|
72
74
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -115,11 +117,11 @@
|
|
|
115
117
|
if (os === "linux") {
|
|
116
118
|
const servicePath = "/etc/systemd/system/bm2.service";
|
|
117
119
|
// Extract just the unit content
|
|
118
|
-
const unitContent = content.split("\n\n").slice(1).join("\n\n");
|
|
119
|
-
await Bun.write(servicePath,
|
|
120
|
+
//const unitContent = content.split("\n\n").slice(1).join("\n\n");
|
|
121
|
+
await Bun.write(servicePath, content);
|
|
120
122
|
|
|
121
|
-
Bun.spawn(["sudo", "systemctl", "daemon-reload"], { stdout: "inherit" });
|
|
122
|
-
Bun.spawn(["sudo", "systemctl", "enable", "bm2"], { stdout: "inherit" });
|
|
123
|
+
Bun.spawn(["sudo", "systemctl", "daemon-reload"], { stdout: "inherit" }).exited;
|
|
124
|
+
Bun.spawn(["sudo", "systemctl", "enable", "bm2"], { stdout: "inherit" }).exited;
|
|
123
125
|
|
|
124
126
|
return `Service installed at ${servicePath}\nRun: sudo systemctl start bm2`;
|
|
125
127
|
} else if (os === "darwin") {
|