mindwire 0.1.29 → 0.1.32
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/dist/{chunk-QPZMGVY7.js → chunk-5X5PAFM7.js} +40 -13
- package/dist/chunk-5X5PAFM7.js.map +1 -0
- package/dist/cli.js +59 -18
- package/dist/cli.js.map +1 -1
- package/dist/computer/connection-flow.d.ts +3 -0
- package/dist/computer/connection-progress.d.ts +14 -0
- package/dist/computer/controller-handoff.d.ts +6 -0
- package/dist/computer/lifecycle.d.ts +5 -0
- package/dist/computer/relay-health.d.ts +21 -0
- package/dist/computer/relay-output.d.ts +19 -0
- package/dist/computer/relay.d.ts +4 -1
- package/dist/computer.d.ts +5 -2
- package/dist/{controller-MNMIJHH5.js → controller-CHJ5MMZS.js} +335 -111
- package/dist/controller-CHJ5MMZS.js.map +1 -0
- package/dist/controller-handoff-7S6SKFHX.js +71 -0
- package/dist/controller-handoff-7S6SKFHX.js.map +1 -0
- package/dist/index.cjs +16 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -12
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/dist/workspace.d.ts +21 -4
- package/package.json +7 -3
- package/dist/chunk-QPZMGVY7.js.map +0 -1
- package/dist/controller-MNMIJHH5.js.map +0 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { readJSON, processStateAlive, currentProcessIdentity, ensureDaemonBinary, SDK_VERSION, computerClient, processIdentity, adoptProcess, writeJSON, directRoutes, ownChild } from './chunk-QPZMGVY7.js';
|
|
2
1
|
import { binaryCacheDirectory, cachedExecutable, verifyReleaseBytes, cacheExecutable, executableChecksum } from './chunk-3FR7BFAW.js';
|
|
2
|
+
import { readJSON, processStateAlive, currentProcessIdentity, ensureDaemonBinary, SDK_VERSION, computerClient, processIdentity, adoptProcess, writeJSON, directRoutes, CONTROLLER_PROTOCOL, ownChild } from './chunk-5X5PAFM7.js';
|
|
3
3
|
import './chunk-T6TD76UA.js';
|
|
4
4
|
import { execFile, execFileSync, spawn } from 'child_process';
|
|
5
|
-
import { randomBytes } from 'crypto';
|
|
5
|
+
import { randomBytes, randomUUID } from 'crypto';
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
|
-
import { readFile, mkdtemp, writeFile
|
|
8
|
-
import * as
|
|
7
|
+
import { open, rm, readFile, mkdtemp, writeFile } from 'fs/promises';
|
|
8
|
+
import * as path3 from 'path';
|
|
9
9
|
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
10
10
|
import { tmpdir } from 'os';
|
|
11
11
|
import { promisify } from 'util';
|
|
12
|
+
import { request } from 'https';
|
|
13
|
+
import { lookup } from 'dns';
|
|
14
|
+
import { Resolver } from 'dns/promises';
|
|
15
|
+
import WebSocket from 'ws';
|
|
12
16
|
|
|
13
17
|
var CLOUDFLARED_VERSION = "2026.9.3";
|
|
14
18
|
var execute = promisify(execFile);
|
|
@@ -19,7 +23,7 @@ async function ensureCloudflareBinary(options = {}) {
|
|
|
19
23
|
throw new Error("Install cloudflared for this computer, or select another connection provider.");
|
|
20
24
|
}
|
|
21
25
|
const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
|
|
22
|
-
const bin =
|
|
26
|
+
const bin = path3.join(
|
|
23
27
|
options.cacheDir ?? await binaryCacheDirectory(),
|
|
24
28
|
"cloudflared",
|
|
25
29
|
CLOUDFLARED_VERSION,
|
|
@@ -29,8 +33,8 @@ async function ensureCloudflareBinary(options = {}) {
|
|
|
29
33
|
const cached = await cachedExecutable(bin);
|
|
30
34
|
if (cached) return cached;
|
|
31
35
|
await options.onProgress?.("Downloading the secure connection helper\u2026");
|
|
32
|
-
const
|
|
33
|
-
const release = await
|
|
36
|
+
const request2 = options.fetch ?? fetch;
|
|
37
|
+
const release = await request2(
|
|
34
38
|
`https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
|
|
35
39
|
{ headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
|
|
36
40
|
);
|
|
@@ -40,15 +44,15 @@ async function ensureCloudflareBinary(options = {}) {
|
|
|
40
44
|
const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
|
|
41
45
|
const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
|
|
42
46
|
if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
|
|
43
|
-
const downloaded = await
|
|
47
|
+
const downloaded = await request2(expectedURL, { signal: AbortSignal.timeout(12e4) });
|
|
44
48
|
if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
|
|
45
49
|
const archive = new Uint8Array(await downloaded.arrayBuffer());
|
|
46
50
|
await verifyReleaseBytes(archive, checksum, asset);
|
|
47
51
|
let executable = archive;
|
|
48
52
|
if (platform === "darwin") {
|
|
49
|
-
const temporary = await mkdtemp(
|
|
53
|
+
const temporary = await mkdtemp(path3.join(tmpdir(), "mindwire-cloudflared-"));
|
|
50
54
|
try {
|
|
51
|
-
const archivePath =
|
|
55
|
+
const archivePath = path3.join(temporary, asset);
|
|
52
56
|
await writeFile(archivePath, archive, { mode: 384 });
|
|
53
57
|
const result = await execute(
|
|
54
58
|
"/usr/bin/tar",
|
|
@@ -71,8 +75,70 @@ async function cloudflareCommand(options) {
|
|
|
71
75
|
return ensureCloudflareBinary(options);
|
|
72
76
|
}
|
|
73
77
|
}
|
|
78
|
+
var RelayOutput = class _RelayOutput {
|
|
79
|
+
constructor(file, handle) {
|
|
80
|
+
this.file = file;
|
|
81
|
+
this.handle = handle;
|
|
82
|
+
}
|
|
83
|
+
file;
|
|
84
|
+
handle;
|
|
85
|
+
position = 0;
|
|
86
|
+
timer;
|
|
87
|
+
pending = Promise.resolve();
|
|
88
|
+
closing;
|
|
89
|
+
static async create(directory = tmpdir()) {
|
|
90
|
+
const file = path3.join(directory, `.relay-output-${randomUUID()}.log`);
|
|
91
|
+
return new _RelayOutput(file, await open(file, "ax+", 384));
|
|
92
|
+
}
|
|
93
|
+
static async adopt(directory, file) {
|
|
94
|
+
if (path3.dirname(file) !== path3.resolve(directory) || !/^\.relay-output-[a-f0-9-]{36}\.log$/.test(path3.basename(file))) return;
|
|
95
|
+
try {
|
|
96
|
+
const output = new _RelayOutput(file, await open(file, "r+"));
|
|
97
|
+
output.discard();
|
|
98
|
+
return output;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.code !== "ENOENT") throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
get fd() {
|
|
104
|
+
return this.handle.fd;
|
|
105
|
+
}
|
|
106
|
+
async read() {
|
|
107
|
+
const { size } = await this.handle.stat();
|
|
108
|
+
if (size > 2 * 1024 * 1024) throw new Error("The tunnel helper produced too much startup output. Check its configuration.");
|
|
109
|
+
const data = Buffer.alloc(Math.min(65536, Math.max(0, size - this.position)));
|
|
110
|
+
const { bytesRead } = await this.handle.read(data, 0, data.length, this.position);
|
|
111
|
+
this.position += bytesRead;
|
|
112
|
+
return data.subarray(0, bytesRead);
|
|
113
|
+
}
|
|
114
|
+
discard() {
|
|
115
|
+
if (this.timer || this.closing) return;
|
|
116
|
+
const drain = () => {
|
|
117
|
+
this.pending = this.pending.then(() => this.handle.truncate(0)).catch(() => {
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
drain();
|
|
121
|
+
this.timer = setInterval(drain, 2e3);
|
|
122
|
+
this.timer.unref();
|
|
123
|
+
}
|
|
124
|
+
close() {
|
|
125
|
+
return this.closing ??= (async () => {
|
|
126
|
+
clearInterval(this.timer);
|
|
127
|
+
await this.pending;
|
|
128
|
+
await this.handle.close();
|
|
129
|
+
await rm(this.file, { force: true });
|
|
130
|
+
})();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
74
133
|
|
|
75
134
|
// src/computer/relay.ts
|
|
135
|
+
async function adoptRelay(owner, route, directory, outputFile) {
|
|
136
|
+
const output = outputFile ? await RelayOutput.adopt(directory, outputFile) : void 0;
|
|
137
|
+
return { route, owner, outputFile: output?.file, async close() {
|
|
138
|
+
await owner.close();
|
|
139
|
+
await output?.close();
|
|
140
|
+
} };
|
|
141
|
+
}
|
|
76
142
|
function websocketURL(value) {
|
|
77
143
|
const url = new URL(value);
|
|
78
144
|
if (url.protocol === "https:") url.protocol = "wss:";
|
|
@@ -114,83 +180,166 @@ async function startRelay(options, port, runtime = {}) {
|
|
|
114
180
|
}
|
|
115
181
|
}
|
|
116
182
|
runtime.signal?.throwIfAborted();
|
|
117
|
-
const
|
|
118
|
-
|
|
183
|
+
const output = await RelayOutput.create(runtime.outputDirectory);
|
|
184
|
+
const child = spawn(command, args, { env, detached: true, stdio: ["ignore", output.fd, output.fd], windowsHide: true });
|
|
185
|
+
let failure;
|
|
186
|
+
child.on("error", (error) => {
|
|
187
|
+
failure = error;
|
|
188
|
+
});
|
|
189
|
+
let owner;
|
|
119
190
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let cloudflareConnected = false;
|
|
146
|
-
const receive = (chunk) => {
|
|
147
|
-
const received = buffer + chunk.toString();
|
|
148
|
-
buffer = received.slice(-16384);
|
|
149
|
-
let found;
|
|
150
|
-
if (options.kind === "cloudflare") {
|
|
151
|
-
cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
|
|
152
|
-
cloudflareConnected ||= /Registered tunnel connection/.test(received);
|
|
153
|
-
if (cloudflareConnected) found = cloudflareAddress;
|
|
154
|
-
} else {
|
|
155
|
-
for (const line of received.split(/\r?\n/)) {
|
|
156
|
-
try {
|
|
157
|
-
const event = JSON.parse(line);
|
|
158
|
-
if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
191
|
+
await new Promise((resolve3, reject) => {
|
|
192
|
+
child.once("spawn", resolve3);
|
|
193
|
+
child.once("error", reject);
|
|
194
|
+
});
|
|
195
|
+
owner = await ownChild(child);
|
|
196
|
+
await runtime.onSpawn?.(owner, output.file);
|
|
197
|
+
const deadline = Date.now() + 45e3;
|
|
198
|
+
let buffer = "", cloudflareAddress = options.url, cloudflareConnected = false;
|
|
199
|
+
while (Date.now() < deadline) {
|
|
200
|
+
runtime.signal?.throwIfAborted();
|
|
201
|
+
if (failure) throw new Error(`Could not run ${options.kind}. Check its installation.`, { cause: failure });
|
|
202
|
+
if (!owner.alive()) throw new Error(`${options.kind} exited before the tunnel connected. Check its account configuration.`);
|
|
203
|
+
const received = buffer + (await output.read()).toString();
|
|
204
|
+
buffer = received.slice(-16384);
|
|
205
|
+
let found;
|
|
206
|
+
if (options.kind === "cloudflare") {
|
|
207
|
+
cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
|
|
208
|
+
cloudflareConnected ||= /Registered tunnel connection/.test(received);
|
|
209
|
+
if (cloudflareConnected) found = cloudflareAddress;
|
|
210
|
+
} else {
|
|
211
|
+
for (const line of received.split(/\r?\n/)) {
|
|
212
|
+
try {
|
|
213
|
+
const event = JSON.parse(line);
|
|
214
|
+
if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
|
|
215
|
+
} catch {
|
|
161
216
|
}
|
|
162
217
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
218
|
+
}
|
|
219
|
+
if (found) {
|
|
220
|
+
const route = { kind: "websocket", url: websocketURL(found) };
|
|
221
|
+
output.discard();
|
|
222
|
+
const provider = owner;
|
|
223
|
+
return {
|
|
224
|
+
route,
|
|
225
|
+
process: child,
|
|
226
|
+
owner,
|
|
227
|
+
outputFile: output.file,
|
|
228
|
+
async close() {
|
|
229
|
+
await provider.close();
|
|
230
|
+
await output.close();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
await setTimeout$1(50, void 0, { signal: runtime.signal });
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`${options.kind} did not establish its tunnel. Check your internet connection and its configuration.`);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (owner) await owner.close();
|
|
239
|
+
else if (child.pid) await (await ownChild(child)).close();
|
|
240
|
+
await output.close();
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
var RelayCheckError = class extends Error {
|
|
245
|
+
constructor(code, message, cause) {
|
|
246
|
+
super(message, cause ? { cause } : void 0);
|
|
247
|
+
this.code = code;
|
|
248
|
+
this.name = "RelayCheckError";
|
|
249
|
+
}
|
|
250
|
+
code;
|
|
251
|
+
};
|
|
252
|
+
function relayNeedsRestart(error, failures, failedForMs) {
|
|
253
|
+
return failures >= 2 && failedForMs >= 9e4 && !(error instanceof RelayCheckError && error.code === "dns");
|
|
254
|
+
}
|
|
255
|
+
var relayLookup = (hostname, options, callback) => {
|
|
256
|
+
lookup(hostname, options, (error, address, family) => {
|
|
257
|
+
if (error?.code !== "ENOTFOUND" || !/^[a-z0-9-]+\.trycloudflare\.com$/i.test(hostname)) {
|
|
258
|
+
callback(error, address, family);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const resolver = new Resolver({ timeout: 3e3, tries: 1 });
|
|
262
|
+
void Promise.all([
|
|
263
|
+
options.family === 6 ? [] : resolver.resolve4(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 4 })), () => []),
|
|
264
|
+
options.family === 4 ? [] : resolver.resolve6(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 6 })), () => [])
|
|
265
|
+
]).then((results) => {
|
|
266
|
+
const addresses = results.flat();
|
|
267
|
+
if (!addresses.length) {
|
|
268
|
+
callback(error, address, family);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (options.all) callback(null, addresses);
|
|
272
|
+
else callback(null, addresses[0].address, addresses[0].family);
|
|
174
273
|
});
|
|
175
|
-
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
async function checkRelay(route, options = {}) {
|
|
277
|
+
options.signal?.throwIfAborted();
|
|
278
|
+
if (route.kind !== "websocket") throw new Error("Expected an internet tunnel address.");
|
|
279
|
+
await new Promise((resolve3, reject) => {
|
|
280
|
+
const socket = new WebSocket(route.url, { maxPayload: 4096, perMessageDeflate: false, followRedirects: false, lookup: options.lookup ?? relayLookup });
|
|
281
|
+
let finished = false, banner = "";
|
|
282
|
+
const finish = (error) => {
|
|
283
|
+
if (finished) return;
|
|
284
|
+
finished = true;
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
options.signal?.removeEventListener("abort", aborted);
|
|
287
|
+
socket.terminate();
|
|
288
|
+
if (error) reject(error);
|
|
289
|
+
else resolve3();
|
|
290
|
+
};
|
|
291
|
+
const aborted = () => finish(options.signal?.reason ?? new Error("Connection check cancelled."));
|
|
292
|
+
const timer = setTimeout(() => finish(new RelayCheckError("timeout", "The internet tunnel isn't responding. Retrying\u2026")), options.timeoutMs ?? 8e3);
|
|
293
|
+
socket.on("error", (error) => {
|
|
294
|
+
if (["ENOTFOUND", "EAI_AGAIN"].includes(error.code ?? "")) {
|
|
295
|
+
const provider = new URL(route.url).hostname.endsWith(".trycloudflare.com") ? "Cloudflare's" : "the tunnel's";
|
|
296
|
+
finish(new RelayCheckError("dns", `Waiting for ${provider} internet address (DNS)\u2026`, error));
|
|
297
|
+
} else if (/(?:[Uu]nexpected server response|[Ee]xpected 101)/.test(error.message)) {
|
|
298
|
+
finish(new RelayCheckError("proxy", "The internet tunnel isn't reaching Mindwire. Retrying\u2026", error));
|
|
299
|
+
} else {
|
|
300
|
+
finish(new RelayCheckError("connection", "The internet tunnel couldn't connect. Retrying\u2026", error));
|
|
301
|
+
}
|
|
176
302
|
});
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
303
|
+
socket.on("close", () => finish(new RelayCheckError("connection", "The internet tunnel closed before reaching Mindwire. Retrying\u2026")));
|
|
304
|
+
socket.on("message", (data, binary) => {
|
|
305
|
+
if (!binary) {
|
|
306
|
+
finish(new RelayCheckError("response", "The internet tunnel returned an invalid SSH response."));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
banner += data.toString();
|
|
310
|
+
if (banner.length > 1024) {
|
|
311
|
+
finish(new RelayCheckError("response", "The internet tunnel returned an invalid SSH response."));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (banner.includes("\n")) {
|
|
315
|
+
finish(banner.split("\n")[0]?.trim() === "SSH-2.0-Mindwire" ? void 0 : new RelayCheckError("response", "The internet tunnel isn't connected to the Mindwire SSH service."));
|
|
316
|
+
}
|
|
186
317
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
318
|
+
options.signal?.addEventListener("abort", aborted, { once: true });
|
|
319
|
+
if (options.signal?.aborted) aborted();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function relayProviderReachable(options, signal) {
|
|
323
|
+
if (options.kind !== "cloudflare" && options.kind !== "ngrok") return false;
|
|
324
|
+
signal?.throwIfAborted();
|
|
325
|
+
return new Promise((resolve3) => {
|
|
326
|
+
const url = options.kind === "cloudflare" ? "https://api.trycloudflare.com/" : "https://api.ngrok.com/";
|
|
327
|
+
let finished = false;
|
|
328
|
+
const done = (reachable) => {
|
|
329
|
+
if (finished) return;
|
|
330
|
+
finished = true;
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
probe.destroy();
|
|
333
|
+
resolve3(reachable);
|
|
334
|
+
};
|
|
335
|
+
const probe = request(url, { method: "HEAD", signal }, (response) => {
|
|
336
|
+
response.destroy();
|
|
337
|
+
done(true);
|
|
190
338
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
339
|
+
const timer = setTimeout(() => done(false), 5e3);
|
|
340
|
+
probe.on("error", () => done(false));
|
|
341
|
+
probe.end();
|
|
342
|
+
});
|
|
194
343
|
}
|
|
195
344
|
|
|
196
345
|
// src/computer/controller.ts
|
|
@@ -198,19 +347,21 @@ function recoveryDelay(failures) {
|
|
|
198
347
|
return Math.min(6e4, 1e3 * 2 ** Math.min(6, Math.max(0, failures - 1)));
|
|
199
348
|
}
|
|
200
349
|
async function superviseComputer(directory) {
|
|
201
|
-
if ((await readJSON(
|
|
202
|
-
let config = await readJSON(
|
|
350
|
+
if ((await readJSON(path3.join(directory, "computer-stopped.json")))?.stopped) return;
|
|
351
|
+
let config = await readJSON(path3.join(directory, "computer-config.json"));
|
|
203
352
|
if (!config) throw new Error("Computer configuration is missing.");
|
|
204
|
-
const controllerPath =
|
|
205
|
-
const relayPath =
|
|
353
|
+
const controllerPath = path3.join(directory, "computer-controller.json");
|
|
354
|
+
const relayPath = path3.join(directory, "computer-relay.json");
|
|
206
355
|
const previous = await readJSON(controllerPath);
|
|
207
356
|
if (await processStateAlive(previous)) throw new Error("The computer already has a service controller.");
|
|
208
357
|
const owner = await currentProcessIdentity();
|
|
209
|
-
const phase = (message) => writeJSON(controllerPath, { ...owner, ready: false, phase: message, recovering: true });
|
|
358
|
+
const phase = (message) => writeJSON(controllerPath, { ...owner, protocol: CONTROLLER_PROTOCOL, cliVersion: SDK_VERSION, ready: false, phase: message, recovering: true });
|
|
210
359
|
await phase("Preparing Mindwire\u2026");
|
|
211
360
|
let child, relay, stopping = false;
|
|
212
361
|
let relayTask, updateTask;
|
|
213
362
|
let relayResult;
|
|
363
|
+
let checkTask;
|
|
364
|
+
let checkResult;
|
|
214
365
|
const abort = new AbortController();
|
|
215
366
|
const stop = () => {
|
|
216
367
|
stopping = true;
|
|
@@ -220,30 +371,30 @@ async function superviseComputer(directory) {
|
|
|
220
371
|
process.on("SIGINT", stop);
|
|
221
372
|
let token;
|
|
222
373
|
try {
|
|
223
|
-
token = (await fs.readFile(
|
|
374
|
+
token = (await fs.readFile(path3.join(directory, "daemon.token"), "utf8")).trim();
|
|
224
375
|
} catch (error) {
|
|
225
376
|
if (error.code !== "ENOENT") throw error;
|
|
226
377
|
token = randomBytes(32).toString("hex");
|
|
227
|
-
await fs.writeFile(
|
|
378
|
+
await fs.writeFile(path3.join(directory, "daemon.token"), token, { mode: 384, flag: "wx" });
|
|
228
379
|
}
|
|
229
380
|
const daemonEnv = { ...process.env };
|
|
230
381
|
if (process.platform === "win32") {
|
|
231
382
|
try {
|
|
232
383
|
const git = execFileSync("where.exe", ["git"], { encoding: "utf8", timeout: 3e3, windowsHide: true }).split(/\r?\n/)[0];
|
|
233
384
|
if (git) {
|
|
234
|
-
const shellDirectory =
|
|
235
|
-
await fs.access(
|
|
385
|
+
const shellDirectory = path3.resolve(path3.dirname(git), "..", "bin");
|
|
386
|
+
await fs.access(path3.join(shellDirectory, "bash.exe"));
|
|
236
387
|
const pathKey = Object.keys(daemonEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
237
|
-
daemonEnv[pathKey] = `${shellDirectory}${
|
|
388
|
+
daemonEnv[pathKey] = `${shellDirectory}${path3.delimiter}${daemonEnv[pathKey] ?? ""}`;
|
|
238
389
|
}
|
|
239
390
|
} catch {
|
|
240
391
|
}
|
|
241
392
|
}
|
|
242
|
-
const oldRuntime = await readJSON(
|
|
393
|
+
const oldRuntime = await readJSON(path3.join(directory, "computer-runtime.json"));
|
|
243
394
|
let sshPort = config.sshPort || oldRuntime?.sshPort || 0;
|
|
244
395
|
let websocketPort = config.websocketPort || oldRuntime?.websocketPort || 0;
|
|
245
396
|
const launch = async (binary) => {
|
|
246
|
-
await fs.rm(
|
|
397
|
+
await fs.rm(path3.join(directory, "computer-runtime.json"), { force: true });
|
|
247
398
|
const process2 = spawn(binary, ["--computer"], {
|
|
248
399
|
cwd: config.directory,
|
|
249
400
|
stdio: ["ignore", "inherit", "inherit"],
|
|
@@ -252,8 +403,8 @@ async function superviseComputer(directory) {
|
|
|
252
403
|
...daemonEnv,
|
|
253
404
|
DAEMON_TOKEN: token,
|
|
254
405
|
ADDR: "127.0.0.1:0",
|
|
255
|
-
STATE_PATH:
|
|
256
|
-
WORKSPACE_DB_PATH:
|
|
406
|
+
STATE_PATH: path3.join(directory, "agent-state.json"),
|
|
407
|
+
WORKSPACE_DB_PATH: path3.join(directory, "workspace.db"),
|
|
257
408
|
AGENT_CWD: config.directory,
|
|
258
409
|
COMPUTER_SSH_ADDR: `${config.bind.includes(":") ? `[${config.bind}]` : config.bind}:${sshPort}`,
|
|
259
410
|
COMPUTER_WS_ADDR: `127.0.0.1:${websocketPort}`,
|
|
@@ -264,8 +415,8 @@ async function superviseComputer(directory) {
|
|
|
264
415
|
process2.on("error", (error) => {
|
|
265
416
|
failure = error;
|
|
266
417
|
});
|
|
267
|
-
await new Promise((
|
|
268
|
-
process2.once("spawn",
|
|
418
|
+
await new Promise((resolve3, reject) => {
|
|
419
|
+
process2.once("spawn", resolve3);
|
|
269
420
|
process2.once("error", reject);
|
|
270
421
|
});
|
|
271
422
|
child = await ownChild(process2);
|
|
@@ -292,9 +443,13 @@ async function superviseComputer(directory) {
|
|
|
292
443
|
let routes = [], published = "";
|
|
293
444
|
let client;
|
|
294
445
|
let relayMessage = "Preparing your internet connection\u2026", relayError;
|
|
446
|
+
let relayErrorCode;
|
|
295
447
|
let daemonError, nextDaemonAt = 0, daemonFailures = 0;
|
|
296
448
|
let nextRelayAt = 0, relayFailures = 0, changingDaemon = false;
|
|
297
449
|
let daemonReadyAt = Date.now(), relayReadyAt = Date.now();
|
|
450
|
+
let relayReachable = false, relayWasReachable = false, relayHealthFailures = 0;
|
|
451
|
+
let relayFailedAt = 0;
|
|
452
|
+
let nextCheckAt = 0, checkedAt = 0;
|
|
298
453
|
try {
|
|
299
454
|
await phase("Checking the Mindwire service\u2026");
|
|
300
455
|
let binary = config.daemonBin ?? await ensureDaemonBinary({ version: config.version ?? SDK_VERSION, cacheDir: process.env.MINDWIRE_RELEASE_CACHE_DIR });
|
|
@@ -317,11 +472,16 @@ async function superviseComputer(directory) {
|
|
|
317
472
|
if (saved) {
|
|
318
473
|
const owner2 = await adoptProcess(saved.owner);
|
|
319
474
|
if (owner2) {
|
|
320
|
-
if (saved.route && saved.port === websocketPort && saved.options === JSON.stringify(config.relay)) {
|
|
321
|
-
relay =
|
|
475
|
+
if (saved.outputFile && saved.route && saved.port === websocketPort && saved.options === JSON.stringify(config.relay)) {
|
|
476
|
+
relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
|
|
322
477
|
} else {
|
|
323
|
-
await
|
|
478
|
+
if (!saved.outputFile) await phase("Refreshing the internet tunnel for this update\u2026");
|
|
479
|
+
const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
|
|
480
|
+
await abandoned.close();
|
|
481
|
+
await fs.rm(relayPath, { force: true });
|
|
324
482
|
}
|
|
483
|
+
} else if (saved.outputFile) {
|
|
484
|
+
await (await RelayOutput.adopt(directory, saved.outputFile))?.close();
|
|
325
485
|
}
|
|
326
486
|
}
|
|
327
487
|
let lastController = "";
|
|
@@ -333,7 +493,7 @@ async function superviseComputer(directory) {
|
|
|
333
493
|
nextDaemonAt = Date.now() + recoveryDelay(++daemonFailures);
|
|
334
494
|
}
|
|
335
495
|
if (child?.alive() && Date.now() - daemonReadyAt > 6e4) daemonFailures = 0;
|
|
336
|
-
if (relay?.owner?.alive() && Date.now() - relayReadyAt > 6e4) relayFailures = 0;
|
|
496
|
+
if (relayReachable && relay?.owner?.alive() && Date.now() - relayReadyAt > 6e4) relayFailures = 0;
|
|
337
497
|
if (!changingDaemon && !child?.alive() && Date.now() >= nextDaemonAt) {
|
|
338
498
|
daemonError = "The service stopped. Restarting it\u2026";
|
|
339
499
|
await phase(daemonError);
|
|
@@ -351,7 +511,11 @@ async function superviseComputer(directory) {
|
|
|
351
511
|
}
|
|
352
512
|
}
|
|
353
513
|
if (relay?.owner && !relay.owner.alive()) {
|
|
514
|
+
await relay.close();
|
|
354
515
|
relay = void 0;
|
|
516
|
+
relayReachable = false;
|
|
517
|
+
relayWasReachable = false;
|
|
518
|
+
relayErrorCode = void 0;
|
|
355
519
|
relayError = "The internet tunnel disconnected. Reconnecting\u2026";
|
|
356
520
|
nextRelayAt = Date.now() + recoveryDelay(++relayFailures);
|
|
357
521
|
}
|
|
@@ -362,12 +526,19 @@ async function superviseComputer(directory) {
|
|
|
362
526
|
if (result.handle) {
|
|
363
527
|
relay = result.handle;
|
|
364
528
|
relayError = void 0;
|
|
529
|
+
relayErrorCode = void 0;
|
|
365
530
|
relayReadyAt = Date.now();
|
|
531
|
+
relayReachable = false;
|
|
532
|
+
relayWasReachable = false;
|
|
533
|
+
relayHealthFailures = 0;
|
|
534
|
+
relayFailedAt = 0;
|
|
535
|
+
nextCheckAt = 0;
|
|
366
536
|
if (relay.owner?.identity) await writeJSON(relayPath, {
|
|
367
537
|
owner: relay.owner.identity,
|
|
368
538
|
port: websocketPort,
|
|
369
539
|
options: JSON.stringify(config.relay),
|
|
370
|
-
route: relay.route
|
|
540
|
+
route: relay.route,
|
|
541
|
+
outputFile: relay.outputFile
|
|
371
542
|
});
|
|
372
543
|
} else {
|
|
373
544
|
relayError = result.error instanceof Error ? result.error.message : "The internet tunnel couldn't connect.";
|
|
@@ -375,18 +546,22 @@ async function superviseComputer(directory) {
|
|
|
375
546
|
}
|
|
376
547
|
}
|
|
377
548
|
if (!relay && !relayTask && Date.now() >= nextRelayAt) {
|
|
549
|
+
relayError = void 0;
|
|
550
|
+
relayErrorCode = void 0;
|
|
378
551
|
relayMessage = "Reconnecting your internet tunnel\u2026";
|
|
379
552
|
relayTask = startRelay(config.relay, websocketPort, {
|
|
380
|
-
cacheDir:
|
|
553
|
+
cacheDir: path3.join(directory, "tools"),
|
|
554
|
+
outputDirectory: directory,
|
|
381
555
|
signal: abort.signal,
|
|
382
556
|
onProgress: (message) => {
|
|
383
557
|
relayMessage = message;
|
|
384
558
|
},
|
|
385
|
-
onSpawn: async (owner2) => {
|
|
559
|
+
onSpawn: async (owner2, outputFile) => {
|
|
386
560
|
if (owner2.identity) await writeJSON(relayPath, {
|
|
387
561
|
owner: owner2.identity,
|
|
388
562
|
port: websocketPort,
|
|
389
|
-
options: JSON.stringify(config.relay)
|
|
563
|
+
options: JSON.stringify(config.relay),
|
|
564
|
+
outputFile
|
|
390
565
|
});
|
|
391
566
|
}
|
|
392
567
|
}).then((handle) => {
|
|
@@ -395,8 +570,52 @@ async function superviseComputer(directory) {
|
|
|
395
570
|
relayResult = { error };
|
|
396
571
|
});
|
|
397
572
|
}
|
|
573
|
+
if (checkResult) {
|
|
574
|
+
const result = checkResult;
|
|
575
|
+
checkResult = void 0;
|
|
576
|
+
checkTask = void 0;
|
|
577
|
+
if (result.relay === relay) {
|
|
578
|
+
checkedAt = Date.now();
|
|
579
|
+
relayReachable = !result.error;
|
|
580
|
+
if (!result.error) {
|
|
581
|
+
relayWasReachable = true;
|
|
582
|
+
relayHealthFailures = 0;
|
|
583
|
+
relayFailedAt = 0;
|
|
584
|
+
relayError = void 0;
|
|
585
|
+
relayErrorCode = void 0;
|
|
586
|
+
nextCheckAt = checkedAt + 3e4;
|
|
587
|
+
} else {
|
|
588
|
+
relayError = result.error instanceof Error ? result.error.message : "The internet tunnel couldn't connect. Retrying\u2026";
|
|
589
|
+
relayErrorCode = result.error instanceof RelayCheckError ? result.error.code : void 0;
|
|
590
|
+
relayFailedAt ||= checkedAt;
|
|
591
|
+
relayHealthFailures++;
|
|
592
|
+
nextCheckAt = checkedAt + Math.min(15e3, recoveryDelay(relayHealthFailures));
|
|
593
|
+
if (result.canRestart) {
|
|
594
|
+
await relay.close();
|
|
595
|
+
relay = void 0;
|
|
596
|
+
relayWasReachable = false;
|
|
597
|
+
nextRelayAt = Date.now() + recoveryDelay(++relayFailures);
|
|
598
|
+
await fs.rm(relayPath, { force: true });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const requestedCheck = await readJSON(path3.join(directory, "computer-check.json"));
|
|
604
|
+
if (config.relay.kind === "none") checkedAt = Math.max(checkedAt, requestedCheck?.requestedAt ?? 0);
|
|
605
|
+
if (relay?.route && !checkTask && (Date.now() >= nextCheckAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
|
|
606
|
+
const checking = relay;
|
|
607
|
+
checkTask = (async () => {
|
|
608
|
+
try {
|
|
609
|
+
await checkRelay(checking.route, { signal: abort.signal });
|
|
610
|
+
checkResult = { relay: checking };
|
|
611
|
+
} catch (error) {
|
|
612
|
+
const canRestart = !!checking.owner && relayFailedAt > 0 && relayNeedsRestart(error, relayHealthFailures, Date.now() - relayFailedAt) && await relayProviderReachable(config.relay, abort.signal).catch(() => false);
|
|
613
|
+
checkResult = { relay: checking, error, canRestart };
|
|
614
|
+
}
|
|
615
|
+
})();
|
|
616
|
+
}
|
|
398
617
|
routes = directRoutes(config, sshPort);
|
|
399
|
-
if (relay?.route) routes.push(relay.route);
|
|
618
|
+
if (relay?.route && relayWasReachable) routes.push(relay.route);
|
|
400
619
|
const routeJSON = JSON.stringify(routes);
|
|
401
620
|
if (!changingDaemon && client && child?.alive() && routeJSON !== published) {
|
|
402
621
|
try {
|
|
@@ -405,13 +624,17 @@ async function superviseComputer(directory) {
|
|
|
405
624
|
} catch {
|
|
406
625
|
}
|
|
407
626
|
}
|
|
408
|
-
const ready = !!client && !!child?.alive() && !changingDaemon && routes.length > 0 && published === routeJSON && (config.relay.kind === "none" ||
|
|
627
|
+
const ready = !!client && !!child?.alive() && !changingDaemon && routes.length > 0 && published === routeJSON && (config.relay.kind === "none" || relayReachable);
|
|
409
628
|
const state = {
|
|
410
629
|
...owner,
|
|
630
|
+
protocol: CONTROLLER_PROTOCOL,
|
|
631
|
+
cliVersion: SDK_VERSION,
|
|
411
632
|
ready,
|
|
412
633
|
routes,
|
|
413
|
-
|
|
634
|
+
checkedAt,
|
|
635
|
+
phase: ready ? void 0 : daemonError ?? relayError ?? relayMessage,
|
|
414
636
|
error: daemonError ?? relayError,
|
|
637
|
+
errorCode: daemonError ? void 0 : relayErrorCode,
|
|
415
638
|
recovering: !ready
|
|
416
639
|
};
|
|
417
640
|
const serialized = JSON.stringify(state);
|
|
@@ -419,7 +642,7 @@ async function superviseComputer(directory) {
|
|
|
419
642
|
await writeJSON(controllerPath, state);
|
|
420
643
|
lastController = serialized;
|
|
421
644
|
}
|
|
422
|
-
const updatePath =
|
|
645
|
+
const updatePath = path3.join(directory, "computer-update.json");
|
|
423
646
|
const update = !updateTask && client && child?.alive() ? await readJSON(updatePath) : void 0;
|
|
424
647
|
if (update && ["queued", "downloading", "waiting", "restarting"].includes(update.status)) {
|
|
425
648
|
updateTask = (async () => {
|
|
@@ -459,7 +682,7 @@ async function superviseComputer(directory) {
|
|
|
459
682
|
}
|
|
460
683
|
binary = replacement;
|
|
461
684
|
config = { ...config, daemonBin: void 0, version: update.version };
|
|
462
|
-
await writeJSON(
|
|
685
|
+
await writeJSON(path3.join(directory, "computer-config.json"), config);
|
|
463
686
|
await client.computer.setRoutes(routes);
|
|
464
687
|
published = JSON.stringify(routes);
|
|
465
688
|
await status("complete");
|
|
@@ -481,6 +704,7 @@ async function superviseComputer(directory) {
|
|
|
481
704
|
} finally {
|
|
482
705
|
abort.abort();
|
|
483
706
|
await relayTask;
|
|
707
|
+
await checkTask;
|
|
484
708
|
await updateTask;
|
|
485
709
|
if (relayResult?.handle && relayResult.handle !== relay) await relayResult.handle.close();
|
|
486
710
|
await relay?.close();
|
|
@@ -492,5 +716,5 @@ async function superviseComputer(directory) {
|
|
|
492
716
|
}
|
|
493
717
|
|
|
494
718
|
export { recoveryDelay, superviseComputer };
|
|
495
|
-
//# sourceMappingURL=controller-
|
|
496
|
-
//# sourceMappingURL=controller-
|
|
719
|
+
//# sourceMappingURL=controller-CHJ5MMZS.js.map
|
|
720
|
+
//# sourceMappingURL=controller-CHJ5MMZS.js.map
|