git-watchtower 2.3.16 → 2.3.18
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 +8 -4
- package/package.json +1 -1
- package/src/server/coordinator.js +20 -1
- package/src/server/static.js +40 -0
package/bin/git-watchtower.js
CHANGED
|
@@ -878,7 +878,7 @@ const { parseDiffStats, stash: gitStash, stashPop: gitStashPop } = require('../s
|
|
|
878
878
|
|
|
879
879
|
// Server process command parsing and static server utilities
|
|
880
880
|
const { parseCommand } = require('../src/server/process');
|
|
881
|
-
const { getMimeType, injectLiveReload, resolveStaticPath } = require('../src/server/static');
|
|
881
|
+
const { getMimeType, injectLiveReload, resolveStaticPath, broadcastReload } = require('../src/server/static');
|
|
882
882
|
|
|
883
883
|
// State (non-store globals)
|
|
884
884
|
let previousBranchStates = new Map(); // branch name -> commit hash
|
|
@@ -2299,9 +2299,13 @@ function restartPolling() {
|
|
|
2299
2299
|
|
|
2300
2300
|
function notifyClients() {
|
|
2301
2301
|
if (NO_SERVER) return; // No clients in no-server mode
|
|
2302
|
-
clients.
|
|
2303
|
-
|
|
2304
|
-
|
|
2302
|
+
if (clients.size === 0) return;
|
|
2303
|
+
const { delivered, dropped } = broadcastReload(clients);
|
|
2304
|
+
if (delivered > 0) {
|
|
2305
|
+
addLog(`Reloading ${delivered} browser(s)`, 'info');
|
|
2306
|
+
}
|
|
2307
|
+
if (dropped > 0) {
|
|
2308
|
+
addLog(`Dropped ${dropped} dead live-reload client(s)`, 'warning');
|
|
2305
2309
|
}
|
|
2306
2310
|
}
|
|
2307
2311
|
|
package/package.json
CHANGED
|
@@ -108,6 +108,24 @@ function ensureDir() {
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Check if a process with the given PID is alive.
|
|
111
|
+
*
|
|
112
|
+
* `process.kill(pid, 0)` is the standard "is this PID alive?" probe. It
|
|
113
|
+
* sends signal 0 (no-op) and surfaces the kernel's answer via errno:
|
|
114
|
+
*
|
|
115
|
+
* - resolves cleanly → process exists and we can signal it
|
|
116
|
+
* - throws ESRCH → no such process; safe to call dead
|
|
117
|
+
* - throws EPERM → process exists but is owned by another
|
|
118
|
+
* user or in a different cgroup. STILL
|
|
119
|
+
* alive — we just can't signal it.
|
|
120
|
+
*
|
|
121
|
+
* Treating EPERM as "dead" was a real bug for the coordinator lock: if
|
|
122
|
+
* a coordinator's PID was reused by another local user's process after
|
|
123
|
+
* a crash, a peer instance would read the lock, see EPERM, decide the
|
|
124
|
+
* coordinator was dead, unlink the lock, and try to take over while
|
|
125
|
+
* the original (or reused) PID was still running. Mirroring the same
|
|
126
|
+
* EPERM-aware check used by src/utils/monitor-lock.js prevents that
|
|
127
|
+
* cleanup-then-clobber race.
|
|
128
|
+
*
|
|
111
129
|
* @param {number} pid
|
|
112
130
|
* @returns {boolean}
|
|
113
131
|
*/
|
|
@@ -116,7 +134,8 @@ function isProcessAlive(pid) {
|
|
|
116
134
|
process.kill(pid, 0);
|
|
117
135
|
return true;
|
|
118
136
|
} catch (e) {
|
|
119
|
-
|
|
137
|
+
// ESRCH = no such process; EPERM = exists but owned by another user.
|
|
138
|
+
return e.code === 'EPERM';
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
|
package/src/server/static.js
CHANGED
|
@@ -125,10 +125,50 @@ function resolveStaticPath(candidate, realStaticDir) {
|
|
|
125
125
|
return { status: 'ok', path: realPath };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Broadcast an SSE frame to every live-reload client, isolating per-client
|
|
130
|
+
* write failures so one dead socket can't abort the whole iteration.
|
|
131
|
+
*
|
|
132
|
+
* Previously this was a one-liner in bin/git-watchtower.js:
|
|
133
|
+
* clients.forEach(c => c.write('data: reload\n\n'));
|
|
134
|
+
* If any client's underlying socket had been destroyed (browser tab
|
|
135
|
+
* closed, network reset, proxy hangup) without 'close' firing yet, the
|
|
136
|
+
* synchronous write threw `ERR_STREAM_DESTROYED` and aborted the
|
|
137
|
+
* forEach mid-iteration — every later client in the Set never received
|
|
138
|
+
* the reload event. Wrapping each write in try/catch and removing the
|
|
139
|
+
* failed client from the Set keeps the broadcast atomic-per-client and
|
|
140
|
+
* also prunes the dead entry so the next call doesn't trip on it again.
|
|
141
|
+
*
|
|
142
|
+
* @param {Set<{write: function, end?: function}>} clients - SSE response objects
|
|
143
|
+
* @param {string} [frame='data: reload\n\n'] - Pre-formatted SSE frame
|
|
144
|
+
* @returns {{delivered: number, dropped: number}}
|
|
145
|
+
*/
|
|
146
|
+
function broadcastReload(clients, frame) {
|
|
147
|
+
const message = frame || 'data: reload\n\n';
|
|
148
|
+
let delivered = 0;
|
|
149
|
+
let dropped = 0;
|
|
150
|
+
// Iterate a snapshot — Set.delete during forEach is safe in V8, but
|
|
151
|
+
// copying makes the contract explicit and survives any future
|
|
152
|
+
// iterator-protocol changes.
|
|
153
|
+
for (const client of Array.from(clients)) {
|
|
154
|
+
try {
|
|
155
|
+
client.write(message);
|
|
156
|
+
delivered++;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
// Dead socket. Drop it and keep going so subsequent clients still
|
|
159
|
+
// see the broadcast.
|
|
160
|
+
clients.delete(client);
|
|
161
|
+
dropped++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { delivered, dropped };
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
module.exports = {
|
|
129
168
|
MIME_TYPES,
|
|
130
169
|
getMimeType,
|
|
131
170
|
LIVE_RELOAD_SCRIPT,
|
|
132
171
|
injectLiveReload,
|
|
133
172
|
resolveStaticPath,
|
|
173
|
+
broadcastReload,
|
|
134
174
|
};
|