blitzwing 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzwing",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Interactive setup wizard to join a Blitzwing Petals mother swarm as a compute contributor",
5
5
  "bin": {
6
6
  "blitzwing": "bin/blitzwing.js"
package/runtime/app.py CHANGED
@@ -301,6 +301,7 @@ def _prewarm_local_runner() -> None:
301
301
 
302
302
  @app.on_event("startup")
303
303
  def on_startup() -> None:
304
+ manager.last_exit_code = None
304
305
  if manager._auto_start:
305
306
  try:
306
307
  manager.start(bootstrap=manager.new_swarm)
package/src/install.js CHANGED
@@ -10,6 +10,72 @@ const PACKAGE_ROOT = path.join(__dirname, "..");
10
10
  const RUNTIME_SRC = path.join(PACKAGE_ROOT, "runtime");
11
11
  const RUNTIME_DEST = path.join(HOME_DIR, "runtime");
12
12
 
13
+ function sleep(ms) {
14
+ return new Promise((r) => setTimeout(r, ms));
15
+ }
16
+
17
+ /**
18
+ * Kill leftover contributor shard/Petals processes so a new join does not talk to a
19
+ * stale uvicorn that still reports last_exit_code=-15 (SIGTERM from a prior leave).
20
+ */
21
+ export function stopLocalContributorStack(extraPids = []) {
22
+ for (const pid of extraPids) {
23
+ if (!pid || !Number.isFinite(Number(pid))) continue;
24
+ try {
25
+ process.kill(Number(pid), "SIGTERM");
26
+ } catch {
27
+ /* already gone */
28
+ }
29
+ }
30
+
31
+ if (process.platform === "win32") {
32
+ for (const port of [SHARD_PORT, PETALS_PORT]) {
33
+ try {
34
+ const out = execFileSync("netstat", ["-ano"], { encoding: "utf8" });
35
+ const re = new RegExp(`:${port}\\s+.*LISTENING\\s+(\\d+)`, "i");
36
+ const m = out.match(re);
37
+ if (m) {
38
+ execFileSync("taskkill", ["/PID", m[1], "/T", "/F"], { stdio: "ignore" });
39
+ }
40
+ } catch {
41
+ /* ignore */
42
+ }
43
+ }
44
+ return;
45
+ }
46
+
47
+ try {
48
+ execFileSync(
49
+ "bash",
50
+ [
51
+ "-lc",
52
+ [
53
+ `fuser -k ${SHARD_PORT}/tcp ${PETALS_PORT}/tcp 2>/dev/null || true`,
54
+ `pkill -f 'uvicorn app:app --host 0.0.0.0 --port ${SHARD_PORT}' 2>/dev/null || true`,
55
+ `pkill -f 'petals.cli.run_server' 2>/dev/null || true`,
56
+ "sleep 1",
57
+ ].join("; "),
58
+ ],
59
+ { stdio: "ignore" }
60
+ );
61
+ } catch {
62
+ /* ignore */
63
+ }
64
+ }
65
+
66
+ function petalsLogShowsSessionStart(logPath) {
67
+ try {
68
+ if (!fs.existsSync(logPath)) return false;
69
+ const text = fs.readFileSync(logPath, "utf8");
70
+ return (
71
+ /Starting Petals:/.test(text) ||
72
+ /\[INFO\] Started\b/.test(text) ||
73
+ /Running a server on/.test(text)
74
+ );
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
13
79
  function venvPython() {
14
80
  if (process.platform === "win32") {
15
81
  return path.join(VENV_PATH, "Scripts", "python.exe");
@@ -226,7 +292,16 @@ export function writeLocalShardManager() {
226
292
  }
227
293
 
228
294
  export function startShardManagerProcess({ python, env, logPath }) {
295
+ // Always clear stale listeners before binding — leave() used to stop Petals only.
296
+ stopLocalContributorStack();
229
297
  const appPath = syncRuntimeFiles();
298
+ fs.mkdirSync(HOME_DIR, { recursive: true });
299
+ // Fresh logs so wait/error tails are from this session.
300
+ try {
301
+ fs.writeFileSync(path.join(HOME_DIR, "petals.log"), "");
302
+ } catch {
303
+ /* ignore */
304
+ }
230
305
  const out = fs.openSync(logPath, "a");
231
306
  const petalsPy = petalsImportable(venvPython()) ? venvPython() : python;
232
307
  const child = spawn(
@@ -253,28 +328,38 @@ export async function waitForShardRunning({ timeoutMs = 300000, statusHost } = {
253
328
  const host = statusHost || process.env.BLITZWING_STATUS_HOST || "127.0.0.1";
254
329
  const petalsLog = path.join(HOME_DIR, "petals.log");
255
330
  const start = Date.now();
331
+ let sawRunning = false;
332
+
256
333
  while (Date.now() - start < timeoutMs) {
257
334
  try {
258
335
  const res = await fetch(`http://${host}:${SHARD_PORT}/status`);
259
336
  if (res.ok) {
260
337
  const body = await res.json();
261
- if (body.running && body.pid) return body;
338
+ if (body.running && body.pid) {
339
+ sawRunning = true;
340
+ return body;
341
+ }
262
342
  if (body.last_exit_code != null) {
263
- const tail = fs.existsSync(petalsLog)
264
- ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
265
- : "";
266
- throw new Error(
267
- `Petals exited (code ${body.last_exit_code}). ${tail || "See " + petalsLog}`
268
- );
343
+ // A prior leave/stop leaves uvicorn up with last_exit_code=-15. Ignore that
344
+ // until this session's Petals has actually started (or we already saw running).
345
+ const sessionStarted = petalsLogShowsSessionStart(petalsLog);
346
+ if (sawRunning || sessionStarted) {
347
+ const tail = fs.existsSync(petalsLog)
348
+ ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-8).join("\n")
349
+ : "";
350
+ throw new Error(
351
+ `Petals exited (code ${body.last_exit_code}). ${tail || "See " + petalsLog}`
352
+ );
353
+ }
269
354
  }
270
355
  }
271
356
  } catch (err) {
272
357
  if (err instanceof Error && err.message.startsWith("Petals exited")) throw err;
273
358
  }
274
- await new Promise((r) => setTimeout(r, 2000));
359
+ await sleep(2000);
275
360
  }
276
361
  const tail = fs.existsSync(petalsLog)
277
- ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
362
+ ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-8).join("\n")
278
363
  : "";
279
364
  throw new Error(
280
365
  `Timed out waiting for Petals on http://${host}:${SHARD_PORT}/status. ${tail || "See " + petalsLog}`
package/src/wizard.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  ensureVenvAndPetals,
9
9
  startShardManagerProcess,
10
10
  waitForShardRunning,
11
+ stopLocalContributorStack,
11
12
  syncRuntimeFiles,
12
13
  venvPython,
13
14
  } from "./install.js";
@@ -370,6 +371,7 @@ async function doLeave() {
370
371
  } catch {
371
372
  /* ignore */
372
373
  }
374
+ stopLocalContributorStack([state.shard_pid, state.tunnel_pid].filter(Boolean));
373
375
  if (state.tunnel_pid) {
374
376
  stopTunnelProcess(state.tunnel_pid);
375
377
  }