plum-e2e 2.6.5 → 2.6.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.
|
@@ -77,12 +77,17 @@ function findPidOnPort(port) {
|
|
|
77
77
|
if (process.platform === 'win32') {
|
|
78
78
|
const out = execSync('netstat -ano', { encoding: 'utf8' });
|
|
79
79
|
for (const line of out.split('\n')) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
if (!line.toUpperCase().includes('LISTENING')) continue;
|
|
81
|
+
const parts = line.trim().split(/\s+/);
|
|
82
|
+
// Columns: Proto, Local Address, Foreign Address, State, PID.
|
|
83
|
+
// Match the port exactly off the local address (e.g. "0.0.0.0:3002"
|
|
84
|
+
// or "[::]:3002") — a substring check would false-positive port
|
|
85
|
+
// "300" against "3002", "13000", etc.
|
|
86
|
+
const localAddress = parts[1] ?? '';
|
|
87
|
+
const localPort = localAddress.slice(localAddress.lastIndexOf(':') + 1);
|
|
88
|
+
if (localPort !== portStr) continue;
|
|
89
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
90
|
+
if (!isNaN(pid) && pid > 0) return pid;
|
|
86
91
|
}
|
|
87
92
|
} else {
|
|
88
93
|
const out = execSync(`lsof -i :${portStr} -t -sTCP:LISTEN`, { encoding: 'utf8' }).trim();
|
|
@@ -122,12 +127,18 @@ function parsePort(url) {
|
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Clears the pid of registry entries whose process has died, and persists the
|
|
132
|
+
* result. Keeps the entry itself (with its `port`) rather than deleting it —
|
|
133
|
+
* that's the only place the last-used port for a runner is remembered, so a
|
|
134
|
+
* later Start can reuse it instead of falling back to whatever's in the
|
|
135
|
+
* runner's URL (or a hardcoded default, if the URL has no explicit port).
|
|
136
|
+
*/
|
|
126
137
|
function pruneDead(registry = loadRegistry()) {
|
|
127
138
|
let changed = false;
|
|
128
139
|
for (const [id, entry] of Object.entries(registry)) {
|
|
129
|
-
if (
|
|
130
|
-
|
|
140
|
+
if (entry?.pid && !isAlive(entry.pid)) {
|
|
141
|
+
registry[id] = { ...entry, pid: null };
|
|
131
142
|
changed = true;
|
|
132
143
|
}
|
|
133
144
|
}
|
|
@@ -233,10 +244,10 @@ function stopNode(id, fallbackPort = null) {
|
|
|
233
244
|
let signalled = false;
|
|
234
245
|
|
|
235
246
|
let pid = entry?.pid && isAlive(entry.pid) ? entry.pid : null;
|
|
247
|
+
const port = fallbackPort ?? (entry?.port ? Number(entry.port) : null);
|
|
236
248
|
|
|
237
|
-
if (!pid) {
|
|
238
|
-
|
|
239
|
-
if (port) pid = findPidOnPort(port);
|
|
249
|
+
if (!pid && port) {
|
|
250
|
+
pid = findPidOnPort(port);
|
|
240
251
|
}
|
|
241
252
|
|
|
242
253
|
if (pid) {
|
|
@@ -245,11 +256,53 @@ function stopNode(id, fallbackPort = null) {
|
|
|
245
256
|
signalled = true;
|
|
246
257
|
} catch {}
|
|
247
258
|
}
|
|
248
|
-
|
|
259
|
+
|
|
260
|
+
// Keep the port on record (clearing only the pid) so a later Start reuses
|
|
261
|
+
// the same port instead of falling back to the runner's URL — which may
|
|
262
|
+
// not even have an explicit port — or a hardcoded default.
|
|
263
|
+
if (port) {
|
|
264
|
+
registry[id] = { ...entry, pid: null, port: String(port) };
|
|
265
|
+
} else {
|
|
266
|
+
delete registry[id];
|
|
267
|
+
}
|
|
249
268
|
saveRegistry(registry);
|
|
250
269
|
return signalled;
|
|
251
270
|
}
|
|
252
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Frees a TCP port by killing whatever process is bound to it, so Start can't
|
|
274
|
+
* fail with EADDRINUSE against a stale/orphaned process this manager lost
|
|
275
|
+
* track of (e.g. one that ignored SIGTERM from a previous stop, or was never
|
|
276
|
+
* in the registry to begin with). SIGTERM first, escalating to SIGKILL if the
|
|
277
|
+
* process is still alive after a short grace period.
|
|
278
|
+
*
|
|
279
|
+
* Returns true if a process was found (and signalled), false if the port was
|
|
280
|
+
* already free.
|
|
281
|
+
*/
|
|
282
|
+
async function killPort(port) {
|
|
283
|
+
let pid = findPidOnPort(port);
|
|
284
|
+
if (!pid) return false;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
process.kill(pid, 'SIGTERM');
|
|
288
|
+
} catch {}
|
|
289
|
+
|
|
290
|
+
const deadline = Date.now() + 2000;
|
|
291
|
+
while (Date.now() < deadline && isAlive(pid)) {
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
pid = findPidOnPort(port);
|
|
296
|
+
if (pid && isAlive(pid)) {
|
|
297
|
+
try {
|
|
298
|
+
process.kill(pid, 'SIGKILL');
|
|
299
|
+
} catch {}
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
253
306
|
module.exports = {
|
|
254
307
|
BACKEND_DIR,
|
|
255
308
|
LOGS_DIR,
|
|
@@ -260,6 +313,7 @@ module.exports = {
|
|
|
260
313
|
isLocalUrl,
|
|
261
314
|
parsePort,
|
|
262
315
|
findPidOnPort,
|
|
316
|
+
killPort,
|
|
263
317
|
pruneDead,
|
|
264
318
|
statusOf,
|
|
265
319
|
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 3996 (node/runner mode)
|
|
@@ -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 3996 (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
|
|
|
@@ -203,15 +204,26 @@ async function runAction(r) {
|
|
|
203
204
|
const action = await clack.select({ message: `${r.name} — ${r.url}`, options });
|
|
204
205
|
if (cancelled(action) || action === 'back') return;
|
|
205
206
|
|
|
206
|
-
|
|
207
|
+
// Prefer the port this runner actually last ran on (remembered in the
|
|
208
|
+
// local registry even after being stopped) over parsing the URL — the URL
|
|
209
|
+
// may not carry an explicit port at all, and would otherwise silently fall
|
|
210
|
+
// back to the primary's own default port.
|
|
211
|
+
const remembered = runnerProcess.loadRegistry()[r.id]?.port;
|
|
212
|
+
const port = remembered || parsePort(r.url);
|
|
207
213
|
|
|
208
214
|
if (action === 'start') {
|
|
215
|
+
const s = clack.spinner();
|
|
216
|
+
s.start(`Freeing port ${port}...`);
|
|
217
|
+
const killed = await killPort(Number(port));
|
|
218
|
+
s.stop(killed ? pc.dim(`Freed port ${port}`) : pc.dim(`Port ${port} was already free`));
|
|
219
|
+
|
|
209
220
|
prepareNodeEnv();
|
|
210
221
|
const entry = startNode({ id: r.id, port, token: r.token });
|
|
211
222
|
clack.log.success(pc.green(`Started "${r.name}" on port ${port} (pid ${entry.pid})`));
|
|
212
223
|
} else if (action === 'stop') {
|
|
213
224
|
if (r.managed) {
|
|
214
225
|
const ok = stopNode(r.id);
|
|
226
|
+
await killPort(Number(port));
|
|
215
227
|
clack.log.success(
|
|
216
228
|
ok ? pc.green(`Stopped "${r.name}"`) : pc.dim(`"${r.name}" was not running`)
|
|
217
229
|
);
|
|
@@ -223,6 +235,11 @@ async function runAction(r) {
|
|
|
223
235
|
s.stop(pc.green(`Stopped "${r.name}"`));
|
|
224
236
|
} catch (e) {
|
|
225
237
|
s.stop(pc.red(`Could not stop "${r.name}": ${e.message}`));
|
|
238
|
+
} finally {
|
|
239
|
+
// Belt-and-suspenders: if this runner happens to be local (or
|
|
240
|
+
// the network shutdown call above silently failed), make sure
|
|
241
|
+
// nothing is left bound to its port.
|
|
242
|
+
await killPort(Number(port));
|
|
226
243
|
}
|
|
227
244
|
}
|
|
228
245
|
} else if (action === 'restart') {
|
|
@@ -230,7 +247,7 @@ async function runAction(r) {
|
|
|
230
247
|
const s = clack.spinner();
|
|
231
248
|
s.start(`Restarting "${r.name}"...`);
|
|
232
249
|
stopNode(r.id);
|
|
233
|
-
await
|
|
250
|
+
await killPort(Number(port));
|
|
234
251
|
const entry = startNode({ id: r.id, port, token: r.token });
|
|
235
252
|
s.stop(pc.green(`Restarted "${r.name}" (pid ${entry.pid})`));
|
|
236
253
|
} else {
|
package/backend/server.js
CHANGED
|
@@ -92,16 +92,30 @@ async function start() {
|
|
|
92
92
|
try {
|
|
93
93
|
const reg = loadRegistry();
|
|
94
94
|
// A self-restart already wrote the replacement's pid under this
|
|
95
|
-
// id before this process exits — only
|
|
96
|
-
// still ours, so we don't
|
|
95
|
+
// id before this process exits — only touch the entry if it's
|
|
96
|
+
// still ours, so we don't clobber the new process's registration.
|
|
97
|
+
// Keep the entry (with its port) rather than deleting it — that's
|
|
98
|
+
// the only place a later manual Start can find the port this
|
|
99
|
+
// runner was last running on.
|
|
97
100
|
if (reg[runnerId]?.pid === process.pid) {
|
|
98
|
-
|
|
101
|
+
reg[runnerId] = { ...reg[runnerId], pid: null };
|
|
99
102
|
saveRegistry(reg);
|
|
100
103
|
}
|
|
101
104
|
} catch {}
|
|
102
105
|
};
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
// Adding a SIGTERM/SIGINT listener suppresses Node's default
|
|
107
|
+
// "terminate immediately" behavior — the handler must exit itself,
|
|
108
|
+
// or the process (and the port it's bound to) lives on forever
|
|
109
|
+
// after a plain `kill`/SIGTERM with nothing left to stop it short
|
|
110
|
+
// of SIGKILL.
|
|
111
|
+
process.once('SIGTERM', () => {
|
|
112
|
+
cleanup();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
115
|
+
process.once('SIGINT', () => {
|
|
116
|
+
cleanup();
|
|
117
|
+
process.exit(0);
|
|
118
|
+
});
|
|
105
119
|
process.once('exit', cleanup);
|
|
106
120
|
}
|
|
107
121
|
return;
|