git-watchtower 1.12.5 → 1.12.7
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/bin/git-watchtower.js +88 -31
- package/package.json +1 -1
package/bin/git-watchtower.js
CHANGED
|
@@ -3271,6 +3271,18 @@ async function startWebDashboard(openBrowser) {
|
|
|
3271
3271
|
render();
|
|
3272
3272
|
} catch (err) {
|
|
3273
3273
|
addLog(`Web dashboard failed: ${err.message}`, 'error');
|
|
3274
|
+
// Defensive: if we got far enough to arm the state-push interval,
|
|
3275
|
+
// clear it. The current ordering starts the interval only after
|
|
3276
|
+
// webDashboard.start() resolves, but this keeps cleanup robust
|
|
3277
|
+
// against future reordering and against failures in the
|
|
3278
|
+
// post-bind statements (e.g. openInBrowser, addLog).
|
|
3279
|
+
if (webStateInterval) {
|
|
3280
|
+
clearInterval(webStateInterval);
|
|
3281
|
+
webStateInterval = null;
|
|
3282
|
+
}
|
|
3283
|
+
if (webDashboard) {
|
|
3284
|
+
try { webDashboard.stop(); } catch (_) { /* ignore */ }
|
|
3285
|
+
}
|
|
3274
3286
|
if (coordinator) {
|
|
3275
3287
|
try { coordinator.stop(); } catch (_) { /* ignore */ }
|
|
3276
3288
|
}
|
|
@@ -3355,39 +3367,80 @@ function restartProcess() {
|
|
|
3355
3367
|
// ============================================================================
|
|
3356
3368
|
|
|
3357
3369
|
let isShuttingDown = false;
|
|
3370
|
+
let _resourcesCleaned = false;
|
|
3358
3371
|
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3372
|
+
/**
|
|
3373
|
+
* Idempotent, best-effort cleanup of every long-lived resource we own:
|
|
3374
|
+
* terminal state, timers, file watcher, live-reload SSE clients, the
|
|
3375
|
+
* user's dev-server child process, and the web-dashboard / coordinator
|
|
3376
|
+
* (which unlinks the lock file and IPC socket). Safe to call multiple
|
|
3377
|
+
* times and from any exit path (shutdown, uncaughtException, 'exit').
|
|
3378
|
+
*
|
|
3379
|
+
* Every step is wrapped in try/catch so a failure in one resource does
|
|
3380
|
+
* not prevent the rest from being cleaned up. Stays synchronous so it
|
|
3381
|
+
* can run inside an 'exit' handler where async callbacks won't execute.
|
|
3382
|
+
*/
|
|
3383
|
+
function cleanupResources() {
|
|
3384
|
+
if (_resourcesCleaned) return;
|
|
3385
|
+
_resourcesCleaned = true;
|
|
3386
|
+
|
|
3387
|
+
// Restore terminal first so the user sees a clean prompt even if a
|
|
3388
|
+
// later step throws.
|
|
3389
|
+
try { write(ansi.showCursor); } catch (_) { /* ignore */ }
|
|
3390
|
+
try { write(ansi.restoreScreen); } catch (_) { /* ignore */ }
|
|
3391
|
+
try { restoreTerminalTitle(); } catch (_) { /* ignore */ }
|
|
3392
|
+
try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch (_) { /* ignore */ }
|
|
3393
|
+
try { process.stdin.pause(); } catch (_) { /* ignore */ }
|
|
3362
3394
|
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3395
|
+
if (pollIntervalId) {
|
|
3396
|
+
try { clearTimeout(pollIntervalId); } catch (_) { /* ignore */ }
|
|
3397
|
+
pollIntervalId = null;
|
|
3398
|
+
}
|
|
3367
3399
|
|
|
3368
|
-
if (
|
|
3369
|
-
|
|
3400
|
+
if (periodicUpdateCheck) {
|
|
3401
|
+
try { periodicUpdateCheck.stop(); } catch (_) { /* ignore */ }
|
|
3370
3402
|
}
|
|
3371
|
-
process.stdin.pause();
|
|
3372
3403
|
|
|
3373
|
-
if (fileWatcher)
|
|
3374
|
-
|
|
3404
|
+
if (fileWatcher) {
|
|
3405
|
+
try { fileWatcher.close(); } catch (_) { /* ignore */ }
|
|
3406
|
+
fileWatcher = null;
|
|
3407
|
+
}
|
|
3375
3408
|
|
|
3376
|
-
//
|
|
3409
|
+
// Live-reload SSE clients (static mode)
|
|
3410
|
+
if (SERVER_MODE === 'static') {
|
|
3411
|
+
try {
|
|
3412
|
+
clients.forEach((client) => {
|
|
3413
|
+
try { client.end(); } catch (_) { /* ignore */ }
|
|
3414
|
+
});
|
|
3415
|
+
clients.clear();
|
|
3416
|
+
} catch (_) { /* ignore */ }
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
// User's dev-server process (command mode)
|
|
3377
3420
|
if (SERVER_MODE === 'command') {
|
|
3378
|
-
stopServerProcess();
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3421
|
+
try { stopServerProcess(); } catch (_) { /* ignore */ }
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
// Web dashboard + worker/coordinator (unlinks lock file + IPC socket)
|
|
3425
|
+
try { stopWebDashboard(); } catch (_) { /* ignore */ }
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
async function shutdown() {
|
|
3429
|
+
if (isShuttingDown) return;
|
|
3430
|
+
isShuttingDown = true;
|
|
3431
|
+
|
|
3432
|
+
cleanupResources();
|
|
3382
3433
|
|
|
3383
|
-
|
|
3384
|
-
|
|
3434
|
+
// For the static HTTP server, give in-flight connections a brief
|
|
3435
|
+
// grace period to drain. cleanupResources ended the SSE clients; this
|
|
3436
|
+
// races server.close against SERVER_CLOSE_TIMEOUT_MS so we never hang
|
|
3437
|
+
// forever on a stuck browser.
|
|
3438
|
+
if (SERVER_MODE === 'static' && server) {
|
|
3439
|
+
const serverClosePromise = new Promise((resolve) => server.close(resolve));
|
|
3440
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, SERVER_CLOSE_TIMEOUT_MS));
|
|
3385
3441
|
await Promise.race([serverClosePromise, timeoutPromise]);
|
|
3386
3442
|
}
|
|
3387
3443
|
|
|
3388
|
-
// Stop web dashboard and coordinator
|
|
3389
|
-
stopWebDashboard();
|
|
3390
|
-
|
|
3391
3444
|
// Flush telemetry
|
|
3392
3445
|
telemetry.capture('session_ended', {
|
|
3393
3446
|
duration_seconds: sessionStartTime ? Math.round((Date.now() - sessionStartTime) / 1000) : 0,
|
|
@@ -3402,19 +3455,23 @@ async function shutdown() {
|
|
|
3402
3455
|
|
|
3403
3456
|
process.on('SIGINT', shutdown);
|
|
3404
3457
|
process.on('SIGTERM', shutdown);
|
|
3405
|
-
//
|
|
3406
|
-
//
|
|
3458
|
+
// Belt-and-suspenders: if we exit via a path that didn't call
|
|
3459
|
+
// cleanupResources (e.g. a hard crash in startup before handlers were
|
|
3460
|
+
// registered), still do synchronous best-effort cleanup.
|
|
3407
3461
|
process.on('exit', () => {
|
|
3408
|
-
|
|
3462
|
+
cleanupResources();
|
|
3409
3463
|
});
|
|
3410
3464
|
process.on('uncaughtException', async (err) => {
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
if
|
|
3465
|
+
isShuttingDown = true;
|
|
3466
|
+
|
|
3467
|
+
// Synchronous teardown first — so the coordinator socket, lock file,
|
|
3468
|
+
// dev-server child process group, and SSE clients are released even
|
|
3469
|
+
// if telemetry shutdown hangs or throws.
|
|
3470
|
+
cleanupResources();
|
|
3471
|
+
|
|
3472
|
+
try { telemetry.captureError(err); } catch (_) { /* ignore */ }
|
|
3416
3473
|
console.error('Uncaught exception:', err);
|
|
3417
|
-
await telemetry.shutdown();
|
|
3474
|
+
try { await telemetry.shutdown(); } catch (_) { /* ignore */ }
|
|
3418
3475
|
process.exit(1);
|
|
3419
3476
|
});
|
|
3420
3477
|
|
package/package.json
CHANGED