blitzwing 0.1.9 → 0.2.0
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/README.md +14 -5
- package/package.json +2 -1
- package/runtime/app.py +398 -0
- package/runtime/http_chain.py +189 -0
- package/runtime/inference.py +13 -0
- package/runtime/local_runner.py +148 -0
- package/runtime/tensor_codec.py +26 -0
- package/src/heartbeat.js +67 -0
- package/src/install.js +51 -180
- package/src/tunnel.js +231 -0
- package/src/wizard.js +73 -132
package/src/tunnel.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { createWriteStream } from "node:fs";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { HOME_DIR } from "./config.js";
|
|
8
|
+
import { which } from "./net.js";
|
|
9
|
+
|
|
10
|
+
const BIN_DIR = path.join(HOME_DIR, "bin");
|
|
11
|
+
const URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
12
|
+
|
|
13
|
+
function platformAsset() {
|
|
14
|
+
const plat = process.platform;
|
|
15
|
+
const arch = process.arch;
|
|
16
|
+
if (plat === "linux" && arch === "x64") {
|
|
17
|
+
return { name: "cloudflared-linux-amd64", archive: false };
|
|
18
|
+
}
|
|
19
|
+
if (plat === "linux" && (arch === "arm64" || arch === "aarch64")) {
|
|
20
|
+
return { name: "cloudflared-linux-arm64", archive: false };
|
|
21
|
+
}
|
|
22
|
+
if (plat === "darwin" && arch === "arm64") {
|
|
23
|
+
return { name: "cloudflared-darwin-arm64.tgz", archive: true };
|
|
24
|
+
}
|
|
25
|
+
if (plat === "darwin" && arch === "x64") {
|
|
26
|
+
return { name: "cloudflared-darwin-amd64.tgz", archive: true };
|
|
27
|
+
}
|
|
28
|
+
if (plat === "win32" && arch === "x64") {
|
|
29
|
+
return { name: "cloudflared-windows-amd64.exe", archive: false, exe: true };
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Unsupported platform for cloudflared auto-install: ${plat}/${arch}. Install cloudflared manually and retry.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function localBinaryPath() {
|
|
37
|
+
const exe = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
38
|
+
return path.join(BIN_DIR, exe);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve cloudflared binary: PATH first, then ~/.blitzwing/bin (download if missing).
|
|
43
|
+
* @param {{ onLog?: (msg: string) => void }} [opts]
|
|
44
|
+
*/
|
|
45
|
+
export async function ensureCloudflared({ onLog } = {}) {
|
|
46
|
+
const onPath = which("cloudflared");
|
|
47
|
+
if (onPath) return onPath;
|
|
48
|
+
|
|
49
|
+
const dest = localBinaryPath();
|
|
50
|
+
if (fs.existsSync(dest)) {
|
|
51
|
+
try {
|
|
52
|
+
fs.chmodSync(dest, 0o755);
|
|
53
|
+
} catch {
|
|
54
|
+
/* ignore */
|
|
55
|
+
}
|
|
56
|
+
return dest;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const asset = platformAsset();
|
|
60
|
+
const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset.name}`;
|
|
61
|
+
onLog?.(`Downloading cloudflared (${asset.name})…`);
|
|
62
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
65
|
+
if (!res.ok || !res.body) {
|
|
66
|
+
throw new Error(`Failed to download cloudflared: HTTP ${res.status} from ${url}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (asset.archive) {
|
|
70
|
+
const tgzPath = path.join(BIN_DIR, asset.name);
|
|
71
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(tgzPath));
|
|
72
|
+
execFileSync("tar", ["-xzf", tgzPath, "-C", BIN_DIR], { stdio: "ignore" });
|
|
73
|
+
try {
|
|
74
|
+
fs.unlinkSync(tgzPath);
|
|
75
|
+
} catch {
|
|
76
|
+
/* ignore */
|
|
77
|
+
}
|
|
78
|
+
// tgz extracts a binary named cloudflared
|
|
79
|
+
const extracted = path.join(BIN_DIR, "cloudflared");
|
|
80
|
+
if (!fs.existsSync(extracted) && fs.existsSync(dest)) {
|
|
81
|
+
/* already named correctly */
|
|
82
|
+
} else if (fs.existsSync(extracted) && extracted !== dest) {
|
|
83
|
+
fs.renameSync(extracted, dest);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
fs.chmodSync(dest, 0o755);
|
|
91
|
+
} catch {
|
|
92
|
+
/* windows */
|
|
93
|
+
}
|
|
94
|
+
if (!fs.existsSync(dest)) {
|
|
95
|
+
throw new Error(`cloudflared download finished but binary missing at ${dest}`);
|
|
96
|
+
}
|
|
97
|
+
onLog?.(`cloudflared ready at ${dest}`);
|
|
98
|
+
return dest;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Kill a process (and its group on Unix).
|
|
103
|
+
* @param {number} pid
|
|
104
|
+
*/
|
|
105
|
+
export function stopTunnelProcess(pid) {
|
|
106
|
+
if (!pid || !Number.isFinite(pid)) return;
|
|
107
|
+
try {
|
|
108
|
+
if (process.platform === "win32") {
|
|
109
|
+
execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
110
|
+
} else {
|
|
111
|
+
try {
|
|
112
|
+
process.kill(-pid, "SIGTERM");
|
|
113
|
+
} catch {
|
|
114
|
+
process.kill(pid, "SIGTERM");
|
|
115
|
+
}
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
try {
|
|
118
|
+
process.kill(-pid, "SIGKILL");
|
|
119
|
+
} catch {
|
|
120
|
+
try {
|
|
121
|
+
process.kill(pid, "SIGKILL");
|
|
122
|
+
} catch {
|
|
123
|
+
/* already gone */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, 2000).unref?.();
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
/* already gone */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start a Cloudflare Quick Tunnel to http://127.0.0.1:port (no account/token).
|
|
135
|
+
* @param {{ port: number, binary?: string, logPath?: string, timeoutMs?: number }} opts
|
|
136
|
+
* @returns {Promise<{ url: string, pid: number, stop: () => void }>}
|
|
137
|
+
*/
|
|
138
|
+
export async function startQuickTunnel({
|
|
139
|
+
port,
|
|
140
|
+
binary,
|
|
141
|
+
logPath = path.join(HOME_DIR, "cloudflared.log"),
|
|
142
|
+
timeoutMs = 90_000,
|
|
143
|
+
}) {
|
|
144
|
+
const bin = binary || (await ensureCloudflared());
|
|
145
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
146
|
+
const logFd = fs.openSync(logPath, "a");
|
|
147
|
+
|
|
148
|
+
const child = spawn(
|
|
149
|
+
bin,
|
|
150
|
+
["tunnel", "--url", `http://127.0.0.1:${port}`, "--no-autoupdate"],
|
|
151
|
+
{
|
|
152
|
+
detached: process.platform !== "win32",
|
|
153
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
154
|
+
windowsHide: true,
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
let settled = false;
|
|
159
|
+
let buffer = "";
|
|
160
|
+
|
|
161
|
+
const appendLog = (chunk) => {
|
|
162
|
+
const text = chunk.toString();
|
|
163
|
+
buffer += text;
|
|
164
|
+
try {
|
|
165
|
+
fs.writeSync(logFd, text);
|
|
166
|
+
} catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
child.stdout?.on("data", appendLog);
|
|
172
|
+
child.stderr?.on("data", appendLog);
|
|
173
|
+
|
|
174
|
+
const stop = () => {
|
|
175
|
+
stopTunnelProcess(child.pid);
|
|
176
|
+
try {
|
|
177
|
+
fs.closeSync(logFd);
|
|
178
|
+
} catch {
|
|
179
|
+
/* ignore */
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
if (settled) return;
|
|
186
|
+
settled = true;
|
|
187
|
+
stop();
|
|
188
|
+
reject(
|
|
189
|
+
new Error(
|
|
190
|
+
`Timed out waiting for Cloudflare Quick Tunnel URL. See ${logPath}`
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
}, timeoutMs);
|
|
194
|
+
|
|
195
|
+
const tryMatch = () => {
|
|
196
|
+
const m = buffer.match(URL_RE);
|
|
197
|
+
if (!m || settled) return;
|
|
198
|
+
settled = true;
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
if (process.platform !== "win32") {
|
|
201
|
+
child.unref();
|
|
202
|
+
}
|
|
203
|
+
resolve({
|
|
204
|
+
url: m[0].replace(/\/$/, ""),
|
|
205
|
+
pid: child.pid,
|
|
206
|
+
stop,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
child.stdout?.on("data", tryMatch);
|
|
211
|
+
child.stderr?.on("data", tryMatch);
|
|
212
|
+
|
|
213
|
+
child.on("error", (err) => {
|
|
214
|
+
if (settled) return;
|
|
215
|
+
settled = true;
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
reject(err);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
child.on("exit", (code) => {
|
|
221
|
+
if (settled) return;
|
|
222
|
+
settled = true;
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
reject(
|
|
225
|
+
new Error(
|
|
226
|
+
`cloudflared exited early (code ${code}). See ${logPath}`
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
package/src/wizard.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import color from "picocolors";
|
|
3
|
-
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
4
|
import { DEFAULT_DISCOVERY_URL, HOME_DIR, SHARD_PORT, PETALS_PORT } from "./config.js";
|
|
7
5
|
import { listMothers, motherHosts, joinHost, readyHost, leaveHost } from "./api.js";
|
|
8
|
-
import {
|
|
9
|
-
detectPublicIp,
|
|
10
|
-
detectLocalIp,
|
|
11
|
-
discoverNgrokTcpAnnounce,
|
|
12
|
-
extractPeerMultiaddrFromLog,
|
|
13
|
-
} from "./net.js";
|
|
6
|
+
import { detectPublicIp, detectLocalIp } from "./net.js";
|
|
14
7
|
import {
|
|
15
8
|
ensureVenvAndPetals,
|
|
16
9
|
startShardManagerProcess,
|
|
17
10
|
waitForShardRunning,
|
|
11
|
+
syncRuntimeFiles,
|
|
18
12
|
venvPython,
|
|
19
13
|
} from "./install.js";
|
|
14
|
+
import { ensureCloudflared, startQuickTunnel, stopTunnelProcess } from "./tunnel.js";
|
|
20
15
|
import { saveState, loadState, clearState, ensureHome } from "./state.js";
|
|
16
|
+
import { startContributorHeartbeatDaemon } from "./heartbeat.js";
|
|
21
17
|
|
|
22
18
|
function parseArgs(argv) {
|
|
23
19
|
const out = { cmd: null, discoveryUrl: process.env.BLITZWING_DISCOVERY_URL || DEFAULT_DISCOVERY_URL };
|
|
@@ -59,10 +55,9 @@ ${color.bold("blitzwing")} — join a Blitzwing mother swarm as a compute node
|
|
|
59
55
|
blitzwing leave Leave the swarm and reclaim your layers
|
|
60
56
|
|
|
61
57
|
Env:
|
|
62
|
-
BLITZWING_DISCOVERY_URL
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
PETALS_USE_AUTO_RELAY=0 Disable libp2p auto-relay (use with public IP / port-forward)
|
|
58
|
+
BLITZWING_DISCOVERY_URL Override discovery service (default ${DEFAULT_DISCOVERY_URL})
|
|
59
|
+
BLITZWING_HEDERA_ACCOUNT_ID Prefill Hedera payout account (0.0.N)
|
|
60
|
+
BLITZWING_SHARD_PORT Local shard HTTP port (default ${SHARD_PORT})
|
|
66
61
|
`);
|
|
67
62
|
}
|
|
68
63
|
|
|
@@ -70,6 +65,7 @@ async function wizard(args) {
|
|
|
70
65
|
p.intro(color.bgCyan(color.black(" blitzwing ")));
|
|
71
66
|
ensureHome();
|
|
72
67
|
|
|
68
|
+
const existing = loadState();
|
|
73
69
|
const spin = p.spinner();
|
|
74
70
|
spin.start("Loading network from Discovery Service…");
|
|
75
71
|
let mothers;
|
|
@@ -137,74 +133,14 @@ async function wizard(args) {
|
|
|
137
133
|
}
|
|
138
134
|
const layersN = Number(layers);
|
|
139
135
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
value: "relay",
|
|
146
|
-
label: "Auto (recommended for home / NAT)",
|
|
147
|
-
hint: "Uses Petals libp2p relay — no port forwarding",
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
value: "public",
|
|
151
|
-
label: "Public IP / cloud VM",
|
|
152
|
-
hint: "TCP 31337 (and 8001) open on the internet",
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
value: "ngrok",
|
|
156
|
-
label: "Dev tunnel (ngrok TCP on :4040)",
|
|
157
|
-
hint: "Only if ngrok is already running",
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
initialValue: "relay",
|
|
161
|
-
});
|
|
162
|
-
if (p.isCancel(networkMode)) {
|
|
163
|
-
p.cancel("Setup cancelled");
|
|
164
|
-
process.exit(0);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let publicIp = "";
|
|
168
|
-
let announceMaddrs = "";
|
|
169
|
-
let useAutoRelay = "1";
|
|
170
|
-
|
|
171
|
-
if (networkMode === "public") {
|
|
172
|
-
publicIp = await p.text({
|
|
173
|
-
message: "Public IPv4 (must be reachable on TCP " + PETALS_PORT + ")",
|
|
174
|
-
initialValue: detected || "",
|
|
175
|
-
validate(v) {
|
|
176
|
-
if (!v || !/^\d+\.\d+\.\d+\.\d+$/.test(String(v).trim())) {
|
|
177
|
-
return "Enter a valid public IPv4 address";
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
if (p.isCancel(publicIp)) {
|
|
182
|
-
p.cancel("Setup cancelled");
|
|
183
|
-
process.exit(0);
|
|
184
|
-
}
|
|
185
|
-
publicIp = String(publicIp).trim();
|
|
186
|
-
announceMaddrs = `/ip4/${publicIp}/tcp/${PETALS_PORT}`;
|
|
187
|
-
useAutoRelay = "0";
|
|
188
|
-
} else if (networkMode === "ngrok") {
|
|
189
|
-
const ngrok = await discoverNgrokTcpAnnounce(PETALS_PORT);
|
|
190
|
-
if (!ngrok) {
|
|
191
|
-
p.cancel("No ngrok TCP tunnel on http://127.0.0.1:4040. Start: ngrok tcp " + PETALS_PORT);
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
announceMaddrs = ngrok;
|
|
195
|
-
publicIp = ngrok.match(/dns4\/([^/]+)/)?.[1] || detected || "relay";
|
|
196
|
-
useAutoRelay = "0";
|
|
197
|
-
p.log.info(`Using ngrok announce ${announceMaddrs}`);
|
|
198
|
-
} else {
|
|
199
|
-
publicIp = detected || "relay";
|
|
200
|
-
p.log.info("Using Petals libp2p auto-relay (no inbound port forward required).");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const localIp = await detectLocalIp();
|
|
204
|
-
|
|
136
|
+
const hederaPrefill =
|
|
137
|
+
process.env.BLITZWING_HEDERA_ACCOUNT_ID ||
|
|
138
|
+
process.env.HEDERA_ACCOUNT_ID ||
|
|
139
|
+
existing?.hedera_account_id ||
|
|
140
|
+
"";
|
|
205
141
|
const hederaAccount = await p.text({
|
|
206
142
|
message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
|
|
207
|
-
initialValue:
|
|
143
|
+
initialValue: hederaPrefill,
|
|
208
144
|
validate(v) {
|
|
209
145
|
const s = String(v || "").trim();
|
|
210
146
|
if (!/^0\.0\.\d+$/.test(s)) return "Enter a Hedera account like 0.0.123456";
|
|
@@ -217,7 +153,7 @@ async function wizard(args) {
|
|
|
217
153
|
const hederaAccountId = String(hederaAccount).trim();
|
|
218
154
|
|
|
219
155
|
const confirm = await p.confirm({
|
|
220
|
-
message: `Join ${selected.model} hosting ${layersN} layers
|
|
156
|
+
message: `Join ${selected.model} hosting ${layersN} layers, payouts to ${hederaAccountId}?`,
|
|
221
157
|
initialValue: true,
|
|
222
158
|
});
|
|
223
159
|
if (p.isCancel(confirm) || !confirm) {
|
|
@@ -232,6 +168,7 @@ async function wizard(args) {
|
|
|
232
168
|
spin.message(msg);
|
|
233
169
|
},
|
|
234
170
|
});
|
|
171
|
+
syncRuntimeFiles();
|
|
235
172
|
} catch (err) {
|
|
236
173
|
spin.stop("Install failed");
|
|
237
174
|
p.cancel(err.message);
|
|
@@ -240,8 +177,28 @@ async function wizard(args) {
|
|
|
240
177
|
const python = venvPython();
|
|
241
178
|
spin.stop("Environment ready");
|
|
242
179
|
|
|
243
|
-
|
|
244
|
-
|
|
180
|
+
spin.start("Setting up Cloudflare Quick Tunnel…");
|
|
181
|
+
let tunnel;
|
|
182
|
+
try {
|
|
183
|
+
const cfBin = await ensureCloudflared({
|
|
184
|
+
onLog: (msg) => {
|
|
185
|
+
spin.message(msg);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
tunnel = await startQuickTunnel({
|
|
189
|
+
port: SHARD_PORT,
|
|
190
|
+
binary: cfBin,
|
|
191
|
+
logPath: path.join(HOME_DIR, "cloudflared.log"),
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
spin.stop("Tunnel failed");
|
|
195
|
+
p.cancel(err.message);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
spin.stop(`Public URL ${tunnel.url}`);
|
|
199
|
+
|
|
200
|
+
const localIp = await detectLocalIp();
|
|
201
|
+
const publicIp = (await detectPublicIp()) || localIp || "tunnel";
|
|
245
202
|
|
|
246
203
|
spin.start("Requesting layer assignment from mother…");
|
|
247
204
|
let assignment;
|
|
@@ -250,10 +207,11 @@ async function wizard(args) {
|
|
|
250
207
|
model: selected.model,
|
|
251
208
|
layers: layersN,
|
|
252
209
|
public_ip: String(publicIp).trim(),
|
|
253
|
-
shard_manager_url:
|
|
210
|
+
shard_manager_url: tunnel.url,
|
|
254
211
|
hedera_account_id: hederaAccountId,
|
|
255
212
|
});
|
|
256
213
|
} catch (err) {
|
|
214
|
+
tunnel.stop();
|
|
257
215
|
spin.stop("Join rejected");
|
|
258
216
|
const max = err.body?.detail?.max_layers;
|
|
259
217
|
p.cancel(`${err.message}${max != null ? ` (max available: ${max})` : ""}`);
|
|
@@ -264,19 +222,21 @@ async function wizard(args) {
|
|
|
264
222
|
const logPath = path.join(HOME_DIR, "shard_manager.log");
|
|
265
223
|
spin.start("Starting local Petals server…");
|
|
266
224
|
const shardEnv = {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
225
|
+
MODEL_NAME: selected.model,
|
|
226
|
+
PUBLIC_IP: String(publicIp).trim(),
|
|
227
|
+
BLOCK_INDICES: assignment.block_indices,
|
|
228
|
+
INITIAL_PEERS: "",
|
|
229
|
+
NEW_SWARM: "1",
|
|
230
|
+
BLITZWING_HTTP_ONLY: "1",
|
|
231
|
+
PETALS_PORT: String(PETALS_PORT),
|
|
232
|
+
IDENTITY_PATH: path.join(HOME_DIR, "petals-identity"),
|
|
233
|
+
SHARD_AUTO_START: "1",
|
|
234
|
+
PETALS_USE_AUTO_RELAY: "0",
|
|
235
|
+
PETALS_SKIP_REACHABILITY_CHECK: "1",
|
|
236
|
+
BLITZWING_MOTHER_URL: selected.mother_url,
|
|
237
|
+
MOTHER_PUBLIC_SHARD_URL: selected.mother_url,
|
|
238
|
+
BLITZWING_HOST_ID: assignment.host_id,
|
|
239
|
+
};
|
|
280
240
|
const { pid } = startShardManagerProcess({
|
|
281
241
|
python,
|
|
282
242
|
logPath,
|
|
@@ -286,6 +246,7 @@ async function wizard(args) {
|
|
|
286
246
|
try {
|
|
287
247
|
await waitForShardRunning({ timeoutMs: 600000, statusHost: "127.0.0.1" });
|
|
288
248
|
} catch (err) {
|
|
249
|
+
tunnel.stop();
|
|
289
250
|
spin.stop("Petals did not become ready");
|
|
290
251
|
p.cancel(`${err.message}. See ${logPath}`);
|
|
291
252
|
process.exit(1);
|
|
@@ -293,13 +254,12 @@ async function wizard(args) {
|
|
|
293
254
|
spin.stop("Petals is serving your layers");
|
|
294
255
|
|
|
295
256
|
spin.start("Finalizing handoff with mother…");
|
|
296
|
-
const peerMultiaddr = extractPeerMultiaddrFromLog(petalsLogPath, fs);
|
|
297
257
|
try {
|
|
298
258
|
await readyHost(selected.mother_url, {
|
|
299
259
|
host_id: assignment.host_id,
|
|
300
|
-
peer_multiaddr: peerMultiaddr || undefined,
|
|
301
260
|
});
|
|
302
261
|
} catch (err) {
|
|
262
|
+
tunnel.stop();
|
|
303
263
|
spin.stop("Handoff failed");
|
|
304
264
|
p.cancel(err.message);
|
|
305
265
|
process.exit(1);
|
|
@@ -313,49 +273,28 @@ async function wizard(args) {
|
|
|
313
273
|
block_indices: assignment.block_indices,
|
|
314
274
|
layers_hosted: assignment.layers_hosted,
|
|
315
275
|
public_ip: String(publicIp).trim(),
|
|
316
|
-
network_mode:
|
|
317
|
-
|
|
318
|
-
|
|
276
|
+
network_mode: "cloudflare",
|
|
277
|
+
shard_manager_url: tunnel.url,
|
|
278
|
+
tunnel_url: tunnel.url,
|
|
279
|
+
tunnel_pid: tunnel.pid,
|
|
319
280
|
shard_pid: pid,
|
|
320
281
|
discovery_url: args.discoveryUrl,
|
|
321
282
|
hedera_account_id: hederaAccountId,
|
|
322
283
|
joined_at: new Date().toISOString(),
|
|
323
284
|
};
|
|
324
285
|
saveState(state);
|
|
325
|
-
|
|
286
|
+
startContributorHeartbeatDaemon({
|
|
287
|
+
motherUrl: state.mother_url,
|
|
288
|
+
hostId: state.host_id,
|
|
289
|
+
});
|
|
326
290
|
|
|
327
291
|
p.outro(
|
|
328
292
|
`${color.green("You are online.")} Hosting ${color.cyan(String(state.layers_hosted))} layers of ${color.cyan(state.model)} at ${state.block_indices}\n` +
|
|
293
|
+
`Tunnel: ${color.cyan(tunnel.url)}\n` +
|
|
329
294
|
`Run ${color.bold("blitzwing status")} anytime, or ${color.bold("blitzwing leave")} to exit.`
|
|
330
295
|
);
|
|
331
296
|
}
|
|
332
297
|
|
|
333
|
-
function startHeartbeatDaemon(state) {
|
|
334
|
-
const hbPath = path.join(HOME_DIR, "heartbeat.mjs");
|
|
335
|
-
fs.writeFileSync(
|
|
336
|
-
hbPath,
|
|
337
|
-
`const mother = ${JSON.stringify(state.mother_url)};
|
|
338
|
-
const hostId = ${JSON.stringify(state.host_id)};
|
|
339
|
-
async function beat() {
|
|
340
|
-
try {
|
|
341
|
-
await fetch(mother.replace(/\\/$/, "") + "/v1/hosts/heartbeat", {
|
|
342
|
-
method: "POST",
|
|
343
|
-
headers: { "Content-Type": "application/json" },
|
|
344
|
-
body: JSON.stringify({ host_id: hostId }),
|
|
345
|
-
});
|
|
346
|
-
} catch {}
|
|
347
|
-
}
|
|
348
|
-
setInterval(beat, 60000);
|
|
349
|
-
beat();
|
|
350
|
-
`
|
|
351
|
-
);
|
|
352
|
-
const child = spawn(process.execPath, [hbPath], {
|
|
353
|
-
detached: true,
|
|
354
|
-
stdio: "ignore",
|
|
355
|
-
});
|
|
356
|
-
child.unref();
|
|
357
|
-
}
|
|
358
|
-
|
|
359
298
|
async function showStatus() {
|
|
360
299
|
const state = loadState();
|
|
361
300
|
if (!state) {
|
|
@@ -368,10 +307,10 @@ async function showStatus() {
|
|
|
368
307
|
console.log(` layers_hosted: ${state.layers_hosted}`);
|
|
369
308
|
console.log(` block_indices: ${state.block_indices}`);
|
|
370
309
|
console.log(` mother: ${state.mother_url}`);
|
|
371
|
-
console.log(`
|
|
310
|
+
console.log(` tunnel: ${state.tunnel_url || state.shard_manager_url || "—"}`);
|
|
311
|
+
console.log(` hedera: ${state.hedera_account_id || "—"}`);
|
|
372
312
|
try {
|
|
373
|
-
const
|
|
374
|
-
const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
|
|
313
|
+
const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
|
|
375
314
|
if (res.ok) {
|
|
376
315
|
const st = await res.json();
|
|
377
316
|
console.log(` petals_running: ${st.running}`);
|
|
@@ -398,11 +337,13 @@ async function doLeave() {
|
|
|
398
337
|
p.log.warn(`Mother leave call failed: ${err.message}`);
|
|
399
338
|
}
|
|
400
339
|
try {
|
|
401
|
-
|
|
402
|
-
await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
|
|
340
|
+
await fetch(`http://127.0.0.1:${SHARD_PORT}/stop`, { method: "POST" });
|
|
403
341
|
} catch {
|
|
404
342
|
/* ignore */
|
|
405
343
|
}
|
|
344
|
+
if (state.tunnel_pid) {
|
|
345
|
+
stopTunnelProcess(state.tunnel_pid);
|
|
346
|
+
}
|
|
406
347
|
clearState();
|
|
407
348
|
spin.stop("Left swarm");
|
|
408
349
|
p.outro("Your layers were reclaimed by the mother (when reachable).");
|