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.
@@ -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, rm } from 'fs/promises';
8
- import * as path2 from 'path';
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 = path2.join(
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 request = options.fetch ?? fetch;
33
- const release = await request(
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 request(expectedURL, { signal: AbortSignal.timeout(12e4) });
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(path2.join(tmpdir(), "mindwire-cloudflared-"));
53
+ const temporary = await mkdtemp(path3.join(tmpdir(), "mindwire-cloudflared-"));
50
54
  try {
51
- const archivePath = path2.join(temporary, asset);
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 child = spawn(command, args, { env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
118
- let buffer = "";
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
- const address = new Promise((resolve2, reject) => {
121
- const timer = setTimeout(() => {
122
- cleanup();
123
- reject(new Error(`${command} did not establish its tunnel. Check its account and tunnel configuration.`));
124
- }, 45e3);
125
- const cleanup = () => {
126
- clearTimeout(timer);
127
- child.off("error", fail);
128
- child.off("exit", exited);
129
- runtime.signal?.removeEventListener("abort", aborted);
130
- };
131
- const fail = (error) => {
132
- cleanup();
133
- reject(new Error(`Could not run ${command}. Install it on the computer first.`, { cause: error }));
134
- };
135
- const exited = () => {
136
- cleanup();
137
- reject(new Error(`${command} exited before the tunnel connected. Check its account configuration.`));
138
- };
139
- const aborted = () => {
140
- cleanup();
141
- child.kill();
142
- reject(runtime.signal?.reason ?? new Error("Tunnel startup cancelled."));
143
- };
144
- let cloudflareAddress = options.url;
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
- if (found) {
164
- cleanup();
165
- resolve2(websocketURL(found));
166
- }
167
- };
168
- child.once("error", fail);
169
- child.once("exit", exited);
170
- child.stdout.on("data", receive);
171
- child.stderr.on("data", receive);
172
- runtime.signal?.addEventListener("abort", aborted, { once: true });
173
- if (runtime.signal?.aborted) aborted();
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
- void address.catch(() => {
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
- const owner = await ownChild(child);
178
- await runtime.onSpawn?.(owner);
179
- const url = await address;
180
- runtime.signal?.throwIfAborted();
181
- child.stdout.removeAllListeners("data");
182
- child.stderr.removeAllListeners("data");
183
- child.stdout.resume();
184
- child.stderr.resume();
185
- child.on("error", () => {
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
- return { route: { kind: "websocket", url }, process: child, owner, close: () => owner.close() };
188
- } catch (error) {
189
- child.on("error", () => {
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
- if (child.pid) await (await ownChild(child)).close();
192
- throw error;
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(path2.join(directory, "computer-stopped.json")))?.stopped) return;
202
- let config = await readJSON(path2.join(directory, "computer-config.json"));
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 = path2.join(directory, "computer-controller.json");
205
- const relayPath = path2.join(directory, "computer-relay.json");
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(path2.join(directory, "daemon.token"), "utf8")).trim();
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(path2.join(directory, "daemon.token"), token, { mode: 384, flag: "wx" });
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 = path2.resolve(path2.dirname(git), "..", "bin");
235
- await fs.access(path2.join(shellDirectory, "bash.exe"));
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}${path2.delimiter}${daemonEnv[pathKey] ?? ""}`;
388
+ daemonEnv[pathKey] = `${shellDirectory}${path3.delimiter}${daemonEnv[pathKey] ?? ""}`;
238
389
  }
239
390
  } catch {
240
391
  }
241
392
  }
242
- const oldRuntime = await readJSON(path2.join(directory, "computer-runtime.json"));
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(path2.join(directory, "computer-runtime.json"), { force: true });
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: path2.join(directory, "agent-state.json"),
256
- WORKSPACE_DB_PATH: path2.join(directory, "workspace.db"),
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((resolve2, reject) => {
268
- process2.once("spawn", resolve2);
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 = { route: saved.route, owner: owner2, close: () => owner2.close() };
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 owner2.close();
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: path2.join(directory, "tools"),
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" || !!relay?.route);
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
- phase: ready ? void 0 : daemonError ?? relayMessage,
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 = path2.join(directory, "computer-update.json");
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(path2.join(directory, "computer-config.json"), config);
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-MNMIJHH5.js.map
496
- //# sourceMappingURL=controller-MNMIJHH5.js.map
719
+ //# sourceMappingURL=controller-CHJ5MMZS.js.map
720
+ //# sourceMappingURL=controller-CHJ5MMZS.js.map