mindwire 0.1.34 → 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-3QFDSYKC.js → chunk-RTGZDLSG.js} +523 -172
- 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 +2 -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 +2 -2
- 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-AGT6MCJG.js → controller-2SJLZMJZ.js} +199 -279
- package/dist/controller-2SJLZMJZ.js.map +1 -0
- package/dist/{controller-handoff-3LAYBJUL.js → controller-handoff-RUUHODNC.js} +4 -4
- package/dist/{controller-handoff-3LAYBJUL.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-3QFDSYKC.js.map +0 -1
- package/dist/controller-AGT6MCJG.js.map +0 -1
|
@@ -1,247 +1,17 @@
|
|
|
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';
|
|
15
12
|
import { isIP } from 'net';
|
|
16
13
|
import WebSocket from 'ws';
|
|
17
14
|
|
|
18
|
-
var CLOUDFLARED_VERSION = "2026.9.3";
|
|
19
|
-
var execute = promisify(execFile);
|
|
20
|
-
async function ensureCloudflareBinary(options = {}) {
|
|
21
|
-
const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
|
|
22
|
-
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : "";
|
|
23
|
-
if (!cpu || !["darwin", "linux", "win32"].includes(platform) || platform === "win32" && cpu !== "amd64") {
|
|
24
|
-
throw new Error("Install cloudflared for this computer, or select another connection provider.");
|
|
25
|
-
}
|
|
26
|
-
const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
|
|
27
|
-
const bin = path3.join(
|
|
28
|
-
options.cacheDir ?? await binaryCacheDirectory(),
|
|
29
|
-
"cloudflared",
|
|
30
|
-
CLOUDFLARED_VERSION,
|
|
31
|
-
`${platform}-${arch}`,
|
|
32
|
-
platform === "win32" ? "cloudflared.exe" : "cloudflared"
|
|
33
|
-
);
|
|
34
|
-
const cached = await cachedExecutable(bin);
|
|
35
|
-
if (cached) return cached;
|
|
36
|
-
await options.onProgress?.("Downloading the secure connection helper\u2026");
|
|
37
|
-
const request2 = options.fetch ?? fetch;
|
|
38
|
-
const release = await request2(
|
|
39
|
-
`https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
|
|
40
|
-
{ headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
|
|
41
|
-
);
|
|
42
|
-
if (!release.ok) throw new Error("Couldn't download the connection helper. Retry, install cloudflared yourself, or choose another provider.");
|
|
43
|
-
const metadata = await release.json();
|
|
44
|
-
const item = metadata.assets?.find((value) => value.name === asset);
|
|
45
|
-
const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
|
|
46
|
-
const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
|
|
47
|
-
if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
|
|
48
|
-
const downloaded = await request2(expectedURL, { signal: AbortSignal.timeout(12e4) });
|
|
49
|
-
if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
|
|
50
|
-
const archive = new Uint8Array(await downloaded.arrayBuffer());
|
|
51
|
-
await verifyReleaseBytes(archive, checksum, asset);
|
|
52
|
-
let executable = archive;
|
|
53
|
-
if (platform === "darwin") {
|
|
54
|
-
const temporary = await mkdtemp(path3.join(tmpdir(), "mindwire-cloudflared-"));
|
|
55
|
-
try {
|
|
56
|
-
const archivePath = path3.join(temporary, asset);
|
|
57
|
-
await writeFile(archivePath, archive, { mode: 384 });
|
|
58
|
-
const result = await execute(
|
|
59
|
-
"/usr/bin/tar",
|
|
60
|
-
["-xzOf", archivePath, "cloudflared"],
|
|
61
|
-
{ encoding: "buffer", maxBuffer: 128 * 1024 * 1024, timeout: 3e4 }
|
|
62
|
-
);
|
|
63
|
-
executable = new Uint8Array(result.stdout);
|
|
64
|
-
} finally {
|
|
65
|
-
await rm(temporary, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return cacheExecutable(bin, executable, await executableChecksum(executable));
|
|
69
|
-
}
|
|
70
|
-
async function cloudflareCommand(options) {
|
|
71
|
-
try {
|
|
72
|
-
await execute("cloudflared", ["--version"], { timeout: 3e3, windowsHide: true });
|
|
73
|
-
return "cloudflared";
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (error.code !== "ENOENT") throw new Error("The installed cloudflared could not start. Repair it or choose another connection provider.");
|
|
76
|
-
return ensureCloudflareBinary(options);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
var RelayOutput = class _RelayOutput {
|
|
80
|
-
constructor(file, handle) {
|
|
81
|
-
this.file = file;
|
|
82
|
-
this.handle = handle;
|
|
83
|
-
}
|
|
84
|
-
file;
|
|
85
|
-
handle;
|
|
86
|
-
position = 0;
|
|
87
|
-
timer;
|
|
88
|
-
pending = Promise.resolve();
|
|
89
|
-
closing;
|
|
90
|
-
static async create(directory = tmpdir()) {
|
|
91
|
-
const file = path3.join(directory, `.relay-output-${randomUUID()}.log`);
|
|
92
|
-
return new _RelayOutput(file, await open(file, "ax+", 384));
|
|
93
|
-
}
|
|
94
|
-
static async adopt(directory, file) {
|
|
95
|
-
if (path3.dirname(file) !== path3.resolve(directory) || !/^\.relay-output-[a-f0-9-]{36}\.log$/.test(path3.basename(file))) return;
|
|
96
|
-
try {
|
|
97
|
-
const output = new _RelayOutput(file, await open(file, "r+"));
|
|
98
|
-
output.discard();
|
|
99
|
-
return output;
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error.code !== "ENOENT") throw error;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
get fd() {
|
|
105
|
-
return this.handle.fd;
|
|
106
|
-
}
|
|
107
|
-
async read() {
|
|
108
|
-
const { size } = await this.handle.stat();
|
|
109
|
-
if (size > 2 * 1024 * 1024) throw new Error("The tunnel helper produced too much startup output. Check its configuration.");
|
|
110
|
-
const data = Buffer.alloc(Math.min(65536, Math.max(0, size - this.position)));
|
|
111
|
-
const { bytesRead } = await this.handle.read(data, 0, data.length, this.position);
|
|
112
|
-
this.position += bytesRead;
|
|
113
|
-
return data.subarray(0, bytesRead);
|
|
114
|
-
}
|
|
115
|
-
discard() {
|
|
116
|
-
if (this.timer || this.closing) return;
|
|
117
|
-
const drain = () => {
|
|
118
|
-
this.pending = this.pending.then(() => this.handle.truncate(0)).catch(() => {
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
drain();
|
|
122
|
-
this.timer = setInterval(drain, 2e3);
|
|
123
|
-
this.timer.unref();
|
|
124
|
-
}
|
|
125
|
-
close() {
|
|
126
|
-
return this.closing ??= (async () => {
|
|
127
|
-
clearInterval(this.timer);
|
|
128
|
-
await this.pending;
|
|
129
|
-
await this.handle.close();
|
|
130
|
-
await rm(this.file, { force: true });
|
|
131
|
-
})();
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// src/computer/relay.ts
|
|
136
|
-
async function adoptRelay(owner, route, directory, outputFile) {
|
|
137
|
-
const output = outputFile ? await RelayOutput.adopt(directory, outputFile) : void 0;
|
|
138
|
-
return { route, owner, outputFile: output?.file, async close() {
|
|
139
|
-
await owner.close();
|
|
140
|
-
await output?.close();
|
|
141
|
-
} };
|
|
142
|
-
}
|
|
143
|
-
function websocketURL(value) {
|
|
144
|
-
const url = new URL(value);
|
|
145
|
-
if (url.protocol === "https:") url.protocol = "wss:";
|
|
146
|
-
if (url.protocol !== "wss:" || !url.hostname || url.username || url.password || url.hash) {
|
|
147
|
-
throw new Error("Relay URLs must use wss:// with no embedded credentials or fragment.");
|
|
148
|
-
}
|
|
149
|
-
if (!url.pathname || url.pathname === "/") url.pathname = "/ssh";
|
|
150
|
-
return url.toString();
|
|
151
|
-
}
|
|
152
|
-
async function startRelay(options, port, runtime = {}) {
|
|
153
|
-
runtime.signal?.throwIfAborted();
|
|
154
|
-
if (options.kind === "none") return { close() {
|
|
155
|
-
} };
|
|
156
|
-
if (options.kind === "custom") {
|
|
157
|
-
if (!options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
|
|
158
|
-
return { route: { kind: "websocket", url: websocketURL(options.url) }, close() {
|
|
159
|
-
} };
|
|
160
|
-
}
|
|
161
|
-
const origin = `http://127.0.0.1:${port}`;
|
|
162
|
-
const env = { ...process.env };
|
|
163
|
-
let command, args;
|
|
164
|
-
if (options.kind === "cloudflare") {
|
|
165
|
-
command = await cloudflareCommand(runtime);
|
|
166
|
-
await runtime.onProgress?.("Connecting through Cloudflare\u2026");
|
|
167
|
-
if (options.cloudflareTokenFile) {
|
|
168
|
-
if (!options.url) throw new Error("A named Cloudflare tunnel needs --relay-url for its configured hostname.");
|
|
169
|
-
env.TUNNEL_TOKEN = (await readFile(options.cloudflareTokenFile, "utf8")).trim();
|
|
170
|
-
args = ["tunnel", "--no-autoupdate", "run"];
|
|
171
|
-
} else {
|
|
172
|
-
if (options.url) throw new Error("Use --cloudflare-token-file with a named tunnel, or --relay custom for an existing tunnel.");
|
|
173
|
-
args = ["tunnel", "--no-autoupdate", "--url", origin];
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
command = "ngrok";
|
|
177
|
-
args = ["http", origin, "--log", "stdout", "--log-format", "json"];
|
|
178
|
-
if (options.url) {
|
|
179
|
-
const url = new URL(websocketURL(options.url));
|
|
180
|
-
args.push("--url", `https://${url.host}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
runtime.signal?.throwIfAborted();
|
|
184
|
-
const output = await RelayOutput.create(runtime.outputDirectory);
|
|
185
|
-
const child = spawn(command, args, { env, detached: true, stdio: ["ignore", output.fd, output.fd], windowsHide: true });
|
|
186
|
-
let failure;
|
|
187
|
-
child.on("error", (error) => {
|
|
188
|
-
failure = error;
|
|
189
|
-
});
|
|
190
|
-
let owner;
|
|
191
|
-
try {
|
|
192
|
-
await new Promise((resolve3, reject) => {
|
|
193
|
-
child.once("spawn", resolve3);
|
|
194
|
-
child.once("error", reject);
|
|
195
|
-
});
|
|
196
|
-
owner = await ownChild(child);
|
|
197
|
-
await runtime.onSpawn?.(owner, output.file);
|
|
198
|
-
const deadline = Date.now() + 45e3;
|
|
199
|
-
let buffer = "", cloudflareAddress = options.url, cloudflareConnected = false;
|
|
200
|
-
while (Date.now() < deadline) {
|
|
201
|
-
runtime.signal?.throwIfAborted();
|
|
202
|
-
if (failure) throw new Error(`Could not run ${options.kind}. Check its installation.`, { cause: failure });
|
|
203
|
-
if (!owner.alive()) throw new Error(`${options.kind} exited before the tunnel connected. Check its account configuration.`);
|
|
204
|
-
const received = buffer + (await output.read()).toString();
|
|
205
|
-
buffer = received.slice(-16384);
|
|
206
|
-
let found;
|
|
207
|
-
if (options.kind === "cloudflare") {
|
|
208
|
-
cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
|
|
209
|
-
cloudflareConnected ||= /Registered tunnel connection/.test(received);
|
|
210
|
-
if (cloudflareConnected) found = cloudflareAddress;
|
|
211
|
-
} else {
|
|
212
|
-
for (const line of received.split(/\r?\n/)) {
|
|
213
|
-
try {
|
|
214
|
-
const event = JSON.parse(line);
|
|
215
|
-
if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
|
|
216
|
-
} catch {
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (found) {
|
|
221
|
-
const route = { kind: "websocket", url: websocketURL(found) };
|
|
222
|
-
output.discard();
|
|
223
|
-
const provider = owner;
|
|
224
|
-
return {
|
|
225
|
-
route,
|
|
226
|
-
process: child,
|
|
227
|
-
owner,
|
|
228
|
-
outputFile: output.file,
|
|
229
|
-
async close() {
|
|
230
|
-
await provider.close();
|
|
231
|
-
await output.close();
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
await setTimeout$1(50, void 0, { signal: runtime.signal });
|
|
236
|
-
}
|
|
237
|
-
throw new Error(`${options.kind} did not establish its tunnel. Check your internet connection and its configuration.`);
|
|
238
|
-
} catch (error) {
|
|
239
|
-
if (owner) await owner.close();
|
|
240
|
-
else if (child.pid) await (await ownChild(child)).close();
|
|
241
|
-
await output.close();
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
15
|
var RelayCheckError = class extends Error {
|
|
246
16
|
constructor(code, message, cause, addressMissing = false) {
|
|
247
17
|
super(message, cause ? { cause } : void 0);
|
|
@@ -253,7 +23,7 @@ var RelayCheckError = class extends Error {
|
|
|
253
23
|
addressMissing;
|
|
254
24
|
};
|
|
255
25
|
function relayNeedsRestart(error, failures, failedForMs) {
|
|
256
|
-
return failures >= 2 && failedForMs >= 9e4 && (!(error instanceof RelayCheckError && error.code === "dns") || error.addressMissing);
|
|
26
|
+
return failures >= 2 && failedForMs >= 9e4 && !(error instanceof RelayCheckError && error.code === "provider_certificate") && (!(error instanceof RelayCheckError && error.code === "dns") || error.addressMissing);
|
|
257
27
|
}
|
|
258
28
|
var quickTunnelHost = /^[a-z0-9-]+\.trycloudflare\.com$/i;
|
|
259
29
|
async function resolveCloudflareAddress(hostname, family = 0, options = {}) {
|
|
@@ -336,7 +106,7 @@ var relayLookup = (hostname, options, callback) => {
|
|
|
336
106
|
async function checkRelay(route, options = {}) {
|
|
337
107
|
options.signal?.throwIfAborted();
|
|
338
108
|
if (route.kind !== "websocket") throw new Error("Expected an internet tunnel address.");
|
|
339
|
-
await new Promise((
|
|
109
|
+
await new Promise((resolve2, reject) => {
|
|
340
110
|
const socket = new WebSocket(route.url, { maxPayload: 4096, perMessageDeflate: false, followRedirects: false, lookup: options.lookup ?? relayLookup });
|
|
341
111
|
let finished = false, banner = "";
|
|
342
112
|
const finish = (error) => {
|
|
@@ -346,12 +116,14 @@ async function checkRelay(route, options = {}) {
|
|
|
346
116
|
options.signal?.removeEventListener("abort", aborted);
|
|
347
117
|
socket.terminate();
|
|
348
118
|
if (error) reject(error);
|
|
349
|
-
else
|
|
119
|
+
else resolve2();
|
|
350
120
|
};
|
|
351
121
|
const aborted = () => finish(options.signal?.reason ?? new Error("Connection check cancelled."));
|
|
352
122
|
const timer = setTimeout(() => finish(new RelayCheckError("timeout", "The internet tunnel isn't responding. Retrying\u2026")), options.timeoutMs ?? 8e3);
|
|
353
123
|
socket.on("error", (error) => {
|
|
354
|
-
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 ?? "")) {
|
|
355
127
|
const provider = new URL(route.url).hostname.endsWith(".trycloudflare.com") ? "Cloudflare's" : "the tunnel's";
|
|
356
128
|
finish(new RelayCheckError(
|
|
357
129
|
"dns",
|
|
@@ -385,17 +157,17 @@ async function checkRelay(route, options = {}) {
|
|
|
385
157
|
});
|
|
386
158
|
}
|
|
387
159
|
async function relayProviderReachable(options, signal) {
|
|
388
|
-
if (
|
|
160
|
+
if (!["cloudflare", "ngrok", "oblien"].includes(options.kind)) return false;
|
|
389
161
|
signal?.throwIfAborted();
|
|
390
|
-
return new Promise((
|
|
391
|
-
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/";
|
|
392
164
|
let finished = false;
|
|
393
165
|
const done = (reachable) => {
|
|
394
166
|
if (finished) return;
|
|
395
167
|
finished = true;
|
|
396
168
|
clearTimeout(timer);
|
|
397
169
|
probe.destroy();
|
|
398
|
-
|
|
170
|
+
resolve2(reachable);
|
|
399
171
|
};
|
|
400
172
|
const probe = request(url, { method: "HEAD", signal }, (response) => {
|
|
401
173
|
response.destroy();
|
|
@@ -408,15 +180,26 @@ async function relayProviderReachable(options, signal) {
|
|
|
408
180
|
}
|
|
409
181
|
|
|
410
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
|
+
}
|
|
411
194
|
function recoveryDelay(failures) {
|
|
412
195
|
return Math.min(6e4, 1e3 * 2 ** Math.min(6, Math.max(0, failures - 1)));
|
|
413
196
|
}
|
|
414
197
|
async function superviseComputer(directory) {
|
|
415
|
-
if ((await readJSON(
|
|
416
|
-
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"));
|
|
417
200
|
if (!config) throw new Error("Computer configuration is missing.");
|
|
418
|
-
const controllerPath =
|
|
419
|
-
const relayPath =
|
|
201
|
+
const controllerPath = path.join(directory, "computer-controller.json");
|
|
202
|
+
const relayPath = path.join(directory, "computer-relay.json");
|
|
420
203
|
const previous = await readJSON(controllerPath);
|
|
421
204
|
if (await processStateAlive(previous)) throw new Error("The computer already has a service controller.");
|
|
422
205
|
const owner = await currentProcessIdentity();
|
|
@@ -424,6 +207,7 @@ async function superviseComputer(directory) {
|
|
|
424
207
|
await phase("Preparing Mindwire\u2026");
|
|
425
208
|
let child, relay, stopping = false;
|
|
426
209
|
let relayTask, updateTask;
|
|
210
|
+
let connectionTask;
|
|
427
211
|
let relayResult;
|
|
428
212
|
let checkTask;
|
|
429
213
|
let checkResult;
|
|
@@ -436,30 +220,30 @@ async function superviseComputer(directory) {
|
|
|
436
220
|
process.on("SIGINT", stop);
|
|
437
221
|
let token;
|
|
438
222
|
try {
|
|
439
|
-
token = (await fs.readFile(
|
|
223
|
+
token = (await fs.readFile(path.join(directory, "daemon.token"), "utf8")).trim();
|
|
440
224
|
} catch (error) {
|
|
441
225
|
if (error.code !== "ENOENT") throw error;
|
|
442
226
|
token = randomBytes(32).toString("hex");
|
|
443
|
-
await fs.writeFile(
|
|
227
|
+
await fs.writeFile(path.join(directory, "daemon.token"), token, { mode: 384, flag: "wx" });
|
|
444
228
|
}
|
|
445
229
|
const daemonEnv = { ...process.env };
|
|
446
230
|
if (process.platform === "win32") {
|
|
447
231
|
try {
|
|
448
232
|
const git = execFileSync("where.exe", ["git"], { encoding: "utf8", timeout: 3e3, windowsHide: true }).split(/\r?\n/)[0];
|
|
449
233
|
if (git) {
|
|
450
|
-
const shellDirectory =
|
|
451
|
-
await fs.access(
|
|
234
|
+
const shellDirectory = path.resolve(path.dirname(git), "..", "bin");
|
|
235
|
+
await fs.access(path.join(shellDirectory, "bash.exe"));
|
|
452
236
|
const pathKey = Object.keys(daemonEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
453
|
-
daemonEnv[pathKey] = `${shellDirectory}${
|
|
237
|
+
daemonEnv[pathKey] = `${shellDirectory}${path.delimiter}${daemonEnv[pathKey] ?? ""}`;
|
|
454
238
|
}
|
|
455
239
|
} catch {
|
|
456
240
|
}
|
|
457
241
|
}
|
|
458
|
-
const oldRuntime = await readJSON(
|
|
242
|
+
const oldRuntime = await readJSON(path.join(directory, "computer-runtime.json"));
|
|
459
243
|
let sshPort = config.sshPort || oldRuntime?.sshPort || 0;
|
|
460
244
|
let websocketPort = config.websocketPort || oldRuntime?.websocketPort || 0;
|
|
461
245
|
const launch = async (binary) => {
|
|
462
|
-
await fs.rm(
|
|
246
|
+
await fs.rm(path.join(directory, "computer-runtime.json"), { force: true });
|
|
463
247
|
const process2 = spawn(binary, ["--computer"], {
|
|
464
248
|
cwd: config.directory,
|
|
465
249
|
stdio: ["ignore", "inherit", "inherit"],
|
|
@@ -468,8 +252,8 @@ async function superviseComputer(directory) {
|
|
|
468
252
|
...daemonEnv,
|
|
469
253
|
DAEMON_TOKEN: token,
|
|
470
254
|
ADDR: "127.0.0.1:0",
|
|
471
|
-
STATE_PATH:
|
|
472
|
-
WORKSPACE_DB_PATH:
|
|
255
|
+
STATE_PATH: path.join(directory, "agent-state.json"),
|
|
256
|
+
WORKSPACE_DB_PATH: path.join(directory, "workspace.db"),
|
|
473
257
|
AGENT_CWD: config.directory,
|
|
474
258
|
COMPUTER_SSH_ADDR: `${config.bind.includes(":") ? `[${config.bind}]` : config.bind}:${sshPort}`,
|
|
475
259
|
COMPUTER_WS_ADDR: `127.0.0.1:${websocketPort}`,
|
|
@@ -480,8 +264,8 @@ async function superviseComputer(directory) {
|
|
|
480
264
|
process2.on("error", (error) => {
|
|
481
265
|
failure = error;
|
|
482
266
|
});
|
|
483
|
-
await new Promise((
|
|
484
|
-
process2.once("spawn",
|
|
267
|
+
await new Promise((resolve2, reject) => {
|
|
268
|
+
process2.once("spawn", resolve2);
|
|
485
269
|
process2.once("error", reject);
|
|
486
270
|
});
|
|
487
271
|
child = await ownChild(process2);
|
|
@@ -534,11 +318,18 @@ async function superviseComputer(directory) {
|
|
|
534
318
|
client = await launch(binary);
|
|
535
319
|
}
|
|
536
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 });
|
|
537
328
|
if (saved) {
|
|
538
329
|
const owner2 = await adoptProcess(saved.owner);
|
|
539
330
|
if (owner2) {
|
|
540
331
|
if (saved.outputFile && saved.route && saved.port === websocketPort && saved.options === JSON.stringify(config.relay)) {
|
|
541
|
-
relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
|
|
332
|
+
relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
|
|
542
333
|
relayWasReachable = !!previous?.routes?.some((route) => JSON.stringify(route) === JSON.stringify(saved.route));
|
|
543
334
|
if (previous?.relayFailureSince && relayWasReachable) {
|
|
544
335
|
relayFailedAt = Math.min(Date.now(), previous.relayFailureSince);
|
|
@@ -546,7 +337,7 @@ async function superviseComputer(directory) {
|
|
|
546
337
|
}
|
|
547
338
|
} else {
|
|
548
339
|
if (!saved.outputFile) await phase("Refreshing the internet tunnel for this update\u2026");
|
|
549
|
-
const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
|
|
340
|
+
const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
|
|
550
341
|
await abandoned.close();
|
|
551
342
|
await fs.rm(relayPath, { force: true });
|
|
552
343
|
}
|
|
@@ -556,6 +347,128 @@ async function superviseComputer(directory) {
|
|
|
556
347
|
}
|
|
557
348
|
let lastController = "";
|
|
558
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
|
+
}
|
|
559
472
|
if (!changingDaemon && child && !child.alive()) {
|
|
560
473
|
child = void 0;
|
|
561
474
|
client = void 0;
|
|
@@ -580,7 +493,8 @@ async function superviseComputer(directory) {
|
|
|
580
493
|
nextDaemonAt = Date.now() + recoveryDelay(++daemonFailures);
|
|
581
494
|
}
|
|
582
495
|
}
|
|
583
|
-
|
|
496
|
+
const requestedCheck = await readJSON(path.join(directory, "computer-check.json"));
|
|
497
|
+
if (!connectionTask && relay?.owner && !relay.owner.alive()) {
|
|
584
498
|
await relay.close();
|
|
585
499
|
relay = void 0;
|
|
586
500
|
relayReachable = false;
|
|
@@ -608,30 +522,35 @@ async function superviseComputer(directory) {
|
|
|
608
522
|
port: websocketPort,
|
|
609
523
|
options: JSON.stringify(config.relay),
|
|
610
524
|
route: relay.route,
|
|
611
|
-
outputFile: relay.outputFile
|
|
525
|
+
outputFile: relay.outputFile,
|
|
526
|
+
configFile: relay.configFile
|
|
612
527
|
});
|
|
613
528
|
} else {
|
|
529
|
+
checkedAt = Date.now();
|
|
614
530
|
relayError = result.error instanceof Error ? result.error.message : "The internet tunnel couldn't connect.";
|
|
615
|
-
|
|
531
|
+
relayErrorCode = result.error instanceof ProviderConnectionError ? result.error.code : void 0;
|
|
532
|
+
nextRelayAt = Date.now() + (providerNeedsAction(relayErrorCode) ? 6e4 : recoveryDelay(++relayFailures));
|
|
616
533
|
}
|
|
617
534
|
}
|
|
618
|
-
if (!relay && !relayTask && Date.now() >= nextRelayAt) {
|
|
535
|
+
if (!relay && !relayTask && !connectionTask && (Date.now() >= nextRelayAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
|
|
619
536
|
relayError = void 0;
|
|
620
537
|
relayErrorCode = void 0;
|
|
621
538
|
relayMessage = "Reconnecting your internet tunnel\u2026";
|
|
622
539
|
relayTask = startRelay(config.relay, websocketPort, {
|
|
623
|
-
cacheDir:
|
|
540
|
+
cacheDir: path.join(directory, "tools"),
|
|
624
541
|
outputDirectory: directory,
|
|
542
|
+
cliPath: process.argv[1],
|
|
625
543
|
signal: abort.signal,
|
|
626
544
|
onProgress: (message) => {
|
|
627
545
|
relayMessage = message;
|
|
628
546
|
},
|
|
629
|
-
onSpawn: async (owner2, outputFile) => {
|
|
547
|
+
onSpawn: async (owner2, outputFile, configFile) => {
|
|
630
548
|
if (owner2.identity) await writeJSON(relayPath, {
|
|
631
549
|
owner: owner2.identity,
|
|
632
550
|
port: websocketPort,
|
|
633
551
|
options: JSON.stringify(config.relay),
|
|
634
|
-
outputFile
|
|
552
|
+
outputFile,
|
|
553
|
+
configFile
|
|
635
554
|
});
|
|
636
555
|
}
|
|
637
556
|
}).then((handle) => {
|
|
@@ -660,7 +579,7 @@ async function superviseComputer(directory) {
|
|
|
660
579
|
relayFailedAt ||= checkedAt;
|
|
661
580
|
relayHealthFailures++;
|
|
662
581
|
nextCheckAt = checkedAt + Math.min(15e3, recoveryDelay(relayHealthFailures));
|
|
663
|
-
if (result.canRestart) {
|
|
582
|
+
if (result.canRestart && !connectionTask) {
|
|
664
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");
|
|
665
584
|
await relay.close();
|
|
666
585
|
relay = void 0;
|
|
@@ -671,7 +590,6 @@ async function superviseComputer(directory) {
|
|
|
671
590
|
}
|
|
672
591
|
}
|
|
673
592
|
}
|
|
674
|
-
const requestedCheck = await readJSON(path3.join(directory, "computer-check.json"));
|
|
675
593
|
if (config.relay.kind === "none") checkedAt = Math.max(checkedAt, requestedCheck?.requestedAt ?? 0);
|
|
676
594
|
if (relay?.route && !checkTask && (Date.now() >= nextCheckAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
|
|
677
595
|
const checking = relay;
|
|
@@ -687,10 +605,11 @@ async function superviseComputer(directory) {
|
|
|
687
605
|
}
|
|
688
606
|
routes = directRoutes(config, sshPort);
|
|
689
607
|
if (relay?.route && relayWasReachable) routes.push(relay.route);
|
|
690
|
-
const
|
|
608
|
+
const metadata = connectionInfo(config.relay, config.host);
|
|
609
|
+
const routeJSON = JSON.stringify({ routes, connection: metadata });
|
|
691
610
|
if (!changingDaemon && client && child?.alive() && routeJSON !== published) {
|
|
692
611
|
try {
|
|
693
|
-
await client.computer.setRoutes(routes);
|
|
612
|
+
await client.computer.setRoutes(routes, metadata);
|
|
694
613
|
published = routeJSON;
|
|
695
614
|
} catch {
|
|
696
615
|
}
|
|
@@ -715,8 +634,8 @@ async function superviseComputer(directory) {
|
|
|
715
634
|
await writeJSON(controllerPath, state);
|
|
716
635
|
lastController = serialized;
|
|
717
636
|
}
|
|
718
|
-
const updatePath =
|
|
719
|
-
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;
|
|
720
639
|
if (update && ["queued", "downloading", "waiting", "restarting"].includes(update.status)) {
|
|
721
640
|
updateTask = (async () => {
|
|
722
641
|
const status = async (state2, error) => writeJSON(
|
|
@@ -755,9 +674,9 @@ async function superviseComputer(directory) {
|
|
|
755
674
|
}
|
|
756
675
|
binary = replacement;
|
|
757
676
|
config = { ...config, daemonBin: void 0, version: update.version };
|
|
758
|
-
await writeJSON(
|
|
759
|
-
await client.computer.setRoutes(routes);
|
|
760
|
-
published =
|
|
677
|
+
await writeJSON(path.join(directory, "computer-config.json"), config);
|
|
678
|
+
await client.computer.setRoutes(routes, connectionInfo(config.relay, config.host));
|
|
679
|
+
published = "";
|
|
761
680
|
await status("complete");
|
|
762
681
|
} catch (error) {
|
|
763
682
|
await status("failed", error instanceof Error ? error.message : "Service update failed.");
|
|
@@ -779,6 +698,7 @@ async function superviseComputer(directory) {
|
|
|
779
698
|
await relayTask;
|
|
780
699
|
await checkTask;
|
|
781
700
|
await updateTask;
|
|
701
|
+
await connectionTask;
|
|
782
702
|
if (relayResult?.handle && relayResult.handle !== relay) await relayResult.handle.close();
|
|
783
703
|
await relay?.close();
|
|
784
704
|
if (child) await child.close();
|
|
@@ -789,5 +709,5 @@ async function superviseComputer(directory) {
|
|
|
789
709
|
}
|
|
790
710
|
|
|
791
711
|
export { recoveryDelay, superviseComputer };
|
|
792
|
-
//# sourceMappingURL=controller-
|
|
793
|
-
//# sourceMappingURL=controller-
|
|
712
|
+
//# sourceMappingURL=controller-2SJLZMJZ.js.map
|
|
713
|
+
//# sourceMappingURL=controller-2SJLZMJZ.js.map
|