mindwire 0.1.33 → 0.1.36
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/binary-cache-CG52TXCZ.js +3 -0
- package/dist/{binary-cache-VM7VHCEU.js.map → binary-cache-CG52TXCZ.js.map} +1 -1
- package/dist/chunk-67BNDGRK.js +20 -0
- package/dist/chunk-67BNDGRK.js.map +1 -0
- package/dist/{chunk-3FR7BFAW.js → chunk-PFYKJ6LX.js} +56 -4
- package/dist/chunk-PFYKJ6LX.js.map +1 -0
- package/dist/{chunk-H2I3VRBS.js → chunk-RTGZDLSG.js} +529 -177
- package/dist/chunk-RTGZDLSG.js.map +1 -0
- package/dist/cli.js +763 -32
- package/dist/cli.js.map +1 -1
- package/dist/computer/connection-change.d.ts +13 -0
- package/dist/computer/connection-flow.d.ts +3 -3
- package/dist/computer/directory-http.d.ts +8 -0
- package/dist/computer/discovery.d.ts +13 -0
- package/dist/computer/lifecycle.d.ts +5 -1
- package/dist/computer/managed-relay.d.ts +27 -0
- package/dist/computer/oblien-auth.d.ts +25 -0
- package/dist/computer/provider-errors.d.ts +9 -0
- package/dist/computer/provider-info.d.ts +7 -0
- package/dist/computer/provider-setup.d.ts +20 -0
- package/dist/computer/relay-health.d.ts +22 -6
- package/dist/computer/relay.d.ts +11 -3
- package/dist/computer/setup-prompt.d.ts +9 -0
- package/dist/computer.d.ts +27 -1
- package/dist/connection-change-IBNL42BL.js +41 -0
- package/dist/connection-change-IBNL42BL.js.map +1 -0
- package/dist/{controller-QBC6L7AT.js → controller-2SJLZMJZ.js} +283 -290
- package/dist/controller-2SJLZMJZ.js.map +1 -0
- package/dist/{controller-handoff-NFDOLPJL.js → controller-handoff-RUUHODNC.js} +4 -4
- package/dist/{controller-handoff-NFDOLPJL.js.map → controller-handoff-RUUHODNC.js.map} +1 -1
- package/dist/index.cjs +12 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/binary-cache-VM7VHCEU.js +0 -4
- package/dist/chunk-3FR7BFAW.js.map +0 -1
- package/dist/chunk-H2I3VRBS.js.map +0 -1
- package/dist/controller-QBC6L7AT.js.map +0 -1
|
@@ -1,272 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readJSON, processStateAlive, currentProcessIdentity, ensureDaemonBinary, SDK_VERSION, computerClient, processIdentity, adoptProcess, writeJSON, directRoutes, CONTROLLER_PROTOCOL, ownChild } from './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import {
|
|
5
|
-
import { randomBytes
|
|
1
|
+
import { connectionInfo } from './chunk-67BNDGRK.js';
|
|
2
|
+
import { readJSON, processStateAlive, currentProcessIdentity, ensureDaemonBinary, SDK_VERSION, computerClient, processIdentity, adoptProcess, adoptRelay, RelayOutput, writeJSON, startRelay, providerNeedsAction, directRoutes, ProviderConnectionError, CONTROLLER_PROTOCOL, ownChild, hasCertificateError } from './chunk-RTGZDLSG.js';
|
|
3
|
+
import './chunk-PFYKJ6LX.js';
|
|
4
|
+
import { execFileSync, spawn } from 'child_process';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
|
-
import
|
|
8
|
-
import * as path3 from 'path';
|
|
7
|
+
import * as path from 'path';
|
|
9
8
|
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
10
|
-
import { tmpdir } from 'os';
|
|
11
|
-
import { promisify } from 'util';
|
|
12
9
|
import { request } from 'https';
|
|
13
10
|
import { lookup } from 'dns';
|
|
14
11
|
import { Resolver } from 'dns/promises';
|
|
12
|
+
import { isIP } from 'net';
|
|
15
13
|
import WebSocket from 'ws';
|
|
16
14
|
|
|
17
|
-
var CLOUDFLARED_VERSION = "2026.9.3";
|
|
18
|
-
var execute = promisify(execFile);
|
|
19
|
-
async function ensureCloudflareBinary(options = {}) {
|
|
20
|
-
const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
|
|
21
|
-
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : "";
|
|
22
|
-
if (!cpu || !["darwin", "linux", "win32"].includes(platform) || platform === "win32" && cpu !== "amd64") {
|
|
23
|
-
throw new Error("Install cloudflared for this computer, or select another connection provider.");
|
|
24
|
-
}
|
|
25
|
-
const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
|
|
26
|
-
const bin = path3.join(
|
|
27
|
-
options.cacheDir ?? await binaryCacheDirectory(),
|
|
28
|
-
"cloudflared",
|
|
29
|
-
CLOUDFLARED_VERSION,
|
|
30
|
-
`${platform}-${arch}`,
|
|
31
|
-
platform === "win32" ? "cloudflared.exe" : "cloudflared"
|
|
32
|
-
);
|
|
33
|
-
const cached = await cachedExecutable(bin);
|
|
34
|
-
if (cached) return cached;
|
|
35
|
-
await options.onProgress?.("Downloading the secure connection helper\u2026");
|
|
36
|
-
const request2 = options.fetch ?? fetch;
|
|
37
|
-
const release = await request2(
|
|
38
|
-
`https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
|
|
39
|
-
{ headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
|
|
40
|
-
);
|
|
41
|
-
if (!release.ok) throw new Error("Couldn't download the connection helper. Retry, install cloudflared yourself, or choose another provider.");
|
|
42
|
-
const metadata = await release.json();
|
|
43
|
-
const item = metadata.assets?.find((value) => value.name === asset);
|
|
44
|
-
const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
|
|
45
|
-
const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
|
|
46
|
-
if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
|
|
47
|
-
const downloaded = await request2(expectedURL, { signal: AbortSignal.timeout(12e4) });
|
|
48
|
-
if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
|
|
49
|
-
const archive = new Uint8Array(await downloaded.arrayBuffer());
|
|
50
|
-
await verifyReleaseBytes(archive, checksum, asset);
|
|
51
|
-
let executable = archive;
|
|
52
|
-
if (platform === "darwin") {
|
|
53
|
-
const temporary = await mkdtemp(path3.join(tmpdir(), "mindwire-cloudflared-"));
|
|
54
|
-
try {
|
|
55
|
-
const archivePath = path3.join(temporary, asset);
|
|
56
|
-
await writeFile(archivePath, archive, { mode: 384 });
|
|
57
|
-
const result = await execute(
|
|
58
|
-
"/usr/bin/tar",
|
|
59
|
-
["-xzOf", archivePath, "cloudflared"],
|
|
60
|
-
{ encoding: "buffer", maxBuffer: 128 * 1024 * 1024, timeout: 3e4 }
|
|
61
|
-
);
|
|
62
|
-
executable = new Uint8Array(result.stdout);
|
|
63
|
-
} finally {
|
|
64
|
-
await rm(temporary, { recursive: true, force: true });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return cacheExecutable(bin, executable, await executableChecksum(executable));
|
|
68
|
-
}
|
|
69
|
-
async function cloudflareCommand(options) {
|
|
70
|
-
try {
|
|
71
|
-
await execute("cloudflared", ["--version"], { timeout: 3e3, windowsHide: true });
|
|
72
|
-
return "cloudflared";
|
|
73
|
-
} catch (error) {
|
|
74
|
-
if (error.code !== "ENOENT") throw new Error("The installed cloudflared could not start. Repair it or choose another connection provider.");
|
|
75
|
-
return ensureCloudflareBinary(options);
|
|
76
|
-
}
|
|
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
|
-
};
|
|
133
|
-
|
|
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
|
-
}
|
|
142
|
-
function websocketURL(value) {
|
|
143
|
-
const url = new URL(value);
|
|
144
|
-
if (url.protocol === "https:") url.protocol = "wss:";
|
|
145
|
-
if (url.protocol !== "wss:" || !url.hostname || url.username || url.password || url.hash) {
|
|
146
|
-
throw new Error("Relay URLs must use wss:// with no embedded credentials or fragment.");
|
|
147
|
-
}
|
|
148
|
-
if (!url.pathname || url.pathname === "/") url.pathname = "/ssh";
|
|
149
|
-
return url.toString();
|
|
150
|
-
}
|
|
151
|
-
async function startRelay(options, port, runtime = {}) {
|
|
152
|
-
runtime.signal?.throwIfAborted();
|
|
153
|
-
if (options.kind === "none") return { close() {
|
|
154
|
-
} };
|
|
155
|
-
if (options.kind === "custom") {
|
|
156
|
-
if (!options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
|
|
157
|
-
return { route: { kind: "websocket", url: websocketURL(options.url) }, close() {
|
|
158
|
-
} };
|
|
159
|
-
}
|
|
160
|
-
const origin = `http://127.0.0.1:${port}`;
|
|
161
|
-
const env = { ...process.env };
|
|
162
|
-
let command, args;
|
|
163
|
-
if (options.kind === "cloudflare") {
|
|
164
|
-
command = await cloudflareCommand(runtime);
|
|
165
|
-
await runtime.onProgress?.("Connecting through Cloudflare\u2026");
|
|
166
|
-
if (options.cloudflareTokenFile) {
|
|
167
|
-
if (!options.url) throw new Error("A named Cloudflare tunnel needs --relay-url for its configured hostname.");
|
|
168
|
-
env.TUNNEL_TOKEN = (await readFile(options.cloudflareTokenFile, "utf8")).trim();
|
|
169
|
-
args = ["tunnel", "--no-autoupdate", "run"];
|
|
170
|
-
} else {
|
|
171
|
-
if (options.url) throw new Error("Use --cloudflare-token-file with a named tunnel, or --relay custom for an existing tunnel.");
|
|
172
|
-
args = ["tunnel", "--no-autoupdate", "--url", origin];
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
command = "ngrok";
|
|
176
|
-
args = ["http", origin, "--log", "stdout", "--log-format", "json"];
|
|
177
|
-
if (options.url) {
|
|
178
|
-
const url = new URL(websocketURL(options.url));
|
|
179
|
-
args.push("--url", `https://${url.host}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
runtime.signal?.throwIfAborted();
|
|
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;
|
|
190
|
-
try {
|
|
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 {
|
|
216
|
-
}
|
|
217
|
-
}
|
|
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
15
|
var RelayCheckError = class extends Error {
|
|
245
|
-
constructor(code, message, cause) {
|
|
16
|
+
constructor(code, message, cause, addressMissing = false) {
|
|
246
17
|
super(message, cause ? { cause } : void 0);
|
|
247
18
|
this.code = code;
|
|
19
|
+
this.addressMissing = addressMissing;
|
|
248
20
|
this.name = "RelayCheckError";
|
|
249
21
|
}
|
|
250
22
|
code;
|
|
23
|
+
addressMissing;
|
|
251
24
|
};
|
|
252
25
|
function relayNeedsRestart(error, failures, failedForMs) {
|
|
253
|
-
return failures >= 2 && failedForMs >= 9e4 && !(error instanceof RelayCheckError && error.code === "dns");
|
|
26
|
+
return failures >= 2 && failedForMs >= 9e4 && !(error instanceof RelayCheckError && error.code === "provider_certificate") && (!(error instanceof RelayCheckError && error.code === "dns") || error.addressMissing);
|
|
27
|
+
}
|
|
28
|
+
var quickTunnelHost = /^[a-z0-9-]+\.trycloudflare\.com$/i;
|
|
29
|
+
async function resolveCloudflareAddress(hostname, family = 0, options = {}) {
|
|
30
|
+
if (!quickTunnelHost.test(hostname)) return { addresses: [], missing: false };
|
|
31
|
+
const abort = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? 3e3);
|
|
33
|
+
try {
|
|
34
|
+
const types = family === 4 ? [1] : family === 6 ? [28] : [1, 28];
|
|
35
|
+
const results = await Promise.all(types.map(async (type) => {
|
|
36
|
+
try {
|
|
37
|
+
const response = await (options.fetch ?? fetch)(
|
|
38
|
+
`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(hostname)}&type=${type}`,
|
|
39
|
+
{ headers: { accept: "application/dns-json" }, redirect: "error", signal: abort.signal }
|
|
40
|
+
);
|
|
41
|
+
if (response.status !== 200 || !response.body) {
|
|
42
|
+
await response.body?.cancel();
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
const reader = response.body.getReader();
|
|
46
|
+
let text = "", size = 0;
|
|
47
|
+
const decoder = new TextDecoder();
|
|
48
|
+
try {
|
|
49
|
+
for (; ; ) {
|
|
50
|
+
const chunk = await reader.read();
|
|
51
|
+
if (chunk.done) break;
|
|
52
|
+
size += chunk.value.length;
|
|
53
|
+
if (size > 16384) return void 0;
|
|
54
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
await reader.cancel();
|
|
58
|
+
}
|
|
59
|
+
const result = JSON.parse(text + decoder.decode());
|
|
60
|
+
if (!Array.isArray(result.Question) || !result.Question.some((question) => question.name?.replace(/\.$/, "").toLowerCase() === hostname.toLowerCase() && question.type === type)) return void 0;
|
|
61
|
+
if (result.Status === 3) return { addresses: [], missing: true };
|
|
62
|
+
if (result.Status !== 0) return void 0;
|
|
63
|
+
const addresses = (Array.isArray(result.Answer) ? result.Answer : []).flatMap((answer) => {
|
|
64
|
+
const family2 = answer.type === 1 ? 4 : answer.type === 28 ? 6 : 0;
|
|
65
|
+
return family2 && answer.type === type && typeof answer.data === "string" && isIP(answer.data) === family2 ? [{ address: answer.data, family: family2 }] : [];
|
|
66
|
+
});
|
|
67
|
+
return { addresses, missing: false };
|
|
68
|
+
} catch {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
return {
|
|
73
|
+
addresses: results.flatMap((result) => result?.addresses ?? []),
|
|
74
|
+
missing: results.every((result) => result?.missing === true)
|
|
75
|
+
};
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
254
79
|
}
|
|
255
80
|
var relayLookup = (hostname, options, callback) => {
|
|
256
81
|
lookup(hostname, options, (error, address, family) => {
|
|
257
|
-
if (error?.code
|
|
82
|
+
if (!["ENOTFOUND", "EAI_AGAIN"].includes(error?.code ?? "") || !quickTunnelHost.test(hostname)) {
|
|
258
83
|
callback(error, address, family);
|
|
259
84
|
return;
|
|
260
85
|
}
|
|
261
86
|
const resolver = new Resolver({ timeout: 3e3, tries: 1 });
|
|
87
|
+
const requestedFamily = options.family === "IPv4" ? 4 : options.family === "IPv6" ? 6 : options.family ?? 0;
|
|
262
88
|
void Promise.all([
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
]).then((results) => {
|
|
266
|
-
|
|
89
|
+
requestedFamily === 6 ? [] : resolver.resolve4(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 4 })), () => []),
|
|
90
|
+
requestedFamily === 4 ? [] : resolver.resolve6(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 6 })), () => [])
|
|
91
|
+
]).then(async (results) => {
|
|
92
|
+
let addresses = results.flat();
|
|
267
93
|
if (!addresses.length) {
|
|
268
|
-
|
|
269
|
-
|
|
94
|
+
const provider = await resolveCloudflareAddress(hostname, requestedFamily);
|
|
95
|
+
addresses = provider.addresses;
|
|
96
|
+
if (!addresses.length) {
|
|
97
|
+
callback(Object.assign(error, { relayAddressMissing: provider.missing }), address, family);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
270
100
|
}
|
|
271
101
|
if (options.all) callback(null, addresses);
|
|
272
102
|
else callback(null, addresses[0].address, addresses[0].family);
|
|
@@ -276,7 +106,7 @@ var relayLookup = (hostname, options, callback) => {
|
|
|
276
106
|
async function checkRelay(route, options = {}) {
|
|
277
107
|
options.signal?.throwIfAborted();
|
|
278
108
|
if (route.kind !== "websocket") throw new Error("Expected an internet tunnel address.");
|
|
279
|
-
await new Promise((
|
|
109
|
+
await new Promise((resolve2, reject) => {
|
|
280
110
|
const socket = new WebSocket(route.url, { maxPayload: 4096, perMessageDeflate: false, followRedirects: false, lookup: options.lookup ?? relayLookup });
|
|
281
111
|
let finished = false, banner = "";
|
|
282
112
|
const finish = (error) => {
|
|
@@ -286,14 +116,21 @@ async function checkRelay(route, options = {}) {
|
|
|
286
116
|
options.signal?.removeEventListener("abort", aborted);
|
|
287
117
|
socket.terminate();
|
|
288
118
|
if (error) reject(error);
|
|
289
|
-
else
|
|
119
|
+
else resolve2();
|
|
290
120
|
};
|
|
291
121
|
const aborted = () => finish(options.signal?.reason ?? new Error("Connection check cancelled."));
|
|
292
122
|
const timer = setTimeout(() => finish(new RelayCheckError("timeout", "The internet tunnel isn't responding. Retrying\u2026")), options.timeoutMs ?? 8e3);
|
|
293
123
|
socket.on("error", (error) => {
|
|
294
|
-
if (
|
|
124
|
+
if (hasCertificateError(error)) {
|
|
125
|
+
finish(new RelayCheckError("provider_certificate", "The tunnel's TLS certificate could not be verified. Check the provider's certificate and this computer's date and time.", error));
|
|
126
|
+
} else if (["ENOTFOUND", "EAI_AGAIN"].includes(error.code ?? "")) {
|
|
295
127
|
const provider = new URL(route.url).hostname.endsWith(".trycloudflare.com") ? "Cloudflare's" : "the tunnel's";
|
|
296
|
-
finish(new RelayCheckError(
|
|
128
|
+
finish(new RelayCheckError(
|
|
129
|
+
"dns",
|
|
130
|
+
`Waiting for ${provider} internet address (DNS)\u2026`,
|
|
131
|
+
error,
|
|
132
|
+
error.relayAddressMissing === true
|
|
133
|
+
));
|
|
297
134
|
} else if (/(?:[Uu]nexpected server response|[Ee]xpected 101)/.test(error.message)) {
|
|
298
135
|
finish(new RelayCheckError("proxy", "The internet tunnel isn't reaching Mindwire. Retrying\u2026", error));
|
|
299
136
|
} else {
|
|
@@ -320,17 +157,17 @@ async function checkRelay(route, options = {}) {
|
|
|
320
157
|
});
|
|
321
158
|
}
|
|
322
159
|
async function relayProviderReachable(options, signal) {
|
|
323
|
-
if (
|
|
160
|
+
if (!["cloudflare", "ngrok", "oblien"].includes(options.kind)) return false;
|
|
324
161
|
signal?.throwIfAborted();
|
|
325
|
-
return new Promise((
|
|
326
|
-
const url = options.kind === "cloudflare" ? "https://api.trycloudflare.com/" : "https://api.ngrok.com/";
|
|
162
|
+
return new Promise((resolve2) => {
|
|
163
|
+
const url = options.kind === "cloudflare" ? "https://api.trycloudflare.com/" : options.kind === "oblien" ? "https://api.oblien.com/" : "https://api.ngrok.com/";
|
|
327
164
|
let finished = false;
|
|
328
165
|
const done = (reachable) => {
|
|
329
166
|
if (finished) return;
|
|
330
167
|
finished = true;
|
|
331
168
|
clearTimeout(timer);
|
|
332
169
|
probe.destroy();
|
|
333
|
-
|
|
170
|
+
resolve2(reachable);
|
|
334
171
|
};
|
|
335
172
|
const probe = request(url, { method: "HEAD", signal }, (response) => {
|
|
336
173
|
response.destroy();
|
|
@@ -343,15 +180,26 @@ async function relayProviderReachable(options, signal) {
|
|
|
343
180
|
}
|
|
344
181
|
|
|
345
182
|
// src/computer/controller.ts
|
|
183
|
+
async function saveRelay(file, handle, port, options) {
|
|
184
|
+
if (handle?.owner?.identity) await writeJSON(file, {
|
|
185
|
+
owner: handle.owner.identity,
|
|
186
|
+
port,
|
|
187
|
+
options: JSON.stringify(options),
|
|
188
|
+
route: handle.route,
|
|
189
|
+
outputFile: handle.outputFile,
|
|
190
|
+
configFile: handle.configFile
|
|
191
|
+
});
|
|
192
|
+
else await fs.rm(file, { force: true });
|
|
193
|
+
}
|
|
346
194
|
function recoveryDelay(failures) {
|
|
347
195
|
return Math.min(6e4, 1e3 * 2 ** Math.min(6, Math.max(0, failures - 1)));
|
|
348
196
|
}
|
|
349
197
|
async function superviseComputer(directory) {
|
|
350
|
-
if ((await readJSON(
|
|
351
|
-
let config = await readJSON(
|
|
198
|
+
if ((await readJSON(path.join(directory, "computer-stopped.json")))?.stopped) return;
|
|
199
|
+
let config = await readJSON(path.join(directory, "computer-config.json"));
|
|
352
200
|
if (!config) throw new Error("Computer configuration is missing.");
|
|
353
|
-
const controllerPath =
|
|
354
|
-
const relayPath =
|
|
201
|
+
const controllerPath = path.join(directory, "computer-controller.json");
|
|
202
|
+
const relayPath = path.join(directory, "computer-relay.json");
|
|
355
203
|
const previous = await readJSON(controllerPath);
|
|
356
204
|
if (await processStateAlive(previous)) throw new Error("The computer already has a service controller.");
|
|
357
205
|
const owner = await currentProcessIdentity();
|
|
@@ -359,6 +207,7 @@ async function superviseComputer(directory) {
|
|
|
359
207
|
await phase("Preparing Mindwire\u2026");
|
|
360
208
|
let child, relay, stopping = false;
|
|
361
209
|
let relayTask, updateTask;
|
|
210
|
+
let connectionTask;
|
|
362
211
|
let relayResult;
|
|
363
212
|
let checkTask;
|
|
364
213
|
let checkResult;
|
|
@@ -371,30 +220,30 @@ async function superviseComputer(directory) {
|
|
|
371
220
|
process.on("SIGINT", stop);
|
|
372
221
|
let token;
|
|
373
222
|
try {
|
|
374
|
-
token = (await fs.readFile(
|
|
223
|
+
token = (await fs.readFile(path.join(directory, "daemon.token"), "utf8")).trim();
|
|
375
224
|
} catch (error) {
|
|
376
225
|
if (error.code !== "ENOENT") throw error;
|
|
377
226
|
token = randomBytes(32).toString("hex");
|
|
378
|
-
await fs.writeFile(
|
|
227
|
+
await fs.writeFile(path.join(directory, "daemon.token"), token, { mode: 384, flag: "wx" });
|
|
379
228
|
}
|
|
380
229
|
const daemonEnv = { ...process.env };
|
|
381
230
|
if (process.platform === "win32") {
|
|
382
231
|
try {
|
|
383
232
|
const git = execFileSync("where.exe", ["git"], { encoding: "utf8", timeout: 3e3, windowsHide: true }).split(/\r?\n/)[0];
|
|
384
233
|
if (git) {
|
|
385
|
-
const shellDirectory =
|
|
386
|
-
await fs.access(
|
|
234
|
+
const shellDirectory = path.resolve(path.dirname(git), "..", "bin");
|
|
235
|
+
await fs.access(path.join(shellDirectory, "bash.exe"));
|
|
387
236
|
const pathKey = Object.keys(daemonEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
388
|
-
daemonEnv[pathKey] = `${shellDirectory}${
|
|
237
|
+
daemonEnv[pathKey] = `${shellDirectory}${path.delimiter}${daemonEnv[pathKey] ?? ""}`;
|
|
389
238
|
}
|
|
390
239
|
} catch {
|
|
391
240
|
}
|
|
392
241
|
}
|
|
393
|
-
const oldRuntime = await readJSON(
|
|
242
|
+
const oldRuntime = await readJSON(path.join(directory, "computer-runtime.json"));
|
|
394
243
|
let sshPort = config.sshPort || oldRuntime?.sshPort || 0;
|
|
395
244
|
let websocketPort = config.websocketPort || oldRuntime?.websocketPort || 0;
|
|
396
245
|
const launch = async (binary) => {
|
|
397
|
-
await fs.rm(
|
|
246
|
+
await fs.rm(path.join(directory, "computer-runtime.json"), { force: true });
|
|
398
247
|
const process2 = spawn(binary, ["--computer"], {
|
|
399
248
|
cwd: config.directory,
|
|
400
249
|
stdio: ["ignore", "inherit", "inherit"],
|
|
@@ -403,8 +252,8 @@ async function superviseComputer(directory) {
|
|
|
403
252
|
...daemonEnv,
|
|
404
253
|
DAEMON_TOKEN: token,
|
|
405
254
|
ADDR: "127.0.0.1:0",
|
|
406
|
-
STATE_PATH:
|
|
407
|
-
WORKSPACE_DB_PATH:
|
|
255
|
+
STATE_PATH: path.join(directory, "agent-state.json"),
|
|
256
|
+
WORKSPACE_DB_PATH: path.join(directory, "workspace.db"),
|
|
408
257
|
AGENT_CWD: config.directory,
|
|
409
258
|
COMPUTER_SSH_ADDR: `${config.bind.includes(":") ? `[${config.bind}]` : config.bind}:${sshPort}`,
|
|
410
259
|
COMPUTER_WS_ADDR: `127.0.0.1:${websocketPort}`,
|
|
@@ -415,8 +264,8 @@ async function superviseComputer(directory) {
|
|
|
415
264
|
process2.on("error", (error) => {
|
|
416
265
|
failure = error;
|
|
417
266
|
});
|
|
418
|
-
await new Promise((
|
|
419
|
-
process2.once("spawn",
|
|
267
|
+
await new Promise((resolve2, reject) => {
|
|
268
|
+
process2.once("spawn", resolve2);
|
|
420
269
|
process2.once("error", reject);
|
|
421
270
|
});
|
|
422
271
|
child = await ownChild(process2);
|
|
@@ -469,14 +318,26 @@ async function superviseComputer(directory) {
|
|
|
469
318
|
client = await launch(binary);
|
|
470
319
|
}
|
|
471
320
|
const saved = await readJSON(relayPath);
|
|
321
|
+
const candidatePath = path.join(directory, "computer-relay-candidate.json");
|
|
322
|
+
const abandonedCandidate = await readJSON(candidatePath);
|
|
323
|
+
if (abandonedCandidate && (abandonedCandidate.owner.pid !== saved?.owner.pid || abandonedCandidate.owner.start !== saved?.owner.start)) {
|
|
324
|
+
const abandonedOwner = await adoptProcess(abandonedCandidate.owner);
|
|
325
|
+
if (abandonedOwner) await (await adoptRelay(abandonedOwner, abandonedCandidate.route, directory, abandonedCandidate.outputFile, abandonedCandidate.configFile)).close();
|
|
326
|
+
}
|
|
327
|
+
await fs.rm(candidatePath, { force: true });
|
|
472
328
|
if (saved) {
|
|
473
329
|
const owner2 = await adoptProcess(saved.owner);
|
|
474
330
|
if (owner2) {
|
|
475
331
|
if (saved.outputFile && saved.route && saved.port === websocketPort && saved.options === JSON.stringify(config.relay)) {
|
|
476
|
-
relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
|
|
332
|
+
relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
|
|
333
|
+
relayWasReachable = !!previous?.routes?.some((route) => JSON.stringify(route) === JSON.stringify(saved.route));
|
|
334
|
+
if (previous?.relayFailureSince && relayWasReachable) {
|
|
335
|
+
relayFailedAt = Math.min(Date.now(), previous.relayFailureSince);
|
|
336
|
+
relayHealthFailures = previous.relayFailures ?? 0;
|
|
337
|
+
}
|
|
477
338
|
} else {
|
|
478
339
|
if (!saved.outputFile) await phase("Refreshing the internet tunnel for this update\u2026");
|
|
479
|
-
const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
|
|
340
|
+
const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
|
|
480
341
|
await abandoned.close();
|
|
481
342
|
await fs.rm(relayPath, { force: true });
|
|
482
343
|
}
|
|
@@ -486,6 +347,128 @@ async function superviseComputer(directory) {
|
|
|
486
347
|
}
|
|
487
348
|
let lastController = "";
|
|
488
349
|
while (!stopping) {
|
|
350
|
+
const changePath = path.join(directory, "computer-connection-change.json");
|
|
351
|
+
const change = !connectionTask && !relayTask && !updateTask && !changingDaemon && client && child?.alive() ? await readJSON(changePath) : void 0;
|
|
352
|
+
if (change && ["queued", "connecting"].includes(change.status)) {
|
|
353
|
+
connectionTask = (async () => {
|
|
354
|
+
const previousConfig = config;
|
|
355
|
+
const next = { ...previousConfig, ...change.patch };
|
|
356
|
+
let previousRelay = relay;
|
|
357
|
+
let releasedPrevious = false;
|
|
358
|
+
let candidate;
|
|
359
|
+
let committed = false;
|
|
360
|
+
let lastVerificationError;
|
|
361
|
+
const changing = new AbortController();
|
|
362
|
+
const cancel = () => changing.abort();
|
|
363
|
+
abort.signal.addEventListener("abort", cancel, { once: true });
|
|
364
|
+
const timeout = setTimeout(cancel, 115e3);
|
|
365
|
+
const report = (message) => writeJSON(changePath, { ...change, status: "connecting", message });
|
|
366
|
+
try {
|
|
367
|
+
await report("Preparing the new connection; your current service keeps running\u2026");
|
|
368
|
+
if (JSON.stringify(next.relay) === JSON.stringify(previousConfig.relay) && previousRelay && (!previousRelay.owner || previousRelay.owner.alive())) candidate = previousRelay;
|
|
369
|
+
else {
|
|
370
|
+
if (previousRelay?.owner && ["ngrok", "oblien"].includes(next.relay.kind) && next.relay.kind === previousConfig.relay.kind && next.relay.url === previousConfig.relay.url) {
|
|
371
|
+
await report("Refreshing the saved provider connection; your terminals keep running\u2026");
|
|
372
|
+
releasedPrevious = true;
|
|
373
|
+
relayReachable = false;
|
|
374
|
+
await previousRelay.close();
|
|
375
|
+
}
|
|
376
|
+
candidate = await startRelay(next.relay, websocketPort, {
|
|
377
|
+
cacheDir: path.join(directory, "tools"),
|
|
378
|
+
outputDirectory: directory,
|
|
379
|
+
cliPath: process.argv[1],
|
|
380
|
+
signal: changing.signal,
|
|
381
|
+
onProgress: report,
|
|
382
|
+
onSpawn: async (owner2, outputFile, configFile) => {
|
|
383
|
+
if (owner2.identity) await writeJSON(candidatePath, {
|
|
384
|
+
owner: owner2.identity,
|
|
385
|
+
port: websocketPort,
|
|
386
|
+
options: JSON.stringify(next.relay),
|
|
387
|
+
outputFile,
|
|
388
|
+
configFile
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (next.relay.kind !== "none") {
|
|
394
|
+
if (!candidate?.route) throw new Error("The provider didn't return an address.");
|
|
395
|
+
await report("Verifying the new secure connection\u2026");
|
|
396
|
+
for (; ; ) {
|
|
397
|
+
changing.signal.throwIfAborted();
|
|
398
|
+
try {
|
|
399
|
+
await checkRelay(candidate.route, { signal: changing.signal });
|
|
400
|
+
break;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof Error) lastVerificationError = error;
|
|
403
|
+
if (error instanceof RelayCheckError && providerNeedsAction(error.code)) throw error;
|
|
404
|
+
await setTimeout$1(2e3, void 0, { signal: changing.signal });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
changing.signal.throwIfAborted();
|
|
409
|
+
const nextRoutes = directRoutes(next, sshPort);
|
|
410
|
+
if (candidate?.route) nextRoutes.push(candidate.route);
|
|
411
|
+
if (!nextRoutes.length) throw new Error("No reachable address was configured.");
|
|
412
|
+
await client.computer.setRoutes(nextRoutes, connectionInfo(next.relay, next.host));
|
|
413
|
+
await writeJSON(path.join(directory, "computer-config.json"), next);
|
|
414
|
+
await saveRelay(relayPath, candidate, websocketPort, next.relay);
|
|
415
|
+
config = next;
|
|
416
|
+
relay = candidate;
|
|
417
|
+
routes = nextRoutes;
|
|
418
|
+
published = "";
|
|
419
|
+
relayReachable = true;
|
|
420
|
+
relayWasReachable = true;
|
|
421
|
+
relayHealthFailures = 0;
|
|
422
|
+
relayFailedAt = 0;
|
|
423
|
+
relayError = void 0;
|
|
424
|
+
relayErrorCode = void 0;
|
|
425
|
+
checkedAt = Date.now();
|
|
426
|
+
nextCheckAt = checkedAt + 3e4;
|
|
427
|
+
committed = true;
|
|
428
|
+
if (previousRelay && previousRelay !== candidate) await previousRelay.close();
|
|
429
|
+
await writeJSON(changePath, { ...change, status: "complete", message: "Persistent connection is ready." });
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (!committed) {
|
|
432
|
+
if (candidate && candidate !== previousRelay) await candidate.close();
|
|
433
|
+
await writeJSON(path.join(directory, "computer-config.json"), previousConfig);
|
|
434
|
+
if (releasedPrevious) {
|
|
435
|
+
previousRelay = void 0;
|
|
436
|
+
if (!stopping) {
|
|
437
|
+
await report("Restoring the previous provider connection\u2026");
|
|
438
|
+
try {
|
|
439
|
+
previousRelay = await startRelay(previousConfig.relay, websocketPort, {
|
|
440
|
+
cacheDir: path.join(directory, "tools"),
|
|
441
|
+
outputDirectory: directory,
|
|
442
|
+
cliPath: process.argv[1],
|
|
443
|
+
signal: abort.signal
|
|
444
|
+
});
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
relayReachable = false;
|
|
449
|
+
nextCheckAt = 0;
|
|
450
|
+
nextRelayAt = 0;
|
|
451
|
+
}
|
|
452
|
+
relay = previousRelay;
|
|
453
|
+
await saveRelay(relayPath, previousRelay, websocketPort, previousConfig.relay);
|
|
454
|
+
published = "";
|
|
455
|
+
}
|
|
456
|
+
await writeJSON(changePath, {
|
|
457
|
+
...change,
|
|
458
|
+
status: committed ? "complete" : "failed",
|
|
459
|
+
message: committed ? "Connection is ready." : `Connection setup didn't finish. ${error instanceof Error && error.name !== "AbortError" ? error.message : lastVerificationError?.message ?? "Check the provider configuration and internet connection."} The previous service keeps running.`
|
|
460
|
+
});
|
|
461
|
+
} finally {
|
|
462
|
+
clearTimeout(timeout);
|
|
463
|
+
abort.signal.removeEventListener("abort", cancel);
|
|
464
|
+
await fs.rm(candidatePath, { force: true });
|
|
465
|
+
}
|
|
466
|
+
})().catch(() => {
|
|
467
|
+
relayError = "The connection change could not be saved. Check free disk space, then run mindwire connection again.";
|
|
468
|
+
}).finally(() => {
|
|
469
|
+
connectionTask = void 0;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
489
472
|
if (!changingDaemon && child && !child.alive()) {
|
|
490
473
|
child = void 0;
|
|
491
474
|
client = void 0;
|
|
@@ -510,7 +493,8 @@ async function superviseComputer(directory) {
|
|
|
510
493
|
nextDaemonAt = Date.now() + recoveryDelay(++daemonFailures);
|
|
511
494
|
}
|
|
512
495
|
}
|
|
513
|
-
|
|
496
|
+
const requestedCheck = await readJSON(path.join(directory, "computer-check.json"));
|
|
497
|
+
if (!connectionTask && relay?.owner && !relay.owner.alive()) {
|
|
514
498
|
await relay.close();
|
|
515
499
|
relay = void 0;
|
|
516
500
|
relayReachable = false;
|
|
@@ -538,30 +522,35 @@ async function superviseComputer(directory) {
|
|
|
538
522
|
port: websocketPort,
|
|
539
523
|
options: JSON.stringify(config.relay),
|
|
540
524
|
route: relay.route,
|
|
541
|
-
outputFile: relay.outputFile
|
|
525
|
+
outputFile: relay.outputFile,
|
|
526
|
+
configFile: relay.configFile
|
|
542
527
|
});
|
|
543
528
|
} else {
|
|
529
|
+
checkedAt = Date.now();
|
|
544
530
|
relayError = result.error instanceof Error ? result.error.message : "The internet tunnel couldn't connect.";
|
|
545
|
-
|
|
531
|
+
relayErrorCode = result.error instanceof ProviderConnectionError ? result.error.code : void 0;
|
|
532
|
+
nextRelayAt = Date.now() + (providerNeedsAction(relayErrorCode) ? 6e4 : recoveryDelay(++relayFailures));
|
|
546
533
|
}
|
|
547
534
|
}
|
|
548
|
-
if (!relay && !relayTask && Date.now() >= nextRelayAt) {
|
|
535
|
+
if (!relay && !relayTask && !connectionTask && (Date.now() >= nextRelayAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
|
|
549
536
|
relayError = void 0;
|
|
550
537
|
relayErrorCode = void 0;
|
|
551
538
|
relayMessage = "Reconnecting your internet tunnel\u2026";
|
|
552
539
|
relayTask = startRelay(config.relay, websocketPort, {
|
|
553
|
-
cacheDir:
|
|
540
|
+
cacheDir: path.join(directory, "tools"),
|
|
554
541
|
outputDirectory: directory,
|
|
542
|
+
cliPath: process.argv[1],
|
|
555
543
|
signal: abort.signal,
|
|
556
544
|
onProgress: (message) => {
|
|
557
545
|
relayMessage = message;
|
|
558
546
|
},
|
|
559
|
-
onSpawn: async (owner2, outputFile) => {
|
|
547
|
+
onSpawn: async (owner2, outputFile, configFile) => {
|
|
560
548
|
if (owner2.identity) await writeJSON(relayPath, {
|
|
561
549
|
owner: owner2.identity,
|
|
562
550
|
port: websocketPort,
|
|
563
551
|
options: JSON.stringify(config.relay),
|
|
564
|
-
outputFile
|
|
552
|
+
outputFile,
|
|
553
|
+
configFile
|
|
565
554
|
});
|
|
566
555
|
}
|
|
567
556
|
}).then((handle) => {
|
|
@@ -590,7 +579,8 @@ async function superviseComputer(directory) {
|
|
|
590
579
|
relayFailedAt ||= checkedAt;
|
|
591
580
|
relayHealthFailures++;
|
|
592
581
|
nextCheckAt = checkedAt + Math.min(15e3, recoveryDelay(relayHealthFailures));
|
|
593
|
-
if (result.canRestart) {
|
|
582
|
+
if (result.canRestart && !connectionTask) {
|
|
583
|
+
await phase(result.error instanceof RelayCheckError && result.error.addressMissing ? "Cloudflare's address expired. Creating a new secure tunnel\u2026" : "The internet tunnel stopped responding. Replacing it\u2026");
|
|
594
584
|
await relay.close();
|
|
595
585
|
relay = void 0;
|
|
596
586
|
relayWasReachable = false;
|
|
@@ -600,7 +590,6 @@ async function superviseComputer(directory) {
|
|
|
600
590
|
}
|
|
601
591
|
}
|
|
602
592
|
}
|
|
603
|
-
const requestedCheck = await readJSON(path3.join(directory, "computer-check.json"));
|
|
604
593
|
if (config.relay.kind === "none") checkedAt = Math.max(checkedAt, requestedCheck?.requestedAt ?? 0);
|
|
605
594
|
if (relay?.route && !checkTask && (Date.now() >= nextCheckAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
|
|
606
595
|
const checking = relay;
|
|
@@ -609,17 +598,18 @@ async function superviseComputer(directory) {
|
|
|
609
598
|
await checkRelay(checking.route, { signal: abort.signal });
|
|
610
599
|
checkResult = { relay: checking };
|
|
611
600
|
} catch (error) {
|
|
612
|
-
const canRestart = !!checking.owner && relayFailedAt > 0 && relayNeedsRestart(error, relayHealthFailures, Date.now() - relayFailedAt) && await relayProviderReachable(config.relay, abort.signal).catch(() => false);
|
|
601
|
+
const canRestart = !!checking.owner && relayFailedAt > 0 && relayNeedsRestart(error, relayHealthFailures, Date.now() - relayFailedAt) && (error instanceof RelayCheckError && error.addressMissing || await relayProviderReachable(config.relay, abort.signal).catch(() => false));
|
|
613
602
|
checkResult = { relay: checking, error, canRestart };
|
|
614
603
|
}
|
|
615
604
|
})();
|
|
616
605
|
}
|
|
617
606
|
routes = directRoutes(config, sshPort);
|
|
618
607
|
if (relay?.route && relayWasReachable) routes.push(relay.route);
|
|
619
|
-
const
|
|
608
|
+
const metadata = connectionInfo(config.relay, config.host);
|
|
609
|
+
const routeJSON = JSON.stringify({ routes, connection: metadata });
|
|
620
610
|
if (!changingDaemon && client && child?.alive() && routeJSON !== published) {
|
|
621
611
|
try {
|
|
622
|
-
await client.computer.setRoutes(routes);
|
|
612
|
+
await client.computer.setRoutes(routes, metadata);
|
|
623
613
|
published = routeJSON;
|
|
624
614
|
} catch {
|
|
625
615
|
}
|
|
@@ -635,15 +625,17 @@ async function superviseComputer(directory) {
|
|
|
635
625
|
phase: ready ? void 0 : daemonError ?? relayError ?? relayMessage,
|
|
636
626
|
error: daemonError ?? relayError,
|
|
637
627
|
errorCode: daemonError ? void 0 : relayErrorCode,
|
|
638
|
-
recovering: !ready
|
|
628
|
+
recovering: !ready,
|
|
629
|
+
relayFailureSince: relayFailedAt || void 0,
|
|
630
|
+
relayFailures: relayHealthFailures || void 0
|
|
639
631
|
};
|
|
640
632
|
const serialized = JSON.stringify(state);
|
|
641
633
|
if (serialized !== lastController) {
|
|
642
634
|
await writeJSON(controllerPath, state);
|
|
643
635
|
lastController = serialized;
|
|
644
636
|
}
|
|
645
|
-
const updatePath =
|
|
646
|
-
const update = !updateTask && client && child?.alive() ? await readJSON(updatePath) : void 0;
|
|
637
|
+
const updatePath = path.join(directory, "computer-update.json");
|
|
638
|
+
const update = !updateTask && !connectionTask && client && child?.alive() ? await readJSON(updatePath) : void 0;
|
|
647
639
|
if (update && ["queued", "downloading", "waiting", "restarting"].includes(update.status)) {
|
|
648
640
|
updateTask = (async () => {
|
|
649
641
|
const status = async (state2, error) => writeJSON(
|
|
@@ -682,9 +674,9 @@ async function superviseComputer(directory) {
|
|
|
682
674
|
}
|
|
683
675
|
binary = replacement;
|
|
684
676
|
config = { ...config, daemonBin: void 0, version: update.version };
|
|
685
|
-
await writeJSON(
|
|
686
|
-
await client.computer.setRoutes(routes);
|
|
687
|
-
published =
|
|
677
|
+
await writeJSON(path.join(directory, "computer-config.json"), config);
|
|
678
|
+
await client.computer.setRoutes(routes, connectionInfo(config.relay, config.host));
|
|
679
|
+
published = "";
|
|
688
680
|
await status("complete");
|
|
689
681
|
} catch (error) {
|
|
690
682
|
await status("failed", error instanceof Error ? error.message : "Service update failed.");
|
|
@@ -706,6 +698,7 @@ async function superviseComputer(directory) {
|
|
|
706
698
|
await relayTask;
|
|
707
699
|
await checkTask;
|
|
708
700
|
await updateTask;
|
|
701
|
+
await connectionTask;
|
|
709
702
|
if (relayResult?.handle && relayResult.handle !== relay) await relayResult.handle.close();
|
|
710
703
|
await relay?.close();
|
|
711
704
|
if (child) await child.close();
|
|
@@ -716,5 +709,5 @@ async function superviseComputer(directory) {
|
|
|
716
709
|
}
|
|
717
710
|
|
|
718
711
|
export { recoveryDelay, superviseComputer };
|
|
719
|
-
//# sourceMappingURL=controller-
|
|
720
|
-
//# sourceMappingURL=controller-
|
|
712
|
+
//# sourceMappingURL=controller-2SJLZMJZ.js.map
|
|
713
|
+
//# sourceMappingURL=controller-2SJLZMJZ.js.map
|