git-watchtower 1.12.5 → 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.
@@ -3355,39 +3355,80 @@ function restartProcess() {
3355
3355
  // ============================================================================
3356
3356
 
3357
3357
  let isShuttingDown = false;
3358
+ let _resourcesCleaned = false;
3358
3359
 
3359
- async function shutdown() {
3360
- if (isShuttingDown) return;
3361
- isShuttingDown = true;
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 */ }
3362
3382
 
3363
- // Restore terminal
3364
- write(ansi.showCursor);
3365
- write(ansi.restoreScreen);
3366
- restoreTerminalTitle();
3383
+ if (pollIntervalId) {
3384
+ try { clearTimeout(pollIntervalId); } catch (_) { /* ignore */ }
3385
+ pollIntervalId = null;
3386
+ }
3367
3387
 
3368
- if (process.stdin.isTTY) {
3369
- process.stdin.setRawMode(false);
3388
+ if (periodicUpdateCheck) {
3389
+ try { periodicUpdateCheck.stop(); } catch (_) { /* ignore */ }
3370
3390
  }
3371
- process.stdin.pause();
3372
3391
 
3373
- if (fileWatcher) fileWatcher.close();
3374
- if (pollIntervalId) clearTimeout(pollIntervalId);
3392
+ if (fileWatcher) {
3393
+ try { fileWatcher.close(); } catch (_) { /* ignore */ }
3394
+ fileWatcher = null;
3395
+ }
3375
3396
 
3376
- // Stop server based on mode
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
+ }
3406
+
3407
+ // User's dev-server process (command mode)
3377
3408
  if (SERVER_MODE === 'command') {
3378
- stopServerProcess();
3379
- } else if (SERVER_MODE === 'static') {
3380
- clients.forEach(client => client.end());
3381
- clients.clear();
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();
3382
3421
 
3383
- const serverClosePromise = new Promise(resolve => server.close(resolve));
3384
- const timeoutPromise = new Promise(resolve => setTimeout(resolve, SERVER_CLOSE_TIMEOUT_MS));
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));
3385
3429
  await Promise.race([serverClosePromise, timeoutPromise]);
3386
3430
  }
3387
3431
 
3388
- // Stop web dashboard and coordinator
3389
- stopWebDashboard();
3390
-
3391
3432
  // Flush telemetry
3392
3433
  telemetry.capture('session_ended', {
3393
3434
  duration_seconds: sessionStartTime ? Math.round((Date.now() - sessionStartTime) / 1000) : 0,
@@ -3402,19 +3443,23 @@ async function shutdown() {
3402
3443
 
3403
3444
  process.on('SIGINT', shutdown);
3404
3445
  process.on('SIGTERM', shutdown);
3405
- // Clean up long-lived timers on exit, regardless of which code path got us
3406
- // here (normal exit, uncaught exception, early failure in start()).
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.
3407
3449
  process.on('exit', () => {
3408
- if (periodicUpdateCheck) periodicUpdateCheck.stop();
3450
+ cleanupResources();
3409
3451
  });
3410
3452
  process.on('uncaughtException', async (err) => {
3411
- telemetry.captureError(err);
3412
- write(ansi.showCursor);
3413
- write(ansi.restoreScreen);
3414
- restoreTerminalTitle();
3415
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
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 */ }
3416
3461
  console.error('Uncaught exception:', err);
3417
- await telemetry.shutdown();
3462
+ try { await telemetry.shutdown(); } catch (_) { /* ignore */ }
3418
3463
  process.exit(1);
3419
3464
  });
3420
3465
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.12.5",
3
+ "version": "1.12.6",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {