bgrun 3.12.16 → 3.12.21
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 +50 -14
- package/dashboard/app/api/guard/route.ts +5 -13
- package/dashboard/app/api/guard-all/route.ts +6 -14
- package/dashboard/app/api/guard-events/route.ts +3 -3
- package/dashboard/app/api/next-port/route.ts +27 -7
- package/dashboard/app/api/processes/route.ts +31 -5
- package/dashboard/app/page.client.tsx +2 -19
- package/dashboard/app/page.tsx +1 -5
- package/dashboard/lib/runtime.ts +58 -49
- package/dist/api.js +912 -234
- package/dist/deploy.js +691 -231
- package/dist/deps.js +151 -70
- package/dist/index.js +1272 -716
- package/dist/server.js +151 -639
- package/package.json +3 -2
package/dist/server.js
CHANGED
|
@@ -16,49 +16,131 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
var __require = import.meta.require;
|
|
18
18
|
|
|
19
|
-
// src/
|
|
20
|
-
var
|
|
21
|
-
__export(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
readFileTail: () => readFileTail,
|
|
26
|
-
psExec: () => psExec,
|
|
27
|
-
parseUnixListeningPorts: () => parseUnixListeningPorts,
|
|
28
|
-
killProcessOnPort: () => killProcessOnPort,
|
|
29
|
-
isWindows: () => isWindows,
|
|
30
|
-
isProcessRunning: () => isProcessRunning,
|
|
31
|
-
isPortFree: () => isPortFree,
|
|
32
|
-
getShellCommand: () => getShellCommand,
|
|
33
|
-
getProcessPorts: () => getProcessPorts,
|
|
34
|
-
getProcessMemory: () => getProcessMemory,
|
|
35
|
-
getProcessBatchResources: () => getProcessBatchResources,
|
|
36
|
-
getPortInfo: () => getPortInfo,
|
|
37
|
-
getHomeDir: () => getHomeDir,
|
|
38
|
-
findPidByPort: () => findPidByPort,
|
|
39
|
-
findChildPid: () => findChildPid,
|
|
40
|
-
ensureDir: () => ensureDir,
|
|
41
|
-
copyFile: () => copyFile
|
|
19
|
+
// src/log-rotation.ts
|
|
20
|
+
var exports_log_rotation = {};
|
|
21
|
+
__export(exports_log_rotation, {
|
|
22
|
+
startLogRotation: () => startLogRotation,
|
|
23
|
+
rotateLogFile: () => rotateLogFile,
|
|
24
|
+
rotateAllLogs: () => rotateAllLogs
|
|
42
25
|
});
|
|
26
|
+
import { existsSync, statSync, readFileSync, writeFileSync } from "fs";
|
|
27
|
+
function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
28
|
+
try {
|
|
29
|
+
if (!existsSync(filePath))
|
|
30
|
+
return false;
|
|
31
|
+
const stat = statSync(filePath);
|
|
32
|
+
if (stat.size <= maxBytes)
|
|
33
|
+
return false;
|
|
34
|
+
const content = readFileSync(filePath, "utf-8");
|
|
35
|
+
const lines = content.split(`
|
|
36
|
+
`);
|
|
37
|
+
if (lines.length <= keepLines)
|
|
38
|
+
return false;
|
|
39
|
+
const truncated = lines.slice(-keepLines);
|
|
40
|
+
const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
|
|
41
|
+
`;
|
|
42
|
+
writeFileSync(filePath, header + truncated.join(`
|
|
43
|
+
`));
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function rotateAllLogs(getProcesses, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
50
|
+
const processes = getProcesses();
|
|
51
|
+
const rotated = [];
|
|
52
|
+
let checked = 0;
|
|
53
|
+
for (const proc of processes) {
|
|
54
|
+
if (proc.stdout_path) {
|
|
55
|
+
checked++;
|
|
56
|
+
if (rotateLogFile(proc.stdout_path, maxBytes, keepLines)) {
|
|
57
|
+
rotated.push(`${proc.name}/stdout`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (proc.stderr_path) {
|
|
61
|
+
checked++;
|
|
62
|
+
if (rotateLogFile(proc.stderr_path, maxBytes, keepLines)) {
|
|
63
|
+
rotated.push(`${proc.name}/stderr`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { rotated, checked };
|
|
68
|
+
}
|
|
69
|
+
function startLogRotation(getProcesses, intervalMs = DEFAULT_CHECK_INTERVAL_MS, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
70
|
+
console.log(`[logs] Log rotation active: max ${formatBytes(maxBytes)}/file, keep ${keepLines} lines, check every ${intervalMs / 1000}s`);
|
|
71
|
+
return setInterval(() => {
|
|
72
|
+
const { rotated } = rotateAllLogs(getProcesses, maxBytes, keepLines);
|
|
73
|
+
if (rotated.length > 0) {
|
|
74
|
+
console.log(`[logs] Rotated ${rotated.length} log(s): ${rotated.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
}, intervalMs);
|
|
77
|
+
}
|
|
78
|
+
function formatBytes(bytes) {
|
|
79
|
+
if (bytes >= 1e6)
|
|
80
|
+
return `${(bytes / 1e6).toFixed(1)}MB`;
|
|
81
|
+
if (bytes >= 1000)
|
|
82
|
+
return `${(bytes / 1000).toFixed(0)}KB`;
|
|
83
|
+
return `${bytes}B`;
|
|
84
|
+
}
|
|
85
|
+
var DEFAULT_MAX_BYTES, DEFAULT_KEEP_LINES = 5000, DEFAULT_CHECK_INTERVAL_MS = 60000;
|
|
86
|
+
var init_log_rotation = __esm(() => {
|
|
87
|
+
DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// src/platform.ts
|
|
43
91
|
import * as fs from "fs";
|
|
44
92
|
import * as os from "os";
|
|
45
93
|
import { join } from "path";
|
|
46
94
|
var {$ } = globalThis.Bun;
|
|
47
95
|
import { createMeasure } from "measure-fn";
|
|
48
|
-
function psExec(command,
|
|
49
|
-
const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
|
|
96
|
+
async function psExec(command, timeoutMs = 3000) {
|
|
97
|
+
const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.ps1`);
|
|
50
98
|
try {
|
|
51
|
-
|
|
52
|
-
const
|
|
99
|
+
await Bun.write(tmpFile, command);
|
|
100
|
+
const proc = Bun.spawn([
|
|
101
|
+
"powershell",
|
|
102
|
+
"-NoProfile",
|
|
103
|
+
"-ExecutionPolicy",
|
|
104
|
+
"Bypass",
|
|
105
|
+
"-File",
|
|
106
|
+
tmpFile
|
|
107
|
+
], {
|
|
108
|
+
stdout: "pipe",
|
|
109
|
+
stderr: "pipe"
|
|
110
|
+
});
|
|
111
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
112
|
+
setTimeout(() => reject(new Error("PowerShell command timed out")), timeoutMs);
|
|
113
|
+
});
|
|
114
|
+
const resultPromise = new Promise(async (resolve, reject) => {
|
|
115
|
+
try {
|
|
116
|
+
const stdoutPromise = proc.stdout ? new Response(proc.stdout).text() : Promise.resolve("");
|
|
117
|
+
const stderrPromise = proc.stderr ? new Response(proc.stderr).text() : Promise.resolve("");
|
|
118
|
+
const exitCode = await proc.exited;
|
|
119
|
+
const stdout = await stdoutPromise;
|
|
120
|
+
const stderr = await stderrPromise;
|
|
121
|
+
if (exitCode === 0) {
|
|
122
|
+
resolve(stdout);
|
|
123
|
+
} else {
|
|
124
|
+
resolve(stderr || "");
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
reject(error);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
53
130
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
131
|
+
const result = await Promise.race([resultPromise, timeoutPromise]);
|
|
132
|
+
return result.trim();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return "";
|
|
135
|
+
} finally {
|
|
136
|
+
try {
|
|
137
|
+
await Bun.sleep(100);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
58
141
|
try {
|
|
59
|
-
fs.
|
|
142
|
+
fs.rmSync(tmpFile, { force: true });
|
|
60
143
|
} catch {}
|
|
61
|
-
return "";
|
|
62
144
|
}
|
|
63
145
|
}
|
|
64
146
|
function isWindows() {
|
|
@@ -80,7 +162,7 @@ async function isProcessRunning(pid, command) {
|
|
|
80
162
|
process.kill(pid, 0);
|
|
81
163
|
return true;
|
|
82
164
|
} catch {
|
|
83
|
-
const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`)
|
|
165
|
+
const output = await psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`);
|
|
84
166
|
return output === String(pid);
|
|
85
167
|
}
|
|
86
168
|
} else {
|
|
@@ -173,35 +255,6 @@ async function isPortFree(port) {
|
|
|
173
255
|
return true;
|
|
174
256
|
}
|
|
175
257
|
}
|
|
176
|
-
async function getPortInfo(port) {
|
|
177
|
-
try {
|
|
178
|
-
if (isWindows()) {
|
|
179
|
-
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
180
|
-
for (const line of result.split(`
|
|
181
|
-
`)) {
|
|
182
|
-
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
183
|
-
if (match) {
|
|
184
|
-
const pid = parseInt(match[2]);
|
|
185
|
-
if (pid > 0 && await isProcessRunning(pid)) {
|
|
186
|
-
const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
|
|
187
|
-
return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return { inUse: false };
|
|
192
|
-
} else {
|
|
193
|
-
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
194
|
-
const lines = result.trim().split(`
|
|
195
|
-
`).filter((l) => l.trim());
|
|
196
|
-
if (lines.length > 1) {
|
|
197
|
-
return { inUse: true };
|
|
198
|
-
}
|
|
199
|
-
return { inUse: false };
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
return { inUse: false };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
258
|
async function waitForPortFree(port, timeoutMs = 5000) {
|
|
206
259
|
const startTime = Date.now();
|
|
207
260
|
const pollInterval = 300;
|
|
@@ -409,9 +462,6 @@ async function readFileTail(filePath, lines) {
|
|
|
409
462
|
}
|
|
410
463
|
}) ?? "";
|
|
411
464
|
}
|
|
412
|
-
function copyFile(src, dest) {
|
|
413
|
-
fs.copyFileSync(src, dest);
|
|
414
|
-
}
|
|
415
465
|
async function getProcessMemory(pid) {
|
|
416
466
|
const map = await getProcessBatchResources([pid]);
|
|
417
467
|
return map.get(pid)?.memory || 0;
|
|
@@ -424,7 +474,7 @@ async function getProcessBatchResources(pids) {
|
|
|
424
474
|
const pidSet = new Set(pids);
|
|
425
475
|
try {
|
|
426
476
|
if (isWindows()) {
|
|
427
|
-
const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
|
|
477
|
+
const output = await psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
|
|
428
478
|
for (const line of output.split(`
|
|
429
479
|
`)) {
|
|
430
480
|
const sepIdx = line.indexOf("|");
|
|
@@ -504,6 +554,21 @@ async function getProcessPorts(pid) {
|
|
|
504
554
|
return [];
|
|
505
555
|
}
|
|
506
556
|
}
|
|
557
|
+
async function resolvePidWithPorts(pid) {
|
|
558
|
+
const ports = await getProcessPorts(pid);
|
|
559
|
+
if (ports.length > 0 || !isWindows() || pid <= 0) {
|
|
560
|
+
return { pid, ports };
|
|
561
|
+
}
|
|
562
|
+
const childPid = await findChildPid(pid);
|
|
563
|
+
if (childPid === pid || childPid <= 0) {
|
|
564
|
+
return { pid, ports };
|
|
565
|
+
}
|
|
566
|
+
const childPorts = await getProcessPorts(childPid);
|
|
567
|
+
if (childPorts.length > 0) {
|
|
568
|
+
return { pid: childPid, ports: childPorts };
|
|
569
|
+
}
|
|
570
|
+
return { pid, ports };
|
|
571
|
+
}
|
|
507
572
|
var plat;
|
|
508
573
|
var init_platform = __esm(() => {
|
|
509
574
|
plat = createMeasure("platform");
|
|
@@ -548,7 +613,7 @@ __export(exports_db, {
|
|
|
548
613
|
import { Database, z } from "sqlite-zod-orm";
|
|
549
614
|
import { join as join2 } from "path";
|
|
550
615
|
var {sleep } = globalThis.Bun;
|
|
551
|
-
import { existsSync as
|
|
616
|
+
import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
|
|
552
617
|
function shouldAutoMigrateLegacyDb() {
|
|
553
618
|
const raw = (process.env.BGRUN_DISABLE_LEGACY_MIGRATION || "").trim().toLowerCase();
|
|
554
619
|
return !(raw === "1" || raw === "true" || raw === "yes");
|
|
@@ -580,7 +645,7 @@ function removeProcessByName(name) {
|
|
|
580
645
|
function updateProcessPid(name, newPid) {
|
|
581
646
|
const proc = db.process.select().where({ name }).limit(1).get();
|
|
582
647
|
if (proc) {
|
|
583
|
-
|
|
648
|
+
proc.update({ pid: newPid });
|
|
584
649
|
}
|
|
585
650
|
}
|
|
586
651
|
function removeAllProcesses() {
|
|
@@ -592,7 +657,7 @@ function removeAllProcesses() {
|
|
|
592
657
|
function updateProcessEnv(name, envJson) {
|
|
593
658
|
const proc = db.process.select().where({ name }).limit(1).get();
|
|
594
659
|
if (proc) {
|
|
595
|
-
|
|
660
|
+
proc.update({ env: envJson });
|
|
596
661
|
}
|
|
597
662
|
}
|
|
598
663
|
function getAllTemplates() {
|
|
@@ -748,7 +813,7 @@ function getDbInfo() {
|
|
|
748
813
|
dbPath,
|
|
749
814
|
bgrHome,
|
|
750
815
|
dbFilename,
|
|
751
|
-
exists:
|
|
816
|
+
exists: existsSync3(dbPath)
|
|
752
817
|
};
|
|
753
818
|
}
|
|
754
819
|
async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
@@ -807,7 +872,7 @@ var init_db = __esm(() => {
|
|
|
807
872
|
dbPath = join2(bgrDir, dbFilename);
|
|
808
873
|
bgrHome = bgrDir;
|
|
809
874
|
legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
|
|
810
|
-
if (shouldAutoMigrateLegacyDb() && !
|
|
875
|
+
if (shouldAutoMigrateLegacyDb() && !existsSync3(dbPath) && existsSync3(legacyDbPath)) {
|
|
811
876
|
try {
|
|
812
877
|
copyFileSync2(legacyDbPath, dbPath);
|
|
813
878
|
console.log(`[bgrun] Migrated database: ${legacyDbPath} \u2192 ${dbPath}`);
|
|
@@ -828,486 +893,6 @@ var init_db = __esm(() => {
|
|
|
828
893
|
});
|
|
829
894
|
});
|
|
830
895
|
|
|
831
|
-
// src/utils.ts
|
|
832
|
-
import * as fs2 from "fs";
|
|
833
|
-
function parseEnvString(envString) {
|
|
834
|
-
const env = {};
|
|
835
|
-
envString.split(",").forEach((pair) => {
|
|
836
|
-
const [key, value] = pair.split("=");
|
|
837
|
-
if (key && value)
|
|
838
|
-
env[key] = value;
|
|
839
|
-
});
|
|
840
|
-
return env;
|
|
841
|
-
}
|
|
842
|
-
function calculateRuntime(startTime) {
|
|
843
|
-
const start = new Date(startTime).getTime();
|
|
844
|
-
const now = new Date().getTime();
|
|
845
|
-
const diffInMinutes = Math.floor((now - start) / (1000 * 60));
|
|
846
|
-
return `${diffInMinutes} minutes`;
|
|
847
|
-
}
|
|
848
|
-
async function getVersion() {
|
|
849
|
-
try {
|
|
850
|
-
const { join: join3 } = await import("path");
|
|
851
|
-
const pkgPath = join3(import.meta.dir, "../package.json");
|
|
852
|
-
const pkg = await Bun.file(pkgPath).json();
|
|
853
|
-
return pkg.version || "0.0.0";
|
|
854
|
-
} catch {
|
|
855
|
-
return "0.0.0";
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
function validateDirectory(directory) {
|
|
859
|
-
if (!directory || !fs2.existsSync(directory)) {
|
|
860
|
-
throw new Error(`Directory not found or invalid: '${directory}'`);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
function tailFile(path, prefix, colorFn, lines) {
|
|
864
|
-
let position = 0;
|
|
865
|
-
let lastPartial = "";
|
|
866
|
-
if (!fs2.existsSync(path)) {
|
|
867
|
-
return () => {};
|
|
868
|
-
}
|
|
869
|
-
const fd = fs2.openSync(path, "r");
|
|
870
|
-
const printNewContent = () => {
|
|
871
|
-
try {
|
|
872
|
-
const stats = fs2.statSync(path);
|
|
873
|
-
if (stats.size <= position)
|
|
874
|
-
return;
|
|
875
|
-
const buffer = Buffer.alloc(stats.size - position);
|
|
876
|
-
fs2.readSync(fd, buffer, 0, buffer.length, position);
|
|
877
|
-
let content = buffer.toString();
|
|
878
|
-
content = lastPartial + content;
|
|
879
|
-
lastPartial = "";
|
|
880
|
-
const lineArray = content.split(/\r?\n/);
|
|
881
|
-
if (!content.endsWith(`
|
|
882
|
-
`)) {
|
|
883
|
-
lastPartial = lineArray.pop() || "";
|
|
884
|
-
}
|
|
885
|
-
lineArray.forEach((line) => {
|
|
886
|
-
if (line) {
|
|
887
|
-
console.log(colorFn(prefix + line));
|
|
888
|
-
}
|
|
889
|
-
});
|
|
890
|
-
position = stats.size;
|
|
891
|
-
} catch (e) {}
|
|
892
|
-
};
|
|
893
|
-
const watcher = fs2.watch(path, { persistent: true }, (event) => {
|
|
894
|
-
if (event === "change") {
|
|
895
|
-
printNewContent();
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
printNewContent();
|
|
899
|
-
return () => {
|
|
900
|
-
watcher.close();
|
|
901
|
-
try {
|
|
902
|
-
fs2.closeSync(fd);
|
|
903
|
-
} catch {}
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
var init_utils = __esm(() => {
|
|
907
|
-
init_platform();
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
// src/deps.ts
|
|
911
|
-
var exports_deps = {};
|
|
912
|
-
__export(exports_deps, {
|
|
913
|
-
getUnmetDeps: () => getUnmetDeps,
|
|
914
|
-
getDependencies: () => getDependencies2,
|
|
915
|
-
buildDepGraph: () => buildDepGraph
|
|
916
|
-
});
|
|
917
|
-
function getDependencies2(envStr) {
|
|
918
|
-
const env = parseEnvString(envStr);
|
|
919
|
-
const raw = env.BGR_DEPENDS_ON || "";
|
|
920
|
-
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
921
|
-
}
|
|
922
|
-
async function buildDepGraph() {
|
|
923
|
-
const processes = getAllProcesses();
|
|
924
|
-
const nodeMap = new Map;
|
|
925
|
-
for (const proc of processes) {
|
|
926
|
-
const deps = getDependencies2(proc.env);
|
|
927
|
-
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
928
|
-
nodeMap.set(proc.name, {
|
|
929
|
-
name: proc.name,
|
|
930
|
-
dependsOn: deps,
|
|
931
|
-
dependedBy: [],
|
|
932
|
-
running: alive,
|
|
933
|
-
pid: proc.pid
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
for (const node of nodeMap.values()) {
|
|
937
|
-
for (const dep of node.dependsOn) {
|
|
938
|
-
const depNode = nodeMap.get(dep);
|
|
939
|
-
if (depNode) {
|
|
940
|
-
depNode.dependedBy.push(node.name);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
const inDegree = new Map;
|
|
945
|
-
for (const node of nodeMap.values()) {
|
|
946
|
-
inDegree.set(node.name, node.dependsOn.filter((d) => nodeMap.has(d)).length);
|
|
947
|
-
}
|
|
948
|
-
const queue = [];
|
|
949
|
-
for (const [name, degree] of inDegree) {
|
|
950
|
-
if (degree === 0)
|
|
951
|
-
queue.push(name);
|
|
952
|
-
}
|
|
953
|
-
const order = [];
|
|
954
|
-
while (queue.length > 0) {
|
|
955
|
-
const current = queue.shift();
|
|
956
|
-
order.push(current);
|
|
957
|
-
const node = nodeMap.get(current);
|
|
958
|
-
for (const dependent of node.dependedBy) {
|
|
959
|
-
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
960
|
-
inDegree.set(dependent, newDegree);
|
|
961
|
-
if (newDegree === 0)
|
|
962
|
-
queue.push(dependent);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
const hasCycle = order.length < nodeMap.size;
|
|
966
|
-
const cycleNodes = hasCycle ? [...nodeMap.keys()].filter((n) => !order.includes(n)) : undefined;
|
|
967
|
-
return {
|
|
968
|
-
nodes: [...nodeMap.values()],
|
|
969
|
-
order,
|
|
970
|
-
hasCycle,
|
|
971
|
-
cycleNodes
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
async function getUnmetDeps(name) {
|
|
975
|
-
const proc = getProcess(name);
|
|
976
|
-
if (!proc)
|
|
977
|
-
return [];
|
|
978
|
-
const deps = getDependencies2(proc.env);
|
|
979
|
-
const unmet = [];
|
|
980
|
-
for (const depName of deps) {
|
|
981
|
-
const depProc = getProcess(depName);
|
|
982
|
-
if (!depProc) {
|
|
983
|
-
unmet.push(depName);
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
|
-
const alive = await isProcessRunning(depProc.pid, depProc.command);
|
|
987
|
-
if (!alive) {
|
|
988
|
-
unmet.push(depName);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
return unmet;
|
|
992
|
-
}
|
|
993
|
-
var init_deps = __esm(() => {
|
|
994
|
-
init_db();
|
|
995
|
-
init_platform();
|
|
996
|
-
init_utils();
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
// src/log-rotation.ts
|
|
1000
|
-
var exports_log_rotation = {};
|
|
1001
|
-
__export(exports_log_rotation, {
|
|
1002
|
-
startLogRotation: () => startLogRotation,
|
|
1003
|
-
rotateLogFile: () => rotateLogFile,
|
|
1004
|
-
rotateAllLogs: () => rotateAllLogs
|
|
1005
|
-
});
|
|
1006
|
-
import { existsSync as existsSync4, statSync as statSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1007
|
-
function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
1008
|
-
try {
|
|
1009
|
-
if (!existsSync4(filePath))
|
|
1010
|
-
return false;
|
|
1011
|
-
const stat = statSync2(filePath);
|
|
1012
|
-
if (stat.size <= maxBytes)
|
|
1013
|
-
return false;
|
|
1014
|
-
const content = readFileSync(filePath, "utf-8");
|
|
1015
|
-
const lines = content.split(`
|
|
1016
|
-
`);
|
|
1017
|
-
if (lines.length <= keepLines)
|
|
1018
|
-
return false;
|
|
1019
|
-
const truncated = lines.slice(-keepLines);
|
|
1020
|
-
const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
|
|
1021
|
-
`;
|
|
1022
|
-
writeFileSync2(filePath, header + truncated.join(`
|
|
1023
|
-
`));
|
|
1024
|
-
return true;
|
|
1025
|
-
} catch {
|
|
1026
|
-
return false;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
function rotateAllLogs(getProcesses, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
1030
|
-
const processes = getProcesses();
|
|
1031
|
-
const rotated = [];
|
|
1032
|
-
let checked = 0;
|
|
1033
|
-
for (const proc of processes) {
|
|
1034
|
-
if (proc.stdout_path) {
|
|
1035
|
-
checked++;
|
|
1036
|
-
if (rotateLogFile(proc.stdout_path, maxBytes, keepLines)) {
|
|
1037
|
-
rotated.push(`${proc.name}/stdout`);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
if (proc.stderr_path) {
|
|
1041
|
-
checked++;
|
|
1042
|
-
if (rotateLogFile(proc.stderr_path, maxBytes, keepLines)) {
|
|
1043
|
-
rotated.push(`${proc.name}/stderr`);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return { rotated, checked };
|
|
1048
|
-
}
|
|
1049
|
-
function startLogRotation(getProcesses, intervalMs = DEFAULT_CHECK_INTERVAL_MS, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
|
|
1050
|
-
console.log(`[logs] Log rotation active: max ${formatBytes(maxBytes)}/file, keep ${keepLines} lines, check every ${intervalMs / 1000}s`);
|
|
1051
|
-
return setInterval(() => {
|
|
1052
|
-
const { rotated } = rotateAllLogs(getProcesses, maxBytes, keepLines);
|
|
1053
|
-
if (rotated.length > 0) {
|
|
1054
|
-
console.log(`[logs] Rotated ${rotated.length} log(s): ${rotated.join(", ")}`);
|
|
1055
|
-
}
|
|
1056
|
-
}, intervalMs);
|
|
1057
|
-
}
|
|
1058
|
-
function formatBytes(bytes) {
|
|
1059
|
-
if (bytes >= 1e6)
|
|
1060
|
-
return `${(bytes / 1e6).toFixed(1)}MB`;
|
|
1061
|
-
if (bytes >= 1000)
|
|
1062
|
-
return `${(bytes / 1000).toFixed(0)}KB`;
|
|
1063
|
-
return `${bytes}B`;
|
|
1064
|
-
}
|
|
1065
|
-
var DEFAULT_MAX_BYTES, DEFAULT_KEEP_LINES = 5000, DEFAULT_CHECK_INTERVAL_MS = 60000;
|
|
1066
|
-
var init_log_rotation = __esm(() => {
|
|
1067
|
-
DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
// src/logger.ts
|
|
1071
|
-
import boxen from "boxen";
|
|
1072
|
-
import chalk from "chalk";
|
|
1073
|
-
function announce(message, title) {
|
|
1074
|
-
console.log(boxen(message, {
|
|
1075
|
-
padding: 1,
|
|
1076
|
-
margin: 1,
|
|
1077
|
-
borderColor: "green",
|
|
1078
|
-
title: title || "bgrun",
|
|
1079
|
-
titleAlignment: "center",
|
|
1080
|
-
borderStyle: "round"
|
|
1081
|
-
}));
|
|
1082
|
-
}
|
|
1083
|
-
function error(message) {
|
|
1084
|
-
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
1085
|
-
console.error(boxen(chalk.red(text), {
|
|
1086
|
-
padding: 1,
|
|
1087
|
-
margin: 1,
|
|
1088
|
-
borderColor: "red",
|
|
1089
|
-
title: "Error",
|
|
1090
|
-
titleAlignment: "center",
|
|
1091
|
-
borderStyle: "double"
|
|
1092
|
-
}));
|
|
1093
|
-
throw new BgrunError(text);
|
|
1094
|
-
}
|
|
1095
|
-
var BgrunError;
|
|
1096
|
-
var init_logger = __esm(() => {
|
|
1097
|
-
BgrunError = class BgrunError extends Error {
|
|
1098
|
-
constructor(message) {
|
|
1099
|
-
super(message);
|
|
1100
|
-
this.name = "BgrunError";
|
|
1101
|
-
}
|
|
1102
|
-
};
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
// src/config.ts
|
|
1106
|
-
function formatEnvKey(key) {
|
|
1107
|
-
return key.toUpperCase().replace(/\./g, "_");
|
|
1108
|
-
}
|
|
1109
|
-
function flattenConfig(obj, prefix = "") {
|
|
1110
|
-
return Object.keys(obj).reduce((acc, key) => {
|
|
1111
|
-
const value = obj[key];
|
|
1112
|
-
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1113
|
-
if (Array.isArray(value)) {
|
|
1114
|
-
value.forEach((item, index) => {
|
|
1115
|
-
const indexedPrefix = `${newPrefix}.${index}`;
|
|
1116
|
-
if (typeof item === "object" && item !== null) {
|
|
1117
|
-
Object.assign(acc, flattenConfig(item, indexedPrefix));
|
|
1118
|
-
} else {
|
|
1119
|
-
acc[formatEnvKey(indexedPrefix)] = String(item);
|
|
1120
|
-
}
|
|
1121
|
-
});
|
|
1122
|
-
} else if (typeof value === "object" && value !== null) {
|
|
1123
|
-
Object.assign(acc, flattenConfig(value, newPrefix));
|
|
1124
|
-
} else {
|
|
1125
|
-
acc[formatEnvKey(newPrefix)] = String(value);
|
|
1126
|
-
}
|
|
1127
|
-
return acc;
|
|
1128
|
-
}, {});
|
|
1129
|
-
}
|
|
1130
|
-
async function parseConfigFile(configPath) {
|
|
1131
|
-
const importPath = `${configPath}?t=${Date.now()}`;
|
|
1132
|
-
const parsedConfig = await import(importPath).then((m) => m.default);
|
|
1133
|
-
return flattenConfig(parsedConfig);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// src/commands/run.ts
|
|
1137
|
-
var {$: $2 } = globalThis.Bun;
|
|
1138
|
-
var {sleep: sleep2 } = globalThis.Bun;
|
|
1139
|
-
import { join as join3 } from "path";
|
|
1140
|
-
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
1141
|
-
async function handleRun(options) {
|
|
1142
|
-
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
1143
|
-
const existingProcess = name ? getProcess(name) : null;
|
|
1144
|
-
if (name && existingProcess) {
|
|
1145
|
-
const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
|
|
1146
|
-
const unmet = await getUnmetDeps2(name);
|
|
1147
|
-
if (unmet.length > 0) {
|
|
1148
|
-
await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
|
|
1149
|
-
for (const depName of unmet) {
|
|
1150
|
-
const depProc = getProcess(depName);
|
|
1151
|
-
if (depProc) {
|
|
1152
|
-
announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
|
|
1153
|
-
await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
if (existingProcess) {
|
|
1160
|
-
const finalDirectory2 = directory || existingProcess.workdir;
|
|
1161
|
-
validateDirectory(finalDirectory2);
|
|
1162
|
-
$2.cwd(finalDirectory2);
|
|
1163
|
-
if (fetch) {
|
|
1164
|
-
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
1165
|
-
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
1166
|
-
}
|
|
1167
|
-
await run.measure(`Git fetch "${name}"`, async () => {
|
|
1168
|
-
try {
|
|
1169
|
-
await $2`git fetch origin`;
|
|
1170
|
-
const localHash = (await $2`git rev-parse HEAD`.text()).trim();
|
|
1171
|
-
const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
1172
|
-
if (localHash !== remoteHash) {
|
|
1173
|
-
await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
1174
|
-
announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
|
|
1175
|
-
}
|
|
1176
|
-
} catch (err) {
|
|
1177
|
-
error(`Failed to pull latest changes: ${err}`);
|
|
1178
|
-
}
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
1182
|
-
if (isRunning && !force) {
|
|
1183
|
-
error(`Process '${name}' is currently running. Use --force to restart.`);
|
|
1184
|
-
}
|
|
1185
|
-
let detectedPorts = [];
|
|
1186
|
-
if (isRunning) {
|
|
1187
|
-
detectedPorts = await getProcessPorts(existingProcess.pid);
|
|
1188
|
-
}
|
|
1189
|
-
if (isRunning) {
|
|
1190
|
-
await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
|
|
1191
|
-
await terminateProcess(existingProcess.pid);
|
|
1192
|
-
announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
if (detectedPorts.length > 0) {
|
|
1196
|
-
await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
|
|
1197
|
-
for (const port of detectedPorts) {
|
|
1198
|
-
await killProcessOnPort(port);
|
|
1199
|
-
}
|
|
1200
|
-
for (const port of detectedPorts) {
|
|
1201
|
-
const freed = await waitForPortFree(port, 5000);
|
|
1202
|
-
if (!freed) {
|
|
1203
|
-
await killProcessOnPort(port);
|
|
1204
|
-
await waitForPortFree(port, 3000);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
const cmdToMatch = existingProcess.command;
|
|
1210
|
-
if (cmdToMatch) {
|
|
1211
|
-
await run.measure("Zombie sweep", async () => {
|
|
1212
|
-
try {
|
|
1213
|
-
const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
|
|
1214
|
-
const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
|
|
1215
|
-
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
1216
|
-
return;
|
|
1217
|
-
}
|
|
1218
|
-
const currentPid = process.pid;
|
|
1219
|
-
const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${cmdKeyword.replace(/'/g, "''")}' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
1220
|
-
const zombiePids = result.split(`
|
|
1221
|
-
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
1222
|
-
for (const zPid of zombiePids) {
|
|
1223
|
-
await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
1224
|
-
}
|
|
1225
|
-
if (zombiePids.length > 0) {
|
|
1226
|
-
announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
|
|
1227
|
-
}
|
|
1228
|
-
} catch {}
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
await retryDatabaseOperation(() => removeProcessByName(name));
|
|
1232
|
-
} else {
|
|
1233
|
-
if (!directory || !name || !command) {
|
|
1234
|
-
error("'directory', 'name', and 'command' parameters are required for new processes.");
|
|
1235
|
-
}
|
|
1236
|
-
validateDirectory(directory);
|
|
1237
|
-
$2.cwd(directory);
|
|
1238
|
-
}
|
|
1239
|
-
const finalCommand = command || existingProcess.command;
|
|
1240
|
-
const finalDirectory = directory || existingProcess?.workdir;
|
|
1241
|
-
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
1242
|
-
if (!("BGR_KEEP_ALIVE" in finalEnv)) {
|
|
1243
|
-
finalEnv.BGR_KEEP_ALIVE = "true";
|
|
1244
|
-
}
|
|
1245
|
-
let finalConfigPath;
|
|
1246
|
-
if (configPath !== undefined) {
|
|
1247
|
-
finalConfigPath = configPath;
|
|
1248
|
-
} else if (existingProcess) {
|
|
1249
|
-
finalConfigPath = existingProcess.configPath;
|
|
1250
|
-
} else {
|
|
1251
|
-
finalConfigPath = ".config.toml";
|
|
1252
|
-
}
|
|
1253
|
-
if (finalConfigPath) {
|
|
1254
|
-
const fullConfigPath = join3(finalDirectory, finalConfigPath);
|
|
1255
|
-
if (await Bun.file(fullConfigPath).exists()) {
|
|
1256
|
-
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
1257
|
-
try {
|
|
1258
|
-
return await parseConfigFile(fullConfigPath);
|
|
1259
|
-
} catch (err) {
|
|
1260
|
-
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
1261
|
-
return null;
|
|
1262
|
-
}
|
|
1263
|
-
});
|
|
1264
|
-
if (configEnv) {
|
|
1265
|
-
finalEnv = { ...finalEnv, ...configEnv };
|
|
1266
|
-
console.log(`Loaded config from ${finalConfigPath}`);
|
|
1267
|
-
}
|
|
1268
|
-
} else {
|
|
1269
|
-
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
|
|
1273
|
-
Bun.write(stdoutPath, "");
|
|
1274
|
-
const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
|
|
1275
|
-
Bun.write(stderrPath, "");
|
|
1276
|
-
const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
|
|
1277
|
-
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
1278
|
-
env: { ...Bun.env, ...finalEnv },
|
|
1279
|
-
cwd: finalDirectory,
|
|
1280
|
-
stdout: Bun.file(stdoutPath),
|
|
1281
|
-
stderr: Bun.file(stderrPath)
|
|
1282
|
-
});
|
|
1283
|
-
newProcess.unref();
|
|
1284
|
-
await sleep2(100);
|
|
1285
|
-
const pid = await findChildPid(newProcess.pid);
|
|
1286
|
-
await sleep2(400);
|
|
1287
|
-
return pid;
|
|
1288
|
-
}) ?? 0;
|
|
1289
|
-
await retryDatabaseOperation(() => insertProcess({
|
|
1290
|
-
pid: actualPid,
|
|
1291
|
-
workdir: finalDirectory,
|
|
1292
|
-
command: finalCommand,
|
|
1293
|
-
name,
|
|
1294
|
-
env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
|
|
1295
|
-
configPath: finalConfigPath || "",
|
|
1296
|
-
stdout_path: stdoutPath,
|
|
1297
|
-
stderr_path: stderrPath
|
|
1298
|
-
}));
|
|
1299
|
-
announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
|
|
1300
|
-
}
|
|
1301
|
-
var homePath2, run;
|
|
1302
|
-
var init_run = __esm(() => {
|
|
1303
|
-
init_db();
|
|
1304
|
-
init_platform();
|
|
1305
|
-
init_logger();
|
|
1306
|
-
init_utils();
|
|
1307
|
-
homePath2 = getHomeDir();
|
|
1308
|
-
run = createMeasure2("run");
|
|
1309
|
-
});
|
|
1310
|
-
|
|
1311
896
|
// src/server.ts
|
|
1312
897
|
var exports_server = {};
|
|
1313
898
|
__export(exports_server, {
|
|
@@ -1343,18 +928,20 @@ async function cleanupPort(port) {
|
|
|
1343
928
|
async function startServer() {
|
|
1344
929
|
const { start } = await import("melina");
|
|
1345
930
|
const appDir = path.join(import.meta.dir, "../dashboard/app");
|
|
1346
|
-
const
|
|
931
|
+
const rawRequestedPort = process.env.BUN_PORT?.trim();
|
|
932
|
+
const explicitPort = rawRequestedPort ? parseInt(rawRequestedPort, 10) : null;
|
|
933
|
+
const hasExplicitPort = explicitPort !== null && !isNaN(explicitPort) && explicitPort > 0;
|
|
934
|
+
const requestedPort = hasExplicitPort ? explicitPort : 3000;
|
|
1347
935
|
_originalPort = requestedPort;
|
|
1348
|
-
const resolvedPort = await cleanupPort(requestedPort);
|
|
936
|
+
const resolvedPort = hasExplicitPort ? await cleanupPort(requestedPort) : requestedPort;
|
|
1349
937
|
_currentPort = resolvedPort;
|
|
1350
|
-
const needsExplicitPort =
|
|
938
|
+
const needsExplicitPort = hasExplicitPort || resolvedPort !== requestedPort;
|
|
1351
939
|
await start({
|
|
1352
940
|
appDir,
|
|
1353
941
|
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
1354
942
|
globalCss: path.join(appDir, "globals.css"),
|
|
1355
943
|
...needsExplicitPort && { port: resolvedPort }
|
|
1356
944
|
});
|
|
1357
|
-
startGuard();
|
|
1358
945
|
const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
|
|
1359
946
|
startLogRotation2(() => getAllProcesses());
|
|
1360
947
|
if (resolvedPort !== requestedPort) {
|
|
@@ -1383,92 +970,17 @@ function startStickyPortChecker() {
|
|
|
1383
970
|
} catch {}
|
|
1384
971
|
}, CHECK_INTERVAL_MS);
|
|
1385
972
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
setInterval(async () => {
|
|
1389
|
-
try {
|
|
1390
|
-
const processes = getAllProcesses();
|
|
1391
|
-
if (processes.length === 0)
|
|
1392
|
-
return;
|
|
1393
|
-
for (const proc of processes) {
|
|
1394
|
-
if (GUARD_SKIP_NAMES.has(proc.name))
|
|
1395
|
-
continue;
|
|
1396
|
-
const env = proc.env ? parseEnvString(proc.env) : {};
|
|
1397
|
-
if (env.BGR_KEEP_ALIVE !== "true")
|
|
1398
|
-
continue;
|
|
1399
|
-
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
1400
|
-
if (!alive) {
|
|
1401
|
-
const now = Date.now();
|
|
1402
|
-
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
1403
|
-
if (now < nextRestart)
|
|
1404
|
-
continue;
|
|
1405
|
-
console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
|
|
1406
|
-
let success = false;
|
|
1407
|
-
try {
|
|
1408
|
-
await handleRun({
|
|
1409
|
-
action: "run",
|
|
1410
|
-
name: proc.name,
|
|
1411
|
-
force: true,
|
|
1412
|
-
remoteName: ""
|
|
1413
|
-
});
|
|
1414
|
-
success = true;
|
|
1415
|
-
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
1416
|
-
const newCount = prevCount + 1;
|
|
1417
|
-
guardRestartCounts.set(proc.name, newCount);
|
|
1418
|
-
try {
|
|
1419
|
-
addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
|
|
1420
|
-
} catch {}
|
|
1421
|
-
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
|
|
1422
|
-
if (guardEvents.length > 100)
|
|
1423
|
-
guardEvents.pop();
|
|
1424
|
-
if (newCount > 5) {
|
|
1425
|
-
const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
|
|
1426
|
-
guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
|
|
1427
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
|
|
1428
|
-
} else {
|
|
1429
|
-
console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
|
|
1430
|
-
}
|
|
1431
|
-
} catch (err) {
|
|
1432
|
-
console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
|
|
1433
|
-
guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
|
|
1434
|
-
if (guardEvents.length > 100)
|
|
1435
|
-
guardEvents.pop();
|
|
1436
|
-
}
|
|
1437
|
-
} else {
|
|
1438
|
-
const prevCount = guardRestartCounts.get(proc.name) || 0;
|
|
1439
|
-
if (prevCount > 0) {
|
|
1440
|
-
const nextRestart = guardNextRestartTime.get(proc.name) || 0;
|
|
1441
|
-
if (Date.now() > nextRestart + 60000) {
|
|
1442
|
-
guardRestartCounts.delete(proc.name);
|
|
1443
|
-
guardNextRestartTime.delete(proc.name);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
} catch (err) {
|
|
1449
|
-
console.error(`[guard] Error in guard loop: ${err.message}`);
|
|
1450
|
-
}
|
|
1451
|
-
}, GUARD_INTERVAL_MS);
|
|
1452
|
-
}
|
|
1453
|
-
var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
|
|
1454
|
-
var init_server = __esm(() => {
|
|
973
|
+
var guardRestartCounts, guardEvents, _originalPort = 3000, _currentPort = 3000;
|
|
974
|
+
var init_server = __esm(async () => {
|
|
1455
975
|
init_db();
|
|
1456
976
|
init_platform();
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
_g.__bgrGuardRestartCounts = new Map;
|
|
1463
|
-
if (!_g.__bgrGuardNextRestartTime)
|
|
1464
|
-
_g.__bgrGuardNextRestartTime = new Map;
|
|
1465
|
-
if (!_g.__bgrGuardEvents)
|
|
1466
|
-
_g.__bgrGuardEvents = [];
|
|
1467
|
-
guardRestartCounts = _g.__bgrGuardRestartCounts;
|
|
1468
|
-
guardNextRestartTime = _g.__bgrGuardNextRestartTime;
|
|
1469
|
-
guardEvents = _g.__bgrGuardEvents;
|
|
977
|
+
guardRestartCounts = new Map;
|
|
978
|
+
guardEvents = [];
|
|
979
|
+
if (import.meta.main) {
|
|
980
|
+
await startServer();
|
|
981
|
+
}
|
|
1470
982
|
});
|
|
1471
|
-
init_server();
|
|
983
|
+
await init_server();
|
|
1472
984
|
|
|
1473
985
|
export {
|
|
1474
986
|
startServer,
|