bgrun 3.10.2 → 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 +1 -1
- 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 +452 -36
- 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/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 +43 -3
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) {
|
|
@@ -528,7 +538,7 @@ var init_db = __esm(() => {
|
|
|
528
538
|
import boxen from "boxen";
|
|
529
539
|
import chalk2 from "chalk";
|
|
530
540
|
function announce(message, title) {
|
|
531
|
-
console.log(boxen(
|
|
541
|
+
console.log(boxen(message, {
|
|
532
542
|
padding: 1,
|
|
533
543
|
margin: 1,
|
|
534
544
|
borderColor: "green",
|
|
@@ -538,7 +548,8 @@ function announce(message, title) {
|
|
|
538
548
|
}));
|
|
539
549
|
}
|
|
540
550
|
function error(message) {
|
|
541
|
-
|
|
551
|
+
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
552
|
+
console.error(boxen(chalk2.red(text), {
|
|
542
553
|
padding: 1,
|
|
543
554
|
margin: 1,
|
|
544
555
|
borderColor: "red",
|
|
@@ -581,6 +592,95 @@ async function parseConfigFile(configPath) {
|
|
|
581
592
|
return flattenConfig(parsedConfig);
|
|
582
593
|
}
|
|
583
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
|
+
|
|
584
684
|
// src/commands/run.ts
|
|
585
685
|
var {$: $2 } = globalThis.Bun;
|
|
586
686
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
@@ -589,6 +689,21 @@ import { createMeasure as createMeasure2 } from "measure-fn";
|
|
|
589
689
|
async function handleRun(options) {
|
|
590
690
|
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
591
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
|
+
}
|
|
592
707
|
if (existingProcess) {
|
|
593
708
|
const finalDirectory2 = directory || existingProcess.workdir;
|
|
594
709
|
validateDirectory(finalDirectory2);
|
|
@@ -732,10 +847,82 @@ var init_run = __esm(() => {
|
|
|
732
847
|
run = createMeasure2("run");
|
|
733
848
|
});
|
|
734
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
|
+
|
|
735
921
|
// src/server.ts
|
|
736
922
|
var exports_server = {};
|
|
737
923
|
__export(exports_server, {
|
|
738
|
-
startServer: () => startServer
|
|
924
|
+
startServer: () => startServer,
|
|
925
|
+
guardRestartCounts: () => guardRestartCounts
|
|
739
926
|
});
|
|
740
927
|
import path2 from "path";
|
|
741
928
|
async function startServer() {
|
|
@@ -749,6 +936,8 @@ async function startServer() {
|
|
|
749
936
|
...explicitPort !== undefined && { port: explicitPort }
|
|
750
937
|
});
|
|
751
938
|
startGuard();
|
|
939
|
+
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
940
|
+
startLogRotation2(() => getAllProcesses());
|
|
752
941
|
}
|
|
753
942
|
function startGuard() {
|
|
754
943
|
console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
|
|
@@ -760,17 +949,15 @@ function startGuard() {
|
|
|
760
949
|
for (const proc of processes) {
|
|
761
950
|
if (GUARD_SKIP_NAMES.has(proc.name))
|
|
762
951
|
continue;
|
|
763
|
-
const env = proc.env ?
|
|
764
|
-
try {
|
|
765
|
-
return JSON.parse(proc.env);
|
|
766
|
-
} catch {
|
|
767
|
-
return {};
|
|
768
|
-
}
|
|
769
|
-
})() : proc.env : {};
|
|
952
|
+
const env = proc.env ? parseEnvString(proc.env) : {};
|
|
770
953
|
if (env.BGR_KEEP_ALIVE !== "true")
|
|
771
954
|
continue;
|
|
772
955
|
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
773
956
|
if (!alive) {
|
|
957
|
+
const now = Date.now();
|
|
958
|
+
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
959
|
+
if (now < nextRestart)
|
|
960
|
+
continue;
|
|
774
961
|
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
775
962
|
try {
|
|
776
963
|
await handleRun({
|
|
@@ -779,10 +966,28 @@ function startGuard() {
|
|
|
779
966
|
force: true,
|
|
780
967
|
remoteName: ""
|
|
781
968
|
});
|
|
782
|
-
|
|
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
|
+
}
|
|
783
979
|
} catch (err) {
|
|
784
980
|
console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
|
|
785
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
|
+
}
|
|
786
991
|
}
|
|
787
992
|
}
|
|
788
993
|
} catch (err) {
|
|
@@ -790,12 +995,139 @@ function startGuard() {
|
|
|
790
995
|
}
|
|
791
996
|
}, GUARD_INTERVAL_MS);
|
|
792
997
|
}
|
|
793
|
-
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES;
|
|
998
|
+
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
|
|
794
999
|
var init_server = __esm(() => {
|
|
795
1000
|
init_db();
|
|
796
1001
|
init_platform();
|
|
797
1002
|
init_run();
|
|
798
|
-
|
|
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
|
+
};
|
|
799
1131
|
});
|
|
800
1132
|
|
|
801
1133
|
// src/index.ts
|
|
@@ -1009,11 +1341,11 @@ async function showAll(opts) {
|
|
|
1009
1341
|
}
|
|
1010
1342
|
const tableData = [];
|
|
1011
1343
|
const allPids = filtered.map((p) => p.pid);
|
|
1012
|
-
const
|
|
1344
|
+
const resourceMap = await getProcessBatchResources(allPids);
|
|
1013
1345
|
for (const proc of filtered) {
|
|
1014
1346
|
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
1015
1347
|
const runtime = calculateRuntime(proc.timestamp);
|
|
1016
|
-
const mem = isRunning ?
|
|
1348
|
+
const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
|
|
1017
1349
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
1018
1350
|
tableData.push({
|
|
1019
1351
|
id: proc.id,
|
|
@@ -1470,6 +1802,7 @@ async function showHelp() {
|
|
|
1470
1802
|
bgrun List all processes
|
|
1471
1803
|
bgrun [name] Show details for a process
|
|
1472
1804
|
bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
1805
|
+
bgrun --guard Launch standalone process guard
|
|
1473
1806
|
bgrun --restart [name] Restart a process
|
|
1474
1807
|
bgrun --restart-all Restart ALL registered processes
|
|
1475
1808
|
bgrun --stop [name] Stop a process (keep in registry)
|
|
@@ -1535,8 +1868,10 @@ async function run2() {
|
|
|
1535
1868
|
stdout: { type: "string" },
|
|
1536
1869
|
stderr: { type: "string" },
|
|
1537
1870
|
dashboard: { type: "boolean" },
|
|
1871
|
+
guard: { type: "boolean" },
|
|
1538
1872
|
debug: { type: "boolean" },
|
|
1539
1873
|
_serve: { type: "boolean" },
|
|
1874
|
+
"_guard-loop": { type: "boolean" },
|
|
1540
1875
|
port: { type: "string" }
|
|
1541
1876
|
},
|
|
1542
1877
|
strict: false,
|
|
@@ -1547,6 +1882,13 @@ async function run2() {
|
|
|
1547
1882
|
await startServer2();
|
|
1548
1883
|
return;
|
|
1549
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
|
+
}
|
|
1550
1892
|
if (values.dashboard) {
|
|
1551
1893
|
const dashboardName = "bgr-dashboard";
|
|
1552
1894
|
const homePath3 = getHomeDir();
|
|
@@ -1593,6 +1935,18 @@ async function run2() {
|
|
|
1593
1935
|
if (requestedPort) {
|
|
1594
1936
|
spawnEnv.BUN_PORT = requestedPort;
|
|
1595
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
|
+
}
|
|
1596
1950
|
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
1597
1951
|
env: spawnEnv,
|
|
1598
1952
|
cwd: bgrDir2,
|
|
@@ -1641,6 +1995,69 @@ async function run2() {
|
|
|
1641
1995
|
announce(msg, "BGR Dashboard");
|
|
1642
1996
|
return;
|
|
1643
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
|
+
}
|
|
1644
2061
|
if (values.version) {
|
|
1645
2062
|
console.log(`bgrun version: ${await getVersion()}`);
|
|
1646
2063
|
return;
|
|
@@ -1813,6 +2230,5 @@ async function run2() {
|
|
|
1813
2230
|
}
|
|
1814
2231
|
}
|
|
1815
2232
|
run2().catch((err) => {
|
|
1816
|
-
|
|
1817
|
-
process.exit(1);
|
|
2233
|
+
error(err);
|
|
1818
2234
|
});
|