git-watchtower 1.12.4 → 1.12.6
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 +84 -31
- package/package.json +1 -1
package/bin/git-watchtower.js
CHANGED
|
@@ -2059,13 +2059,21 @@ async function pollGitChanges() {
|
|
|
2059
2059
|
}
|
|
2060
2060
|
|
|
2061
2061
|
function schedulePoll() {
|
|
2062
|
+
// Bail out if shutdown has started: both here (no new timer) and again
|
|
2063
|
+
// inside the timer callback after each await (the in-flight poll may
|
|
2064
|
+
// have started before shutdown() cleared pollIntervalId, and clearTimeout
|
|
2065
|
+
// on a timer whose callback is already executing is a no-op).
|
|
2066
|
+
if (isShuttingDown) return;
|
|
2062
2067
|
pollIntervalId = setTimeout(async () => {
|
|
2068
|
+
if (isShuttingDown) return;
|
|
2063
2069
|
await pollGitChanges();
|
|
2070
|
+
if (isShuttingDown) return;
|
|
2064
2071
|
schedulePoll();
|
|
2065
2072
|
}, store.get('adaptivePollInterval'));
|
|
2066
2073
|
}
|
|
2067
2074
|
|
|
2068
2075
|
function restartPolling() {
|
|
2076
|
+
if (isShuttingDown) return;
|
|
2069
2077
|
if (pollIntervalId) {
|
|
2070
2078
|
clearTimeout(pollIntervalId);
|
|
2071
2079
|
}
|
|
@@ -3347,39 +3355,80 @@ function restartProcess() {
|
|
|
3347
3355
|
// ============================================================================
|
|
3348
3356
|
|
|
3349
3357
|
let isShuttingDown = false;
|
|
3358
|
+
let _resourcesCleaned = false;
|
|
3350
3359
|
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3360
|
+
/**
|
|
3361
|
+
* Idempotent, best-effort cleanup of every long-lived resource we own:
|
|
3362
|
+
* terminal state, timers, file watcher, live-reload SSE clients, the
|
|
3363
|
+
* user's dev-server child process, and the web-dashboard / coordinator
|
|
3364
|
+
* (which unlinks the lock file and IPC socket). Safe to call multiple
|
|
3365
|
+
* times and from any exit path (shutdown, uncaughtException, 'exit').
|
|
3366
|
+
*
|
|
3367
|
+
* Every step is wrapped in try/catch so a failure in one resource does
|
|
3368
|
+
* not prevent the rest from being cleaned up. Stays synchronous so it
|
|
3369
|
+
* can run inside an 'exit' handler where async callbacks won't execute.
|
|
3370
|
+
*/
|
|
3371
|
+
function cleanupResources() {
|
|
3372
|
+
if (_resourcesCleaned) return;
|
|
3373
|
+
_resourcesCleaned = true;
|
|
3374
|
+
|
|
3375
|
+
// Restore terminal first so the user sees a clean prompt even if a
|
|
3376
|
+
// later step throws.
|
|
3377
|
+
try { write(ansi.showCursor); } catch (_) { /* ignore */ }
|
|
3378
|
+
try { write(ansi.restoreScreen); } catch (_) { /* ignore */ }
|
|
3379
|
+
try { restoreTerminalTitle(); } catch (_) { /* ignore */ }
|
|
3380
|
+
try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch (_) { /* ignore */ }
|
|
3381
|
+
try { process.stdin.pause(); } catch (_) { /* ignore */ }
|
|
3354
3382
|
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3383
|
+
if (pollIntervalId) {
|
|
3384
|
+
try { clearTimeout(pollIntervalId); } catch (_) { /* ignore */ }
|
|
3385
|
+
pollIntervalId = null;
|
|
3386
|
+
}
|
|
3359
3387
|
|
|
3360
|
-
if (
|
|
3361
|
-
|
|
3388
|
+
if (periodicUpdateCheck) {
|
|
3389
|
+
try { periodicUpdateCheck.stop(); } catch (_) { /* ignore */ }
|
|
3362
3390
|
}
|
|
3363
|
-
process.stdin.pause();
|
|
3364
3391
|
|
|
3365
|
-
if (fileWatcher)
|
|
3366
|
-
|
|
3392
|
+
if (fileWatcher) {
|
|
3393
|
+
try { fileWatcher.close(); } catch (_) { /* ignore */ }
|
|
3394
|
+
fileWatcher = null;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
// Live-reload SSE clients (static mode)
|
|
3398
|
+
if (SERVER_MODE === 'static') {
|
|
3399
|
+
try {
|
|
3400
|
+
clients.forEach((client) => {
|
|
3401
|
+
try { client.end(); } catch (_) { /* ignore */ }
|
|
3402
|
+
});
|
|
3403
|
+
clients.clear();
|
|
3404
|
+
} catch (_) { /* ignore */ }
|
|
3405
|
+
}
|
|
3367
3406
|
|
|
3368
|
-
//
|
|
3407
|
+
// User's dev-server process (command mode)
|
|
3369
3408
|
if (SERVER_MODE === 'command') {
|
|
3370
|
-
stopServerProcess();
|
|
3371
|
-
}
|
|
3372
|
-
|
|
3373
|
-
|
|
3409
|
+
try { stopServerProcess(); } catch (_) { /* ignore */ }
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
// Web dashboard + worker/coordinator (unlinks lock file + IPC socket)
|
|
3413
|
+
try { stopWebDashboard(); } catch (_) { /* ignore */ }
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
async function shutdown() {
|
|
3417
|
+
if (isShuttingDown) return;
|
|
3418
|
+
isShuttingDown = true;
|
|
3419
|
+
|
|
3420
|
+
cleanupResources();
|
|
3374
3421
|
|
|
3375
|
-
|
|
3376
|
-
|
|
3422
|
+
// For the static HTTP server, give in-flight connections a brief
|
|
3423
|
+
// grace period to drain. cleanupResources ended the SSE clients; this
|
|
3424
|
+
// races server.close against SERVER_CLOSE_TIMEOUT_MS so we never hang
|
|
3425
|
+
// forever on a stuck browser.
|
|
3426
|
+
if (SERVER_MODE === 'static' && server) {
|
|
3427
|
+
const serverClosePromise = new Promise((resolve) => server.close(resolve));
|
|
3428
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, SERVER_CLOSE_TIMEOUT_MS));
|
|
3377
3429
|
await Promise.race([serverClosePromise, timeoutPromise]);
|
|
3378
3430
|
}
|
|
3379
3431
|
|
|
3380
|
-
// Stop web dashboard and coordinator
|
|
3381
|
-
stopWebDashboard();
|
|
3382
|
-
|
|
3383
3432
|
// Flush telemetry
|
|
3384
3433
|
telemetry.capture('session_ended', {
|
|
3385
3434
|
duration_seconds: sessionStartTime ? Math.round((Date.now() - sessionStartTime) / 1000) : 0,
|
|
@@ -3394,19 +3443,23 @@ async function shutdown() {
|
|
|
3394
3443
|
|
|
3395
3444
|
process.on('SIGINT', shutdown);
|
|
3396
3445
|
process.on('SIGTERM', shutdown);
|
|
3397
|
-
//
|
|
3398
|
-
//
|
|
3446
|
+
// Belt-and-suspenders: if we exit via a path that didn't call
|
|
3447
|
+
// cleanupResources (e.g. a hard crash in startup before handlers were
|
|
3448
|
+
// registered), still do synchronous best-effort cleanup.
|
|
3399
3449
|
process.on('exit', () => {
|
|
3400
|
-
|
|
3450
|
+
cleanupResources();
|
|
3401
3451
|
});
|
|
3402
3452
|
process.on('uncaughtException', async (err) => {
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
if
|
|
3453
|
+
isShuttingDown = true;
|
|
3454
|
+
|
|
3455
|
+
// Synchronous teardown first — so the coordinator socket, lock file,
|
|
3456
|
+
// dev-server child process group, and SSE clients are released even
|
|
3457
|
+
// if telemetry shutdown hangs or throws.
|
|
3458
|
+
cleanupResources();
|
|
3459
|
+
|
|
3460
|
+
try { telemetry.captureError(err); } catch (_) { /* ignore */ }
|
|
3408
3461
|
console.error('Uncaught exception:', err);
|
|
3409
|
-
await telemetry.shutdown();
|
|
3462
|
+
try { await telemetry.shutdown(); } catch (_) { /* ignore */ }
|
|
3410
3463
|
process.exit(1);
|
|
3411
3464
|
});
|
|
3412
3465
|
|
package/package.json
CHANGED