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 (!r.local) detail = pc.dim('remote');
143
- else if (r.managed) detail = pc.green('running');
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
- options.push({ value: 'ping', label: 'Ping' });
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 new Promise((resolve) => setTimeout(resolve, 600));
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.6.4",
3
+ "version": "2.6.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"