plum-e2e 2.6.4 → 2.6.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.
|
@@ -250,6 +250,40 @@ function stopNode(id, fallbackPort = null) {
|
|
|
250
250
|
return signalled;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Frees a TCP port by killing whatever process is bound to it, so Start can't
|
|
255
|
+
* fail with EADDRINUSE against a stale/orphaned process this manager lost
|
|
256
|
+
* track of (e.g. one that ignored SIGTERM from a previous stop, or was never
|
|
257
|
+
* in the registry to begin with). SIGTERM first, escalating to SIGKILL if the
|
|
258
|
+
* process is still alive after a short grace period.
|
|
259
|
+
*
|
|
260
|
+
* Returns true if a process was found (and signalled), false if the port was
|
|
261
|
+
* already free.
|
|
262
|
+
*/
|
|
263
|
+
async function killPort(port) {
|
|
264
|
+
let pid = findPidOnPort(port);
|
|
265
|
+
if (!pid) return false;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
process.kill(pid, 'SIGTERM');
|
|
269
|
+
} catch {}
|
|
270
|
+
|
|
271
|
+
const deadline = Date.now() + 2000;
|
|
272
|
+
while (Date.now() < deadline && isAlive(pid)) {
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
pid = findPidOnPort(port);
|
|
277
|
+
if (pid && isAlive(pid)) {
|
|
278
|
+
try {
|
|
279
|
+
process.kill(pid, 'SIGKILL');
|
|
280
|
+
} catch {}
|
|
281
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
253
287
|
module.exports = {
|
|
254
288
|
BACKEND_DIR,
|
|
255
289
|
LOGS_DIR,
|
|
@@ -260,6 +294,7 @@ module.exports = {
|
|
|
260
294
|
isLocalUrl,
|
|
261
295
|
parsePort,
|
|
262
296
|
findPidOnPort,
|
|
297
|
+
killPort,
|
|
263
298
|
pruneDead,
|
|
264
299
|
statusOf,
|
|
265
300
|
prepareEnv,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 3999 (node/runner mode)
|
|
@@ -40,7 +40,8 @@ const {
|
|
|
40
40
|
prepareEnv,
|
|
41
41
|
startNode,
|
|
42
42
|
stopNode,
|
|
43
|
-
findPidOnPort
|
|
43
|
+
findPidOnPort,
|
|
44
|
+
killPort
|
|
44
45
|
} = runnerProcess;
|
|
45
46
|
const { generateToken, registerWithPrimary, detectLanIp } = nodeRegister;
|
|
46
47
|
|
|
@@ -139,9 +140,8 @@ async function describeRunners() {
|
|
|
139
140
|
function statusBadge(r) {
|
|
140
141
|
const dot = r.online ? pc.green('●') : pc.dim('○');
|
|
141
142
|
let detail;
|
|
142
|
-
if (
|
|
143
|
-
else if (r.
|
|
144
|
-
else if (r.online) detail = pc.yellow('running (unmanaged)');
|
|
143
|
+
if (r.managed) detail = pc.green('running');
|
|
144
|
+
else if (r.online) detail = pc.yellow(r.local ? 'running (unmanaged)' : 'running (remote)');
|
|
145
145
|
else detail = pc.dim('stopped');
|
|
146
146
|
return `${dot} ${detail}`;
|
|
147
147
|
}
|
|
@@ -184,10 +184,15 @@ async function runAction(r) {
|
|
|
184
184
|
{ value: 'restart', label: pc.yellow('Restart') },
|
|
185
185
|
{ value: 'ping', label: 'Ping' }
|
|
186
186
|
);
|
|
187
|
-
} else if (r.local) {
|
|
188
|
-
options.push({ value: 'start', label: pc.green('Start') }, { value: 'ping', label: 'Ping' });
|
|
189
187
|
} else {
|
|
190
|
-
|
|
188
|
+
// Offline and nothing is listening to control remotely — offer Start
|
|
189
|
+
// unconditionally rather than gating on address-based "is this local"
|
|
190
|
+
// detection. That heuristic breaks the moment this machine's address
|
|
191
|
+
// differs from what was registered (DHCP renewal, VPN, multiple NICs),
|
|
192
|
+
// permanently trapping an offline runner with no way back. Starting
|
|
193
|
+
// here is always safe to attempt: it spawns a managed process this menu
|
|
194
|
+
// can immediately Stop again if the address guess was wrong.
|
|
195
|
+
options.push({ value: 'start', label: pc.green('Start') }, { value: 'ping', label: 'Ping' });
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
options.push(
|
|
@@ -202,12 +207,18 @@ async function runAction(r) {
|
|
|
202
207
|
const port = parsePort(r.url);
|
|
203
208
|
|
|
204
209
|
if (action === 'start') {
|
|
210
|
+
const s = clack.spinner();
|
|
211
|
+
s.start(`Freeing port ${port}...`);
|
|
212
|
+
const killed = await killPort(Number(port));
|
|
213
|
+
s.stop(killed ? pc.dim(`Freed port ${port}`) : pc.dim(`Port ${port} was already free`));
|
|
214
|
+
|
|
205
215
|
prepareNodeEnv();
|
|
206
216
|
const entry = startNode({ id: r.id, port, token: r.token });
|
|
207
217
|
clack.log.success(pc.green(`Started "${r.name}" on port ${port} (pid ${entry.pid})`));
|
|
208
218
|
} else if (action === 'stop') {
|
|
209
219
|
if (r.managed) {
|
|
210
220
|
const ok = stopNode(r.id);
|
|
221
|
+
await killPort(Number(port));
|
|
211
222
|
clack.log.success(
|
|
212
223
|
ok ? pc.green(`Stopped "${r.name}"`) : pc.dim(`"${r.name}" was not running`)
|
|
213
224
|
);
|
|
@@ -219,6 +230,11 @@ async function runAction(r) {
|
|
|
219
230
|
s.stop(pc.green(`Stopped "${r.name}"`));
|
|
220
231
|
} catch (e) {
|
|
221
232
|
s.stop(pc.red(`Could not stop "${r.name}": ${e.message}`));
|
|
233
|
+
} finally {
|
|
234
|
+
// Belt-and-suspenders: if this runner happens to be local (or
|
|
235
|
+
// the network shutdown call above silently failed), make sure
|
|
236
|
+
// nothing is left bound to its port.
|
|
237
|
+
await killPort(Number(port));
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
240
|
} else if (action === 'restart') {
|
|
@@ -226,7 +242,7 @@ async function runAction(r) {
|
|
|
226
242
|
const s = clack.spinner();
|
|
227
243
|
s.start(`Restarting "${r.name}"...`);
|
|
228
244
|
stopNode(r.id);
|
|
229
|
-
await
|
|
245
|
+
await killPort(Number(port));
|
|
230
246
|
const entry = startNode({ id: r.id, port, token: r.token });
|
|
231
247
|
s.stop(pc.green(`Restarted "${r.name}" (pid ${entry.pid})`));
|
|
232
248
|
} else {
|