blitzwing 0.1.10 → 0.2.1
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 +3 -2
- 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/config.js +1 -1
- package/src/heartbeat.js +4 -1
- package/src/install.js +51 -180
- package/src/tunnel.js +231 -0
- package/src/wizard.js +119 -127
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,22 +1,17 @@
|
|
|
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";
|
|
21
16
|
import { startContributorHeartbeatDaemon } from "./heartbeat.js";
|
|
22
17
|
|
|
@@ -60,10 +55,9 @@ ${color.bold("blitzwing")} — join a Blitzwing mother swarm as a compute node
|
|
|
60
55
|
blitzwing leave Leave the swarm and reclaim your layers
|
|
61
56
|
|
|
62
57
|
Env:
|
|
63
|
-
BLITZWING_DISCOVERY_URL
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
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})
|
|
67
61
|
`);
|
|
68
62
|
}
|
|
69
63
|
|
|
@@ -71,6 +65,7 @@ async function wizard(args) {
|
|
|
71
65
|
p.intro(color.bgCyan(color.black(" blitzwing ")));
|
|
72
66
|
ensureHome();
|
|
73
67
|
|
|
68
|
+
const existing = loadState();
|
|
74
69
|
const spin = p.spinner();
|
|
75
70
|
spin.start("Loading network from Discovery Service…");
|
|
76
71
|
let mothers;
|
|
@@ -122,108 +117,77 @@ async function wizard(args) {
|
|
|
122
117
|
process.exit(1);
|
|
123
118
|
}
|
|
124
119
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const n = Number(v);
|
|
130
|
-
if (!Number.isInteger(n) || n < 1 || n > maxLayers) {
|
|
131
|
-
return `Enter an integer between 1 and ${maxLayers}`;
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
if (p.isCancel(layers)) {
|
|
136
|
-
p.cancel("Setup cancelled");
|
|
137
|
-
process.exit(0);
|
|
138
|
-
}
|
|
139
|
-
const layersN = Number(layers);
|
|
140
|
-
|
|
141
|
-
const detected = await detectPublicIp();
|
|
142
|
-
const networkMode = await p.select({
|
|
143
|
-
message: "How will other peers reach your Petals node?",
|
|
144
|
-
options: [
|
|
145
|
-
{
|
|
146
|
-
value: "relay",
|
|
147
|
-
label: "Auto (recommended for home / NAT)",
|
|
148
|
-
hint: "Uses Petals libp2p relay — no port forwarding",
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
value: "public",
|
|
152
|
-
label: "Public IP / cloud VM",
|
|
153
|
-
hint: "TCP 31337 (and 8001) open on the internet",
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
value: "ngrok",
|
|
157
|
-
label: "Dev tunnel (ngrok TCP on :4040)",
|
|
158
|
-
hint: "Only if ngrok is already running",
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
initialValue: "relay",
|
|
162
|
-
});
|
|
163
|
-
if (p.isCancel(networkMode)) {
|
|
164
|
-
p.cancel("Setup cancelled");
|
|
165
|
-
process.exit(0);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let publicIp = "";
|
|
169
|
-
let announceMaddrs = "";
|
|
170
|
-
let useAutoRelay = "1";
|
|
120
|
+
const nonInteractive =
|
|
121
|
+
process.env.BLITZWING_NONINTERACTIVE === "1" ||
|
|
122
|
+
process.env.BLITZWING_YES === "1" ||
|
|
123
|
+
!process.stdin.isTTY;
|
|
171
124
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
125
|
+
let layersN;
|
|
126
|
+
if (nonInteractive && process.env.BLITZWING_LAYERS) {
|
|
127
|
+
layersN = Number(process.env.BLITZWING_LAYERS);
|
|
128
|
+
if (!Number.isInteger(layersN) || layersN < 1 || layersN > maxLayers) {
|
|
129
|
+
p.cancel(`BLITZWING_LAYERS must be an integer between 1 and ${maxLayers}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
p.log.info(`Layers: ${layersN} (non-interactive)`);
|
|
133
|
+
} else {
|
|
134
|
+
const layers = await p.text({
|
|
135
|
+
message: `How many layers can this machine host? (1–${maxLayers})`,
|
|
136
|
+
initialValue: String(Math.min(8, maxLayers)),
|
|
176
137
|
validate(v) {
|
|
177
|
-
|
|
178
|
-
|
|
138
|
+
const n = Number(v);
|
|
139
|
+
if (!Number.isInteger(n) || n < 1 || n > maxLayers) {
|
|
140
|
+
return `Enter an integer between 1 and ${maxLayers}`;
|
|
179
141
|
}
|
|
180
142
|
},
|
|
181
143
|
});
|
|
182
|
-
if (p.isCancel(
|
|
144
|
+
if (p.isCancel(layers)) {
|
|
183
145
|
p.cancel("Setup cancelled");
|
|
184
146
|
process.exit(0);
|
|
185
147
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
148
|
+
layersN = Number(layers);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hederaPrefill =
|
|
152
|
+
process.env.BLITZWING_HEDERA_ACCOUNT_ID ||
|
|
153
|
+
process.env.HEDERA_ACCOUNT_ID ||
|
|
154
|
+
existing?.hedera_account_id ||
|
|
155
|
+
"";
|
|
156
|
+
let hederaAccountId;
|
|
157
|
+
if (nonInteractive && hederaPrefill) {
|
|
158
|
+
hederaAccountId = String(hederaPrefill).trim();
|
|
159
|
+
if (!/^0\.0\.\d+$/.test(hederaAccountId)) {
|
|
160
|
+
p.cancel("BLITZWING_HEDERA_ACCOUNT_ID must look like 0.0.123456");
|
|
193
161
|
process.exit(1);
|
|
194
162
|
}
|
|
195
|
-
|
|
196
|
-
publicIp = ngrok.match(/dns4\/([^/]+)/)?.[1] || detected || "relay";
|
|
197
|
-
useAutoRelay = "0";
|
|
198
|
-
p.log.info(`Using ngrok announce ${announceMaddrs}`);
|
|
163
|
+
p.log.info(`Hedera payouts: ${hederaAccountId} (non-interactive)`);
|
|
199
164
|
} else {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
214
|
-
if (p.isCancel(hederaAccount)) {
|
|
215
|
-
p.cancel("Setup cancelled");
|
|
216
|
-
process.exit(0);
|
|
165
|
+
const hederaAccount = await p.text({
|
|
166
|
+
message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
|
|
167
|
+
initialValue: hederaPrefill,
|
|
168
|
+
validate(v) {
|
|
169
|
+
const s = String(v || "").trim();
|
|
170
|
+
if (!/^0\.0\.\d+$/.test(s)) return "Enter a Hedera account like 0.0.123456";
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (p.isCancel(hederaAccount)) {
|
|
174
|
+
p.cancel("Setup cancelled");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
hederaAccountId = String(hederaAccount).trim();
|
|
217
178
|
}
|
|
218
|
-
const hederaAccountId = String(hederaAccount).trim();
|
|
219
179
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
p.
|
|
226
|
-
|
|
180
|
+
if (!nonInteractive) {
|
|
181
|
+
const confirm = await p.confirm({
|
|
182
|
+
message: `Join ${selected.model} hosting ${layersN} layers, payouts to ${hederaAccountId}?`,
|
|
183
|
+
initialValue: true,
|
|
184
|
+
});
|
|
185
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
186
|
+
p.cancel("Setup cancelled");
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
p.log.info(`Joining ${selected.model} with ${layersN} layers…`);
|
|
227
191
|
}
|
|
228
192
|
|
|
229
193
|
spin.start("Preparing Python environment + Petals");
|
|
@@ -233,6 +197,7 @@ async function wizard(args) {
|
|
|
233
197
|
spin.message(msg);
|
|
234
198
|
},
|
|
235
199
|
});
|
|
200
|
+
syncRuntimeFiles();
|
|
236
201
|
} catch (err) {
|
|
237
202
|
spin.stop("Install failed");
|
|
238
203
|
p.cancel(err.message);
|
|
@@ -241,8 +206,28 @@ async function wizard(args) {
|
|
|
241
206
|
const python = venvPython();
|
|
242
207
|
spin.stop("Environment ready");
|
|
243
208
|
|
|
244
|
-
|
|
245
|
-
|
|
209
|
+
spin.start("Setting up Cloudflare Quick Tunnel…");
|
|
210
|
+
let tunnel;
|
|
211
|
+
try {
|
|
212
|
+
const cfBin = await ensureCloudflared({
|
|
213
|
+
onLog: (msg) => {
|
|
214
|
+
spin.message(msg);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
tunnel = await startQuickTunnel({
|
|
218
|
+
port: SHARD_PORT,
|
|
219
|
+
binary: cfBin,
|
|
220
|
+
logPath: path.join(HOME_DIR, "cloudflared.log"),
|
|
221
|
+
});
|
|
222
|
+
} catch (err) {
|
|
223
|
+
spin.stop("Tunnel failed");
|
|
224
|
+
p.cancel(err.message);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
spin.stop(`Public URL ${tunnel.url}`);
|
|
228
|
+
|
|
229
|
+
const localIp = await detectLocalIp();
|
|
230
|
+
const publicIp = (await detectPublicIp()) || localIp || "tunnel";
|
|
246
231
|
|
|
247
232
|
spin.start("Requesting layer assignment from mother…");
|
|
248
233
|
let assignment;
|
|
@@ -251,10 +236,11 @@ async function wizard(args) {
|
|
|
251
236
|
model: selected.model,
|
|
252
237
|
layers: layersN,
|
|
253
238
|
public_ip: String(publicIp).trim(),
|
|
254
|
-
shard_manager_url:
|
|
239
|
+
shard_manager_url: tunnel.url,
|
|
255
240
|
hedera_account_id: hederaAccountId,
|
|
256
241
|
});
|
|
257
242
|
} catch (err) {
|
|
243
|
+
tunnel.stop();
|
|
258
244
|
spin.stop("Join rejected");
|
|
259
245
|
const max = err.body?.detail?.max_layers;
|
|
260
246
|
p.cancel(`${err.message}${max != null ? ` (max available: ${max})` : ""}`);
|
|
@@ -265,19 +251,21 @@ async function wizard(args) {
|
|
|
265
251
|
const logPath = path.join(HOME_DIR, "shard_manager.log");
|
|
266
252
|
spin.start("Starting local Petals server…");
|
|
267
253
|
const shardEnv = {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
254
|
+
MODEL_NAME: selected.model,
|
|
255
|
+
PUBLIC_IP: String(publicIp).trim(),
|
|
256
|
+
BLOCK_INDICES: assignment.block_indices,
|
|
257
|
+
INITIAL_PEERS: "",
|
|
258
|
+
NEW_SWARM: "1",
|
|
259
|
+
BLITZWING_HTTP_ONLY: "1",
|
|
260
|
+
PETALS_PORT: String(PETALS_PORT),
|
|
261
|
+
IDENTITY_PATH: path.join(HOME_DIR, "petals-identity"),
|
|
262
|
+
SHARD_AUTO_START: "1",
|
|
263
|
+
PETALS_USE_AUTO_RELAY: "0",
|
|
264
|
+
PETALS_SKIP_REACHABILITY_CHECK: "1",
|
|
265
|
+
BLITZWING_MOTHER_URL: selected.mother_url,
|
|
266
|
+
MOTHER_PUBLIC_SHARD_URL: selected.mother_url,
|
|
267
|
+
BLITZWING_HOST_ID: assignment.host_id,
|
|
268
|
+
};
|
|
281
269
|
const { pid } = startShardManagerProcess({
|
|
282
270
|
python,
|
|
283
271
|
logPath,
|
|
@@ -287,6 +275,7 @@ async function wizard(args) {
|
|
|
287
275
|
try {
|
|
288
276
|
await waitForShardRunning({ timeoutMs: 600000, statusHost: "127.0.0.1" });
|
|
289
277
|
} catch (err) {
|
|
278
|
+
tunnel.stop();
|
|
290
279
|
spin.stop("Petals did not become ready");
|
|
291
280
|
p.cancel(`${err.message}. See ${logPath}`);
|
|
292
281
|
process.exit(1);
|
|
@@ -294,13 +283,12 @@ async function wizard(args) {
|
|
|
294
283
|
spin.stop("Petals is serving your layers");
|
|
295
284
|
|
|
296
285
|
spin.start("Finalizing handoff with mother…");
|
|
297
|
-
const peerMultiaddr = extractPeerMultiaddrFromLog(petalsLogPath, fs);
|
|
298
286
|
try {
|
|
299
287
|
await readyHost(selected.mother_url, {
|
|
300
288
|
host_id: assignment.host_id,
|
|
301
|
-
peer_multiaddr: peerMultiaddr || undefined,
|
|
302
289
|
});
|
|
303
290
|
} catch (err) {
|
|
291
|
+
tunnel.stop();
|
|
304
292
|
spin.stop("Handoff failed");
|
|
305
293
|
p.cancel(err.message);
|
|
306
294
|
process.exit(1);
|
|
@@ -314,9 +302,10 @@ async function wizard(args) {
|
|
|
314
302
|
block_indices: assignment.block_indices,
|
|
315
303
|
layers_hosted: assignment.layers_hosted,
|
|
316
304
|
public_ip: String(publicIp).trim(),
|
|
317
|
-
network_mode:
|
|
318
|
-
|
|
319
|
-
|
|
305
|
+
network_mode: "cloudflare",
|
|
306
|
+
shard_manager_url: tunnel.url,
|
|
307
|
+
tunnel_url: tunnel.url,
|
|
308
|
+
tunnel_pid: tunnel.pid,
|
|
320
309
|
shard_pid: pid,
|
|
321
310
|
discovery_url: args.discoveryUrl,
|
|
322
311
|
hedera_account_id: hederaAccountId,
|
|
@@ -330,6 +319,7 @@ async function wizard(args) {
|
|
|
330
319
|
|
|
331
320
|
p.outro(
|
|
332
321
|
`${color.green("You are online.")} Hosting ${color.cyan(String(state.layers_hosted))} layers of ${color.cyan(state.model)} at ${state.block_indices}\n` +
|
|
322
|
+
`Tunnel: ${color.cyan(tunnel.url)}\n` +
|
|
333
323
|
`Run ${color.bold("blitzwing status")} anytime, or ${color.bold("blitzwing leave")} to exit.`
|
|
334
324
|
);
|
|
335
325
|
}
|
|
@@ -346,10 +336,10 @@ async function showStatus() {
|
|
|
346
336
|
console.log(` layers_hosted: ${state.layers_hosted}`);
|
|
347
337
|
console.log(` block_indices: ${state.block_indices}`);
|
|
348
338
|
console.log(` mother: ${state.mother_url}`);
|
|
349
|
-
console.log(`
|
|
339
|
+
console.log(` tunnel: ${state.tunnel_url || state.shard_manager_url || "—"}`);
|
|
340
|
+
console.log(` hedera: ${state.hedera_account_id || "—"}`);
|
|
350
341
|
try {
|
|
351
|
-
const
|
|
352
|
-
const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
|
|
342
|
+
const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
|
|
353
343
|
if (res.ok) {
|
|
354
344
|
const st = await res.json();
|
|
355
345
|
console.log(` petals_running: ${st.running}`);
|
|
@@ -376,11 +366,13 @@ async function doLeave() {
|
|
|
376
366
|
p.log.warn(`Mother leave call failed: ${err.message}`);
|
|
377
367
|
}
|
|
378
368
|
try {
|
|
379
|
-
|
|
380
|
-
await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
|
|
369
|
+
await fetch(`http://127.0.0.1:${SHARD_PORT}/stop`, { method: "POST" });
|
|
381
370
|
} catch {
|
|
382
371
|
/* ignore */
|
|
383
372
|
}
|
|
373
|
+
if (state.tunnel_pid) {
|
|
374
|
+
stopTunnelProcess(state.tunnel_pid);
|
|
375
|
+
}
|
|
384
376
|
clearState();
|
|
385
377
|
spin.stop("Left swarm");
|
|
386
378
|
p.outro("Your layers were reclaimed by the mother (when reachable).");
|