bgrun 3.10.0 → 3.10.2
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/dashboard/app/api/guard/route.ts +50 -0
- package/dashboard/app/globals.css +13 -0
- package/dashboard/app/page.client.tsx +8 -5
- package/dist/index.js +222 -139
- package/package.json +2 -2
- package/src/commands/run.ts +18 -0
- package/src/db.ts +8 -0
- package/src/platform.ts +43 -41
- package/src/server.ts +67 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/guard — Toggle BGR_KEEP_ALIVE for a process
|
|
3
|
+
* Body: { name: string, enabled: boolean }
|
|
4
|
+
*
|
|
5
|
+
* When enabled=true, the built-in guard will auto-restart this process if it dies.
|
|
6
|
+
* When enabled=false, the process is left alone.
|
|
7
|
+
*/
|
|
8
|
+
import { getProcess, updateProcessEnv } from '../../../src/db';
|
|
9
|
+
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await req.json() as { name: string; enabled: boolean };
|
|
13
|
+
if (!body.name) {
|
|
14
|
+
return Response.json({ error: 'Missing process name' }, { status: 400 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const proc = getProcess(body.name);
|
|
18
|
+
if (!proc) {
|
|
19
|
+
return Response.json({ error: `Process "${body.name}" not found` }, { status: 404 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Parse existing env
|
|
23
|
+
let env: Record<string, string> = {};
|
|
24
|
+
if (proc.env) {
|
|
25
|
+
try { env = JSON.parse(proc.env); } catch { env = {}; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Toggle BGR_KEEP_ALIVE
|
|
29
|
+
if (body.enabled) {
|
|
30
|
+
env.BGR_KEEP_ALIVE = 'true';
|
|
31
|
+
} else {
|
|
32
|
+
delete env.BGR_KEEP_ALIVE;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Save back
|
|
36
|
+
updateProcessEnv(body.name, JSON.stringify(env));
|
|
37
|
+
|
|
38
|
+
return Response.json({
|
|
39
|
+
ok: true,
|
|
40
|
+
name: body.name,
|
|
41
|
+
guarded: body.enabled
|
|
42
|
+
});
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function GET() {
|
|
49
|
+
return Response.json({ error: 'Use POST to toggle guard' }, { status: 405 });
|
|
50
|
+
}
|
|
@@ -898,6 +898,19 @@ tr.animate-in:nth-child(10) {
|
|
|
898
898
|
border: 1px solid var(--info-border);
|
|
899
899
|
}
|
|
900
900
|
|
|
901
|
+
a.port-link {
|
|
902
|
+
text-decoration: none;
|
|
903
|
+
transition: all var(--transition-fast);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
a.port-link:hover {
|
|
907
|
+
text-decoration: underline;
|
|
908
|
+
background: rgba(56, 189, 248, 0.15);
|
|
909
|
+
/* Slightly brighter info-bg on hover */
|
|
910
|
+
color: #7dd3fc;
|
|
911
|
+
/* Brighter info color */
|
|
912
|
+
}
|
|
913
|
+
|
|
901
914
|
.command {
|
|
902
915
|
font-family: var(--font-mono);
|
|
903
916
|
font-size: 0.75rem;
|
|
@@ -164,7 +164,7 @@ function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
|
|
|
164
164
|
<td className="pid">{String(p.pid)}</td>
|
|
165
165
|
<td>
|
|
166
166
|
{p.port
|
|
167
|
-
? <
|
|
167
|
+
? <a className="port-num port-link" href={`http://localhost:${p.port}`} target="_blank" rel="noopener" title={`Open localhost:${p.port}`} onClick={(e: Event) => e.stopPropagation()}>:{p.port}</a>
|
|
168
168
|
: <span style={{ color: 'var(--text-muted)' }}>–</span>
|
|
169
169
|
}
|
|
170
170
|
</td>
|
|
@@ -228,7 +228,7 @@ function ProcessCard({ p }: { p: ProcessData }) {
|
|
|
228
228
|
</div>
|
|
229
229
|
<div className="card-details">
|
|
230
230
|
<div className="card-detail"><span className="card-label">PID</span><span>{p.pid}</span></div>
|
|
231
|
-
<div className="card-detail"><span className="card-label">Port</span
|
|
231
|
+
<div className="card-detail"><span className="card-label">Port</span>{p.port ? <a className="port-link" href={`http://localhost:${p.port}`} target="_blank" rel="noopener" onClick={(e: Event) => e.stopPropagation()}>:{p.port}</a> : <span>–</span>}</div>
|
|
232
232
|
<div className="card-detail"><span className="card-label">Memory</span><span>{p.memory > 0 ? formatMemory(p.memory) : '–'}</span></div>
|
|
233
233
|
<div className="card-detail"><span className="card-label">Runtime</span><span>{formatRuntime(p.runtime)}</span></div>
|
|
234
234
|
</div>
|
|
@@ -845,7 +845,7 @@ export default function mount(): () => void {
|
|
|
845
845
|
const metaItems = [
|
|
846
846
|
{ label: 'Status', value: proc.running ? '● Running' : '○ Stopped' },
|
|
847
847
|
{ label: 'PID', value: String(proc.pid) },
|
|
848
|
-
{ label: 'Port', value: proc.port ? `:${proc.port}` : '–' },
|
|
848
|
+
{ label: 'Port', value: proc.port ? `:${proc.port}` : '–', href: proc.port ? `http://localhost:${proc.port}` : undefined },
|
|
849
849
|
{ label: 'Runtime', value: formatRuntime(proc.runtime) },
|
|
850
850
|
{ label: 'Command', value: proc.command },
|
|
851
851
|
{ label: 'Directory', value: proc.directory || '–' },
|
|
@@ -853,10 +853,13 @@ export default function mount(): () => void {
|
|
|
853
853
|
{ label: 'Group', value: proc.group || '–' },
|
|
854
854
|
];
|
|
855
855
|
|
|
856
|
-
const items = metaItems.map(m => (
|
|
856
|
+
const items = metaItems.map((m: any) => (
|
|
857
857
|
<div className="meta-item">
|
|
858
858
|
<span className="meta-label">{m.label}</span>
|
|
859
|
-
|
|
859
|
+
{m.href
|
|
860
|
+
? <a className="meta-value port-link" href={m.href} target="_blank" rel="noopener">{m.value}</a>
|
|
861
|
+
: <span className="meta-value">{m.value}</span>
|
|
862
|
+
}
|
|
860
863
|
</div>
|
|
861
864
|
) as unknown as Node);
|
|
862
865
|
meta.replaceChildren(...items);
|
package/dist/index.js
CHANGED
|
@@ -80,36 +80,24 @@ async function getChildPids(pid) {
|
|
|
80
80
|
}
|
|
81
81
|
async function terminateProcess(pid, force = false) {
|
|
82
82
|
await plat.measure(`Terminate PID ${pid}`, async (m) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const signal = force ? "KILL" : "TERM";
|
|
94
|
-
await $`kill -${signal} ${childPid}`.nothrow();
|
|
83
|
+
try {
|
|
84
|
+
if (isWindows()) {
|
|
85
|
+
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
86
|
+
} else {
|
|
87
|
+
const children = await m("Get children", () => getChildPids(pid)) ?? [];
|
|
88
|
+
const signal = force ? "KILL" : "TERM";
|
|
89
|
+
for (const childPid of children) {
|
|
90
|
+
try {
|
|
91
|
+
await $`kill -${signal} ${childPid}`.nothrow();
|
|
92
|
+
} catch {}
|
|
95
93
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await Bun.sleep(500);
|
|
99
|
-
if (await isProcessRunning(pid)) {
|
|
100
|
-
try {
|
|
101
|
-
if (isWindows()) {
|
|
102
|
-
if (force) {
|
|
103
|
-
await $`taskkill /F /PID ${pid}`.nothrow().quiet();
|
|
104
|
-
} else {
|
|
105
|
-
await $`taskkill /PID ${pid}`.nothrow().quiet();
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
const signal = force ? "KILL" : "TERM";
|
|
94
|
+
await Bun.sleep(500);
|
|
95
|
+
if (await isProcessRunning(pid)) {
|
|
109
96
|
await $`kill -${signal} ${pid}`.nothrow();
|
|
110
97
|
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
await Bun.sleep(300);
|
|
113
101
|
});
|
|
114
102
|
}
|
|
115
103
|
async function isPortFree(port) {
|
|
@@ -118,9 +106,13 @@ async function isPortFree(port) {
|
|
|
118
106
|
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
119
107
|
for (const line of result.split(`
|
|
120
108
|
`)) {
|
|
121
|
-
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING`));
|
|
122
|
-
if (match)
|
|
123
|
-
|
|
109
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
110
|
+
if (match) {
|
|
111
|
+
const pid = parseInt(match[2]);
|
|
112
|
+
if (pid > 0 && await isProcessRunning(pid)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
124
116
|
}
|
|
125
117
|
return true;
|
|
126
118
|
} else {
|
|
@@ -159,8 +151,11 @@ async function killProcessOnPort(port) {
|
|
|
159
151
|
}
|
|
160
152
|
}
|
|
161
153
|
for (const pid of pids) {
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
const alive = await isProcessRunning(pid);
|
|
155
|
+
if (alive) {
|
|
156
|
+
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
157
|
+
console.log(`Killed process ${pid} using port ${port}`);
|
|
158
|
+
}
|
|
164
159
|
}
|
|
165
160
|
} else {
|
|
166
161
|
const result = await $`lsof -ti :${port}`.nothrow().text();
|
|
@@ -326,10 +321,92 @@ var init_platform = __esm(() => {
|
|
|
326
321
|
plat = createMeasure("platform");
|
|
327
322
|
});
|
|
328
323
|
|
|
324
|
+
// src/utils.ts
|
|
325
|
+
import * as fs2 from "fs";
|
|
326
|
+
import chalk from "chalk";
|
|
327
|
+
function parseEnvString(envString) {
|
|
328
|
+
const env = {};
|
|
329
|
+
envString.split(",").forEach((pair) => {
|
|
330
|
+
const [key, value] = pair.split("=");
|
|
331
|
+
if (key && value)
|
|
332
|
+
env[key] = value;
|
|
333
|
+
});
|
|
334
|
+
return env;
|
|
335
|
+
}
|
|
336
|
+
function calculateRuntime(startTime) {
|
|
337
|
+
const start = new Date(startTime).getTime();
|
|
338
|
+
const now = new Date().getTime();
|
|
339
|
+
const diffInMinutes = Math.floor((now - start) / (1000 * 60));
|
|
340
|
+
return `${diffInMinutes} minutes`;
|
|
341
|
+
}
|
|
342
|
+
async function getVersion() {
|
|
343
|
+
try {
|
|
344
|
+
const { join } = await import("path");
|
|
345
|
+
const pkgPath = join(import.meta.dir, "../package.json");
|
|
346
|
+
const pkg = await Bun.file(pkgPath).json();
|
|
347
|
+
return pkg.version || "0.0.0";
|
|
348
|
+
} catch {
|
|
349
|
+
return "0.0.0";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function validateDirectory(directory) {
|
|
353
|
+
if (!directory || !fs2.existsSync(directory)) {
|
|
354
|
+
console.log(chalk.red("\u274C Error: 'directory' must be a valid path."));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function tailFile(path, prefix, colorFn, lines) {
|
|
359
|
+
let position = 0;
|
|
360
|
+
let lastPartial = "";
|
|
361
|
+
if (!fs2.existsSync(path)) {
|
|
362
|
+
return () => {};
|
|
363
|
+
}
|
|
364
|
+
const fd = fs2.openSync(path, "r");
|
|
365
|
+
const printNewContent = () => {
|
|
366
|
+
try {
|
|
367
|
+
const stats = fs2.statSync(path);
|
|
368
|
+
if (stats.size <= position)
|
|
369
|
+
return;
|
|
370
|
+
const buffer = Buffer.alloc(stats.size - position);
|
|
371
|
+
fs2.readSync(fd, buffer, 0, buffer.length, position);
|
|
372
|
+
let content = buffer.toString();
|
|
373
|
+
content = lastPartial + content;
|
|
374
|
+
lastPartial = "";
|
|
375
|
+
const lineArray = content.split(/\r?\n/);
|
|
376
|
+
if (!content.endsWith(`
|
|
377
|
+
`)) {
|
|
378
|
+
lastPartial = lineArray.pop() || "";
|
|
379
|
+
}
|
|
380
|
+
lineArray.forEach((line) => {
|
|
381
|
+
if (line) {
|
|
382
|
+
console.log(colorFn(prefix + line));
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
position = stats.size;
|
|
386
|
+
} catch (e) {}
|
|
387
|
+
};
|
|
388
|
+
const watcher = fs2.watch(path, { persistent: true }, (event) => {
|
|
389
|
+
if (event === "change") {
|
|
390
|
+
printNewContent();
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
printNewContent();
|
|
394
|
+
return () => {
|
|
395
|
+
watcher.close();
|
|
396
|
+
try {
|
|
397
|
+
fs2.closeSync(fd);
|
|
398
|
+
} catch {}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
var init_utils = __esm(() => {
|
|
402
|
+
init_platform();
|
|
403
|
+
});
|
|
404
|
+
|
|
329
405
|
// src/db.ts
|
|
330
406
|
var exports_db = {};
|
|
331
407
|
__export(exports_db, {
|
|
332
408
|
updateProcessPid: () => updateProcessPid,
|
|
409
|
+
updateProcessEnv: () => updateProcessEnv,
|
|
333
410
|
retryDatabaseOperation: () => retryDatabaseOperation,
|
|
334
411
|
removeProcessByName: () => removeProcessByName,
|
|
335
412
|
removeProcess: () => removeProcess,
|
|
@@ -383,6 +460,12 @@ function removeAllProcesses() {
|
|
|
383
460
|
db.process.delete(p.id);
|
|
384
461
|
}
|
|
385
462
|
}
|
|
463
|
+
function updateProcessEnv(name, envJson) {
|
|
464
|
+
const proc = db.process.select().where({ name }).limit(1).get();
|
|
465
|
+
if (proc) {
|
|
466
|
+
db.process.update(proc.id, { env: envJson });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
386
469
|
function getDbInfo() {
|
|
387
470
|
return {
|
|
388
471
|
dbPath,
|
|
@@ -441,111 +524,6 @@ var init_db = __esm(() => {
|
|
|
441
524
|
});
|
|
442
525
|
});
|
|
443
526
|
|
|
444
|
-
// src/server.ts
|
|
445
|
-
var exports_server = {};
|
|
446
|
-
__export(exports_server, {
|
|
447
|
-
startServer: () => startServer
|
|
448
|
-
});
|
|
449
|
-
import path2 from "path";
|
|
450
|
-
async function startServer() {
|
|
451
|
-
const { start } = await import("melina");
|
|
452
|
-
const appDir = path2.join(import.meta.dir, "../dashboard/app");
|
|
453
|
-
const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
|
|
454
|
-
await start({
|
|
455
|
-
appDir,
|
|
456
|
-
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
457
|
-
globalCss: path2.join(appDir, "globals.css"),
|
|
458
|
-
...explicitPort !== undefined && { port: explicitPort }
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
var init_server = () => {};
|
|
462
|
-
|
|
463
|
-
// src/index.ts
|
|
464
|
-
import { parseArgs } from "util";
|
|
465
|
-
|
|
466
|
-
// src/utils.ts
|
|
467
|
-
init_platform();
|
|
468
|
-
import * as fs2 from "fs";
|
|
469
|
-
import chalk from "chalk";
|
|
470
|
-
function parseEnvString(envString) {
|
|
471
|
-
const env = {};
|
|
472
|
-
envString.split(",").forEach((pair) => {
|
|
473
|
-
const [key, value] = pair.split("=");
|
|
474
|
-
if (key && value)
|
|
475
|
-
env[key] = value;
|
|
476
|
-
});
|
|
477
|
-
return env;
|
|
478
|
-
}
|
|
479
|
-
function calculateRuntime(startTime) {
|
|
480
|
-
const start = new Date(startTime).getTime();
|
|
481
|
-
const now = new Date().getTime();
|
|
482
|
-
const diffInMinutes = Math.floor((now - start) / (1000 * 60));
|
|
483
|
-
return `${diffInMinutes} minutes`;
|
|
484
|
-
}
|
|
485
|
-
async function getVersion() {
|
|
486
|
-
try {
|
|
487
|
-
const { join } = await import("path");
|
|
488
|
-
const pkgPath = join(import.meta.dir, "../package.json");
|
|
489
|
-
const pkg = await Bun.file(pkgPath).json();
|
|
490
|
-
return pkg.version || "0.0.0";
|
|
491
|
-
} catch {
|
|
492
|
-
return "0.0.0";
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
function validateDirectory(directory) {
|
|
496
|
-
if (!directory || !fs2.existsSync(directory)) {
|
|
497
|
-
console.log(chalk.red("\u274C Error: 'directory' must be a valid path."));
|
|
498
|
-
process.exit(1);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
function tailFile(path, prefix, colorFn, lines) {
|
|
502
|
-
let position = 0;
|
|
503
|
-
let lastPartial = "";
|
|
504
|
-
if (!fs2.existsSync(path)) {
|
|
505
|
-
return () => {};
|
|
506
|
-
}
|
|
507
|
-
const fd = fs2.openSync(path, "r");
|
|
508
|
-
const printNewContent = () => {
|
|
509
|
-
try {
|
|
510
|
-
const stats = fs2.statSync(path);
|
|
511
|
-
if (stats.size <= position)
|
|
512
|
-
return;
|
|
513
|
-
const buffer = Buffer.alloc(stats.size - position);
|
|
514
|
-
fs2.readSync(fd, buffer, 0, buffer.length, position);
|
|
515
|
-
let content = buffer.toString();
|
|
516
|
-
content = lastPartial + content;
|
|
517
|
-
lastPartial = "";
|
|
518
|
-
const lineArray = content.split(/\r?\n/);
|
|
519
|
-
if (!content.endsWith(`
|
|
520
|
-
`)) {
|
|
521
|
-
lastPartial = lineArray.pop() || "";
|
|
522
|
-
}
|
|
523
|
-
lineArray.forEach((line) => {
|
|
524
|
-
if (line) {
|
|
525
|
-
console.log(colorFn(prefix + line));
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
position = stats.size;
|
|
529
|
-
} catch (e) {}
|
|
530
|
-
};
|
|
531
|
-
const watcher = fs2.watch(path, { persistent: true }, (event) => {
|
|
532
|
-
if (event === "change") {
|
|
533
|
-
printNewContent();
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
printNewContent();
|
|
537
|
-
return () => {
|
|
538
|
-
watcher.close();
|
|
539
|
-
try {
|
|
540
|
-
fs2.closeSync(fd);
|
|
541
|
-
} catch {}
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// src/commands/run.ts
|
|
546
|
-
init_db();
|
|
547
|
-
init_platform();
|
|
548
|
-
|
|
549
527
|
// src/logger.ts
|
|
550
528
|
import boxen from "boxen";
|
|
551
529
|
import chalk2 from "chalk";
|
|
@@ -570,6 +548,7 @@ function error(message) {
|
|
|
570
548
|
}));
|
|
571
549
|
process.exit(1);
|
|
572
550
|
}
|
|
551
|
+
var init_logger = () => {};
|
|
573
552
|
|
|
574
553
|
// src/config.ts
|
|
575
554
|
function formatEnvKey(key) {
|
|
@@ -607,8 +586,6 @@ var {$: $2 } = globalThis.Bun;
|
|
|
607
586
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
608
587
|
import { join as join2 } from "path";
|
|
609
588
|
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
610
|
-
var homePath2 = getHomeDir();
|
|
611
|
-
var run = createMeasure2("run");
|
|
612
589
|
async function handleRun(options) {
|
|
613
590
|
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
614
591
|
const existingProcess = name ? getProcess(name) : null;
|
|
@@ -662,6 +639,22 @@ async function handleRun(options) {
|
|
|
662
639
|
}
|
|
663
640
|
});
|
|
664
641
|
}
|
|
642
|
+
const cmdToMatch = existingProcess.command;
|
|
643
|
+
if (cmdToMatch) {
|
|
644
|
+
await run.measure("Zombie sweep", async () => {
|
|
645
|
+
try {
|
|
646
|
+
const result = await $2`powershell -Command "Get-Process -Name bun -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match '${cmdToMatch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").split(" ")[1] || cmdToMatch}' } | Select-Object -ExpandProperty Id"`.nothrow().text();
|
|
647
|
+
const zombiePids = result.split(`
|
|
648
|
+
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
649
|
+
for (const zPid of zombiePids) {
|
|
650
|
+
await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
651
|
+
}
|
|
652
|
+
if (zombiePids.length > 0) {
|
|
653
|
+
announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
|
|
654
|
+
}
|
|
655
|
+
} catch {}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
665
658
|
await retryDatabaseOperation(() => removeProcessByName(name));
|
|
666
659
|
} else {
|
|
667
660
|
if (!directory || !name || !command) {
|
|
@@ -729,6 +722,86 @@ async function handleRun(options) {
|
|
|
729
722
|
}));
|
|
730
723
|
announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
|
|
731
724
|
}
|
|
725
|
+
var homePath2, run;
|
|
726
|
+
var init_run = __esm(() => {
|
|
727
|
+
init_db();
|
|
728
|
+
init_platform();
|
|
729
|
+
init_logger();
|
|
730
|
+
init_utils();
|
|
731
|
+
homePath2 = getHomeDir();
|
|
732
|
+
run = createMeasure2("run");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// src/server.ts
|
|
736
|
+
var exports_server = {};
|
|
737
|
+
__export(exports_server, {
|
|
738
|
+
startServer: () => startServer
|
|
739
|
+
});
|
|
740
|
+
import path2 from "path";
|
|
741
|
+
async function startServer() {
|
|
742
|
+
const { start } = await import("melina");
|
|
743
|
+
const appDir = path2.join(import.meta.dir, "../dashboard/app");
|
|
744
|
+
const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
|
|
745
|
+
await start({
|
|
746
|
+
appDir,
|
|
747
|
+
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
748
|
+
globalCss: path2.join(appDir, "globals.css"),
|
|
749
|
+
...explicitPort !== undefined && { port: explicitPort }
|
|
750
|
+
});
|
|
751
|
+
startGuard();
|
|
752
|
+
}
|
|
753
|
+
function startGuard() {
|
|
754
|
+
console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
|
|
755
|
+
setInterval(async () => {
|
|
756
|
+
try {
|
|
757
|
+
const processes = getAllProcesses();
|
|
758
|
+
if (processes.length === 0)
|
|
759
|
+
return;
|
|
760
|
+
for (const proc of processes) {
|
|
761
|
+
if (GUARD_SKIP_NAMES.has(proc.name))
|
|
762
|
+
continue;
|
|
763
|
+
const env = proc.env ? typeof proc.env === "string" ? (() => {
|
|
764
|
+
try {
|
|
765
|
+
return JSON.parse(proc.env);
|
|
766
|
+
} catch {
|
|
767
|
+
return {};
|
|
768
|
+
}
|
|
769
|
+
})() : proc.env : {};
|
|
770
|
+
if (env.BGR_KEEP_ALIVE !== "true")
|
|
771
|
+
continue;
|
|
772
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
773
|
+
if (!alive) {
|
|
774
|
+
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
775
|
+
try {
|
|
776
|
+
await handleRun({
|
|
777
|
+
action: "run",
|
|
778
|
+
name: proc.name,
|
|
779
|
+
force: true,
|
|
780
|
+
remoteName: ""
|
|
781
|
+
});
|
|
782
|
+
console.log(`[guard] \u2713 Restarted "${proc.name}"`);
|
|
783
|
+
} catch (err) {
|
|
784
|
+
console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
} catch (err) {
|
|
789
|
+
console.error(`[guard] Error in guard loop: ${err.message}`);
|
|
790
|
+
}
|
|
791
|
+
}, GUARD_INTERVAL_MS);
|
|
792
|
+
}
|
|
793
|
+
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES;
|
|
794
|
+
var init_server = __esm(() => {
|
|
795
|
+
init_db();
|
|
796
|
+
init_platform();
|
|
797
|
+
init_run();
|
|
798
|
+
GUARD_SKIP_NAMES = new Set(["bgr-dashboard"]);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// src/index.ts
|
|
802
|
+
init_utils();
|
|
803
|
+
init_run();
|
|
804
|
+
import { parseArgs } from "util";
|
|
732
805
|
|
|
733
806
|
// src/commands/list.ts
|
|
734
807
|
import chalk4 from "chalk";
|
|
@@ -898,6 +971,8 @@ function renderProcessTable(processes, options) {
|
|
|
898
971
|
|
|
899
972
|
// src/commands/list.ts
|
|
900
973
|
init_db();
|
|
974
|
+
init_logger();
|
|
975
|
+
init_utils();
|
|
901
976
|
init_platform();
|
|
902
977
|
function formatMemory(bytes) {
|
|
903
978
|
if (bytes === 0)
|
|
@@ -974,6 +1049,7 @@ async function showAll(opts) {
|
|
|
974
1049
|
// src/commands/cleanup.ts
|
|
975
1050
|
init_db();
|
|
976
1051
|
init_platform();
|
|
1052
|
+
init_logger();
|
|
977
1053
|
import * as fs3 from "fs";
|
|
978
1054
|
async function handleDelete(name) {
|
|
979
1055
|
const process2 = getProcess(name);
|
|
@@ -1093,6 +1169,9 @@ async function handleDeleteAll() {
|
|
|
1093
1169
|
// src/commands/watch.ts
|
|
1094
1170
|
init_db();
|
|
1095
1171
|
init_platform();
|
|
1172
|
+
init_logger();
|
|
1173
|
+
init_utils();
|
|
1174
|
+
init_run();
|
|
1096
1175
|
import * as fs4 from "fs";
|
|
1097
1176
|
import path from "path";
|
|
1098
1177
|
import chalk5 from "chalk";
|
|
@@ -1282,6 +1361,7 @@ SIGINT received...`));
|
|
|
1282
1361
|
|
|
1283
1362
|
// src/commands/logs.ts
|
|
1284
1363
|
init_db();
|
|
1364
|
+
init_logger();
|
|
1285
1365
|
init_platform();
|
|
1286
1366
|
import chalk6 from "chalk";
|
|
1287
1367
|
import * as fs5 from "fs";
|
|
@@ -1326,7 +1406,9 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
1326
1406
|
}
|
|
1327
1407
|
|
|
1328
1408
|
// src/commands/details.ts
|
|
1409
|
+
init_logger();
|
|
1329
1410
|
init_db();
|
|
1411
|
+
init_utils();
|
|
1330
1412
|
init_platform();
|
|
1331
1413
|
import chalk7 from "chalk";
|
|
1332
1414
|
async function showDetails(name) {
|
|
@@ -1363,6 +1445,7 @@ ${Object.entries(envVars).map(([key, value]) => `${chalk7.cyan.bold(key)} = ${ch
|
|
|
1363
1445
|
}
|
|
1364
1446
|
|
|
1365
1447
|
// src/index.ts
|
|
1448
|
+
init_logger();
|
|
1366
1449
|
init_platform();
|
|
1367
1450
|
init_db();
|
|
1368
1451
|
import dedent from "dedent";
|
package/package.json
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -76,6 +76,24 @@ export async function handleRun(options: CommandOptions) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// Zombie sweep: kill any remaining bun processes matching this command
|
|
80
|
+
// This catches orphaned children that survived taskkill when the parent shell exited
|
|
81
|
+
const cmdToMatch = existingProcess.command;
|
|
82
|
+
if (cmdToMatch) {
|
|
83
|
+
await run.measure('Zombie sweep', async () => {
|
|
84
|
+
try {
|
|
85
|
+
const result = await $`powershell -Command "Get-Process -Name bun -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match '${cmdToMatch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').split(' ')[1] || cmdToMatch}' } | Select-Object -ExpandProperty Id"`.nothrow().text();
|
|
86
|
+
const zombiePids = result.split('\n').map((l: string) => parseInt(l.trim())).filter((n: number) => !isNaN(n) && n > 0);
|
|
87
|
+
for (const zPid of zombiePids) {
|
|
88
|
+
await $`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
89
|
+
}
|
|
90
|
+
if (zombiePids.length > 0) {
|
|
91
|
+
announce(`🧹 Swept ${zombiePids.length} zombie process(es)`, 'Zombie Cleanup');
|
|
92
|
+
}
|
|
93
|
+
} catch { /* best effort */ }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
await retryDatabaseOperation(() =>
|
|
80
98
|
removeProcessByName(name!)
|
|
81
99
|
);
|
package/src/db.ts
CHANGED
|
@@ -119,6 +119,14 @@ export function removeAllProcesses() {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** Update the stored env JSON for a process (used by guard toggle) */
|
|
123
|
+
export function updateProcessEnv(name: string, envJson: string) {
|
|
124
|
+
const proc = db.process.select().where({ name }).limit(1).get();
|
|
125
|
+
if (proc) {
|
|
126
|
+
db.process.update(proc.id, { env: envJson });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
122
130
|
// =============================================================================
|
|
123
131
|
// DEBUG / INFO
|
|
124
132
|
// =============================================================================
|
package/src/platform.ts
CHANGED
|
@@ -112,51 +112,38 @@ async function getChildPids(pid: number): Promise<number[]> {
|
|
|
112
112
|
*/
|
|
113
113
|
export async function terminateProcess(pid: number, force: boolean = false): Promise<void> {
|
|
114
114
|
await plat.measure(`Terminate PID ${pid}`, async (m) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
await $`kill -${signal} ${childPid}`.nothrow();
|
|
115
|
+
try {
|
|
116
|
+
if (isWindows()) {
|
|
117
|
+
// Always use /T (tree kill) on Windows to kill the entire process tree
|
|
118
|
+
// This prevents grandchild processes from surviving as zombies
|
|
119
|
+
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
120
|
+
} else {
|
|
121
|
+
// On Unix, kill children first, then parent
|
|
122
|
+
const children = await m('Get children', () => getChildPids(pid)) ?? [];
|
|
123
|
+
const signal = force ? 'KILL' : 'TERM';
|
|
124
|
+
for (const childPid of children) {
|
|
125
|
+
try {
|
|
126
|
+
await $`kill -${signal} ${childPid}`.nothrow();
|
|
127
|
+
} catch { /* already dead */ }
|
|
129
128
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Wait a bit for graceful shutdown
|
|
136
|
-
await Bun.sleep(500);
|
|
137
|
-
|
|
138
|
-
// Then kill the parent if still running
|
|
139
|
-
if (await isProcessRunning(pid)) {
|
|
140
|
-
try {
|
|
141
|
-
if (isWindows()) {
|
|
142
|
-
if (force) {
|
|
143
|
-
await $`taskkill /F /PID ${pid}`.nothrow().quiet();
|
|
144
|
-
} else {
|
|
145
|
-
await $`taskkill /PID ${pid}`.nothrow().quiet();
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
const signal = force ? 'KILL' : 'TERM';
|
|
129
|
+
await Bun.sleep(500);
|
|
130
|
+
if (await isProcessRunning(pid)) {
|
|
149
131
|
await $`kill -${signal} ${pid}`.nothrow();
|
|
150
132
|
}
|
|
151
|
-
} catch {
|
|
152
|
-
// Ignore errors
|
|
153
133
|
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Ignore errors for already-dead processes
|
|
154
136
|
}
|
|
137
|
+
|
|
138
|
+
// Wait for process to fully exit
|
|
139
|
+
await Bun.sleep(300);
|
|
155
140
|
});
|
|
156
141
|
}
|
|
157
142
|
|
|
158
143
|
/**
|
|
159
144
|
* Check if a port is free by attempting to bind to it.
|
|
145
|
+
* On Windows, also checks whether the process holding the port is actually alive
|
|
146
|
+
* (zombie sockets from dead processes don't block new binds on 0.0.0.0).
|
|
160
147
|
*/
|
|
161
148
|
export async function isPortFree(port: number): Promise<boolean> {
|
|
162
149
|
try {
|
|
@@ -165,14 +152,22 @@ export async function isPortFree(port: number): Promise<boolean> {
|
|
|
165
152
|
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
166
153
|
for (const line of result.split('\n')) {
|
|
167
154
|
// Only match exact port (avoid :35560 matching :3556)
|
|
168
|
-
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING`));
|
|
169
|
-
if (match)
|
|
155
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
156
|
+
if (match) {
|
|
157
|
+
const pid = parseInt(match[2]);
|
|
158
|
+
// If the PID behind the socket is dead, it's a zombie socket
|
|
159
|
+
// A new process can still bind to the port on 0.0.0.0
|
|
160
|
+
if (pid > 0 && await isProcessRunning(pid)) {
|
|
161
|
+
return false; // Real process holding the port
|
|
162
|
+
}
|
|
163
|
+
// else: zombie socket — consider port free
|
|
164
|
+
}
|
|
170
165
|
}
|
|
171
166
|
return true;
|
|
172
167
|
} else {
|
|
173
168
|
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
174
169
|
// If output has more than the header line, port is in use
|
|
175
|
-
const lines = result.trim().split('\n').filter(l => l.trim());
|
|
170
|
+
const lines = result.trim().split('\n').filter((l: string) => l.trim());
|
|
176
171
|
return lines.length <= 1;
|
|
177
172
|
}
|
|
178
173
|
} catch {
|
|
@@ -202,6 +197,8 @@ export async function waitForPortFree(port: number, timeoutMs: number = 5000): P
|
|
|
202
197
|
/**
|
|
203
198
|
* Kill processes using a specific port.
|
|
204
199
|
* Force-kills all processes bound to the port and verifies they're gone.
|
|
200
|
+
* On Windows, filters out zombie PIDs (sockets orphaned by dead processes)
|
|
201
|
+
* since taskkill can't kill those — they require a reboot or TCP stack reset.
|
|
205
202
|
*/
|
|
206
203
|
export async function killProcessOnPort(port: number): Promise<void> {
|
|
207
204
|
try {
|
|
@@ -221,9 +218,14 @@ export async function killProcessOnPort(port: number): Promise<void> {
|
|
|
221
218
|
}
|
|
222
219
|
|
|
223
220
|
for (const pid of pids) {
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
// Check if the process actually exists before trying to kill it
|
|
222
|
+
// This avoids the zombie PID issue where sockets linger after process death
|
|
223
|
+
const alive = await isProcessRunning(pid);
|
|
224
|
+
if (alive) {
|
|
225
|
+
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
226
|
+
console.log(`Killed process ${pid} using port ${port}`);
|
|
227
|
+
}
|
|
228
|
+
// else: zombie socket — PID no longer exists but socket lingers in kernel
|
|
227
229
|
}
|
|
228
230
|
} else {
|
|
229
231
|
// On Unix, use lsof
|
package/src/server.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BGR Dashboard Server
|
|
2
|
+
* BGR Dashboard Server + Built-in Process Guard
|
|
3
3
|
*
|
|
4
4
|
* Uses Melina.js to serve the dashboard app with file-based routing.
|
|
5
5
|
* All API endpoints and page rendering are handled by the dashboard/app/ directory.
|
|
6
6
|
*
|
|
7
|
+
* v3.0: Built-in guard loop — the dashboard now monitors ALL registered
|
|
8
|
+
* processes and auto-restarts any that die. This eliminates the need for
|
|
9
|
+
* external guard scripts. The dashboard itself survives because bgrun
|
|
10
|
+
* registers it as a managed process on launch.
|
|
11
|
+
*
|
|
7
12
|
* Port selection is handled entirely by Melina:
|
|
8
13
|
* - If BUN_PORT env var is set → uses that (explicit, will fail if busy)
|
|
9
14
|
* - Otherwise → defaults to 3000, falls back to next available if busy
|
|
10
15
|
*/
|
|
11
16
|
import path from 'path';
|
|
17
|
+
import { getAllProcesses, getProcess } from './db';
|
|
18
|
+
import { isProcessRunning } from './platform';
|
|
19
|
+
import { handleRun } from './commands/run';
|
|
20
|
+
|
|
21
|
+
const GUARD_INTERVAL_MS = 30_000; // Check every 30 seconds
|
|
22
|
+
const GUARD_SKIP_NAMES = new Set(['bgr-dashboard']); // Don't try to restart ourselves
|
|
12
23
|
|
|
13
24
|
export async function startServer() {
|
|
14
25
|
// Dynamic import to avoid melina's side-effect console.log at bundle load time
|
|
@@ -24,4 +35,59 @@ export async function startServer() {
|
|
|
24
35
|
globalCss: path.join(appDir, 'globals.css'),
|
|
25
36
|
...(explicitPort !== undefined && { port: explicitPort }),
|
|
26
37
|
});
|
|
38
|
+
|
|
39
|
+
// Start the built-in process guard
|
|
40
|
+
startGuard();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Built-in Process Guard
|
|
45
|
+
*
|
|
46
|
+
* Runs as a background loop inside the dashboard process.
|
|
47
|
+
* Every GUARD_INTERVAL_MS, checks processes with BGR_KEEP_ALIVE=true
|
|
48
|
+
* in their env and auto-restarts any that died.
|
|
49
|
+
*
|
|
50
|
+
* Only guarded processes (opted-in via dashboard toggle or env var) are
|
|
51
|
+
* monitored. Other processes are left alone even if they crash.
|
|
52
|
+
*
|
|
53
|
+
* Toggle guard per-process:
|
|
54
|
+
* - Dashboard UI: click the shield icon on any process row
|
|
55
|
+
* - CLI: set BGR_KEEP_ALIVE=true in the process env/config
|
|
56
|
+
*/
|
|
57
|
+
function startGuard() {
|
|
58
|
+
console.log(`[guard] ✓ Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
|
|
59
|
+
|
|
60
|
+
setInterval(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const processes = getAllProcesses();
|
|
63
|
+
if (processes.length === 0) return;
|
|
64
|
+
|
|
65
|
+
for (const proc of processes) {
|
|
66
|
+
// Skip the dashboard itself
|
|
67
|
+
if (GUARD_SKIP_NAMES.has(proc.name)) continue;
|
|
68
|
+
|
|
69
|
+
// Only guard processes with BGR_KEEP_ALIVE=true
|
|
70
|
+
const env = proc.env ? (typeof proc.env === 'string' ? (() => { try { return JSON.parse(proc.env); } catch { return {}; } })() : proc.env) : {};
|
|
71
|
+
if (env.BGR_KEEP_ALIVE !== 'true') continue;
|
|
72
|
+
|
|
73
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
74
|
+
if (!alive) {
|
|
75
|
+
console.log(`[guard] ⚠ Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
76
|
+
try {
|
|
77
|
+
await handleRun({
|
|
78
|
+
action: 'run',
|
|
79
|
+
name: proc.name,
|
|
80
|
+
force: true,
|
|
81
|
+
remoteName: '',
|
|
82
|
+
});
|
|
83
|
+
console.log(`[guard] ✓ Restarted "${proc.name}"`);
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
console.error(`[guard] ✗ Failed to restart "${proc.name}": ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
console.error(`[guard] Error in guard loop: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}, GUARD_INTERVAL_MS);
|
|
27
93
|
}
|