blitzwing 0.1.4 → 0.1.8
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 +2 -2
- package/src/config.js +6 -3
- package/src/install.js +134 -31
- package/src/wizard.js +22 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzwing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"config": {
|
|
25
|
-
"discoveryUrl": "
|
|
25
|
+
"discoveryUrl": "http://35.238.86.1:9000"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^0.9.1",
|
package/src/config.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
export const DEFAULT_DISCOVERY_URL =
|
|
3
3
|
process.env.BLITZWING_DISCOVERY_URL ||
|
|
4
4
|
process.env.npm_package_config_discoveryUrl ||
|
|
5
|
-
"
|
|
5
|
+
"http://35.238.86.1:9000";
|
|
6
6
|
|
|
7
7
|
export const HOME_DIR = process.env.BLITZWING_HOME || `${process.env.HOME || process.env.USERPROFILE}/.blitzwing`;
|
|
8
8
|
export const STATE_PATH = `${HOME_DIR}/contributor.json`;
|
|
9
9
|
export const VENV_PATH = `${HOME_DIR}/venv`;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Windows + WSL on same PC: avoid 8001/31337 — WSL forwards those on 127.0.0.1.
|
|
11
|
+
const defaultShardPort = process.platform === "win32" ? 8011 : 8001;
|
|
12
|
+
const defaultPetalsPort = process.platform === "win32" ? 31338 : 31337;
|
|
13
|
+
export const SHARD_PORT = Number(process.env.BLITZWING_SHARD_PORT || defaultShardPort);
|
|
14
|
+
export const PETALS_PORT = Number(process.env.BLITZWING_PETALS_PORT || defaultPetalsPort);
|
package/src/install.js
CHANGED
|
@@ -33,11 +33,25 @@ function isPetalsCompatible(info) {
|
|
|
33
33
|
return info && info.major === 3 && info.minor >= 10 && info.minor <= 11;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function pinnedPythonPath() {
|
|
37
|
+
try {
|
|
38
|
+
const p = path.join(HOME_DIR, "python");
|
|
39
|
+
if (!fs.existsSync(p)) return null;
|
|
40
|
+
const bin = fs.readFileSync(p, "utf8").trim();
|
|
41
|
+
return bin || null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
export function ensurePython() {
|
|
37
48
|
// Petals/hivemind break on 3.12+. Never fall back to a too-new interpreter.
|
|
49
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
38
50
|
const candidates = [];
|
|
39
51
|
const envBin = process.env.BLITZWING_PYTHON || process.env.PETALS_PYTHON;
|
|
40
52
|
if (envBin) candidates.push(envBin);
|
|
53
|
+
const pinned = pinnedPythonPath();
|
|
54
|
+
if (pinned) candidates.push(pinned);
|
|
41
55
|
if (process.env.CONDA_PREFIX) {
|
|
42
56
|
candidates.push(
|
|
43
57
|
path.join(process.env.CONDA_PREFIX, "bin", "python3.11"),
|
|
@@ -45,43 +59,77 @@ export function ensurePython() {
|
|
|
45
59
|
path.join(process.env.CONDA_PREFIX, "bin", "python")
|
|
46
60
|
);
|
|
47
61
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
62
|
+
for (const root of ["miniforge3", "mambaforge", "miniconda", "miniconda3", "anaconda3"]) {
|
|
63
|
+
candidates.push(
|
|
64
|
+
path.join(home, root, "envs", "blitzwing", "bin", "python3.11"),
|
|
65
|
+
path.join(home, root, "envs", "blitzwing", "bin", "python")
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
candidates.push("python3.11", "python3.10");
|
|
56
69
|
|
|
70
|
+
const tried = [];
|
|
57
71
|
const seen = new Set();
|
|
58
72
|
for (const cmd of candidates) {
|
|
59
73
|
if (!cmd || seen.has(cmd)) continue;
|
|
60
74
|
seen.add(cmd);
|
|
61
75
|
const found = cmd.includes("/") || cmd.includes("\\") ? (fs.existsSync(cmd) ? cmd : null) : which(cmd);
|
|
62
|
-
if (!found)
|
|
76
|
+
if (!found) {
|
|
77
|
+
tried.push(`${cmd} (missing)`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
63
80
|
const info = pythonVersion(found);
|
|
64
81
|
if (isPetalsCompatible(info)) {
|
|
82
|
+
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
83
|
+
fs.writeFileSync(path.join(HOME_DIR, "python"), found + "\n");
|
|
65
84
|
return { py: found, ver: info.ver };
|
|
66
85
|
}
|
|
86
|
+
tried.push(`${found} (${info ? info.ver : "unreadable"})`);
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
throw new Error(
|
|
70
|
-
"Python 3.10 or 3.11 is required (3.12+ will not work)
|
|
71
|
-
"
|
|
72
|
-
"
|
|
90
|
+
"Python 3.10 or 3.11 is required (3.12+ will not work).\n" +
|
|
91
|
+
"Checked:\n - " +
|
|
92
|
+
tried.slice(0, 12).join("\n - ") +
|
|
93
|
+
"\nFix:\n" +
|
|
94
|
+
" curl -LsSf https://astral.sh/uv/install.sh | sh && source $HOME/.local/bin/env\n" +
|
|
95
|
+
" uv python install 3.11\n" +
|
|
96
|
+
" export BLITZWING_PYTHON=\"$(uv python find 3.11)\"\n" +
|
|
97
|
+
" mkdir -p ~/.blitzwing && echo \"$BLITZWING_PYTHON\" > ~/.blitzwing/python\n" +
|
|
98
|
+
" blitzwing"
|
|
73
99
|
);
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
function petalsImportable(python) {
|
|
77
103
|
try {
|
|
78
|
-
execFileSync(
|
|
104
|
+
execFileSync(
|
|
105
|
+
python,
|
|
106
|
+
["-c", "import petals.cli.run_server"],
|
|
107
|
+
{ stdio: "ignore" }
|
|
108
|
+
);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function uvicornImportable(python) {
|
|
116
|
+
try {
|
|
117
|
+
execFileSync(python, ["-c", "import uvicorn, fastapi"], { stdio: "ignore" });
|
|
79
118
|
return true;
|
|
80
119
|
} catch {
|
|
81
120
|
return false;
|
|
82
121
|
}
|
|
83
122
|
}
|
|
84
123
|
|
|
124
|
+
function ensureShardManagerDeps(python, pip, env, onLog) {
|
|
125
|
+
if (uvicornImportable(python)) return;
|
|
126
|
+
onLog?.("Installing shard manager deps (uvicorn, fastapi)...");
|
|
127
|
+
execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic"], {
|
|
128
|
+
stdio: "inherit",
|
|
129
|
+
env,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
85
133
|
export function ensureVenvAndPetals({ onLog }) {
|
|
86
134
|
const { py } = ensurePython();
|
|
87
135
|
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
@@ -99,29 +147,43 @@ export function ensureVenvAndPetals({ onLog }) {
|
|
|
99
147
|
};
|
|
100
148
|
|
|
101
149
|
if (petalsImportable(python)) {
|
|
150
|
+
ensureShardManagerDeps(python, pip, env, onLog);
|
|
102
151
|
onLog?.("Petals already installed — skipping dependency install");
|
|
152
|
+
fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
|
|
103
153
|
return { python, pip };
|
|
104
154
|
}
|
|
105
155
|
|
|
156
|
+
if (process.platform === "win32") {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"Petals cannot be installed on native Windows (hivemind requires uvloop/Linux).\n" +
|
|
159
|
+
"Run the contributor inside WSL instead:\n" +
|
|
160
|
+
" wsl bash /mnt/c/Users/MSI/Desktop/blitzwing/scripts/local_wsl_contributor_join.sh\n" +
|
|
161
|
+
"Or run the full E2E script:\n" +
|
|
162
|
+
" powershell -File scripts/run_local_e2e.ps1"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
106
166
|
onLog?.("Installing CPU PyTorch + Petals (may take a few minutes on first run)...");
|
|
107
|
-
execFileSync(
|
|
167
|
+
execFileSync(python, ["-m", "pip", "install", "-U", "pip", "wheel", "setuptools<81"], {
|
|
108
168
|
stdio: "inherit",
|
|
109
169
|
env,
|
|
110
170
|
});
|
|
111
171
|
execFileSync(
|
|
112
|
-
|
|
113
|
-
["install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
|
|
172
|
+
python,
|
|
173
|
+
["-m", "pip", "install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
|
|
114
174
|
{ stdio: "inherit", env }
|
|
115
175
|
);
|
|
116
176
|
// Needed to compile hivemind protobufs during --no-build-isolation install
|
|
117
|
-
execFileSync(
|
|
177
|
+
execFileSync(python, ["-m", "pip", "install", "grpcio", "grpcio-tools", "protobuf"], {
|
|
118
178
|
stdio: "inherit",
|
|
119
179
|
env,
|
|
120
180
|
});
|
|
121
181
|
// Hivemind first with no build isolation (needs pkg_resources from setuptools<81)
|
|
122
182
|
execFileSync(
|
|
123
|
-
|
|
183
|
+
python,
|
|
124
184
|
[
|
|
185
|
+
"-m",
|
|
186
|
+
"pip",
|
|
125
187
|
"install",
|
|
126
188
|
"--no-build-isolation",
|
|
127
189
|
"git+https://github.com/learning-at-home/hivemind.git@213bff98a62accb91f254e2afdccbf1d69ebdea9",
|
|
@@ -129,15 +191,16 @@ export function ensureVenvAndPetals({ onLog }) {
|
|
|
129
191
|
{ stdio: "inherit", env }
|
|
130
192
|
);
|
|
131
193
|
execFileSync(
|
|
132
|
-
|
|
133
|
-
["install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
|
|
194
|
+
python,
|
|
195
|
+
["-m", "pip", "install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
|
|
134
196
|
{ stdio: "inherit", env }
|
|
135
197
|
);
|
|
136
|
-
execFileSync(
|
|
198
|
+
execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
|
|
137
199
|
stdio: "inherit",
|
|
138
200
|
env,
|
|
139
201
|
});
|
|
140
202
|
|
|
203
|
+
fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
|
|
141
204
|
return { python, pip };
|
|
142
205
|
}
|
|
143
206
|
|
|
@@ -176,8 +239,12 @@ class Mgr:
|
|
|
176
239
|
self.block_indices = os.environ["BLOCK_INDICES"]
|
|
177
240
|
peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
|
|
178
241
|
self.initial_peers = peers
|
|
242
|
+
announce_raw = os.environ.get("ANNOUNCE_MADDRS", "")
|
|
243
|
+
self.announce_maddrs = [a.strip() for a in announce_raw.split(",") if a.strip()]
|
|
179
244
|
self.python = os.environ.get("PETALS_PYTHON", "python")
|
|
245
|
+
self.petals_log = os.environ.get("PETALS_LOG", os.path.expanduser("~/.blitzwing/petals.log"))
|
|
180
246
|
self._proc = None
|
|
247
|
+
self._log_fp = None
|
|
181
248
|
self._lock = threading.Lock()
|
|
182
249
|
self.last_exit_code = None
|
|
183
250
|
|
|
@@ -186,7 +253,9 @@ class Mgr:
|
|
|
186
253
|
"--device", "cpu", "--quant_type", "none",
|
|
187
254
|
"--block_indices", bi, "--port", str(self.port),
|
|
188
255
|
"--identity_path", self.identity_path, "--num_handlers", "1"]
|
|
189
|
-
if self.
|
|
256
|
+
if self.announce_maddrs:
|
|
257
|
+
c += ["--announce_maddrs", *self.announce_maddrs]
|
|
258
|
+
elif self.public_ip:
|
|
190
259
|
c += ["--public_ip", self.public_ip]
|
|
191
260
|
if self.initial_peers:
|
|
192
261
|
c += ["--initial_peers", *self.initial_peers]
|
|
@@ -197,7 +266,15 @@ class Mgr:
|
|
|
197
266
|
if bi: self.block_indices = bi
|
|
198
267
|
if self._proc and self._proc.poll() is None:
|
|
199
268
|
return
|
|
200
|
-
self.
|
|
269
|
+
if self._log_fp is None:
|
|
270
|
+
os.makedirs(os.path.dirname(self.petals_log) or ".", exist_ok=True)
|
|
271
|
+
self._log_fp = open(self.petals_log, "a", encoding="utf-8")
|
|
272
|
+
popen_kwargs = {"stdout": self._log_fp, "stderr": subprocess.STDOUT}
|
|
273
|
+
if os.name == "nt":
|
|
274
|
+
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
275
|
+
else:
|
|
276
|
+
popen_kwargs["preexec_fn"] = os.setsid
|
|
277
|
+
self._proc = subprocess.Popen(self.cmd(self.block_indices), **popen_kwargs)
|
|
201
278
|
self.last_exit_code = None
|
|
202
279
|
|
|
203
280
|
def stop(self):
|
|
@@ -206,7 +283,13 @@ class Mgr:
|
|
|
206
283
|
self._proc = None
|
|
207
284
|
return
|
|
208
285
|
p = self._proc
|
|
209
|
-
|
|
286
|
+
try:
|
|
287
|
+
if os.name == "nt":
|
|
288
|
+
p.send_signal(signal.CTRL_BREAK_EVENT)
|
|
289
|
+
else:
|
|
290
|
+
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
|
|
291
|
+
except Exception:
|
|
292
|
+
p.send_signal(signal.SIGTERM)
|
|
210
293
|
try: p.wait(timeout=30)
|
|
211
294
|
except Exception:
|
|
212
295
|
p.kill(); p.wait(timeout=10)
|
|
@@ -217,6 +300,9 @@ class Mgr:
|
|
|
217
300
|
self.stop(); time.sleep(1); self.start(bi)
|
|
218
301
|
|
|
219
302
|
def status(self):
|
|
303
|
+
if self._proc is not None and self._proc.poll() is not None:
|
|
304
|
+
self.last_exit_code = self._proc.returncode
|
|
305
|
+
self._proc = None
|
|
220
306
|
running = self._proc is not None and self._proc.poll() is None
|
|
221
307
|
return StatusResponse(
|
|
222
308
|
running=running,
|
|
@@ -266,12 +352,13 @@ export function startShardManagerProcess({
|
|
|
266
352
|
}) {
|
|
267
353
|
const appPath = writeLocalShardManager();
|
|
268
354
|
const out = fs.openSync(logPath, "a");
|
|
355
|
+
const petalsPy = petalsImportable(venvPython()) ? venvPython() : python;
|
|
269
356
|
const child = spawn(
|
|
270
|
-
|
|
357
|
+
petalsPy,
|
|
271
358
|
["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
|
|
272
359
|
{
|
|
273
360
|
cwd: path.dirname(appPath),
|
|
274
|
-
env: { ...process.env, ...env, PETALS_PYTHON:
|
|
361
|
+
env: { ...process.env, ...env, PETALS_PYTHON: petalsPy, PETALS_LOG: path.join(HOME_DIR, "petals.log") },
|
|
275
362
|
detached: true,
|
|
276
363
|
stdio: ["ignore", out, out],
|
|
277
364
|
}
|
|
@@ -280,21 +367,37 @@ export function startShardManagerProcess({
|
|
|
280
367
|
return { pid: child.pid, logPath, appPath };
|
|
281
368
|
}
|
|
282
369
|
|
|
283
|
-
export async function waitForShardRunning({ timeoutMs = 300000 } = {}) {
|
|
370
|
+
export async function waitForShardRunning({ timeoutMs = 300000, statusHost } = {}) {
|
|
371
|
+
const host = statusHost || process.env.BLITZWING_STATUS_HOST || "127.0.0.1";
|
|
372
|
+
const petalsLog = path.join(HOME_DIR, "petals.log");
|
|
284
373
|
const start = Date.now();
|
|
285
374
|
while (Date.now() - start < timeoutMs) {
|
|
286
375
|
try {
|
|
287
|
-
const res = await fetch(`http
|
|
376
|
+
const res = await fetch(`http://${host}:${SHARD_PORT}/status`);
|
|
288
377
|
if (res.ok) {
|
|
289
378
|
const body = await res.json();
|
|
290
|
-
if (body.running) return body;
|
|
379
|
+
if (body.running && body.pid) return body;
|
|
380
|
+
if (body.last_exit_code != null) {
|
|
381
|
+
const tail = fs.existsSync(petalsLog)
|
|
382
|
+
? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
|
|
383
|
+
: "";
|
|
384
|
+
throw new Error(
|
|
385
|
+
`Petals exited (code ${body.last_exit_code}). ${tail || "See " + petalsLog}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
291
388
|
}
|
|
292
|
-
} catch {
|
|
389
|
+
} catch (err) {
|
|
390
|
+
if (err instanceof Error && err.message.startsWith("Petals exited")) throw err;
|
|
293
391
|
/* retry */
|
|
294
392
|
}
|
|
295
393
|
await new Promise((r) => setTimeout(r, 2000));
|
|
296
394
|
}
|
|
297
|
-
|
|
395
|
+
const tail = fs.existsSync(petalsLog)
|
|
396
|
+
? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
|
|
397
|
+
: "";
|
|
398
|
+
throw new Error(
|
|
399
|
+
`Timed out waiting for Petals on http://${host}:${SHARD_PORT}/status. ${tail || "See " + petalsLog}`
|
|
400
|
+
);
|
|
298
401
|
}
|
|
299
402
|
|
|
300
403
|
export { SHARD_PORT, PETALS_PORT, venvPython };
|
package/src/wizard.js
CHANGED
|
@@ -142,8 +142,22 @@ async function wizard(args) {
|
|
|
142
142
|
process.exit(0);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
const hederaAccount = await p.text({
|
|
146
|
+
message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
|
|
147
|
+
initialValue: process.env.BLITZWING_HEDERA_ACCOUNT_ID || "",
|
|
148
|
+
validate(v) {
|
|
149
|
+
const s = String(v || "").trim();
|
|
150
|
+
if (!/^0\.0\.\d+$/.test(s)) return "Enter a Hedera account like 0.0.123456";
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
if (p.isCancel(hederaAccount)) {
|
|
154
|
+
p.cancel("Setup cancelled");
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
const hederaAccountId = String(hederaAccount).trim();
|
|
158
|
+
|
|
145
159
|
const confirm = await p.confirm({
|
|
146
|
-
message: `Join ${selected.model} hosting ${layersN} layers from ${String(publicIp).trim()}?`,
|
|
160
|
+
message: `Join ${selected.model} hosting ${layersN} layers from ${String(publicIp).trim()} paying to ${hederaAccountId}?`,
|
|
147
161
|
initialValue: true,
|
|
148
162
|
});
|
|
149
163
|
if (p.isCancel(confirm) || !confirm) {
|
|
@@ -176,6 +190,7 @@ async function wizard(args) {
|
|
|
176
190
|
layers: layersN,
|
|
177
191
|
public_ip: String(publicIp).trim(),
|
|
178
192
|
shard_manager_url: shardManagerUrl,
|
|
193
|
+
hedera_account_id: hederaAccountId,
|
|
179
194
|
});
|
|
180
195
|
} catch (err) {
|
|
181
196
|
spin.stop("Join rejected");
|
|
@@ -202,7 +217,7 @@ async function wizard(args) {
|
|
|
202
217
|
});
|
|
203
218
|
|
|
204
219
|
try {
|
|
205
|
-
await waitForShardRunning({ timeoutMs: 600000 });
|
|
220
|
+
await waitForShardRunning({ timeoutMs: 600000, statusHost: String(publicIp).trim() });
|
|
206
221
|
} catch (err) {
|
|
207
222
|
spin.stop("Petals did not become ready");
|
|
208
223
|
p.cancel(`${err.message}. See ${logPath}`);
|
|
@@ -230,6 +245,7 @@ async function wizard(args) {
|
|
|
230
245
|
shard_manager_url: shardManagerUrl,
|
|
231
246
|
shard_pid: pid,
|
|
232
247
|
discovery_url: args.discoveryUrl,
|
|
248
|
+
hedera_account_id: hederaAccountId,
|
|
233
249
|
joined_at: new Date().toISOString(),
|
|
234
250
|
};
|
|
235
251
|
saveState(state);
|
|
@@ -281,7 +297,8 @@ async function showStatus() {
|
|
|
281
297
|
console.log(` mother: ${state.mother_url}`);
|
|
282
298
|
console.log(` public_ip: ${state.public_ip}`);
|
|
283
299
|
try {
|
|
284
|
-
const
|
|
300
|
+
const statusHost = state.public_ip || "127.0.0.1";
|
|
301
|
+
const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
|
|
285
302
|
if (res.ok) {
|
|
286
303
|
const st = await res.json();
|
|
287
304
|
console.log(` petals_running: ${st.running}`);
|
|
@@ -308,7 +325,8 @@ async function doLeave() {
|
|
|
308
325
|
p.log.warn(`Mother leave call failed: ${err.message}`);
|
|
309
326
|
}
|
|
310
327
|
try {
|
|
311
|
-
|
|
328
|
+
const statusHost = state.public_ip || "127.0.0.1";
|
|
329
|
+
await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
|
|
312
330
|
} catch {
|
|
313
331
|
/* ignore */
|
|
314
332
|
}
|