bgrun 3.10.1 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -2
- package/dashboard/app/api/deps/route.ts +49 -0
- package/dashboard/app/api/guard/route.ts +50 -0
- package/dashboard/app/api/guard-all/route.ts +50 -0
- package/dashboard/app/api/logs/rotate/route.ts +45 -0
- package/dashboard/app/api/processes/route.ts +67 -10
- package/dashboard/app/globals.css +386 -6
- package/dashboard/app/page.client.tsx +257 -8
- package/dashboard/app/page.tsx +20 -1
- package/dist/index.js +462 -30
- package/package.json +61 -60
- package/src/api.ts +3 -3
- package/src/commands/list.ts +3 -3
- package/src/commands/run.ts +17 -0
- package/src/db.ts +8 -0
- package/src/deps.ts +126 -0
- package/src/guard.ts +157 -0
- package/src/index.ts +108 -3
- package/src/log-rotation.ts +93 -0
- package/src/logger.ts +4 -3
- package/src/platform.ts +39 -23
- package/src/server.ts +55 -11
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ function getHomeDir() {
|
|
|
27
27
|
async function isProcessRunning(pid, command) {
|
|
28
28
|
if (pid <= 0)
|
|
29
29
|
return false;
|
|
30
|
-
return plat.measure(`PID ${pid} alive?`, async () => {
|
|
30
|
+
return await plat.measure(`PID ${pid} alive?`, async () => {
|
|
31
31
|
try {
|
|
32
32
|
if (command && (command.includes("docker run") || command.includes("docker-compose up") || command.includes("docker compose up"))) {
|
|
33
33
|
return await isDockerContainerRunning(command);
|
|
@@ -42,7 +42,7 @@ async function isProcessRunning(pid, command) {
|
|
|
42
42
|
} catch {
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
|
-
});
|
|
45
|
+
}) ?? false;
|
|
46
46
|
}
|
|
47
47
|
async function isDockerContainerRunning(command) {
|
|
48
48
|
try {
|
|
@@ -155,6 +155,8 @@ async function killProcessOnPort(port) {
|
|
|
155
155
|
if (alive) {
|
|
156
156
|
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
157
157
|
console.log(`Killed process ${pid} using port ${port}`);
|
|
158
|
+
} else {
|
|
159
|
+
console.warn(`\u26A0 Port ${port} held by zombie PID ${pid} (process dead, socket stuck in kernel). Will clear on reboot or TCP timeout.`);
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
} else {
|
|
@@ -210,7 +212,7 @@ async function findChildPid(parentPid) {
|
|
|
210
212
|
return currentPid;
|
|
211
213
|
}
|
|
212
214
|
async function readFileTail(filePath, lines) {
|
|
213
|
-
return plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
|
|
215
|
+
return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
|
|
214
216
|
try {
|
|
215
217
|
const content = await Bun.file(filePath).text();
|
|
216
218
|
if (!lines) {
|
|
@@ -223,17 +225,17 @@ async function readFileTail(filePath, lines) {
|
|
|
223
225
|
} catch (error) {
|
|
224
226
|
throw new Error(`Error reading file: ${error}`);
|
|
225
227
|
}
|
|
226
|
-
});
|
|
228
|
+
}) ?? "";
|
|
227
229
|
}
|
|
228
|
-
async function
|
|
230
|
+
async function getProcessBatchResources(pids) {
|
|
229
231
|
if (pids.length === 0)
|
|
230
232
|
return new Map;
|
|
231
|
-
return await plat.measure(`Batch
|
|
232
|
-
const
|
|
233
|
+
return await plat.measure(`Batch resources (${pids.length} PIDs)`, async () => {
|
|
234
|
+
const resourceMap = new Map;
|
|
233
235
|
const pidSet = new Set(pids);
|
|
234
236
|
try {
|
|
235
237
|
if (isWindows()) {
|
|
236
|
-
const result = await $`powershell -Command "Get-Process | Select-Object Id, WorkingSet"`.nothrow().quiet().text();
|
|
238
|
+
const result = await $`powershell -Command "Get-Process | Select-Object Id, CPU, WorkingSet"`.nothrow().quiet().text();
|
|
237
239
|
const lines = result.trim().split(`
|
|
238
240
|
`);
|
|
239
241
|
for (const line of lines) {
|
|
@@ -241,33 +243,41 @@ async function getProcessBatchMemory(pids) {
|
|
|
241
243
|
if (!trimmed || trimmed.startsWith("Id") || trimmed.startsWith("--"))
|
|
242
244
|
continue;
|
|
243
245
|
const parts = trimmed.split(/\s+/);
|
|
244
|
-
if (parts.length >=
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
if (parts.length >= 3) {
|
|
247
|
+
const pid = parseInt(parts[0]);
|
|
248
|
+
let cpuStr = parts[1];
|
|
249
|
+
let memStr = parts[2];
|
|
250
|
+
if (parts.length === 2) {
|
|
251
|
+
cpuStr = "0";
|
|
252
|
+
memStr = parts[1];
|
|
253
|
+
}
|
|
254
|
+
const cpu = parseFloat(cpuStr) || 0;
|
|
255
|
+
const memory = parseInt(memStr) || 0;
|
|
256
|
+
if (!isNaN(pid) && !isNaN(memory)) {
|
|
257
|
+
if (pidSet.has(pid))
|
|
258
|
+
resourceMap.set(pid, { memory, cpu });
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
}
|
|
253
262
|
} else {
|
|
254
|
-
const result = await $`ps -eo pid,rss`.nothrow().quiet().text();
|
|
263
|
+
const result = await $`ps -eo pid,pcpu,rss`.nothrow().quiet().text();
|
|
255
264
|
const lines = result.trim().split(`
|
|
256
265
|
`);
|
|
257
266
|
for (let i = 1;i < lines.length; i++) {
|
|
258
267
|
const line = lines[i].trim();
|
|
259
268
|
if (!line)
|
|
260
269
|
continue;
|
|
261
|
-
const [pidStr, rssStr] = line.split(/\s+/);
|
|
270
|
+
const [pidStr, cpuStr, rssStr] = line.split(/\s+/);
|
|
262
271
|
const pid = parseInt(pidStr);
|
|
263
|
-
const
|
|
272
|
+
const cpu = parseFloat(cpuStr) || 0;
|
|
273
|
+
const rss = parseInt(rssStr) || 0;
|
|
264
274
|
if (pidSet.has(pid)) {
|
|
265
|
-
|
|
275
|
+
resourceMap.set(pid, { memory: rss * 1024, cpu });
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
279
|
} catch (e) {}
|
|
270
|
-
return
|
|
280
|
+
return resourceMap;
|
|
271
281
|
}) ?? new Map;
|
|
272
282
|
}
|
|
273
283
|
async function getProcessPorts(pid) {
|
|
@@ -406,6 +416,7 @@ var init_utils = __esm(() => {
|
|
|
406
416
|
var exports_db = {};
|
|
407
417
|
__export(exports_db, {
|
|
408
418
|
updateProcessPid: () => updateProcessPid,
|
|
419
|
+
updateProcessEnv: () => updateProcessEnv,
|
|
409
420
|
retryDatabaseOperation: () => retryDatabaseOperation,
|
|
410
421
|
removeProcessByName: () => removeProcessByName,
|
|
411
422
|
removeProcess: () => removeProcess,
|
|
@@ -459,6 +470,12 @@ function removeAllProcesses() {
|
|
|
459
470
|
db.process.delete(p.id);
|
|
460
471
|
}
|
|
461
472
|
}
|
|
473
|
+
function updateProcessEnv(name, envJson) {
|
|
474
|
+
const proc = db.process.select().where({ name }).limit(1).get();
|
|
475
|
+
if (proc) {
|
|
476
|
+
db.process.update(proc.id, { env: envJson });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
462
479
|
function getDbInfo() {
|
|
463
480
|
return {
|
|
464
481
|
dbPath,
|
|
@@ -521,7 +538,7 @@ var init_db = __esm(() => {
|
|
|
521
538
|
import boxen from "boxen";
|
|
522
539
|
import chalk2 from "chalk";
|
|
523
540
|
function announce(message, title) {
|
|
524
|
-
console.log(boxen(
|
|
541
|
+
console.log(boxen(message, {
|
|
525
542
|
padding: 1,
|
|
526
543
|
margin: 1,
|
|
527
544
|
borderColor: "green",
|
|
@@ -531,7 +548,8 @@ function announce(message, title) {
|
|
|
531
548
|
}));
|
|
532
549
|
}
|
|
533
550
|
function error(message) {
|
|
534
|
-
|
|
551
|
+
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
552
|
+
console.error(boxen(chalk2.red(text), {
|
|
535
553
|
padding: 1,
|
|
536
554
|
margin: 1,
|
|
537
555
|
borderColor: "red",
|
|
@@ -574,6 +592,95 @@ async function parseConfigFile(configPath) {
|
|
|
574
592
|
return flattenConfig(parsedConfig);
|
|
575
593
|
}
|
|
576
594
|
|
|
595
|
+
// src/deps.ts
|
|
596
|
+
var exports_deps = {};
|
|
597
|
+
__export(exports_deps, {
|
|
598
|
+
getUnmetDeps: () => getUnmetDeps,
|
|
599
|
+
getDependencies: () => getDependencies,
|
|
600
|
+
buildDepGraph: () => buildDepGraph
|
|
601
|
+
});
|
|
602
|
+
function getDependencies(envStr) {
|
|
603
|
+
const env = parseEnvString(envStr);
|
|
604
|
+
const raw = env.BGR_DEPENDS_ON || "";
|
|
605
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
606
|
+
}
|
|
607
|
+
async function buildDepGraph() {
|
|
608
|
+
const processes = getAllProcesses();
|
|
609
|
+
const nodeMap = new Map;
|
|
610
|
+
for (const proc of processes) {
|
|
611
|
+
const deps = getDependencies(proc.env);
|
|
612
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
613
|
+
nodeMap.set(proc.name, {
|
|
614
|
+
name: proc.name,
|
|
615
|
+
dependsOn: deps,
|
|
616
|
+
dependedBy: [],
|
|
617
|
+
running: alive,
|
|
618
|
+
pid: proc.pid
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
for (const node of nodeMap.values()) {
|
|
622
|
+
for (const dep of node.dependsOn) {
|
|
623
|
+
const depNode = nodeMap.get(dep);
|
|
624
|
+
if (depNode) {
|
|
625
|
+
depNode.dependedBy.push(node.name);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const inDegree = new Map;
|
|
630
|
+
for (const node of nodeMap.values()) {
|
|
631
|
+
inDegree.set(node.name, node.dependsOn.filter((d) => nodeMap.has(d)).length);
|
|
632
|
+
}
|
|
633
|
+
const queue = [];
|
|
634
|
+
for (const [name, degree] of inDegree) {
|
|
635
|
+
if (degree === 0)
|
|
636
|
+
queue.push(name);
|
|
637
|
+
}
|
|
638
|
+
const order = [];
|
|
639
|
+
while (queue.length > 0) {
|
|
640
|
+
const current = queue.shift();
|
|
641
|
+
order.push(current);
|
|
642
|
+
const node = nodeMap.get(current);
|
|
643
|
+
for (const dependent of node.dependedBy) {
|
|
644
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
645
|
+
inDegree.set(dependent, newDegree);
|
|
646
|
+
if (newDegree === 0)
|
|
647
|
+
queue.push(dependent);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const hasCycle = order.length < nodeMap.size;
|
|
651
|
+
const cycleNodes = hasCycle ? [...nodeMap.keys()].filter((n) => !order.includes(n)) : undefined;
|
|
652
|
+
return {
|
|
653
|
+
nodes: [...nodeMap.values()],
|
|
654
|
+
order,
|
|
655
|
+
hasCycle,
|
|
656
|
+
cycleNodes
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
async function getUnmetDeps(name) {
|
|
660
|
+
const proc = getProcess(name);
|
|
661
|
+
if (!proc)
|
|
662
|
+
return [];
|
|
663
|
+
const deps = getDependencies(proc.env);
|
|
664
|
+
const unmet = [];
|
|
665
|
+
for (const depName of deps) {
|
|
666
|
+
const depProc = getProcess(depName);
|
|
667
|
+
if (!depProc) {
|
|
668
|
+
unmet.push(depName);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const alive = await isProcessRunning(depProc.pid, depProc.command);
|
|
672
|
+
if (!alive) {
|
|
673
|
+
unmet.push(depName);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return unmet;
|
|
677
|
+
}
|
|
678
|
+
var init_deps = __esm(() => {
|
|
679
|
+
init_db();
|
|
680
|
+
init_platform();
|
|
681
|
+
init_utils();
|
|
682
|
+
});
|
|
683
|
+
|
|
577
684
|
// src/commands/run.ts
|
|
578
685
|
var {$: $2 } = globalThis.Bun;
|
|
579
686
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
@@ -582,6 +689,21 @@ import { createMeasure as createMeasure2 } from "measure-fn";
|
|
|
582
689
|
async function handleRun(options) {
|
|
583
690
|
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
584
691
|
const existingProcess = name ? getProcess(name) : null;
|
|
692
|
+
if (name && existingProcess) {
|
|
693
|
+
const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
|
|
694
|
+
const unmet = await getUnmetDeps2(name);
|
|
695
|
+
if (unmet.length > 0) {
|
|
696
|
+
await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
|
|
697
|
+
for (const depName of unmet) {
|
|
698
|
+
const depProc = getProcess(depName);
|
|
699
|
+
if (depProc) {
|
|
700
|
+
announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
|
|
701
|
+
await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
585
707
|
if (existingProcess) {
|
|
586
708
|
const finalDirectory2 = directory || existingProcess.workdir;
|
|
587
709
|
validateDirectory(finalDirectory2);
|
|
@@ -725,10 +847,82 @@ var init_run = __esm(() => {
|
|
|
725
847
|
run = createMeasure2("run");
|
|
726
848
|
});
|
|
727
849
|
|
|
850
|
+
// src/log-rotation.ts
|
|
851
|
+
var exports_log_rotation = {};
|
|
852
|
+
__export(exports_log_rotation, {
|
|
853
|
+
startLogRotation: () => startLogRotation,
|
|
854
|
+
rotateLogFile: () => rotateLogFile,
|
|
855
|
+
rotateAllLogs: () => rotateAllLogs
|
|
856
|
+
});
|
|
857
|
+
import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
858
|
+
function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
859
|
+
try {
|
|
860
|
+
if (!existsSync7(filePath))
|
|
861
|
+
return false;
|
|
862
|
+
const stat = statSync3(filePath);
|
|
863
|
+
if (stat.size <= maxBytes)
|
|
864
|
+
return false;
|
|
865
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
866
|
+
const lines = content.split(`
|
|
867
|
+
`);
|
|
868
|
+
if (lines.length <= keepLines)
|
|
869
|
+
return false;
|
|
870
|
+
const truncated = lines.slice(-keepLines);
|
|
871
|
+
const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
|
|
872
|
+
`;
|
|
873
|
+
writeFileSync(filePath, header + truncated.join(`
|
|
874
|
+
`));
|
|
875
|
+
return true;
|
|
876
|
+
} catch {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function rotateAllLogs(getProcesses, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
881
|
+
const processes = getProcesses();
|
|
882
|
+
const rotated = [];
|
|
883
|
+
let checked = 0;
|
|
884
|
+
for (const proc of processes) {
|
|
885
|
+
if (proc.stdout_path) {
|
|
886
|
+
checked++;
|
|
887
|
+
if (rotateLogFile(proc.stdout_path, maxBytes, keepLines)) {
|
|
888
|
+
rotated.push(`${proc.name}/stdout`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (proc.stderr_path) {
|
|
892
|
+
checked++;
|
|
893
|
+
if (rotateLogFile(proc.stderr_path, maxBytes, keepLines)) {
|
|
894
|
+
rotated.push(`${proc.name}/stderr`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return { rotated, checked };
|
|
899
|
+
}
|
|
900
|
+
function startLogRotation(getProcesses, intervalMs = DEFAULT_CHECK_INTERVAL_MS, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
901
|
+
console.log(`[logs] Log rotation active: max ${formatBytes(maxBytes)}/file, keep ${keepLines} lines, check every ${intervalMs / 1000}s`);
|
|
902
|
+
return setInterval(() => {
|
|
903
|
+
const { rotated } = rotateAllLogs(getProcesses, maxBytes, keepLines);
|
|
904
|
+
if (rotated.length > 0) {
|
|
905
|
+
console.log(`[logs] Rotated ${rotated.length} log(s): ${rotated.join(", ")}`);
|
|
906
|
+
}
|
|
907
|
+
}, intervalMs);
|
|
908
|
+
}
|
|
909
|
+
function formatBytes(bytes) {
|
|
910
|
+
if (bytes >= 1e6)
|
|
911
|
+
return `${(bytes / 1e6).toFixed(1)}MB`;
|
|
912
|
+
if (bytes >= 1000)
|
|
913
|
+
return `${(bytes / 1000).toFixed(0)}KB`;
|
|
914
|
+
return `${bytes}B`;
|
|
915
|
+
}
|
|
916
|
+
var DEFAULT_MAX_BYTES, DEFAULT_KEEP_LINES = 5000, DEFAULT_CHECK_INTERVAL_MS = 60000;
|
|
917
|
+
var init_log_rotation = __esm(() => {
|
|
918
|
+
DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
919
|
+
});
|
|
920
|
+
|
|
728
921
|
// src/server.ts
|
|
729
922
|
var exports_server = {};
|
|
730
923
|
__export(exports_server, {
|
|
731
|
-
startServer: () => startServer
|
|
924
|
+
startServer: () => startServer,
|
|
925
|
+
guardRestartCounts: () => guardRestartCounts
|
|
732
926
|
});
|
|
733
927
|
import path2 from "path";
|
|
734
928
|
async function startServer() {
|
|
@@ -742,6 +936,8 @@ async function startServer() {
|
|
|
742
936
|
...explicitPort !== undefined && { port: explicitPort }
|
|
743
937
|
});
|
|
744
938
|
startGuard();
|
|
939
|
+
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
940
|
+
startLogRotation2(() => getAllProcesses());
|
|
745
941
|
}
|
|
746
942
|
function startGuard() {
|
|
747
943
|
console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
|
|
@@ -753,9 +949,16 @@ function startGuard() {
|
|
|
753
949
|
for (const proc of processes) {
|
|
754
950
|
if (GUARD_SKIP_NAMES.has(proc.name))
|
|
755
951
|
continue;
|
|
952
|
+
const env = proc.env ? parseEnvString(proc.env) : {};
|
|
953
|
+
if (env.BGR_KEEP_ALIVE !== "true")
|
|
954
|
+
continue;
|
|
756
955
|
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
757
956
|
if (!alive) {
|
|
758
|
-
|
|
957
|
+
const now = Date.now();
|
|
958
|
+
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
959
|
+
if (now < nextRestart)
|
|
960
|
+
continue;
|
|
961
|
+
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
759
962
|
try {
|
|
760
963
|
await handleRun({
|
|
761
964
|
action: "run",
|
|
@@ -763,10 +966,28 @@ function startGuard() {
|
|
|
763
966
|
force: true,
|
|
764
967
|
remoteName: ""
|
|
765
968
|
});
|
|
766
|
-
|
|
969
|
+
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
970
|
+
const newCount = prevCount + 1;
|
|
971
|
+
guardRestartCounts.set(proc.name, newCount);
|
|
972
|
+
if (newCount > 5) {
|
|
973
|
+
const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
|
|
974
|
+
guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
|
|
975
|
+
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
|
|
976
|
+
} else {
|
|
977
|
+
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
|
|
978
|
+
}
|
|
767
979
|
} catch (err) {
|
|
768
980
|
console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
|
|
769
981
|
}
|
|
982
|
+
} else {
|
|
983
|
+
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
984
|
+
if (prevCount > 0) {
|
|
985
|
+
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
986
|
+
if (Date.now() > nextRestart + 60000) {
|
|
987
|
+
guardRestartCounts.delete(proc.name);
|
|
988
|
+
guardNextRestartTime.delete(proc.name);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
770
991
|
}
|
|
771
992
|
}
|
|
772
993
|
} catch (err) {
|
|
@@ -774,12 +995,139 @@ function startGuard() {
|
|
|
774
995
|
}
|
|
775
996
|
}, GUARD_INTERVAL_MS);
|
|
776
997
|
}
|
|
777
|
-
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES;
|
|
998
|
+
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
|
|
778
999
|
var init_server = __esm(() => {
|
|
779
1000
|
init_db();
|
|
780
1001
|
init_platform();
|
|
781
1002
|
init_run();
|
|
782
|
-
|
|
1003
|
+
init_utils();
|
|
1004
|
+
GUARD_SKIP_NAMES = new Set(["bgr-dashboard", "bgr-guard"]);
|
|
1005
|
+
_g = globalThis;
|
|
1006
|
+
if (!_g.__bgrGuardRestartCounts)
|
|
1007
|
+
_g.__bgrGuardRestartCounts = new Map;
|
|
1008
|
+
if (!_g.__bgrGuardNextRestartTime)
|
|
1009
|
+
_g.__bgrGuardNextRestartTime = new Map;
|
|
1010
|
+
guardRestartCounts = _g.__bgrGuardRestartCounts;
|
|
1011
|
+
guardNextRestartTime = _g.__bgrGuardNextRestartTime;
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// src/guard.ts
|
|
1015
|
+
var exports_guard = {};
|
|
1016
|
+
__export(exports_guard, {
|
|
1017
|
+
startGuardLoop: () => startGuardLoop
|
|
1018
|
+
});
|
|
1019
|
+
async function restartProcess(name) {
|
|
1020
|
+
try {
|
|
1021
|
+
await handleRun({
|
|
1022
|
+
action: "run",
|
|
1023
|
+
name,
|
|
1024
|
+
force: true,
|
|
1025
|
+
remoteName: ""
|
|
1026
|
+
});
|
|
1027
|
+
return true;
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
console.error(`[guard] \u2717 Failed to restart "${name}": ${err.message}`);
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function getBackoffMs(restartCount) {
|
|
1034
|
+
if (restartCount <= CRASH_THRESHOLD)
|
|
1035
|
+
return 0;
|
|
1036
|
+
const exponent = restartCount - CRASH_THRESHOLD;
|
|
1037
|
+
return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
|
|
1038
|
+
}
|
|
1039
|
+
async function guardCycle() {
|
|
1040
|
+
try {
|
|
1041
|
+
const processes = getAllProcesses();
|
|
1042
|
+
if (processes.length === 0)
|
|
1043
|
+
return;
|
|
1044
|
+
const now = Date.now();
|
|
1045
|
+
let checked = 0;
|
|
1046
|
+
let restarted = 0;
|
|
1047
|
+
let skipped = 0;
|
|
1048
|
+
for (const proc of processes) {
|
|
1049
|
+
if (proc.name === "bgr-guard")
|
|
1050
|
+
continue;
|
|
1051
|
+
const env = proc.env ? parseEnvString(proc.env) : {};
|
|
1052
|
+
const isGuarded = env.BGR_KEEP_ALIVE === "true";
|
|
1053
|
+
const isDashboard = proc.name === "bgr-dashboard";
|
|
1054
|
+
if (!isGuarded && !isDashboard)
|
|
1055
|
+
continue;
|
|
1056
|
+
checked++;
|
|
1057
|
+
try {
|
|
1058
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1059
|
+
if (!alive && proc.pid > 0) {
|
|
1060
|
+
const nextRestart = state.nextRestartTime.get(proc.name) || 0;
|
|
1061
|
+
if (now < nextRestart) {
|
|
1062
|
+
const waitSecs = Math.round((nextRestart - now) / 1000);
|
|
1063
|
+
skipped++;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
|
|
1067
|
+
const success = await restartProcess(proc.name);
|
|
1068
|
+
if (success) {
|
|
1069
|
+
const count = (state.restartCounts.get(proc.name) || 0) + 1;
|
|
1070
|
+
state.restartCounts.set(proc.name, count);
|
|
1071
|
+
state.lastSeenAlive.delete(proc.name);
|
|
1072
|
+
const backoff = getBackoffMs(count);
|
|
1073
|
+
if (backoff > 0) {
|
|
1074
|
+
state.nextRestartTime.set(proc.name, now + backoff);
|
|
1075
|
+
console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count}). Crash loop: next check in ${Math.round(backoff / 1000)}s`);
|
|
1076
|
+
} else {
|
|
1077
|
+
console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
|
|
1078
|
+
}
|
|
1079
|
+
restarted++;
|
|
1080
|
+
}
|
|
1081
|
+
} else if (alive) {
|
|
1082
|
+
const count = state.restartCounts.get(proc.name) || 0;
|
|
1083
|
+
if (count > 0) {
|
|
1084
|
+
const lastSeen = state.lastSeenAlive.get(proc.name);
|
|
1085
|
+
if (!lastSeen) {
|
|
1086
|
+
state.lastSeenAlive.set(proc.name, now);
|
|
1087
|
+
} else if (now - lastSeen > STABILITY_WINDOW_MS) {
|
|
1088
|
+
state.restartCounts.delete(proc.name);
|
|
1089
|
+
state.nextRestartTime.delete(proc.name);
|
|
1090
|
+
state.lastSeenAlive.delete(proc.name);
|
|
1091
|
+
console.log(`[guard] \u2713 "${proc.name}" stable for ${Math.round(STABILITY_WINDOW_MS / 1000)}s \u2014 reset counters`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
console.error(`[guard] Error checking "${proc.name}": ${err.message}`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (restarted > 0) {
|
|
1100
|
+
console.log(`[guard] Cycle: ${checked} checked, ${restarted} restarted, ${skipped} in backoff`);
|
|
1101
|
+
}
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.error(`[guard] Error in guard cycle: ${err.message}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
1107
|
+
const interval = intervalMs || DEFAULT_INTERVAL_MS;
|
|
1108
|
+
console.log(`[guard] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
1109
|
+
console.log(`[guard] \uD83D\uDEE1\uFE0F BGR Standalone Guard started`);
|
|
1110
|
+
console.log(`[guard] Check interval: ${interval / 1000}s`);
|
|
1111
|
+
console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
|
|
1112
|
+
console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
|
|
1113
|
+
console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
|
|
1114
|
+
console.log(`[guard] Started: ${new Date().toLocaleString()}`);
|
|
1115
|
+
console.log(`[guard] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
1116
|
+
await guardCycle();
|
|
1117
|
+
setInterval(guardCycle, interval);
|
|
1118
|
+
}
|
|
1119
|
+
var DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
|
|
1120
|
+
var init_guard = __esm(() => {
|
|
1121
|
+
init_db();
|
|
1122
|
+
init_platform();
|
|
1123
|
+
init_run();
|
|
1124
|
+
init_utils();
|
|
1125
|
+
MAX_BACKOFF_MS = 5 * 60000;
|
|
1126
|
+
state = {
|
|
1127
|
+
restartCounts: new Map,
|
|
1128
|
+
nextRestartTime: new Map,
|
|
1129
|
+
lastSeenAlive: new Map
|
|
1130
|
+
};
|
|
783
1131
|
});
|
|
784
1132
|
|
|
785
1133
|
// src/index.ts
|
|
@@ -993,11 +1341,11 @@ async function showAll(opts) {
|
|
|
993
1341
|
}
|
|
994
1342
|
const tableData = [];
|
|
995
1343
|
const allPids = filtered.map((p) => p.pid);
|
|
996
|
-
const
|
|
1344
|
+
const resourceMap = await getProcessBatchResources(allPids);
|
|
997
1345
|
for (const proc of filtered) {
|
|
998
1346
|
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
999
1347
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1000
|
-
const mem = isRunning ?
|
|
1348
|
+
const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
|
|
1001
1349
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
1002
1350
|
tableData.push({
|
|
1003
1351
|
id: proc.id,
|
|
@@ -1454,6 +1802,7 @@ async function showHelp() {
|
|
|
1454
1802
|
bgrun List all processes
|
|
1455
1803
|
bgrun [name] Show details for a process
|
|
1456
1804
|
bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
1805
|
+
bgrun --guard Launch standalone process guard
|
|
1457
1806
|
bgrun --restart [name] Restart a process
|
|
1458
1807
|
bgrun --restart-all Restart ALL registered processes
|
|
1459
1808
|
bgrun --stop [name] Stop a process (keep in registry)
|
|
@@ -1519,8 +1868,10 @@ async function run2() {
|
|
|
1519
1868
|
stdout: { type: "string" },
|
|
1520
1869
|
stderr: { type: "string" },
|
|
1521
1870
|
dashboard: { type: "boolean" },
|
|
1871
|
+
guard: { type: "boolean" },
|
|
1522
1872
|
debug: { type: "boolean" },
|
|
1523
1873
|
_serve: { type: "boolean" },
|
|
1874
|
+
"_guard-loop": { type: "boolean" },
|
|
1524
1875
|
port: { type: "string" }
|
|
1525
1876
|
},
|
|
1526
1877
|
strict: false,
|
|
@@ -1531,6 +1882,13 @@ async function run2() {
|
|
|
1531
1882
|
await startServer2();
|
|
1532
1883
|
return;
|
|
1533
1884
|
}
|
|
1885
|
+
if (values["_guard-loop"]) {
|
|
1886
|
+
const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
|
|
1887
|
+
const intervalStr = positionals[0];
|
|
1888
|
+
const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
|
|
1889
|
+
await startGuardLoop2(intervalMs);
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1534
1892
|
if (values.dashboard) {
|
|
1535
1893
|
const dashboardName = "bgr-dashboard";
|
|
1536
1894
|
const homePath3 = getHomeDir();
|
|
@@ -1577,6 +1935,18 @@ async function run2() {
|
|
|
1577
1935
|
if (requestedPort) {
|
|
1578
1936
|
spawnEnv.BUN_PORT = requestedPort;
|
|
1579
1937
|
}
|
|
1938
|
+
const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
|
|
1939
|
+
if (!isNaN(targetPort) && targetPort > 0) {
|
|
1940
|
+
const portFree = await isPortFree(targetPort);
|
|
1941
|
+
if (!portFree) {
|
|
1942
|
+
console.log(chalk8.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
|
|
1943
|
+
await killProcessOnPort(targetPort);
|
|
1944
|
+
const freed = await waitForPortFree(targetPort, 5000);
|
|
1945
|
+
if (!freed) {
|
|
1946
|
+
console.log(chalk8.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1580
1950
|
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
1581
1951
|
env: spawnEnv,
|
|
1582
1952
|
cwd: bgrDir2,
|
|
@@ -1625,6 +1995,69 @@ async function run2() {
|
|
|
1625
1995
|
announce(msg, "BGR Dashboard");
|
|
1626
1996
|
return;
|
|
1627
1997
|
}
|
|
1998
|
+
if (values.guard) {
|
|
1999
|
+
const guardName = "bgr-guard";
|
|
2000
|
+
const homePath3 = getHomeDir();
|
|
2001
|
+
const bgrDir2 = join3(homePath3, ".bgr");
|
|
2002
|
+
const existing = getProcess(guardName);
|
|
2003
|
+
if (existing && await isProcessRunning(existing.pid)) {
|
|
2004
|
+
announce(`Guard is already running (PID ${existing.pid})
|
|
2005
|
+
|
|
2006
|
+
Use ${chalk8.yellow(`bgrun --stop ${guardName}`)} to stop it
|
|
2007
|
+
Use ${chalk8.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (existing) {
|
|
2011
|
+
if (await isProcessRunning(existing.pid)) {
|
|
2012
|
+
await terminateProcess(existing.pid);
|
|
2013
|
+
}
|
|
2014
|
+
await retryDatabaseOperation(() => removeProcessByName(guardName));
|
|
2015
|
+
}
|
|
2016
|
+
const { resolve } = __require("path");
|
|
2017
|
+
const scriptPath = resolve(process.argv[1]);
|
|
2018
|
+
const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
|
|
2019
|
+
const command = `bgrun --_guard-loop`;
|
|
2020
|
+
const stdoutPath = join3(bgrDir2, `${guardName}-out.txt`);
|
|
2021
|
+
const stderrPath = join3(bgrDir2, `${guardName}-err.txt`);
|
|
2022
|
+
await Bun.write(stdoutPath, "");
|
|
2023
|
+
await Bun.write(stderrPath, "");
|
|
2024
|
+
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
2025
|
+
env: { ...Bun.env },
|
|
2026
|
+
cwd: bgrDir2,
|
|
2027
|
+
stdout: Bun.file(stdoutPath),
|
|
2028
|
+
stderr: Bun.file(stderrPath)
|
|
2029
|
+
});
|
|
2030
|
+
newProcess.unref();
|
|
2031
|
+
await sleep3(1000);
|
|
2032
|
+
const actualPid = await findChildPid(newProcess.pid);
|
|
2033
|
+
await retryDatabaseOperation(() => insertProcess({
|
|
2034
|
+
pid: actualPid,
|
|
2035
|
+
workdir: bgrDir2,
|
|
2036
|
+
command,
|
|
2037
|
+
name: guardName,
|
|
2038
|
+
env: "BGR_KEEP_ALIVE=false",
|
|
2039
|
+
configPath: "",
|
|
2040
|
+
stdout_path: stdoutPath,
|
|
2041
|
+
stderr_path: stderrPath
|
|
2042
|
+
}));
|
|
2043
|
+
const msg = dedent`
|
|
2044
|
+
${chalk8.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
|
|
2045
|
+
${chalk8.gray("\u2500".repeat(40))}
|
|
2046
|
+
|
|
2047
|
+
Monitors: All processes with BGR_KEEP_ALIVE=true
|
|
2048
|
+
Also watches: bgr-dashboard (auto-restart if it dies)
|
|
2049
|
+
Check interval: 30 seconds
|
|
2050
|
+
Backoff: Exponential after 5 rapid crashes
|
|
2051
|
+
|
|
2052
|
+
${chalk8.gray("\u2500".repeat(40))}
|
|
2053
|
+
Process: ${chalk8.white(guardName)} | PID: ${chalk8.white(String(actualPid))}
|
|
2054
|
+
|
|
2055
|
+
${chalk8.yellow(`bgrun ${guardName} --logs`)} View guard logs
|
|
2056
|
+
${chalk8.yellow(`bgrun --stop ${guardName}`)} Stop the guard
|
|
2057
|
+
`;
|
|
2058
|
+
announce(msg, "BGR Guard");
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
1628
2061
|
if (values.version) {
|
|
1629
2062
|
console.log(`bgrun version: ${await getVersion()}`);
|
|
1630
2063
|
return;
|
|
@@ -1797,6 +2230,5 @@ async function run2() {
|
|
|
1797
2230
|
}
|
|
1798
2231
|
}
|
|
1799
2232
|
run2().catch((err) => {
|
|
1800
|
-
|
|
1801
|
-
process.exit(1);
|
|
2233
|
+
error(err);
|
|
1802
2234
|
});
|