bgrun 3.12.0 → 3.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/app/api/check-port/route.ts +35 -0
- package/dashboard/app/api/dependencies/route.ts +40 -0
- package/dashboard/app/api/deploy/[name]/route.ts +6 -41
- package/dashboard/app/api/deploy-all/route.ts +25 -0
- package/dashboard/app/api/guard/route.ts +4 -1
- package/dashboard/app/api/guard-events/route.ts +5 -0
- package/dashboard/app/api/history/route.ts +39 -0
- package/dashboard/app/api/next-port/route.ts +32 -0
- package/dashboard/app/api/processes/route.ts +11 -3
- package/dashboard/app/api/restart/[name]/route.ts +7 -0
- package/dashboard/app/api/start/route.ts +11 -0
- package/dashboard/app/api/stop/[name]/route.ts +4 -1
- package/dashboard/app/api/templates/route.ts +47 -0
- package/dashboard/app/globals.css +1565 -5
- package/dashboard/app/page.client.tsx +1907 -2
- package/dashboard/app/page.tsx +292 -5
- package/dist/index.js +787 -194
- package/package.json +2 -2
- package/scripts/bgr-startup.ps1 +3 -3
- package/scripts/bgrun-startup.ps1 +91 -0
- package/src/bgrun.test.ts +171 -0
- package/src/commands/details.ts +17 -3
- package/src/commands/list.ts +37 -4
- package/src/commands/run.ts +21 -3
- package/src/db.ts +257 -0
- package/src/deploy.ts +163 -0
- package/src/guard.ts +51 -0
- package/src/index.ts +92 -14
- package/src/index_copy.ts +614 -0
- package/src/logger.ts +12 -2
- package/src/platform.ts +101 -56
- package/src/server.ts +87 -3
- package/src/utils.ts +2 -2
package/package.json
CHANGED
package/scripts/bgr-startup.ps1
CHANGED
|
@@ -66,14 +66,14 @@ if ($Install) {
|
|
|
66
66
|
|
|
67
67
|
Register-ScheduledTask `
|
|
68
68
|
-TaskName "bgrun-guard" `
|
|
69
|
-
-Description "bgrun process manager
|
|
69
|
+
-Description "bgrun process manager - auto-starts all guarded processes on login" `
|
|
70
70
|
-Action $action `
|
|
71
71
|
-Trigger $trigger `
|
|
72
72
|
-Settings $settings `
|
|
73
73
|
-RunLevel Highest `
|
|
74
74
|
-Force
|
|
75
75
|
|
|
76
|
-
Write-Log "
|
|
76
|
+
Write-Log "[OK] Task 'bgrun-guard' registered. Will start on next login."
|
|
77
77
|
Write-Log " Script: $scriptPath"
|
|
78
78
|
exit 0
|
|
79
79
|
}
|
|
@@ -115,4 +115,4 @@ $guardProc = Start-Process -FilePath $BunPath `
|
|
|
115
115
|
-WorkingDirectory $BgrunPath
|
|
116
116
|
|
|
117
117
|
Write-Log "Guard PID: $($guardProc.Id)"
|
|
118
|
-
Write-Log "
|
|
118
|
+
Write-Log "[OK] bgrun startup complete. Dashboard: $($dashboardProc.Id), Guard: $($guardProc.Id)"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# bgrun-startup.ps1 — Persistent bgr-dashboard launcher
|
|
2
|
+
# Ensures the bgrun dashboard survives terminal closures.
|
|
3
|
+
# Individual services (geeksy, mm-dash, galaxy-canvas, etc.) are managed
|
|
4
|
+
# by bgr-guard INSIDE the dashboard — this script only keeps the dashboard alive.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# .\bgrun-startup.ps1 # Start bgr-dashboard
|
|
8
|
+
# .\bgrun-startup.ps1 -Install # Register as logon scheduled task
|
|
9
|
+
# .\bgrun-startup.ps1 -Uninstall # Remove scheduled task
|
|
10
|
+
# .\bgrun-startup.ps1 -Guard # Run guard loop (auto-restart dashboard)
|
|
11
|
+
|
|
12
|
+
param(
|
|
13
|
+
[switch]$Install,
|
|
14
|
+
[switch]$Uninstall,
|
|
15
|
+
[switch]$Guard
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
$TaskName = "bgrun-persistence"
|
|
19
|
+
$ScriptPath = $MyInvocation.MyCommand.Path
|
|
20
|
+
|
|
21
|
+
# ─── Install as Scheduled Task ────────────────────────────
|
|
22
|
+
if ($Install) {
|
|
23
|
+
$action = New-ScheduledTaskAction `
|
|
24
|
+
-Execute "pwsh.exe" `
|
|
25
|
+
-Argument "-WindowStyle Hidden -NonInteractive -File `"$ScriptPath`" -Guard"
|
|
26
|
+
|
|
27
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
|
|
28
|
+
$settings = New-ScheduledTaskSettingsSet `
|
|
29
|
+
-AllowStartIfOnBatteries `
|
|
30
|
+
-DontStopIfGoingOnBatteries `
|
|
31
|
+
-StartWhenAvailable `
|
|
32
|
+
-RestartInterval (New-TimeSpan -Minutes 1) `
|
|
33
|
+
-RestartCount 3 `
|
|
34
|
+
-ExecutionTimeLimit (New-TimeSpan -Hours 0) # No time limit
|
|
35
|
+
|
|
36
|
+
Register-ScheduledTask `
|
|
37
|
+
-TaskName $TaskName `
|
|
38
|
+
-Action $action `
|
|
39
|
+
-Trigger $trigger `
|
|
40
|
+
-Settings $settings `
|
|
41
|
+
-Description "Keeps bgr-dashboard alive across terminal closures. Dashboard's own bgr-guard handles individual services." `
|
|
42
|
+
-Force
|
|
43
|
+
|
|
44
|
+
Write-Host "✅ Scheduled task '$TaskName' registered for user $env:USERNAME"
|
|
45
|
+
Write-Host " Runs at logon — keeps bgr-dashboard alive"
|
|
46
|
+
Write-Host " To start immediately: schtasks /run /tn '$TaskName'"
|
|
47
|
+
exit 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# ─── Uninstall ────────────────────────────────────────────
|
|
51
|
+
if ($Uninstall) {
|
|
52
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
53
|
+
Write-Host "🗑️ Scheduled task '$TaskName' removed"
|
|
54
|
+
exit 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ─── Guard mode: keep bgr-dashboard alive ─────────────────
|
|
58
|
+
if ($Guard) {
|
|
59
|
+
Write-Host "🛡️ bgrun dashboard guard started at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
|
60
|
+
|
|
61
|
+
while ($true) {
|
|
62
|
+
try {
|
|
63
|
+
$dashboard = & bgrun --json 2>$null | ConvertFrom-Json | Where-Object { $_.name -eq 'bgr-dashboard' }
|
|
64
|
+
|
|
65
|
+
if (-not $dashboard -or $dashboard.status -ne "running") {
|
|
66
|
+
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] ⚠️ bgr-dashboard is DOWN — restarting..."
|
|
67
|
+
& bgrun --restart bgr-dashboard --force 2>$null
|
|
68
|
+
Start-Sleep -Seconds 3
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] ❌ Guard check failed: $_"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Start-Sleep -Seconds 60
|
|
75
|
+
}
|
|
76
|
+
exit 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ─── Default: one-time start ──────────────────────────────
|
|
80
|
+
Write-Host "🚀 Starting bgr-dashboard..."
|
|
81
|
+
$dashboard = & bgrun --json 2>$null | ConvertFrom-Json | Where-Object { $_.name -eq 'bgr-dashboard' }
|
|
82
|
+
if ($dashboard -and $dashboard.status -eq "running") {
|
|
83
|
+
Write-Host " ✓ bgr-dashboard already running (PID $($dashboard.pid))"
|
|
84
|
+
} else {
|
|
85
|
+
& bgrun --restart bgr-dashboard --force 2>$null
|
|
86
|
+
Start-Sleep -Seconds 2
|
|
87
|
+
Write-Host " ✓ bgr-dashboard started"
|
|
88
|
+
}
|
|
89
|
+
Write-Host "`n✅ Done. Dashboard's bgr-guard handles individual services."
|
|
90
|
+
Write-Host " 💡 Run with -Install to persist across terminal closures"
|
|
91
|
+
|
package/src/bgrun.test.ts
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
import { describe, expect, test } from 'bun:test'
|
|
10
10
|
import { parseEnvString, calculateRuntime } from './utils'
|
|
11
11
|
import { stripAnsi, truncateString, truncatePath } from './table'
|
|
12
|
+
import { detectPackageManager, formatDeployToolError } from './deploy'
|
|
13
|
+
import { isProcessRunning } from './platform'
|
|
14
|
+
import { mkdirSync, rmSync } from 'fs'
|
|
15
|
+
|
|
16
|
+
// Use a test-specific database to avoid polluting real data
|
|
17
|
+
process.env.BGRUN_DB = `bgrun-test-${Date.now()}.sqlite`
|
|
18
|
+
import { addDependency, removeDependency, getDependencyGraph, getDependencies, getDependents, getStartOrder, removeAllDependencies } from './db'
|
|
12
19
|
|
|
13
20
|
// ─── parseEnvString ─────────────────────────────────────
|
|
14
21
|
|
|
@@ -107,3 +114,167 @@ describe('truncatePath', () => {
|
|
|
107
114
|
expect(result).toContain('…')
|
|
108
115
|
})
|
|
109
116
|
})
|
|
117
|
+
|
|
118
|
+
// ─── detectPackageManager ───────────────────────────────
|
|
119
|
+
|
|
120
|
+
// ─── isProcessRunning (Windows liveness fallback) ───────
|
|
121
|
+
|
|
122
|
+
describe('isProcessRunning', () => {
|
|
123
|
+
test('returns true for the current process PID', async () => {
|
|
124
|
+
const alive = await isProcessRunning(process.pid)
|
|
125
|
+
expect(alive).toBe(true)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('returns false for PID 0 (intentionally stopped)', async () => {
|
|
129
|
+
const alive = await isProcessRunning(0)
|
|
130
|
+
expect(alive).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('returns false for a very high unlikely PID', async () => {
|
|
134
|
+
const alive = await isProcessRunning(999999)
|
|
135
|
+
expect(alive).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('returns false for negative PID', async () => {
|
|
139
|
+
const alive = await isProcessRunning(-1)
|
|
140
|
+
expect(alive).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// ─── detectPackageManager ───────────────────────────────
|
|
145
|
+
|
|
146
|
+
describe('formatDeployToolError', () => {
|
|
147
|
+
test('returns actionable message for missing binary', () => {
|
|
148
|
+
const msg = formatDeployToolError('pnpm', new Error('command not found: pnpm'))
|
|
149
|
+
expect(msg).toContain("requires 'pnpm'")
|
|
150
|
+
expect(msg).toContain('PATH')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('preserves non-missing-binary failures', () => {
|
|
154
|
+
const msg = formatDeployToolError('npm', new Error('npm ci failed with exit code 1'))
|
|
155
|
+
expect(msg).toContain('Dependency install failed with npm')
|
|
156
|
+
expect(msg).toContain('exit code 1')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('detectPackageManager', () => {
|
|
161
|
+
test('returns null when no package.json exists', async () => {
|
|
162
|
+
const dir = `${process.cwd()}/tmp-no-package-${Date.now()}`
|
|
163
|
+
mkdirSync(dir, { recursive: true })
|
|
164
|
+
try {
|
|
165
|
+
expect(await detectPackageManager(dir)).toBeNull()
|
|
166
|
+
} finally {
|
|
167
|
+
rmSync(dir, { recursive: true, force: true })
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('prefers bun lockfiles', async () => {
|
|
172
|
+
const dir = `${process.cwd()}/tmp-bun-${Date.now()}`
|
|
173
|
+
mkdirSync(dir, { recursive: true })
|
|
174
|
+
try {
|
|
175
|
+
await Bun.write(`${dir}/package.json`, '{}')
|
|
176
|
+
await Bun.write(`${dir}/bun.lock`, '')
|
|
177
|
+
expect(await detectPackageManager(dir)).toBe('bun')
|
|
178
|
+
} finally {
|
|
179
|
+
rmSync(dir, { recursive: true, force: true })
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('detects pnpm, yarn, and npm lockfiles', async () => {
|
|
184
|
+
const base = `${process.cwd()}/tmp-pm-${Date.now()}`
|
|
185
|
+
|
|
186
|
+
const pnpmDir = `${base}-pnpm`
|
|
187
|
+
mkdirSync(pnpmDir, { recursive: true })
|
|
188
|
+
await Bun.write(`${pnpmDir}/package.json`, '{}')
|
|
189
|
+
await Bun.write(`${pnpmDir}/pnpm-lock.yaml`, '')
|
|
190
|
+
expect(await detectPackageManager(pnpmDir)).toBe('pnpm')
|
|
191
|
+
|
|
192
|
+
const yarnDir = `${base}-yarn`
|
|
193
|
+
mkdirSync(yarnDir, { recursive: true })
|
|
194
|
+
await Bun.write(`${yarnDir}/package.json`, '{}')
|
|
195
|
+
await Bun.write(`${yarnDir}/yarn.lock`, '')
|
|
196
|
+
expect(await detectPackageManager(yarnDir)).toBe('yarn')
|
|
197
|
+
|
|
198
|
+
const npmDir = `${base}-npm`
|
|
199
|
+
mkdirSync(npmDir, { recursive: true })
|
|
200
|
+
await Bun.write(`${npmDir}/package.json`, '{}')
|
|
201
|
+
await Bun.write(`${npmDir}/package-lock.json`, '{}')
|
|
202
|
+
expect(await detectPackageManager(npmDir)).toBe('npm')
|
|
203
|
+
|
|
204
|
+
rmSync(pnpmDir, { recursive: true, force: true })
|
|
205
|
+
rmSync(yarnDir, { recursive: true, force: true })
|
|
206
|
+
rmSync(npmDir, { recursive: true, force: true })
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('defaults to bun for package.json projects without a lockfile', async () => {
|
|
210
|
+
const dir = `${process.cwd()}/tmp-default-bun-${Date.now()}`
|
|
211
|
+
mkdirSync(dir, { recursive: true })
|
|
212
|
+
try {
|
|
213
|
+
await Bun.write(`${dir}/package.json`, '{}')
|
|
214
|
+
expect(await detectPackageManager(dir)).toBe('bun')
|
|
215
|
+
} finally {
|
|
216
|
+
rmSync(dir, { recursive: true, force: true })
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ─── Dependencies ───────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
describe('addDependency', () => {
|
|
224
|
+
test('adds a valid dependency', () => {
|
|
225
|
+
removeAllDependencies('web-server');
|
|
226
|
+
removeAllDependencies('database');
|
|
227
|
+
const ok = addDependency('web-server', 'database');
|
|
228
|
+
expect(ok).toBe(true);
|
|
229
|
+
expect(getDependencies('web-server')).toContain('database');
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('prevents self-dependency', () => {
|
|
233
|
+
expect(addDependency('api', 'api')).toBe(false);
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('prevents duplicate dependency', () => {
|
|
237
|
+
removeAllDependencies('app');
|
|
238
|
+
addDependency('app', 'db');
|
|
239
|
+
expect(addDependency('app', 'db')).toBe(false);
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('prevents circular dependency', () => {
|
|
243
|
+
removeAllDependencies('a');
|
|
244
|
+
removeAllDependencies('b');
|
|
245
|
+
removeAllDependencies('c');
|
|
246
|
+
addDependency('a', 'b');
|
|
247
|
+
addDependency('b', 'c');
|
|
248
|
+
// c -> a would create a cycle
|
|
249
|
+
expect(addDependency('c', 'a')).toBe(false);
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('getDependencyGraph', () => {
|
|
254
|
+
test('returns full graph', () => {
|
|
255
|
+
removeAllDependencies('svc-a');
|
|
256
|
+
removeAllDependencies('svc-b');
|
|
257
|
+
addDependency('svc-a', 'svc-b');
|
|
258
|
+
const graph = getDependencyGraph();
|
|
259
|
+
expect(graph['svc-a']).toContain('svc-b');
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('getDependents', () => {
|
|
264
|
+
test('finds processes that depend on a target', () => {
|
|
265
|
+
removeAllDependencies('frontend');
|
|
266
|
+
removeAllDependencies('backend');
|
|
267
|
+
addDependency('frontend', 'backend');
|
|
268
|
+
expect(getDependents('backend')).toContain('frontend');
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('removeDependency', () => {
|
|
273
|
+
test('removes an existing dependency', () => {
|
|
274
|
+
removeAllDependencies('x');
|
|
275
|
+
addDependency('x', 'y');
|
|
276
|
+
expect(getDependencies('x')).toContain('y');
|
|
277
|
+
removeDependency('x', 'y');
|
|
278
|
+
expect(getDependencies('x')).not.toContain('y');
|
|
279
|
+
})
|
|
280
|
+
})
|
package/src/commands/details.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import { error, announce } from "../logger";
|
|
3
|
-
import { getProcess } from "../db";
|
|
3
|
+
import { getProcess, updateProcessPid } from "../db";
|
|
4
4
|
import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
|
|
5
|
-
import { getProcessPorts } from "../platform";
|
|
5
|
+
import { getProcessPorts, reconcileProcessPids } from "../platform";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
|
|
8
8
|
export async function showDetails(name: string) {
|
|
@@ -12,7 +12,21 @@ export async function showDetails(name: string) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
let isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
16
|
+
|
|
17
|
+
// Reconcile stale PID: cmd.exe wrapper may have exited while bun.exe child lives
|
|
18
|
+
if (!isRunning && proc.pid > 0) {
|
|
19
|
+
const reconciled = await reconcileProcessPids(
|
|
20
|
+
[{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }],
|
|
21
|
+
new Set([proc.pid]),
|
|
22
|
+
);
|
|
23
|
+
const newPid = reconciled.get(proc.name);
|
|
24
|
+
if (newPid) {
|
|
25
|
+
updateProcessPid(proc.name, newPid);
|
|
26
|
+
(proc as any).pid = newPid;
|
|
27
|
+
isRunning = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
const runtime = calculateRuntime(proc.timestamp);
|
|
17
31
|
const envVars = parseEnvString(proc.env);
|
|
18
32
|
|
package/src/commands/list.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { renderProcessTable } from "../table";
|
|
3
3
|
import type { ProcessTableRow } from "../table";
|
|
4
|
-
import { getAllProcesses } from "../db";
|
|
4
|
+
import { getAllProcesses, updateProcessPid } from "../db";
|
|
5
5
|
import { announce } from "../logger";
|
|
6
6
|
import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
|
|
7
|
-
import { getProcessPorts, getProcessBatchResources } from "../platform";
|
|
7
|
+
import { getProcessPorts, getProcessBatchResources, reconcileProcessPids } from "../platform";
|
|
8
8
|
import { measure } from "measure-fn";
|
|
9
9
|
|
|
10
10
|
function formatMemory(bytes: number): string {
|
|
@@ -24,12 +24,45 @@ export async function showAll(opts?: { json?: boolean; filter?: string }) {
|
|
|
24
24
|
return envVars["BGR_GROUP"] === opts.filter;
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
// ─── PID Reconciliation ──────────────────────────────────────────
|
|
28
|
+
// On Windows, the stored PID may be a dead cmd.exe wrapper while the
|
|
29
|
+
// actual bun.exe child is still running. Detect dead PIDs up-front,
|
|
30
|
+
// reconcile them in one batch PowerShell call, and patch the DB so
|
|
31
|
+
// subsequent invocations are stable (no flicker).
|
|
32
|
+
const deadPids = new Set<number>();
|
|
33
|
+
const aliveCache = new Map<number, boolean>();
|
|
34
|
+
|
|
35
|
+
for (const proc of filtered) {
|
|
36
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
37
|
+
aliveCache.set(proc.pid, alive);
|
|
38
|
+
if (!alive && proc.pid > 0) deadPids.add(proc.pid);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (deadPids.size > 0) {
|
|
42
|
+
const reconciled = await reconcileProcessPids(
|
|
43
|
+
filtered.map(p => ({ name: p.name, pid: p.pid, command: p.command, workdir: p.workdir })),
|
|
44
|
+
deadPids,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
for (const [name, newPid] of reconciled) {
|
|
48
|
+
updateProcessPid(name, newPid);
|
|
49
|
+
// Patch the in-memory record so the rest of this function sees
|
|
50
|
+
// the corrected PID without re-querying the DB.
|
|
51
|
+
const proc = filtered.find(p => p.name === name);
|
|
52
|
+
if (proc) {
|
|
53
|
+
(proc as any).pid = newPid;
|
|
54
|
+
aliveCache.set(newPid, true);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
27
60
|
if (opts?.json) {
|
|
28
61
|
// JSON output with filtered env variables
|
|
29
62
|
const jsonData: any[] = [];
|
|
30
63
|
|
|
31
64
|
for (const proc of filtered) {
|
|
32
|
-
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
65
|
+
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
33
66
|
const envVars = parseEnvString(proc.env);
|
|
34
67
|
|
|
35
68
|
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
@@ -54,7 +87,7 @@ export async function showAll(opts?: { json?: boolean; filter?: string }) {
|
|
|
54
87
|
const resourceMap = await getProcessBatchResources(allPids);
|
|
55
88
|
|
|
56
89
|
for (const proc of filtered) {
|
|
57
|
-
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
90
|
+
const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
|
|
58
91
|
const runtime = calculateRuntime(proc.timestamp);
|
|
59
92
|
const mem = isRunning ? (resourceMap.get(proc.pid)?.memory || 0) : 0;
|
|
60
93
|
|
package/src/commands/run.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CommandOptions } from "../types";
|
|
2
2
|
import { getProcess, removeProcessByName, retryDatabaseOperation, insertProcess } from "../db";
|
|
3
|
-
import { isProcessRunning, terminateProcess, getHomeDir, getShellCommand, killProcessOnPort, findChildPid, getProcessPorts, waitForPortFree } from "../platform";
|
|
3
|
+
import { isProcessRunning, terminateProcess, getHomeDir, getShellCommand, killProcessOnPort, findChildPid, getProcessPorts, waitForPortFree, psExec } from "../platform";
|
|
4
4
|
import { error, announce } from "../logger";
|
|
5
5
|
import { validateDirectory, parseEnvString } from "../utils";
|
|
6
6
|
import { parseConfigFile } from "../config";
|
|
@@ -95,12 +95,25 @@ export async function handleRun(options: CommandOptions) {
|
|
|
95
95
|
|
|
96
96
|
// Zombie sweep: kill any remaining bun processes matching this command
|
|
97
97
|
// This catches orphaned children that survived taskkill when the parent shell exited
|
|
98
|
+
// IMPORTANT: Exclude the current bgrun process and dashboard to avoid self-kill
|
|
98
99
|
const cmdToMatch = existingProcess.command;
|
|
99
100
|
if (cmdToMatch) {
|
|
100
101
|
await run.measure('Zombie sweep', async () => {
|
|
101
102
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
103
|
+
const cmdKeyword = cmdToMatch.split(' ')[1] || cmdToMatch;
|
|
104
|
+
// Skip sweep if keyword is too generic (would match unrelated processes)
|
|
105
|
+
const GENERIC_KEYWORDS = ['dev', 'run', 'start', 'serve', 'build', 'test'];
|
|
106
|
+
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
107
|
+
return; // Too dangerous — skip zombie sweep for generic commands
|
|
108
|
+
}
|
|
109
|
+
const currentPid = process.pid;
|
|
110
|
+
const result = await psExec(
|
|
111
|
+
`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${cmdKeyword.replace(/'/g, "''")}' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`,
|
|
112
|
+
3000
|
|
113
|
+
);
|
|
114
|
+
const zombiePids = result.split('\n')
|
|
115
|
+
.map((l: string) => parseInt(l.trim()))
|
|
116
|
+
.filter((n: number) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
104
117
|
for (const zPid of zombiePids) {
|
|
105
118
|
await $`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
106
119
|
}
|
|
@@ -125,6 +138,11 @@ export async function handleRun(options: CommandOptions) {
|
|
|
125
138
|
const finalCommand = command || existingProcess!.command;
|
|
126
139
|
const finalDirectory = directory || (existingProcess?.workdir!);
|
|
127
140
|
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
141
|
+
|
|
142
|
+
// Default BGR_KEEP_ALIVE=true for new processes unless explicitly disabled
|
|
143
|
+
if (!('BGR_KEEP_ALIVE' in finalEnv)) {
|
|
144
|
+
finalEnv.BGR_KEEP_ALIVE = 'true';
|
|
145
|
+
}
|
|
128
146
|
|
|
129
147
|
let finalConfigPath: string | undefined | null;
|
|
130
148
|
if (configPath !== undefined) {
|