@trusty-squire/mcp 1.1.13-rc.8 → 1.1.13
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/README.md +139 -62
- package/assets/login/vnc-input.js +9 -0
- package/assets/login/vnc.html +214 -202
- package/assets/screencaps/connect-walkthrough.png +0 -0
- package/assets/screencaps/connect-walkthrough.svg +1 -1
- package/dist/api-client.d.ts +26 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +19 -31
- package/dist/api-client.js.map +1 -1
- package/dist/bin.js +2 -3
- package/dist/bin.js.map +1 -1
- package/dist/bot/browser.d.ts +134 -77
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +1788 -1543
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/compact-observation-v2.d.ts +167 -0
- package/dist/bot/compact-observation-v2.d.ts.map +1 -0
- package/dist/bot/compact-observation-v2.js +1038 -0
- package/dist/bot/compact-observation-v2.js.map +1 -0
- package/dist/bot/credential-shape.d.ts +2 -0
- package/dist/bot/credential-shape.d.ts.map +1 -1
- package/dist/bot/credential-shape.js +21 -8
- package/dist/bot/credential-shape.js.map +1 -1
- package/dist/bot/element-fingerprint.d.ts +9 -0
- package/dist/bot/element-fingerprint.d.ts.map +1 -0
- package/dist/bot/element-fingerprint.js +147 -0
- package/dist/bot/element-fingerprint.js.map +1 -0
- package/dist/bot/email-verification.d.ts +7 -1
- package/dist/bot/email-verification.d.ts.map +1 -1
- package/dist/bot/email-verification.js +48 -14
- package/dist/bot/email-verification.js.map +1 -1
- package/dist/bot/google-login.d.ts +28 -52
- package/dist/bot/google-login.d.ts.map +1 -1
- package/dist/bot/google-login.js +246 -1119
- package/dist/bot/google-login.js.map +1 -1
- package/dist/bot/install-completion.d.ts +1 -2
- package/dist/bot/install-completion.d.ts.map +1 -1
- package/dist/bot/install-completion.js +1 -2
- package/dist/bot/install-completion.js.map +1 -1
- package/dist/bot/login-state.d.ts +13 -7
- package/dist/bot/login-state.d.ts.map +1 -1
- package/dist/bot/login-state.js +50 -114
- package/dist/bot/login-state.js.map +1 -1
- package/dist/bot/oauth-providers.d.ts +1 -1
- package/dist/bot/oauth-providers.d.ts.map +1 -1
- package/dist/bot/oauth-providers.js +3 -3
- package/dist/bot/oauth-providers.js.map +1 -1
- package/dist/bot/oauth-scope.d.ts +4 -0
- package/dist/bot/oauth-scope.d.ts.map +1 -0
- package/dist/bot/oauth-scope.js +58 -0
- package/dist/bot/oauth-scope.js.map +1 -0
- package/dist/bot/operator-browser-watchdog.d.ts +127 -0
- package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
- package/dist/bot/operator-browser-watchdog.js +438 -0
- package/dist/bot/operator-browser-watchdog.js.map +1 -0
- package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
- package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper-worker.js +17 -0
- package/dist/bot/owner-process-reaper-worker.js.map +1 -0
- package/dist/bot/owner-process-reaper.d.ts +103 -0
- package/dist/bot/owner-process-reaper.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper.js +1058 -0
- package/dist/bot/owner-process-reaper.js.map +1 -0
- package/dist/bot/pay-operator.d.ts +13 -2
- package/dist/bot/pay-operator.d.ts.map +1 -1
- package/dist/bot/pay-operator.js +319 -123
- package/dist/bot/pay-operator.js.map +1 -1
- package/dist/bot/profile.d.ts +10 -1
- package/dist/bot/profile.d.ts.map +1 -1
- package/dist/bot/profile.js +137 -11
- package/dist/bot/profile.js.map +1 -1
- package/dist/bot/promote-to-skill.js +1 -1
- package/dist/bot/promote-to-skill.js.map +1 -1
- package/dist/bot/provision-session.d.ts +60 -164
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +2049 -1546
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/bot/remote-login-display.d.ts +61 -0
- package/dist/bot/remote-login-display.d.ts.map +1 -0
- package/dist/bot/remote-login-display.js +765 -0
- package/dist/bot/remote-login-display.js.map +1 -0
- package/dist/bot/session/hosts.d.ts +8 -0
- package/dist/bot/session/hosts.d.ts.map +1 -0
- package/dist/bot/session/hosts.js +36 -0
- package/dist/bot/session/hosts.js.map +1 -0
- package/dist/bot/session/lifecycle.d.ts +56 -0
- package/dist/bot/session/lifecycle.d.ts.map +1 -0
- package/dist/bot/session/lifecycle.js +962 -0
- package/dist/bot/session/lifecycle.js.map +1 -0
- package/dist/bot/session/model.d.ts +175 -0
- package/dist/bot/session/model.d.ts.map +1 -0
- package/dist/bot/session/model.js +77 -0
- package/dist/bot/session/model.js.map +1 -0
- package/dist/install/cli.d.ts +29 -9
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +243 -322
- package/dist/install/cli.js.map +1 -1
- package/dist/install/interactive.d.ts +1 -0
- package/dist/install/interactive.d.ts.map +1 -1
- package/dist/install/interactive.js +19 -5
- package/dist/install/interactive.js.map +1 -1
- package/dist/install/ui.d.ts.map +1 -1
- package/dist/install/ui.js +3 -15
- package/dist/install/ui.js.map +1 -1
- package/dist/operator-env.js +1 -1
- package/dist/operator-env.js.map +1 -1
- package/dist/server-instance-registry.d.ts +103 -0
- package/dist/server-instance-registry.d.ts.map +1 -0
- package/dist/server-instance-registry.js +392 -0
- package/dist/server-instance-registry.js.map +1 -0
- package/dist/server.d.ts +15 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +164 -62
- package/dist/server.js.map +1 -1
- package/dist/session-guard.d.ts +28 -0
- package/dist/session-guard.d.ts.map +1 -0
- package/dist/session-guard.js +84 -0
- package/dist/session-guard.js.map +1 -0
- package/dist/session.d.ts +57 -13
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +246 -127
- package/dist/session.js.map +1 -1
- package/dist/tools/audit-log.d.ts +16 -2
- package/dist/tools/audit-log.d.ts.map +1 -1
- package/dist/tools/audit-log.js +223 -13
- package/dist/tools/audit-log.js.map +1 -1
- package/dist/tools/audit-rollup.d.ts +80 -0
- package/dist/tools/audit-rollup.d.ts.map +1 -0
- package/dist/tools/audit-rollup.js +0 -0
- package/dist/tools/audit-rollup.js.map +1 -0
- package/dist/tools/fetch-credential.d.ts +37 -0
- package/dist/tools/fetch-credential.d.ts.map +1 -0
- package/dist/tools/fetch-credential.js +153 -0
- package/dist/tools/fetch-credential.js.map +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/operate-pay.d.ts +4 -4
- package/dist/tools/operate-pay.d.ts.map +1 -1
- package/dist/tools/operate-pay.js +163 -96
- package/dist/tools/operate-pay.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +43 -35
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +268 -164
- package/dist/tools/provision-drive.js.map +1 -1
- package/dist/tools/use-credential.d.ts.map +1 -1
- package/dist/tools/use-credential.js +3 -2
- package/dist/tools/use-credential.js.map +1 -1
- package/package.json +4 -6
- package/dist/bot/operator-direct-identity.d.ts +0 -26
- package/dist/bot/operator-direct-identity.d.ts.map +0 -1
- package/dist/bot/operator-direct-identity.js +0 -120
- package/dist/bot/operator-direct-identity.js.map +0 -1
- package/dist/bot/operator-profile-pool.d.ts +0 -97
- package/dist/bot/operator-profile-pool.d.ts.map +0 -1
- package/dist/bot/operator-profile-pool.js +0 -855
- package/dist/bot/operator-profile-pool.js.map +0 -1
- package/dist/bot/xvfb.d.ts +0 -13
- package/dist/bot/xvfb.d.ts.map +0 -1
- package/dist/bot/xvfb.js +0 -146
- package/dist/bot/xvfb.js.map +0 -1
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
// Login-only virtual display + noVNC bridge.
|
|
2
|
+
//
|
|
3
|
+
// Interactive login and headed operator sessions share this Xvfb lifecycle.
|
|
4
|
+
// Only login exposes the display over VNC/noVNC; operator Chrome uses the
|
|
5
|
+
// private display directly and tears it down with its browser session.
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { accessSync, chmodSync, constants, cpSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
9
|
+
import { createConnection, createServer } from "node:net";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { basename, delimiter, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import boxen from "boxen";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { isSelfManagedChromeTerminationSignalExitEnabled, setSelfManagedChromeTerminationSignalExitEnabled, } from "./browser.js";
|
|
16
|
+
import { ownerTrackedHelperState, releaseOwnerTrackedHelper, signalOwnerTrackedHelper, spawnOwnerTrackedHelper, waitForOwnerTrackedHelperExit, } from "./owner-process-reaper.js";
|
|
17
|
+
const LOGIN_WIDTH = Number(process.env.BOT_NOVNC_W) || 720;
|
|
18
|
+
const LOGIN_HEIGHT = Number(process.env.BOT_NOVNC_H) || 1280;
|
|
19
|
+
const NOVNC_INSTALL_DIR = "/usr/share/novnc";
|
|
20
|
+
function loginAssetPath(name) {
|
|
21
|
+
return fileURLToPath(new URL(`../../assets/login/${name}`, import.meta.url));
|
|
22
|
+
}
|
|
23
|
+
function encodeXauthorityField(value) {
|
|
24
|
+
const length = Buffer.allocUnsafe(2);
|
|
25
|
+
length.writeUInt16BE(value.length);
|
|
26
|
+
return Buffer.concat([length, value]);
|
|
27
|
+
}
|
|
28
|
+
function createRemoteLoginXauthority(privateDir) {
|
|
29
|
+
const familyWild = Buffer.allocUnsafe(2);
|
|
30
|
+
familyWild.writeUInt16BE(0xffff);
|
|
31
|
+
const authFile = join(privateDir, "xauthority");
|
|
32
|
+
const contents = Buffer.concat([
|
|
33
|
+
familyWild,
|
|
34
|
+
encodeXauthorityField(Buffer.alloc(0)),
|
|
35
|
+
encodeXauthorityField(Buffer.alloc(0)),
|
|
36
|
+
encodeXauthorityField(Buffer.from("MIT-MAGIC-COOKIE-1", "ascii")),
|
|
37
|
+
encodeXauthorityField(randomBytes(16)),
|
|
38
|
+
]);
|
|
39
|
+
writeFileSync(authFile, contents, { flag: "wx", mode: 0o600 });
|
|
40
|
+
chmodSync(authFile, 0o600);
|
|
41
|
+
return authFile;
|
|
42
|
+
}
|
|
43
|
+
function createRemoteLoginSecrets(rig) {
|
|
44
|
+
const privateDir = mkdtempSync(join(tmpdir(), "tsq-login-"));
|
|
45
|
+
chmodSync(privateDir, 0o700);
|
|
46
|
+
rig.privateDir = privateDir;
|
|
47
|
+
try {
|
|
48
|
+
rig.authFile = createRemoteLoginXauthority(privateDir);
|
|
49
|
+
rig.vncPassword = generateVncPassword();
|
|
50
|
+
rig.passFile = join(privateDir, "vnc.pass");
|
|
51
|
+
writeFileSync(rig.passFile, rig.vncPassword, { flag: "wx", mode: 0o600 });
|
|
52
|
+
chmodSync(rig.passFile, 0o600);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
rmSync(privateDir, { recursive: true, force: true });
|
|
56
|
+
delete rig.privateDir;
|
|
57
|
+
delete rig.authFile;
|
|
58
|
+
delete rig.passFile;
|
|
59
|
+
delete rig.vncPassword;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function remoteLoginEnvironment(rig, baseEnv = process.env) {
|
|
64
|
+
if (rig.display === undefined || rig.authFile === undefined) {
|
|
65
|
+
throw new Error("remote login display authorization is not ready");
|
|
66
|
+
}
|
|
67
|
+
return { ...baseEnv, DISPLAY: rig.display, XAUTHORITY: rig.authFile };
|
|
68
|
+
}
|
|
69
|
+
export function findFreeLoginPort() {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const server = createServer();
|
|
72
|
+
server.on("error", reject);
|
|
73
|
+
server.listen(0, "127.0.0.1", () => {
|
|
74
|
+
const address = server.address();
|
|
75
|
+
if (address === null || typeof address === "string") {
|
|
76
|
+
server.close();
|
|
77
|
+
reject(new Error("could not resolve a free port"));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
server.close(() => resolve(address.port));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Helper stderr is piped but nothing else reads it. Drain it into a bounded
|
|
85
|
+
// tail so a helper death reports WHY (a websockify EADDRINUSE traceback, an
|
|
86
|
+
// x11vnc auth refusal) instead of a bare exit code — and so a chatty helper
|
|
87
|
+
// can never wedge on a full pipe buffer.
|
|
88
|
+
const HELPER_STDERR_TAIL_BYTES = 2_000;
|
|
89
|
+
const helperStderrTails = new WeakMap();
|
|
90
|
+
function captureHelperStderr(child) {
|
|
91
|
+
const stderr = child.stderr;
|
|
92
|
+
if (stderr === null || stderr === undefined)
|
|
93
|
+
return child;
|
|
94
|
+
const tail = { text: "" };
|
|
95
|
+
helperStderrTails.set(child, tail);
|
|
96
|
+
stderr.on("data", (chunk) => {
|
|
97
|
+
tail.text = (tail.text + chunk.toString()).slice(-HELPER_STDERR_TAIL_BYTES);
|
|
98
|
+
});
|
|
99
|
+
return child;
|
|
100
|
+
}
|
|
101
|
+
export function helperStderrTail(child) {
|
|
102
|
+
return helperStderrTails.get(child)?.text.trim() ?? "";
|
|
103
|
+
}
|
|
104
|
+
function withHelperStderr(message, child) {
|
|
105
|
+
const tail = helperStderrTail(child);
|
|
106
|
+
return tail === "" ? message : `${message}\n${tail}`;
|
|
107
|
+
}
|
|
108
|
+
// `exit` can beat the stderr pipe's final `data`, so give the tail a bounded
|
|
109
|
+
// moment to land before a failure message is composed from it.
|
|
110
|
+
function helperExitError(child, message) {
|
|
111
|
+
const stderr = child.stderr;
|
|
112
|
+
if (stderr === null || stderr === undefined || stderr.readableEnded || stderr.destroyed) {
|
|
113
|
+
return Promise.resolve(new Error(withHelperStderr(message, child)));
|
|
114
|
+
}
|
|
115
|
+
return new Promise((settle) => {
|
|
116
|
+
const done = () => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
stderr.removeListener("end", done);
|
|
119
|
+
stderr.removeListener("close", done);
|
|
120
|
+
settle(new Error(withHelperStderr(message, child)));
|
|
121
|
+
};
|
|
122
|
+
const timer = setTimeout(done, 250);
|
|
123
|
+
stderr.once("end", done);
|
|
124
|
+
stderr.once("close", done);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// True when 127.0.0.1:port can still be bound. websockify's readiness probe is
|
|
128
|
+
// a plain connect, so a squatter already listening on the configured port looks
|
|
129
|
+
// "ready" while websockify itself is dying on EADDRINUSE — preflight instead.
|
|
130
|
+
export function loginPortAvailable(port) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const server = createServer();
|
|
133
|
+
server.once("error", () => resolve(false));
|
|
134
|
+
server.listen(port, "127.0.0.1", () => {
|
|
135
|
+
server.close(() => resolve(true));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export function describeLoginPortHolder(port, runCommand = runListeningSocketQuery) {
|
|
140
|
+
const output = runCommand("ss", ["-ltnp"]);
|
|
141
|
+
if (output === null)
|
|
142
|
+
return null;
|
|
143
|
+
const localAddress = new RegExp(`(?:^|\\s)\\S*:${port}(?=\\s|$)`);
|
|
144
|
+
for (const line of output.split("\n")) {
|
|
145
|
+
if (!localAddress.test(line))
|
|
146
|
+
continue;
|
|
147
|
+
const users = line.match(/users:\(\((.*)\)\)\s*$/);
|
|
148
|
+
return users?.[1] ?? line.trim();
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function runListeningSocketQuery(command, args) {
|
|
153
|
+
try {
|
|
154
|
+
return execFileSync(command, args, {
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
timeout: 2_000,
|
|
157
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const STANDARD_BIN_DIRS = [
|
|
165
|
+
"/usr/local/bin",
|
|
166
|
+
"/usr/bin",
|
|
167
|
+
"/bin",
|
|
168
|
+
"/usr/local/sbin",
|
|
169
|
+
"/usr/sbin",
|
|
170
|
+
"/sbin",
|
|
171
|
+
];
|
|
172
|
+
export function resolveLoginBinary(binary, env = process.env) {
|
|
173
|
+
const dirs = [...(env.PATH ?? "").split(delimiter), ...STANDARD_BIN_DIRS];
|
|
174
|
+
for (const dir of dirs) {
|
|
175
|
+
if (dir.length === 0)
|
|
176
|
+
continue;
|
|
177
|
+
const candidate = resolve(dir, binary);
|
|
178
|
+
try {
|
|
179
|
+
if (!statSync(candidate).isFile())
|
|
180
|
+
continue;
|
|
181
|
+
accessSync(candidate, constants.X_OK);
|
|
182
|
+
return candidate;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
export function loginBinaryAvailable(binary) {
|
|
191
|
+
return resolveLoginBinary(binary) !== null;
|
|
192
|
+
}
|
|
193
|
+
function cloudflaredDebArch() {
|
|
194
|
+
switch (process.arch) {
|
|
195
|
+
case "arm64":
|
|
196
|
+
return "arm64";
|
|
197
|
+
case "arm":
|
|
198
|
+
return "arm";
|
|
199
|
+
case "ia32":
|
|
200
|
+
return "386";
|
|
201
|
+
default:
|
|
202
|
+
return "amd64";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function remoteLoginInstallHint(missing) {
|
|
206
|
+
const aptPackages = missing.flatMap((binary) => {
|
|
207
|
+
if (binary === "Xvfb")
|
|
208
|
+
return ["xvfb"];
|
|
209
|
+
if (binary === "x11vnc")
|
|
210
|
+
return ["x11vnc"];
|
|
211
|
+
if (binary === "websockify")
|
|
212
|
+
return ["novnc", "websockify"];
|
|
213
|
+
return [];
|
|
214
|
+
});
|
|
215
|
+
const lines = [];
|
|
216
|
+
if (aptPackages.length > 0) {
|
|
217
|
+
lines.push(`On Debian/Ubuntu: sudo apt-get install -y ${aptPackages.join(" ")}`);
|
|
218
|
+
}
|
|
219
|
+
if (missing.includes("cloudflared")) {
|
|
220
|
+
lines.push("cloudflared is not in Debian/Ubuntu repos — install its package: " +
|
|
221
|
+
`curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cloudflaredDebArch()}.deb ` +
|
|
222
|
+
"-o /tmp/cloudflared.deb && sudo dpkg -i /tmp/cloudflared.deb");
|
|
223
|
+
}
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|
|
226
|
+
function namedTunnelConfig() {
|
|
227
|
+
const hostname = process.env.TS_LOGIN_PUBLIC_HOSTNAME?.trim();
|
|
228
|
+
const rawPort = process.env.TS_LOGIN_LOCAL_PORT?.trim();
|
|
229
|
+
if (hostname === undefined || hostname === "" || rawPort === undefined || rawPort === "") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const port = Number(rawPort);
|
|
233
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
|
234
|
+
throw new Error(`TS_LOGIN_LOCAL_PORT=${JSON.stringify(rawPort)} is not a valid port number`);
|
|
235
|
+
}
|
|
236
|
+
return { hostname, port };
|
|
237
|
+
}
|
|
238
|
+
function requireRemoteLoginBinaries(namedTunnel) {
|
|
239
|
+
const required = [
|
|
240
|
+
"Xvfb",
|
|
241
|
+
"x11vnc",
|
|
242
|
+
"websockify",
|
|
243
|
+
...(namedTunnel ? [] : ["cloudflared"]),
|
|
244
|
+
];
|
|
245
|
+
const resolved = new Map(required.map((binary) => [binary, resolveLoginBinary(binary)]));
|
|
246
|
+
const missing = required.filter((binary) => resolved.get(binary) === null);
|
|
247
|
+
if (missing.length > 0) {
|
|
248
|
+
throw new Error(`headless login needs these not-installed binaries: ${missing.join(", ")}.\n` +
|
|
249
|
+
remoteLoginInstallHint(missing));
|
|
250
|
+
}
|
|
251
|
+
if (!existsSync(NOVNC_INSTALL_DIR)) {
|
|
252
|
+
throw new Error("noVNC web assets not found at /usr/share/novnc — install the `novnc` package");
|
|
253
|
+
}
|
|
254
|
+
// Optional under a named tunnel, but resolve it anyway: it is what the
|
|
255
|
+
// quick-tunnel fallback needs if the tunnel's fixed local port is occupied.
|
|
256
|
+
const cloudflared = resolved.get("cloudflared") ?? resolveLoginBinary("cloudflared");
|
|
257
|
+
return {
|
|
258
|
+
xvfb: resolved.get("Xvfb"),
|
|
259
|
+
x11vnc: resolved.get("x11vnc"),
|
|
260
|
+
websockify: resolved.get("websockify"),
|
|
261
|
+
...(cloudflared === null ? {} : { cloudflared }),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// A named tunnel pins websockify to one fixed local port. When something else
|
|
265
|
+
// already holds it, degrade to the per-login quick tunnel (the manual
|
|
266
|
+
// `env -u TS_LOGIN_LOCAL_PORT -u TS_LOGIN_PUBLIC_HOSTNAME` workaround,
|
|
267
|
+
// automated) rather than letting websockify die on EADDRINUSE.
|
|
268
|
+
export function planLoginTunnel(input) {
|
|
269
|
+
const { namedTunnel } = input;
|
|
270
|
+
if (namedTunnel !== null && input.namedPortAvailable) {
|
|
271
|
+
return { mode: "named", hostname: namedTunnel.hostname, port: namedTunnel.port };
|
|
272
|
+
}
|
|
273
|
+
const cloudflared = input.resolveCloudflared();
|
|
274
|
+
if (namedTunnel === null) {
|
|
275
|
+
if (cloudflared === null) {
|
|
276
|
+
throw new Error("cloudflared was not resolved for the per-login quick tunnel");
|
|
277
|
+
}
|
|
278
|
+
return { mode: "quick", cloudflared };
|
|
279
|
+
}
|
|
280
|
+
const holder = input.describePortHolder(namedTunnel.port);
|
|
281
|
+
const busy = `the configured login port 127.0.0.1:${namedTunnel.port} (TS_LOGIN_LOCAL_PORT) ` +
|
|
282
|
+
`is already in use${holder === null ? "" : ` by ${holder}`}`;
|
|
283
|
+
if (cloudflared === null) {
|
|
284
|
+
throw new Error(`Remote login cannot start: ${busy}.\n` +
|
|
285
|
+
"Free that port or point TS_LOGIN_LOCAL_PORT at a free one. A one-off " +
|
|
286
|
+
"Cloudflare quick tunnel would work around it, but cloudflared is not installed.\n" +
|
|
287
|
+
remoteLoginInstallHint(["cloudflared"]));
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
mode: "quick",
|
|
291
|
+
cloudflared,
|
|
292
|
+
fallbackNotice: `${busy} — using a one-off Cloudflare tunnel for this sign-in instead.`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function spawnBackground(command, args, env) {
|
|
296
|
+
return captureHelperStderr(spawnOwnerTrackedHelper(command, args, {
|
|
297
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
298
|
+
env: env ?? process.env,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
function waitForTunnelUrl(cloudflared, timeoutMs) {
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
let settled = false;
|
|
304
|
+
let output = "";
|
|
305
|
+
const finish = (error, url) => {
|
|
306
|
+
if (settled)
|
|
307
|
+
return;
|
|
308
|
+
settled = true;
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
cloudflared.removeListener("error", onError);
|
|
311
|
+
cloudflared.removeListener("exit", onExit);
|
|
312
|
+
cloudflared.stdout?.removeListener("data", scan);
|
|
313
|
+
cloudflared.stderr?.removeListener("data", scan);
|
|
314
|
+
if (error !== null)
|
|
315
|
+
void Promise.resolve(error).then(reject);
|
|
316
|
+
else
|
|
317
|
+
resolve(url);
|
|
318
|
+
};
|
|
319
|
+
const onError = (error) => finish(new Error(`cloudflared failed to spawn: ${error.message}`));
|
|
320
|
+
const onExit = (code, signal) => finish(helperExitError(cloudflared, `cloudflared exited before producing a URL (${code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`})`));
|
|
321
|
+
const scan = (chunk) => {
|
|
322
|
+
output += chunk.toString();
|
|
323
|
+
const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
324
|
+
if (match !== null) {
|
|
325
|
+
finish(null, match[0]);
|
|
326
|
+
}
|
|
327
|
+
if (output.length > 65_536)
|
|
328
|
+
output = output.slice(-4_096);
|
|
329
|
+
};
|
|
330
|
+
const timer = setTimeout(() => finish(new Error("cloudflared did not produce a URL in time")), timeoutMs);
|
|
331
|
+
cloudflared.once("error", onError);
|
|
332
|
+
cloudflared.once("exit", onExit);
|
|
333
|
+
if (childExited(cloudflared)) {
|
|
334
|
+
onExit(cloudflared.exitCode, cloudflared.signalCode);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
cloudflared.stdout?.on("data", scan);
|
|
338
|
+
cloudflared.stderr?.on("data", scan);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function waitForXvfbDisplay(xvfb, timeoutMs) {
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
const displayOutput = xvfb.stdio[3];
|
|
344
|
+
if (displayOutput === undefined ||
|
|
345
|
+
displayOutput === null ||
|
|
346
|
+
typeof displayOutput.on !== "function") {
|
|
347
|
+
reject(new Error("Xvfb display allocation pipe is unavailable"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
let settled = false;
|
|
351
|
+
let output = "";
|
|
352
|
+
const finish = (error, display) => {
|
|
353
|
+
if (settled)
|
|
354
|
+
return;
|
|
355
|
+
settled = true;
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
xvfb.removeListener("error", onError);
|
|
358
|
+
xvfb.removeListener("exit", onExit);
|
|
359
|
+
displayOutput.removeListener("data", onData);
|
|
360
|
+
if (error !== null)
|
|
361
|
+
void Promise.resolve(error).then(reject);
|
|
362
|
+
else
|
|
363
|
+
resolve(display);
|
|
364
|
+
};
|
|
365
|
+
const onError = (error) => finish(new Error(`Xvfb failed to spawn: ${error.message}`));
|
|
366
|
+
const onExit = (code, signal) => finish(helperExitError(xvfb, `Xvfb exited before becoming ready (${code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`})`));
|
|
367
|
+
const onData = (chunk) => {
|
|
368
|
+
output += chunk.toString();
|
|
369
|
+
const match = output.match(/^(\d+)\s/);
|
|
370
|
+
if (match === null)
|
|
371
|
+
return;
|
|
372
|
+
const displayNumber = Number(match[1]);
|
|
373
|
+
if (!Number.isInteger(displayNumber) || displayNumber < 0) {
|
|
374
|
+
finish(new Error(`Xvfb returned an invalid display number: ${JSON.stringify(match[1])}`));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (childExited(xvfb)) {
|
|
378
|
+
onExit(xvfb.exitCode, xvfb.signalCode);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
finish(null, `:${displayNumber}`);
|
|
382
|
+
};
|
|
383
|
+
const timer = setTimeout(() => finish(new Error("Xvfb did not allocate a display in time")), timeoutMs);
|
|
384
|
+
xvfb.once("error", onError);
|
|
385
|
+
xvfb.once("exit", onExit);
|
|
386
|
+
if (childExited(xvfb)) {
|
|
387
|
+
onExit(xvfb.exitCode, xvfb.signalCode);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
displayOutput.on("data", onData);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function waitForListeningPort(child, port, label, timeoutMs) {
|
|
394
|
+
return new Promise((resolve, reject) => {
|
|
395
|
+
const deadline = Date.now() + timeoutMs;
|
|
396
|
+
let settled = false;
|
|
397
|
+
let retryTimer;
|
|
398
|
+
const finish = (error) => {
|
|
399
|
+
if (settled)
|
|
400
|
+
return;
|
|
401
|
+
settled = true;
|
|
402
|
+
if (retryTimer !== undefined)
|
|
403
|
+
clearTimeout(retryTimer);
|
|
404
|
+
child.removeListener("error", onError);
|
|
405
|
+
child.removeListener("exit", onExit);
|
|
406
|
+
if (error !== undefined)
|
|
407
|
+
void Promise.resolve(error).then(reject);
|
|
408
|
+
else
|
|
409
|
+
resolve();
|
|
410
|
+
};
|
|
411
|
+
const onError = (error) => finish(new Error(`${label} failed to spawn: ${error.message}`));
|
|
412
|
+
const onExit = (code, signal) => finish(helperExitError(child, `${label} exited before becoming ready (${code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`})`));
|
|
413
|
+
const probe = () => {
|
|
414
|
+
if (settled)
|
|
415
|
+
return;
|
|
416
|
+
if (childExited(child)) {
|
|
417
|
+
onExit(child.exitCode, child.signalCode);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (Date.now() >= deadline) {
|
|
421
|
+
finish(new Error(`${label} did not listen on 127.0.0.1:${port} in time`));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const socket = createConnection({ host: "127.0.0.1", port });
|
|
425
|
+
socket.once("connect", () => {
|
|
426
|
+
socket.destroy();
|
|
427
|
+
retryTimer = setTimeout(() => {
|
|
428
|
+
if (childExited(child))
|
|
429
|
+
onExit(child.exitCode, child.signalCode);
|
|
430
|
+
else
|
|
431
|
+
finish();
|
|
432
|
+
}, 100);
|
|
433
|
+
});
|
|
434
|
+
socket.once("error", () => {
|
|
435
|
+
socket.destroy();
|
|
436
|
+
retryTimer = setTimeout(probe, 50);
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
child.once("error", onError);
|
|
440
|
+
child.once("exit", onExit);
|
|
441
|
+
probe();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
export function fallbackCloudflaredArgs(webPort) {
|
|
445
|
+
return ["tunnel", "--protocol", "http2", "--url", `http://127.0.0.1:${webPort}`];
|
|
446
|
+
}
|
|
447
|
+
function buildVncWebDir() {
|
|
448
|
+
const webDir = mkdtempSync(join(tmpdir(), "ts-novnc-"));
|
|
449
|
+
cpSync(NOVNC_INSTALL_DIR, webDir, { recursive: true });
|
|
450
|
+
const requiredCoreAssets = [
|
|
451
|
+
join(webDir, "core", "rfb.js"),
|
|
452
|
+
join(webDir, "core", "input", "keysymdef.js"),
|
|
453
|
+
];
|
|
454
|
+
if (requiredCoreAssets.some((path) => !existsSync(path))) {
|
|
455
|
+
rmSync(webDir, { recursive: true, force: true });
|
|
456
|
+
throw new Error(`noVNC core assets not found under ${NOVNC_INSTALL_DIR}/core — the installed novnc package has an unexpected layout`);
|
|
457
|
+
}
|
|
458
|
+
const brandedPage = readFileSync(loginAssetPath("vnc.html"), "utf8");
|
|
459
|
+
writeFileSync(join(webDir, "vnc.html"), brandedPage);
|
|
460
|
+
writeFileSync(join(webDir, "index.html"), brandedPage);
|
|
461
|
+
cpSync(loginAssetPath("vnc-input.js"), join(webDir, "vnc-input.js"));
|
|
462
|
+
return webDir;
|
|
463
|
+
}
|
|
464
|
+
export function createXvfbDisplayRig() {
|
|
465
|
+
const xvfb = resolveLoginBinary("Xvfb");
|
|
466
|
+
if (xvfb === null) {
|
|
467
|
+
throw new Error("headed Chrome needs Xvfb installed.\n" + remoteLoginInstallHint(["Xvfb"]));
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
width: LOGIN_WIDTH,
|
|
471
|
+
height: LOGIN_HEIGHT,
|
|
472
|
+
procs: [],
|
|
473
|
+
binaries: {
|
|
474
|
+
xvfb,
|
|
475
|
+
x11vnc: "",
|
|
476
|
+
websockify: "",
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
export function createRemoteLoginRig() {
|
|
481
|
+
const namedTunnel = namedTunnelConfig();
|
|
482
|
+
const binaries = requireRemoteLoginBinaries(namedTunnel !== null);
|
|
483
|
+
return {
|
|
484
|
+
...createXvfbDisplayRig(),
|
|
485
|
+
binaries,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
export async function startRemoteLoginDisplay(rig) {
|
|
489
|
+
try {
|
|
490
|
+
createRemoteLoginSecrets(rig);
|
|
491
|
+
const authFile = rig.authFile;
|
|
492
|
+
const xvfb = captureHelperStderr(spawnOwnerTrackedHelper(rig.binaries.xvfb, ["-screen", "0", `${rig.width}x${rig.height}x24`, "-auth", authFile, "-displayfd", "3"], {
|
|
493
|
+
stdio: ["ignore", "pipe", "pipe", "pipe"],
|
|
494
|
+
env: process.env,
|
|
495
|
+
}));
|
|
496
|
+
rig.procs.push(xvfb);
|
|
497
|
+
rig.display = await waitForXvfbDisplay(xvfb, 5_000);
|
|
498
|
+
return rig.display;
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
await teardownRemoteLoginRig(rig);
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
export function generateVncPassword() {
|
|
506
|
+
return randomBytes(6).toString("base64url");
|
|
507
|
+
}
|
|
508
|
+
export async function exposeRemoteLoginDisplay(rig, label) {
|
|
509
|
+
if (rig.display === undefined) {
|
|
510
|
+
throw new Error("remote login display has not been started");
|
|
511
|
+
}
|
|
512
|
+
const password = rig.vncPassword;
|
|
513
|
+
const passFile = rig.passFile;
|
|
514
|
+
if (password === undefined || passFile === undefined) {
|
|
515
|
+
throw new Error("remote login VNC credentials are not ready");
|
|
516
|
+
}
|
|
517
|
+
const namedTunnel = namedTunnelConfig();
|
|
518
|
+
const plan = planLoginTunnel({
|
|
519
|
+
namedTunnel,
|
|
520
|
+
namedPortAvailable: namedTunnel === null || (await loginPortAvailable(namedTunnel.port)),
|
|
521
|
+
resolveCloudflared: () => rig.binaries.cloudflared ?? resolveLoginBinary("cloudflared"),
|
|
522
|
+
describePortHolder: (port) => describeLoginPortHolder(port),
|
|
523
|
+
});
|
|
524
|
+
if (plan.mode === "quick" && plan.fallbackNotice !== undefined) {
|
|
525
|
+
console.error(`\n[login] ${plan.fallbackNotice}\n`);
|
|
526
|
+
}
|
|
527
|
+
// Helpers this call started, so a failure part-way through reaps only them
|
|
528
|
+
// and leaves the caller's ordered browser-then-display teardown intact.
|
|
529
|
+
const started = [];
|
|
530
|
+
try {
|
|
531
|
+
const vncPort = await findFreeLoginPort();
|
|
532
|
+
const webPort = plan.mode === "named" ? plan.port : await findFreeLoginPort();
|
|
533
|
+
const x11vnc = spawnBackground(rig.binaries.x11vnc, [
|
|
534
|
+
"-display",
|
|
535
|
+
rig.display,
|
|
536
|
+
"-rfbport",
|
|
537
|
+
String(vncPort),
|
|
538
|
+
"-passwdfile",
|
|
539
|
+
`rm:${passFile}`,
|
|
540
|
+
"-localhost",
|
|
541
|
+
"-forever",
|
|
542
|
+
"-shared",
|
|
543
|
+
"-noshm",
|
|
544
|
+
"-quiet",
|
|
545
|
+
], remoteLoginEnvironment(rig));
|
|
546
|
+
rig.procs.push(x11vnc);
|
|
547
|
+
started.push(x11vnc);
|
|
548
|
+
await waitForListeningPort(x11vnc, vncPort, "x11vnc", 5_000);
|
|
549
|
+
rig.webDir = buildVncWebDir();
|
|
550
|
+
const websockify = spawnBackground(rig.binaries.websockify, [
|
|
551
|
+
`--web=${rig.webDir}`,
|
|
552
|
+
`127.0.0.1:${webPort}`,
|
|
553
|
+
`localhost:${vncPort}`,
|
|
554
|
+
]);
|
|
555
|
+
rig.procs.push(websockify);
|
|
556
|
+
started.push(websockify);
|
|
557
|
+
await waitForListeningPort(websockify, webPort, "websockify", 5_000);
|
|
558
|
+
let publicUrl;
|
|
559
|
+
if (plan.mode === "named") {
|
|
560
|
+
publicUrl = `https://${plan.hostname}/#p=${password}`;
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
const cloudflared = spawnBackground(plan.cloudflared, fallbackCloudflaredArgs(webPort));
|
|
564
|
+
rig.procs.push(cloudflared);
|
|
565
|
+
started.push(cloudflared);
|
|
566
|
+
const tunnelUrl = await waitForTunnelUrl(cloudflared, 30_000);
|
|
567
|
+
publicUrl = `${tunnelUrl}/#p=${password}`;
|
|
568
|
+
}
|
|
569
|
+
printRemoteLoginBanner({ publicUrl, password, label });
|
|
570
|
+
return publicUrl;
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
await teardownExposedLoginHelpers(rig, started);
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function printRemoteLoginBanner(opts) {
|
|
578
|
+
const width = Math.max(40, Math.min((process.stdout.columns ?? 80) - 2, 78));
|
|
579
|
+
const styledUrl = process.stderr.isTTY
|
|
580
|
+
? `\x1b]8;;${opts.publicUrl}\x1b\\${chalk.hex("#cf3a52").underline(opts.publicUrl)}\x1b]8;;\x1b\\`
|
|
581
|
+
: opts.publicUrl;
|
|
582
|
+
const body = `Open this on any device, any network:\n\n` +
|
|
583
|
+
` ${styledUrl}\n\n` +
|
|
584
|
+
`If asked for a VNC password: ${chalk.bold(opts.password)}\n\n` +
|
|
585
|
+
opts.label;
|
|
586
|
+
console.error("\n" +
|
|
587
|
+
boxen(body, {
|
|
588
|
+
title: "Sign in to Trusty Squire",
|
|
589
|
+
titleAlignment: "left",
|
|
590
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
591
|
+
borderStyle: "single",
|
|
592
|
+
borderColor: "#cf3a52",
|
|
593
|
+
width,
|
|
594
|
+
}) +
|
|
595
|
+
"\n");
|
|
596
|
+
}
|
|
597
|
+
const rigTeardowns = new WeakMap();
|
|
598
|
+
function childExited(child) {
|
|
599
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
600
|
+
}
|
|
601
|
+
export function assertRemoteLoginRigLive(rig) {
|
|
602
|
+
for (const child of rig.procs) {
|
|
603
|
+
if (!childExited(child))
|
|
604
|
+
continue;
|
|
605
|
+
const command = child.spawnfile || child.spawnargs[0] || "unknown-helper";
|
|
606
|
+
const status = child.exitCode === null
|
|
607
|
+
? `signal ${child.signalCode ?? "unknown"}`
|
|
608
|
+
: `code ${child.exitCode}`;
|
|
609
|
+
throw new Error(withHelperStderr(`remote login helper ${basename(command)} exited (${status})`, child));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function removeRigFiles(rig) {
|
|
613
|
+
if (rig.webDir !== undefined)
|
|
614
|
+
rmSync(rig.webDir, { recursive: true, force: true });
|
|
615
|
+
if (rig.privateDir !== undefined)
|
|
616
|
+
rmSync(rig.privateDir, { recursive: true, force: true });
|
|
617
|
+
delete rig.webDir;
|
|
618
|
+
delete rig.privateDir;
|
|
619
|
+
delete rig.passFile;
|
|
620
|
+
delete rig.authFile;
|
|
621
|
+
delete rig.vncPassword;
|
|
622
|
+
}
|
|
623
|
+
function releaseChildHandles(child) {
|
|
624
|
+
child.stdout?.destroy();
|
|
625
|
+
child.stderr?.destroy();
|
|
626
|
+
child.unref();
|
|
627
|
+
}
|
|
628
|
+
function forceTeardownRemoteLoginRig(rig) {
|
|
629
|
+
for (const child of rig.procs) {
|
|
630
|
+
try {
|
|
631
|
+
signalOwnerTrackedHelper(child, "SIGKILL");
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
// best-effort process-exit cleanup
|
|
635
|
+
}
|
|
636
|
+
releaseChildHandles(child);
|
|
637
|
+
}
|
|
638
|
+
removeRigFiles(rig);
|
|
639
|
+
}
|
|
640
|
+
async function terminateLoginHelpers(procs, graceMs) {
|
|
641
|
+
for (const child of procs) {
|
|
642
|
+
try {
|
|
643
|
+
signalOwnerTrackedHelper(child, "SIGTERM");
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// best-effort
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
await Promise.all(procs.map((child) => waitForOwnerTrackedHelperExit(child, graceMs)));
|
|
650
|
+
const resistant = procs.filter((child) => ownerTrackedHelperState(child) !== "stale");
|
|
651
|
+
for (const child of resistant) {
|
|
652
|
+
try {
|
|
653
|
+
signalOwnerTrackedHelper(child, "SIGKILL");
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// best-effort
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
await Promise.all(resistant.map((child) => waitForOwnerTrackedHelperExit(child, graceMs)));
|
|
660
|
+
for (const child of procs) {
|
|
661
|
+
releaseOwnerTrackedHelper(child);
|
|
662
|
+
releaseChildHandles(child);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Failure part-way through exposing the display: reap the VNC/web/tunnel
|
|
666
|
+
// helpers and the noVNC copy so a retry starts clean, but leave Xvfb (and the
|
|
667
|
+
// browser drawing on it) to the caller's ordered teardown.
|
|
668
|
+
async function teardownExposedLoginHelpers(rig, started) {
|
|
669
|
+
await terminateLoginHelpers(started, 1_000);
|
|
670
|
+
rig.procs = rig.procs.filter((child) => !started.includes(child));
|
|
671
|
+
if (rig.webDir !== undefined) {
|
|
672
|
+
rmSync(rig.webDir, { recursive: true, force: true });
|
|
673
|
+
delete rig.webDir;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
export function teardownRemoteLoginRig(rig, graceMs = 1_000) {
|
|
677
|
+
const existing = rigTeardowns.get(rig);
|
|
678
|
+
if (existing !== undefined)
|
|
679
|
+
return existing;
|
|
680
|
+
const teardown = (async () => {
|
|
681
|
+
await terminateLoginHelpers(rig.procs, graceMs);
|
|
682
|
+
removeRigFiles(rig);
|
|
683
|
+
})();
|
|
684
|
+
rigTeardowns.set(rig, teardown);
|
|
685
|
+
return teardown;
|
|
686
|
+
}
|
|
687
|
+
const signalExitCoordination = {
|
|
688
|
+
enabled: isSelfManagedChromeTerminationSignalExitEnabled,
|
|
689
|
+
set: setSelfManagedChromeTerminationSignalExitEnabled,
|
|
690
|
+
};
|
|
691
|
+
export function registerRemoteLoginRigCleanup(rig, activeBrowserTeardown, runtime = process, signalExit = signalExitCoordination) {
|
|
692
|
+
let finishing = false;
|
|
693
|
+
let signalExitSuspended = false;
|
|
694
|
+
const restoreSignalExit = () => {
|
|
695
|
+
if (!signalExitSuspended)
|
|
696
|
+
return;
|
|
697
|
+
signalExitSuspended = false;
|
|
698
|
+
signalExit.set(true);
|
|
699
|
+
};
|
|
700
|
+
const exitAfterCleanup = (code) => {
|
|
701
|
+
if (finishing) {
|
|
702
|
+
forceTeardownRemoteLoginRig(rig);
|
|
703
|
+
restoreSignalExit();
|
|
704
|
+
runtime.exit(code);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
finishing = true;
|
|
708
|
+
const browserTeardown = activeBrowserTeardown();
|
|
709
|
+
const closeBrowser = new Promise((resolve) => {
|
|
710
|
+
if (browserTeardown === undefined) {
|
|
711
|
+
resolve();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const timer = setTimeout(resolve, 3_000);
|
|
715
|
+
void browserTeardown()
|
|
716
|
+
.catch(() => undefined)
|
|
717
|
+
.finally(() => {
|
|
718
|
+
clearTimeout(timer);
|
|
719
|
+
resolve();
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
void closeBrowser.finally(async () => {
|
|
723
|
+
await teardownRemoteLoginRig(rig);
|
|
724
|
+
restoreSignalExit();
|
|
725
|
+
runtime.exit(code);
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
const onExit = () => forceTeardownRemoteLoginRig(rig);
|
|
729
|
+
const onSighup = () => exitAfterCleanup(129);
|
|
730
|
+
const onSigterm = () => exitAfterCleanup(143);
|
|
731
|
+
const onSigint = () => exitAfterCleanup(130);
|
|
732
|
+
const onUncaughtException = (error) => {
|
|
733
|
+
console.error("[login] uncaught exception; tearing down the login browser", error);
|
|
734
|
+
exitAfterCleanup(1);
|
|
735
|
+
};
|
|
736
|
+
const onUnhandledRejection = (reason) => {
|
|
737
|
+
console.error("[login] unhandled rejection; tearing down the login browser", reason);
|
|
738
|
+
exitAfterCleanup(1);
|
|
739
|
+
};
|
|
740
|
+
runtime.once("exit", onExit);
|
|
741
|
+
const ownsTerminationExit = signalExit.enabled();
|
|
742
|
+
if (ownsTerminationExit) {
|
|
743
|
+
signalExit.set(false);
|
|
744
|
+
signalExitSuspended = true;
|
|
745
|
+
runtime.on("SIGHUP", onSighup);
|
|
746
|
+
runtime.on("SIGTERM", onSigterm);
|
|
747
|
+
runtime.on("SIGINT", onSigint);
|
|
748
|
+
runtime.once("uncaughtException", onUncaughtException);
|
|
749
|
+
runtime.once("unhandledRejection", onUnhandledRejection);
|
|
750
|
+
}
|
|
751
|
+
return () => {
|
|
752
|
+
if (finishing)
|
|
753
|
+
return;
|
|
754
|
+
runtime.removeListener("exit", onExit);
|
|
755
|
+
if (!ownsTerminationExit)
|
|
756
|
+
return;
|
|
757
|
+
runtime.removeListener("SIGHUP", onSighup);
|
|
758
|
+
runtime.removeListener("SIGTERM", onSigterm);
|
|
759
|
+
runtime.removeListener("SIGINT", onSigint);
|
|
760
|
+
runtime.removeListener("uncaughtException", onUncaughtException);
|
|
761
|
+
runtime.removeListener("unhandledRejection", onUnhandledRejection);
|
|
762
|
+
restoreSignalExit();
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
//# sourceMappingURL=remote-login-display.js.map
|