mindwire 0.1.30 → 0.1.33

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,11 +1,11 @@
1
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-RN7ERVJ2.js';
2
+ import { readJSON, processStateAlive, currentProcessIdentity, ensureDaemonBinary, SDK_VERSION, computerClient, processIdentity, adoptProcess, writeJSON, directRoutes, CONTROLLER_PROTOCOL, ownChild } from './chunk-H2I3VRBS.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';
@@ -23,7 +23,7 @@ async function ensureCloudflareBinary(options = {}) {
23
23
  throw new Error("Install cloudflared for this computer, or select another connection provider.");
24
24
  }
25
25
  const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
26
- const bin = path2.join(
26
+ const bin = path3.join(
27
27
  options.cacheDir ?? await binaryCacheDirectory(),
28
28
  "cloudflared",
29
29
  CLOUDFLARED_VERSION,
@@ -50,9 +50,9 @@ async function ensureCloudflareBinary(options = {}) {
50
50
  await verifyReleaseBytes(archive, checksum, asset);
51
51
  let executable = archive;
52
52
  if (platform === "darwin") {
53
- const temporary = await mkdtemp(path2.join(tmpdir(), "mindwire-cloudflared-"));
53
+ const temporary = await mkdtemp(path3.join(tmpdir(), "mindwire-cloudflared-"));
54
54
  try {
55
- const archivePath = path2.join(temporary, asset);
55
+ const archivePath = path3.join(temporary, asset);
56
56
  await writeFile(archivePath, archive, { mode: 384 });
57
57
  const result = await execute(
58
58
  "/usr/bin/tar",
@@ -75,8 +75,70 @@ async function cloudflareCommand(options) {
75
75
  return ensureCloudflareBinary(options);
76
76
  }
77
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
+ };
78
133
 
79
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
+ }
80
142
  function websocketURL(value) {
81
143
  const url = new URL(value);
82
144
  if (url.protocol === "https:") url.protocol = "wss:";
@@ -118,84 +180,78 @@ async function startRelay(options, port, runtime = {}) {
118
180
  }
119
181
  }
120
182
  runtime.signal?.throwIfAborted();
121
- const child = spawn(command, args, { env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
122
- 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;
123
190
  try {
124
- const address = new Promise((resolve2, reject) => {
125
- const timer = setTimeout(() => {
126
- cleanup();
127
- reject(new Error(`${command} did not establish its tunnel. Check its account and tunnel configuration.`));
128
- }, 45e3);
129
- const cleanup = () => {
130
- clearTimeout(timer);
131
- child.off("error", fail);
132
- child.off("exit", exited);
133
- runtime.signal?.removeEventListener("abort", aborted);
134
- };
135
- const fail = (error) => {
136
- cleanup();
137
- reject(new Error(`Could not run ${command}. Install it on the computer first.`, { cause: error }));
138
- };
139
- const exited = () => {
140
- cleanup();
141
- reject(new Error(`${command} exited before the tunnel connected. Check its account configuration.`));
142
- };
143
- const aborted = () => {
144
- cleanup();
145
- child.kill();
146
- reject(runtime.signal?.reason ?? new Error("Tunnel startup cancelled."));
147
- };
148
- let cloudflareAddress = options.url;
149
- let cloudflareConnected = false;
150
- const receive = (chunk) => {
151
- const received = buffer + chunk.toString();
152
- buffer = received.slice(-16384);
153
- let found;
154
- if (options.kind === "cloudflare") {
155
- cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
156
- cloudflareConnected ||= /Registered tunnel connection/.test(received);
157
- if (cloudflareConnected) found = cloudflareAddress;
158
- } else {
159
- for (const line of received.split(/\r?\n/)) {
160
- try {
161
- const event = JSON.parse(line);
162
- if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
163
- } catch {
164
- }
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 {
165
216
  }
166
217
  }
167
- if (found) {
168
- cleanup();
169
- resolve2(websocketURL(found));
170
- }
171
- };
172
- child.once("error", fail);
173
- child.once("exit", exited);
174
- child.stdout.on("data", receive);
175
- child.stderr.on("data", receive);
176
- runtime.signal?.addEventListener("abort", aborted, { once: true });
177
- if (runtime.signal?.aborted) aborted();
178
- });
179
- void address.catch(() => {
180
- });
181
- const owner = await ownChild(child);
182
- await runtime.onSpawn?.(owner);
183
- const url = await address;
184
- runtime.signal?.throwIfAborted();
185
- child.stdout.removeAllListeners("data");
186
- child.stderr.removeAllListeners("data");
187
- child.stdout.resume();
188
- child.stderr.resume();
189
- child.on("error", () => {
190
- });
191
- return { route: { kind: "websocket", url }, process: child, owner, close: () => owner.close() };
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.`);
192
237
  } catch (error) {
193
- child.on("error", () => {
194
- });
195
- if (child.pid) await (await ownChild(child)).close();
238
+ if (owner) await owner.close();
239
+ else if (child.pid) await (await ownChild(child)).close();
240
+ await output.close();
196
241
  throw error;
197
242
  }
198
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
+ }
199
255
  var relayLookup = (hostname, options, callback) => {
200
256
  lookup(hostname, options, (error, address, family) => {
201
257
  if (error?.code !== "ENOTFOUND" || !/^[a-z0-9-]+\.trycloudflare\.com$/i.test(hostname)) {
@@ -220,8 +276,8 @@ var relayLookup = (hostname, options, callback) => {
220
276
  async function checkRelay(route, options = {}) {
221
277
  options.signal?.throwIfAborted();
222
278
  if (route.kind !== "websocket") throw new Error("Expected an internet tunnel address.");
223
- await new Promise((resolve2, reject) => {
224
- const socket = new WebSocket(route.url, { maxPayload: 4096, perMessageDeflate: false, followRedirects: false, lookup: relayLookup });
279
+ await new Promise((resolve3, reject) => {
280
+ const socket = new WebSocket(route.url, { maxPayload: 4096, perMessageDeflate: false, followRedirects: false, lookup: options.lookup ?? relayLookup });
225
281
  let finished = false, banner = "";
226
282
  const finish = (error) => {
227
283
  if (finished) return;
@@ -230,27 +286,33 @@ async function checkRelay(route, options = {}) {
230
286
  options.signal?.removeEventListener("abort", aborted);
231
287
  socket.terminate();
232
288
  if (error) reject(error);
233
- else resolve2();
289
+ else resolve3();
234
290
  };
235
291
  const aborted = () => finish(options.signal?.reason ?? new Error("Connection check cancelled."));
236
- const timer = setTimeout(() => finish(new Error("The internet tunnel isn't responding. Reconnecting\u2026")), options.timeoutMs ?? 8e3);
237
- socket.on("error", (error) => finish(new Error(
238
- error.code === "ENOTFOUND" ? "The internet address isn't available. Checking the tunnel\u2026" : /(?:[Uu]nexpected server response|[Ee]xpected 101)/.test(error.message) ? "The internet tunnel isn't reaching Mindwire. Reconnecting\u2026" : "The internet tunnel couldn't connect. Retrying\u2026",
239
- { cause: error }
240
- )));
241
- socket.on("close", () => finish(new Error("The internet tunnel closed before reaching Mindwire. Retrying\u2026")));
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
+ }
302
+ });
303
+ socket.on("close", () => finish(new RelayCheckError("connection", "The internet tunnel closed before reaching Mindwire. Retrying\u2026")));
242
304
  socket.on("message", (data, binary) => {
243
305
  if (!binary) {
244
- finish(new Error("The internet tunnel returned an invalid SSH response."));
306
+ finish(new RelayCheckError("response", "The internet tunnel returned an invalid SSH response."));
245
307
  return;
246
308
  }
247
309
  banner += data.toString();
248
310
  if (banner.length > 1024) {
249
- finish(new Error("The internet tunnel returned an invalid SSH response."));
311
+ finish(new RelayCheckError("response", "The internet tunnel returned an invalid SSH response."));
250
312
  return;
251
313
  }
252
314
  if (banner.includes("\n")) {
253
- finish(banner.split("\n")[0]?.trim() === "SSH-2.0-Mindwire" ? void 0 : new Error("The internet tunnel isn't connected to the Mindwire SSH service."));
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."));
254
316
  }
255
317
  });
256
318
  options.signal?.addEventListener("abort", aborted, { once: true });
@@ -260,7 +322,7 @@ async function checkRelay(route, options = {}) {
260
322
  async function relayProviderReachable(options, signal) {
261
323
  if (options.kind !== "cloudflare" && options.kind !== "ngrok") return false;
262
324
  signal?.throwIfAborted();
263
- return new Promise((resolve2) => {
325
+ return new Promise((resolve3) => {
264
326
  const url = options.kind === "cloudflare" ? "https://api.trycloudflare.com/" : "https://api.ngrok.com/";
265
327
  let finished = false;
266
328
  const done = (reachable) => {
@@ -268,7 +330,7 @@ async function relayProviderReachable(options, signal) {
268
330
  finished = true;
269
331
  clearTimeout(timer);
270
332
  probe.destroy();
271
- resolve2(reachable);
333
+ resolve3(reachable);
272
334
  };
273
335
  const probe = request(url, { method: "HEAD", signal }, (response) => {
274
336
  response.destroy();
@@ -285,11 +347,11 @@ function recoveryDelay(failures) {
285
347
  return Math.min(6e4, 1e3 * 2 ** Math.min(6, Math.max(0, failures - 1)));
286
348
  }
287
349
  async function superviseComputer(directory) {
288
- if ((await readJSON(path2.join(directory, "computer-stopped.json")))?.stopped) return;
289
- 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"));
290
352
  if (!config) throw new Error("Computer configuration is missing.");
291
- const controllerPath = path2.join(directory, "computer-controller.json");
292
- 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");
293
355
  const previous = await readJSON(controllerPath);
294
356
  if (await processStateAlive(previous)) throw new Error("The computer already has a service controller.");
295
357
  const owner = await currentProcessIdentity();
@@ -309,30 +371,30 @@ async function superviseComputer(directory) {
309
371
  process.on("SIGINT", stop);
310
372
  let token;
311
373
  try {
312
- token = (await fs.readFile(path2.join(directory, "daemon.token"), "utf8")).trim();
374
+ token = (await fs.readFile(path3.join(directory, "daemon.token"), "utf8")).trim();
313
375
  } catch (error) {
314
376
  if (error.code !== "ENOENT") throw error;
315
377
  token = randomBytes(32).toString("hex");
316
- 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" });
317
379
  }
318
380
  const daemonEnv = { ...process.env };
319
381
  if (process.platform === "win32") {
320
382
  try {
321
383
  const git = execFileSync("where.exe", ["git"], { encoding: "utf8", timeout: 3e3, windowsHide: true }).split(/\r?\n/)[0];
322
384
  if (git) {
323
- const shellDirectory = path2.resolve(path2.dirname(git), "..", "bin");
324
- 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"));
325
387
  const pathKey = Object.keys(daemonEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
326
- daemonEnv[pathKey] = `${shellDirectory}${path2.delimiter}${daemonEnv[pathKey] ?? ""}`;
388
+ daemonEnv[pathKey] = `${shellDirectory}${path3.delimiter}${daemonEnv[pathKey] ?? ""}`;
327
389
  }
328
390
  } catch {
329
391
  }
330
392
  }
331
- const oldRuntime = await readJSON(path2.join(directory, "computer-runtime.json"));
393
+ const oldRuntime = await readJSON(path3.join(directory, "computer-runtime.json"));
332
394
  let sshPort = config.sshPort || oldRuntime?.sshPort || 0;
333
395
  let websocketPort = config.websocketPort || oldRuntime?.websocketPort || 0;
334
396
  const launch = async (binary) => {
335
- await fs.rm(path2.join(directory, "computer-runtime.json"), { force: true });
397
+ await fs.rm(path3.join(directory, "computer-runtime.json"), { force: true });
336
398
  const process2 = spawn(binary, ["--computer"], {
337
399
  cwd: config.directory,
338
400
  stdio: ["ignore", "inherit", "inherit"],
@@ -341,8 +403,8 @@ async function superviseComputer(directory) {
341
403
  ...daemonEnv,
342
404
  DAEMON_TOKEN: token,
343
405
  ADDR: "127.0.0.1:0",
344
- STATE_PATH: path2.join(directory, "agent-state.json"),
345
- 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"),
346
408
  AGENT_CWD: config.directory,
347
409
  COMPUTER_SSH_ADDR: `${config.bind.includes(":") ? `[${config.bind}]` : config.bind}:${sshPort}`,
348
410
  COMPUTER_WS_ADDR: `127.0.0.1:${websocketPort}`,
@@ -353,8 +415,8 @@ async function superviseComputer(directory) {
353
415
  process2.on("error", (error) => {
354
416
  failure = error;
355
417
  });
356
- await new Promise((resolve2, reject) => {
357
- process2.once("spawn", resolve2);
418
+ await new Promise((resolve3, reject) => {
419
+ process2.once("spawn", resolve3);
358
420
  process2.once("error", reject);
359
421
  });
360
422
  child = await ownChild(process2);
@@ -381,6 +443,7 @@ async function superviseComputer(directory) {
381
443
  let routes = [], published = "";
382
444
  let client;
383
445
  let relayMessage = "Preparing your internet connection\u2026", relayError;
446
+ let relayErrorCode;
384
447
  let daemonError, nextDaemonAt = 0, daemonFailures = 0;
385
448
  let nextRelayAt = 0, relayFailures = 0, changingDaemon = false;
386
449
  let daemonReadyAt = Date.now(), relayReadyAt = Date.now();
@@ -409,11 +472,16 @@ async function superviseComputer(directory) {
409
472
  if (saved) {
410
473
  const owner2 = await adoptProcess(saved.owner);
411
474
  if (owner2) {
412
- if (saved.route && saved.port === websocketPort && saved.options === JSON.stringify(config.relay)) {
413
- 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);
414
477
  } else {
415
- 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 });
416
482
  }
483
+ } else if (saved.outputFile) {
484
+ await (await RelayOutput.adopt(directory, saved.outputFile))?.close();
417
485
  }
418
486
  }
419
487
  let lastController = "";
@@ -443,9 +511,11 @@ async function superviseComputer(directory) {
443
511
  }
444
512
  }
445
513
  if (relay?.owner && !relay.owner.alive()) {
514
+ await relay.close();
446
515
  relay = void 0;
447
516
  relayReachable = false;
448
517
  relayWasReachable = false;
518
+ relayErrorCode = void 0;
449
519
  relayError = "The internet tunnel disconnected. Reconnecting\u2026";
450
520
  nextRelayAt = Date.now() + recoveryDelay(++relayFailures);
451
521
  }
@@ -456,6 +526,7 @@ async function superviseComputer(directory) {
456
526
  if (result.handle) {
457
527
  relay = result.handle;
458
528
  relayError = void 0;
529
+ relayErrorCode = void 0;
459
530
  relayReadyAt = Date.now();
460
531
  relayReachable = false;
461
532
  relayWasReachable = false;
@@ -466,7 +537,8 @@ async function superviseComputer(directory) {
466
537
  owner: relay.owner.identity,
467
538
  port: websocketPort,
468
539
  options: JSON.stringify(config.relay),
469
- route: relay.route
540
+ route: relay.route,
541
+ outputFile: relay.outputFile
470
542
  });
471
543
  } else {
472
544
  relayError = result.error instanceof Error ? result.error.message : "The internet tunnel couldn't connect.";
@@ -474,18 +546,22 @@ async function superviseComputer(directory) {
474
546
  }
475
547
  }
476
548
  if (!relay && !relayTask && Date.now() >= nextRelayAt) {
549
+ relayError = void 0;
550
+ relayErrorCode = void 0;
477
551
  relayMessage = "Reconnecting your internet tunnel\u2026";
478
552
  relayTask = startRelay(config.relay, websocketPort, {
479
- cacheDir: path2.join(directory, "tools"),
553
+ cacheDir: path3.join(directory, "tools"),
554
+ outputDirectory: directory,
480
555
  signal: abort.signal,
481
556
  onProgress: (message) => {
482
557
  relayMessage = message;
483
558
  },
484
- onSpawn: async (owner2) => {
559
+ onSpawn: async (owner2, outputFile) => {
485
560
  if (owner2.identity) await writeJSON(relayPath, {
486
561
  owner: owner2.identity,
487
562
  port: websocketPort,
488
- options: JSON.stringify(config.relay)
563
+ options: JSON.stringify(config.relay),
564
+ outputFile
489
565
  });
490
566
  }
491
567
  }).then((handle) => {
@@ -506,9 +582,11 @@ async function superviseComputer(directory) {
506
582
  relayHealthFailures = 0;
507
583
  relayFailedAt = 0;
508
584
  relayError = void 0;
585
+ relayErrorCode = void 0;
509
586
  nextCheckAt = checkedAt + 3e4;
510
587
  } else {
511
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;
512
590
  relayFailedAt ||= checkedAt;
513
591
  relayHealthFailures++;
514
592
  nextCheckAt = checkedAt + Math.min(15e3, recoveryDelay(relayHealthFailures));
@@ -522,7 +600,7 @@ async function superviseComputer(directory) {
522
600
  }
523
601
  }
524
602
  }
525
- const requestedCheck = await readJSON(path2.join(directory, "computer-check.json"));
603
+ const requestedCheck = await readJSON(path3.join(directory, "computer-check.json"));
526
604
  if (config.relay.kind === "none") checkedAt = Math.max(checkedAt, requestedCheck?.requestedAt ?? 0);
527
605
  if (relay?.route && !checkTask && (Date.now() >= nextCheckAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
528
606
  const checking = relay;
@@ -531,7 +609,7 @@ async function superviseComputer(directory) {
531
609
  await checkRelay(checking.route, { signal: abort.signal });
532
610
  checkResult = { relay: checking };
533
611
  } catch (error) {
534
- const canRestart = !!checking.owner && relayHealthFailures >= 2 && relayFailedAt > 0 && Date.now() - relayFailedAt >= 9e4 && await relayProviderReachable(config.relay, abort.signal).catch(() => false);
612
+ const canRestart = !!checking.owner && relayFailedAt > 0 && relayNeedsRestart(error, relayHealthFailures, Date.now() - relayFailedAt) && await relayProviderReachable(config.relay, abort.signal).catch(() => false);
535
613
  checkResult = { relay: checking, error, canRestart };
536
614
  }
537
615
  })();
@@ -556,6 +634,7 @@ async function superviseComputer(directory) {
556
634
  checkedAt,
557
635
  phase: ready ? void 0 : daemonError ?? relayError ?? relayMessage,
558
636
  error: daemonError ?? relayError,
637
+ errorCode: daemonError ? void 0 : relayErrorCode,
559
638
  recovering: !ready
560
639
  };
561
640
  const serialized = JSON.stringify(state);
@@ -563,7 +642,7 @@ async function superviseComputer(directory) {
563
642
  await writeJSON(controllerPath, state);
564
643
  lastController = serialized;
565
644
  }
566
- const updatePath = path2.join(directory, "computer-update.json");
645
+ const updatePath = path3.join(directory, "computer-update.json");
567
646
  const update = !updateTask && client && child?.alive() ? await readJSON(updatePath) : void 0;
568
647
  if (update && ["queued", "downloading", "waiting", "restarting"].includes(update.status)) {
569
648
  updateTask = (async () => {
@@ -603,7 +682,7 @@ async function superviseComputer(directory) {
603
682
  }
604
683
  binary = replacement;
605
684
  config = { ...config, daemonBin: void 0, version: update.version };
606
- await writeJSON(path2.join(directory, "computer-config.json"), config);
685
+ await writeJSON(path3.join(directory, "computer-config.json"), config);
607
686
  await client.computer.setRoutes(routes);
608
687
  published = JSON.stringify(routes);
609
688
  await status("complete");
@@ -637,5 +716,5 @@ async function superviseComputer(directory) {
637
716
  }
638
717
 
639
718
  export { recoveryDelay, superviseComputer };
640
- //# sourceMappingURL=controller-DIJKMWWU.js.map
641
- //# sourceMappingURL=controller-DIJKMWWU.js.map
719
+ //# sourceMappingURL=controller-QBC6L7AT.js.map
720
+ //# sourceMappingURL=controller-QBC6L7AT.js.map