gitdone-agent 0.7.0 → 0.7.1
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/index.js +120 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
27
27
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
28
28
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
29
29
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
30
|
-
const AGENT_VERSION = '0.7.
|
|
30
|
+
const AGENT_VERSION = '0.7.1'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -253,6 +253,99 @@ function stopRunningAgents() {
|
|
|
253
253
|
} catch { /* best-effort — at worst the old agent lingers until next login */ }
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
// ─── Watchdog (gd-431) ────────────────────────────────────────────────────────
|
|
257
|
+
// Third, OUTER layer of crash safety. Layers 1-2 (survive() in-process, the
|
|
258
|
+
// run-agent.cmd supervisor loop) still leave two ways for a machine to stay
|
|
259
|
+
// offline: the supervisor cmd.exe itself dying (nothing restarts IT), and the
|
|
260
|
+
// agent HANGING without exiting (process alive → supervisor waits forever).
|
|
261
|
+
// So: the agent touches a heartbeat file every 30s, and a Windows scheduled
|
|
262
|
+
// task runs watchdog.ps1 every 5 minutes (hidden via watchdog.vbs). A stale
|
|
263
|
+
// heartbeat (>10 min) means dead OR hung → the watchdog kills any leftover
|
|
264
|
+
// agent/supervisor processes and relaunches run-agent.cmd. The relaunch goes
|
|
265
|
+
// through WMI Win32_Process.Create so the agent does NOT live inside the task's
|
|
266
|
+
// job object — otherwise Task Scheduler would consider the task "still running"
|
|
267
|
+
// forever and never fire it again (and stopping the task would kill the agent).
|
|
268
|
+
//
|
|
269
|
+
// Broken-session guard (gd-405): if a restart attempt does NOT revive the
|
|
270
|
+
// heartbeat (0xc0000142 storms after a hardware crash — every start pops a
|
|
271
|
+
// blocking error dialog), the watchdog backs off to one attempt per 30 min via
|
|
272
|
+
// a stamp file instead of retrying every 5 — and a fresh heartbeat clears it.
|
|
273
|
+
const WATCHDOG_TASK = 'GitdoneAgentWatchdog'
|
|
274
|
+
const WATCHDOG_PS1 = join(AGENT_DIR, 'watchdog.ps1')
|
|
275
|
+
const WATCHDOG_VBS = join(AGENT_DIR, 'watchdog.vbs')
|
|
276
|
+
const HEARTBEAT_PATH = join(AGENT_DIR, 'heartbeat.txt')
|
|
277
|
+
|
|
278
|
+
function startHeartbeat() {
|
|
279
|
+
const beat = () => {
|
|
280
|
+
try { ensureAgentDir(); writeFileSync(HEARTBEAT_PATH, new Date().toISOString()) } catch { /* best-effort */ }
|
|
281
|
+
}
|
|
282
|
+
beat()
|
|
283
|
+
setInterval(beat, 30_000)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Idempotent: (re)writes the watchdog scripts and (re)creates the scheduled
|
|
287
|
+
// task. Called on EVERY agent start — not just --install — so an in-place
|
|
288
|
+
// update (gd-420) gains the watchdog too, and a deleted task self-heals.
|
|
289
|
+
// The ps1 stays ASCII-only: PowerShell 5.1 misreads UTF-8-without-BOM files.
|
|
290
|
+
function ensureWatchdog() {
|
|
291
|
+
if (process.platform !== 'win32') return
|
|
292
|
+
try {
|
|
293
|
+
ensureAgentDir()
|
|
294
|
+
const runCmd = join(AGENT_DIR, 'run-agent.cmd')
|
|
295
|
+
const ps1 = [
|
|
296
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
297
|
+
`$dir = '${AGENT_DIR.replace(/'/g, "''")}'`,
|
|
298
|
+
"$hb = Join-Path $dir 'heartbeat.txt'",
|
|
299
|
+
"$stamp = Join-Path $dir 'watchdog-restart.stamp'",
|
|
300
|
+
"$wlog = Join-Path $dir 'watchdog.log'",
|
|
301
|
+
"$runCmd = Join-Path $dir 'run-agent.cmd'",
|
|
302
|
+
'function WLog([string]$m) { Add-Content -Path $wlog -Value ("[{0}] {1}" -f (Get-Date -Format \'yyyy-MM-dd HH:mm:ss\'), $m) }',
|
|
303
|
+
'if ((Test-Path $wlog) -and ((Get-Item $wlog).Length -gt 1048576)) { Move-Item -Path $wlog -Destination ($wlog + \'.old\') -Force }',
|
|
304
|
+
'# Fresh heartbeat (<10 min) = agent alive and ticking - nothing to do.',
|
|
305
|
+
'if ((Test-Path $hb) -and (((Get-Date) - (Get-Item $hb).LastWriteTime).TotalSeconds -lt 600)) {',
|
|
306
|
+
' if (Test-Path $stamp) { Remove-Item -Path $stamp -Force }',
|
|
307
|
+
' exit 0',
|
|
308
|
+
'}',
|
|
309
|
+
'# Last restart attempt did not revive the heartbeat - broken login session',
|
|
310
|
+
'# (e.g. 0xc0000142 after a hardware crash). Back off to one try per 30 min',
|
|
311
|
+
'# so retries cannot turn into a blocking-dialog storm (gd-405).',
|
|
312
|
+
'if ((Test-Path $stamp) -and (((Get-Date) - (Get-Item $stamp).LastWriteTime).TotalMinutes -lt 30)) { exit 0 }',
|
|
313
|
+
'New-Item -ItemType File -Path $stamp -Force | Out-Null',
|
|
314
|
+
"if (-not (Test-Path $runCmd)) { WLog 'run-agent.cmd missing - cannot restart; run gitdone-agent --install'; exit 0 }",
|
|
315
|
+
"WLog 'heartbeat stale - killing leftover agent processes and restarting'",
|
|
316
|
+
'# Same sweep as stopRunningAgents(): supervisors first, then agents, so a',
|
|
317
|
+
'# surviving supervisor cannot respawn a second agent mid-cleanup.',
|
|
318
|
+
'Get-CimInstance Win32_Process -Filter "Name=\'cmd.exe\'" | Where-Object { $_.CommandLine -like \'*run-agent.cmd*\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
|
|
319
|
+
'Get-CimInstance Win32_Process -Filter "Name=\'node.exe\'" | Where-Object { $_.CommandLine -like \'*gitdone-agent*\' -or $_.CommandLine -like \'*agent.mjs*\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
|
|
320
|
+
'Start-Sleep -Seconds 2',
|
|
321
|
+
'# WMI Create = the new process tree lives OUTSIDE the scheduled task job.',
|
|
322
|
+
'# (wmiclass accelerator, not Invoke-CimMethod - CIM cannot marshal the',
|
|
323
|
+
'# embedded Win32_ProcessStartup instance and dies with "Type mismatch".)',
|
|
324
|
+
"$sup = ([wmiclass]'Win32_ProcessStartup').CreateInstance()",
|
|
325
|
+
'$sup.ShowWindow = 0',
|
|
326
|
+
"$r = ([wmiclass]'Win32_Process').Create(('\"' + $env:ComSpec + '\" /c \"\"' + $runCmd + '\"\"'), $null, $sup)",
|
|
327
|
+
'WLog ("restart issued (ReturnValue={0}, Pid={1})" -f $r.ReturnValue, $r.ProcessId)',
|
|
328
|
+
].join('\r\n')
|
|
329
|
+
writeFileSync(WATCHDOG_PS1, ps1, 'utf8')
|
|
330
|
+
|
|
331
|
+
// VBS wrapper so the 5-minute task never flashes a window.
|
|
332
|
+
const vbs = [
|
|
333
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
334
|
+
`WshShell.Run "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ""${WATCHDOG_PS1}""", 0, True`,
|
|
335
|
+
].join('\r\n')
|
|
336
|
+
writeFileSync(WATCHDOG_VBS, vbs, 'utf8')
|
|
337
|
+
|
|
338
|
+
execFileSync('schtasks', [
|
|
339
|
+
'/Create', '/F', '/SC', 'MINUTE', '/MO', '5',
|
|
340
|
+
'/TN', WATCHDOG_TASK,
|
|
341
|
+
'/TR', `wscript.exe "${WATCHDOG_VBS}"`,
|
|
342
|
+
], { stdio: 'ignore', timeout: 30_000 })
|
|
343
|
+
log(`✓ watchdog: планирана задача „${WATCHDOG_TASK}" проверява агента на всеки 5 мин`)
|
|
344
|
+
} catch (err) {
|
|
345
|
+
log(`✗ watchdog setup failed: ${err?.message || err}`)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
256
349
|
function installStartup() {
|
|
257
350
|
ensureAgentDir()
|
|
258
351
|
const nodePath = getNodePath()
|
|
@@ -317,6 +410,9 @@ function installStartup() {
|
|
|
317
410
|
].join('\r\n')
|
|
318
411
|
|
|
319
412
|
writeFileSync(getVbsPath(), vbs, 'utf8')
|
|
413
|
+
|
|
414
|
+
// Outer safety layer: the 5-minute watchdog task (gd-431).
|
|
415
|
+
ensureWatchdog()
|
|
320
416
|
}
|
|
321
417
|
|
|
322
418
|
function uninstallStartup() {
|
|
@@ -329,6 +425,16 @@ function uninstallStartup() {
|
|
|
329
425
|
}
|
|
330
426
|
const cmdPath = join(AGENT_DIR, 'run-agent.cmd')
|
|
331
427
|
if (existsSync(cmdPath)) unlinkSync(cmdPath)
|
|
428
|
+
// Drop the watchdog task too — otherwise it resurrects the agent in ≤5 min.
|
|
429
|
+
if (process.platform === 'win32') {
|
|
430
|
+
try {
|
|
431
|
+
execFileSync('schtasks', ['/Delete', '/F', '/TN', WATCHDOG_TASK], { stdio: 'ignore', timeout: 30_000 })
|
|
432
|
+
console.log('✓ Премахнат watchdog (планирана задача)')
|
|
433
|
+
} catch { /* not installed */ }
|
|
434
|
+
}
|
|
435
|
+
for (const f of [WATCHDOG_PS1, WATCHDOG_VBS, HEARTBEAT_PATH]) {
|
|
436
|
+
if (existsSync(f)) unlinkSync(f)
|
|
437
|
+
}
|
|
332
438
|
// Also stop the live supervisor + agent — otherwise they keep running (and
|
|
333
439
|
// the supervisor keeps resurrecting the agent) until the next reboot.
|
|
334
440
|
stopRunningAgents()
|
|
@@ -344,6 +450,14 @@ function runDoctor() {
|
|
|
344
450
|
console.log(` stable agent : ${STABLE_AGENT} ${existsSync(STABLE_AGENT) ? '(ok)' : '(not installed)'}`)
|
|
345
451
|
console.log(` autostart vbs : ${getVbsPath()} ${existsSync(getVbsPath()) ? '(ok)' : '(not installed)'}`)
|
|
346
452
|
console.log(` supervisor : ${join(AGENT_DIR, 'run-agent.cmd')} ${existsSync(join(AGENT_DIR, 'run-agent.cmd')) ? '(ok)' : '(not installed — пусни --install за авторестарт при crash)'}`)
|
|
453
|
+
if (process.platform === 'win32') {
|
|
454
|
+
let wd = '(not installed — пусни --install или рестартирай агента)'
|
|
455
|
+
try { execFileSync('schtasks', ['/Query', '/TN', WATCHDOG_TASK], { stdio: 'ignore', timeout: 30_000 }); wd = '(ok — на всеки 5 мин)' } catch { /* missing */ }
|
|
456
|
+
console.log(` watchdog : ${WATCHDOG_TASK} ${wd}`)
|
|
457
|
+
}
|
|
458
|
+
let hb = '(none yet)'
|
|
459
|
+
try { hb = `(последен: преди ${Math.round((Date.now() - statSync(HEARTBEAT_PATH).mtimeMs) / 1000)}s)` } catch { /* no beat yet */ }
|
|
460
|
+
console.log(` heartbeat : ${HEARTBEAT_PATH} ${hb}`)
|
|
347
461
|
console.log(` config : ${CONFIG_PATH} ${existsSync(CONFIG_PATH) ? '(ok)' : '(MISSING)'}`)
|
|
348
462
|
console.log(` log file : ${LOG_PATH} ${existsSync(LOG_PATH) ? '(ok)' : '(none yet)'}`)
|
|
349
463
|
console.log(` crash log : ${join(AGENT_DIR, 'agent-crash.log')} ${existsSync(join(AGENT_DIR, 'agent-crash.log')) ? '(има записи — виж го при проблеми)' : '(празен — няма крашове)'}`)
|
|
@@ -1809,6 +1923,11 @@ async function streamCommands(cfg) {
|
|
|
1809
1923
|
|
|
1810
1924
|
async function runLoop(cfg) {
|
|
1811
1925
|
ensureSingleInstance()
|
|
1926
|
+
// Watchdog food: a heartbeat file every 30s. Stale >10 min (dead OR hung
|
|
1927
|
+
// event loop) → the scheduled task restarts us (gd-431). Registered on every
|
|
1928
|
+
// start so in-place-updated agents get the watchdog without a re-install.
|
|
1929
|
+
startHeartbeat()
|
|
1930
|
+
ensureWatchdog()
|
|
1812
1931
|
log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
|
|
1813
1932
|
log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
|
|
1814
1933
|
|