talon-agent 5.2.0 → 5.2.2
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/package.json +2 -2
- package/src/app.ts +57 -33
- package/src/core/daemon/crash.ts +82 -0
- package/src/core/daemon/handoff.ts +192 -0
- package/src/core/daemon/respawn.ts +127 -52
- package/src/core/update/self-update.ts +44 -0
- package/src/index.ts +13 -5
- package/src/plugins/playwright/index.ts +39 -3
- package/src/plugins/playwright/provision.ts +21 -0
- package/src/plugins/playwright/version-coupling.ts +195 -0
- package/src/util/log.ts +318 -26
- package/src/util/paths.ts +5 -0
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
* source tree on disk, so {@link getRepoRoot} returns `null` and the
|
|
9
9
|
* `/update` command is never registered.
|
|
10
10
|
*
|
|
11
|
+
* The install is verified before anyone hands off to it. `npm install`
|
|
12
|
+
* rewrites node_modules underneath the still-running process, so a bad
|
|
13
|
+
* dependency resolution does not surface until the *successor* imports
|
|
14
|
+
* the tree — detached, with its output going nowhere, at the one moment
|
|
15
|
+
* the daemon has no one left to report to. On 2026-09-18 that cost a
|
|
16
|
+
* 45-minute outage. The last step of an update therefore runs the new
|
|
17
|
+
* tree in a child with {@link BOOT_SMOKE_FLAG}: it resolves the daemon's
|
|
18
|
+
* entire import graph and exits without booting. If it fails, the update
|
|
19
|
+
* fails — the caller reports it to the chat and the current process, the
|
|
20
|
+
* one that still works, keeps running.
|
|
21
|
+
*
|
|
11
22
|
* The update force-syncs the checkout to the remote branch with
|
|
12
23
|
* `git reset --hard` (plus `git clean -fd`), discarding any local edits
|
|
13
24
|
* or diverged commits. A bot host is meant to mirror the remote exactly,
|
|
@@ -22,6 +33,11 @@ import { execFile } from "node:child_process";
|
|
|
22
33
|
import { existsSync } from "node:fs";
|
|
23
34
|
import { dirname, join } from "node:path";
|
|
24
35
|
import { fileURLToPath } from "node:url";
|
|
36
|
+
import {
|
|
37
|
+
BOOT_SMOKE_FLAG,
|
|
38
|
+
BOOT_SMOKE_OK,
|
|
39
|
+
successorCommand,
|
|
40
|
+
} from "../daemon/respawn.js";
|
|
25
41
|
|
|
26
42
|
/** Tuning knobs for {@link runSelfUpdate}. */
|
|
27
43
|
export interface UpdateOptions {
|
|
@@ -36,6 +52,11 @@ export interface UpdateOptions {
|
|
|
36
52
|
setup?: readonly string[];
|
|
37
53
|
/** Override the repo root (tests). Defaults to {@link getRepoRoot}. */
|
|
38
54
|
repoRoot?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The command that re-runs this process, used for the post-install
|
|
57
|
+
* import check. Defaults to our own argv (tests override it).
|
|
58
|
+
*/
|
|
59
|
+
entry?: { cmd: string; args: readonly string[] };
|
|
39
60
|
/** Injectable command runner (tests). */
|
|
40
61
|
runner?: CommandRunner;
|
|
41
62
|
}
|
|
@@ -70,6 +91,8 @@ export type CommandRunner = (
|
|
|
70
91
|
) => Promise<{ ok: boolean; output: string }>;
|
|
71
92
|
|
|
72
93
|
const GIT_TIMEOUT_MS = 60_000;
|
|
94
|
+
/** A cold import of the whole daemon graph, on a busy host. */
|
|
95
|
+
const VERIFY_TIMEOUT_MS = 180_000;
|
|
73
96
|
const INSTALL_TIMEOUT_MS = 300_000;
|
|
74
97
|
const SETUP_TIMEOUT_MS = 300_000;
|
|
75
98
|
const MAX_OUTPUT_BUFFER = 16 * 1024 * 1024;
|
|
@@ -265,5 +288,26 @@ export async function runSelfUpdate(
|
|
|
265
288
|
}
|
|
266
289
|
}
|
|
267
290
|
|
|
291
|
+
const entry = opts.entry ?? successorCommand();
|
|
292
|
+
const verify = await record(
|
|
293
|
+
"verify import",
|
|
294
|
+
entry.cmd,
|
|
295
|
+
[...entry.args, BOOT_SMOKE_FLAG],
|
|
296
|
+
VERIFY_TIMEOUT_MS,
|
|
297
|
+
);
|
|
298
|
+
if (!verify.ok || !verify.output.includes(BOOT_SMOKE_OK)) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
repoRoot,
|
|
302
|
+
steps,
|
|
303
|
+
before,
|
|
304
|
+
after,
|
|
305
|
+
changed,
|
|
306
|
+
error:
|
|
307
|
+
`the updated tree does not import — not restarting into it. ` +
|
|
308
|
+
`Still running ${before}; the checkout is at ${after}.`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
268
312
|
return { ok: true, repoRoot, steps, before, after, changed: true };
|
|
269
313
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Talon entry shim.
|
|
3
3
|
*
|
|
4
|
-
* Dispatches the hidden `_mcp-launch` (MCP supervisor)
|
|
5
|
-
* (WASM Lua trigger runner)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Dispatches the hidden `_mcp-launch` (MCP supervisor), `_lua-run`
|
|
5
|
+
* (WASM Lua trigger runner) and `_handoff-watch` (restart/update
|
|
6
|
+
* witness) subcommands BEFORE the app graph loads — all three are Talon
|
|
7
|
+
* re-invoking itself (see core/mcp-hub/launcher.ts, core/daemon/
|
|
8
|
+
* handoff.ts). The dynamic import keeps these helper processes light:
|
|
9
|
+
* they evaluate this shim and their own module, never the
|
|
10
|
+
* backends/frontends/plugins.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import {
|
|
@@ -13,11 +15,17 @@ import {
|
|
|
13
15
|
runSupervisor,
|
|
14
16
|
} from "./core/mcp-hub/launcher.js";
|
|
15
17
|
import { LUA_RUN_SUBCOMMAND, runLuaMain } from "./core/scripts/lua.js";
|
|
18
|
+
import {
|
|
19
|
+
HANDOFF_WATCH_SUBCOMMAND,
|
|
20
|
+
runHandoffWatch,
|
|
21
|
+
} from "./core/daemon/handoff.js";
|
|
16
22
|
|
|
17
23
|
if (process.argv[2] === MCP_LAUNCH_SUBCOMMAND) {
|
|
18
24
|
await runSupervisor(process.argv.slice(3));
|
|
19
25
|
} else if (process.argv[2] === LUA_RUN_SUBCOMMAND) {
|
|
20
26
|
await runLuaMain(process.argv.slice(3));
|
|
27
|
+
} else if (process.argv[2] === HANDOFF_WATCH_SUBCOMMAND) {
|
|
28
|
+
await runHandoffWatch(process.argv.slice(3));
|
|
21
29
|
} else {
|
|
22
30
|
await import("./app.js");
|
|
23
31
|
}
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
* with "428 Precondition Required". @playwright/mcp is therefore pinned
|
|
26
26
|
* exactly in package.json (0.0.56 → playwright 1.58.x, matching python
|
|
27
27
|
* playwright 1.58 which hosts Camoufox — camoufox itself caps playwright at
|
|
28
|
-
* <1.61, so the node client cannot chase latest).
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* <1.61, so the node client cannot chase latest). The pin is enforced, not
|
|
29
|
+
* just documented: see version-coupling.ts (unit test that fails CI on a
|
|
30
|
+
* bump, validateConfig refusal, and a live handshake probe at init).
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -35,6 +35,12 @@ import { dirname, resolve } from "node:path";
|
|
|
35
35
|
import type { TalonPlugin } from "../../core/plugin/types.js";
|
|
36
36
|
import { files } from "../../util/paths.js";
|
|
37
37
|
import { log } from "../../util/log.js";
|
|
38
|
+
import {
|
|
39
|
+
ENDPOINT_PLAYWRIGHT_MINOR,
|
|
40
|
+
bundledPlaywrightVersion,
|
|
41
|
+
couplingError,
|
|
42
|
+
probeEndpoint,
|
|
43
|
+
} from "./version-coupling.js";
|
|
38
44
|
|
|
39
45
|
export function createPlaywrightPlugin(config: {
|
|
40
46
|
browser?: string;
|
|
@@ -141,6 +147,13 @@ export function createPlaywrightPlugin(config: {
|
|
|
141
147
|
);
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
// Endpoint mode: refuse to start on a client/server minor mismatch
|
|
151
|
+
// rather than fail every tool call later (see version-coupling.ts).
|
|
152
|
+
if (endpoint) {
|
|
153
|
+
const coupling = couplingError(bundledPlaywrightVersion());
|
|
154
|
+
if (coupling) errors.push(coupling);
|
|
155
|
+
}
|
|
156
|
+
|
|
144
157
|
return errors.length > 0 ? errors : undefined;
|
|
145
158
|
},
|
|
146
159
|
|
|
@@ -149,6 +162,29 @@ export function createPlaywrightPlugin(config: {
|
|
|
149
162
|
"playwright",
|
|
150
163
|
`Ready (${endpoint ? `Camoufox @ ${endpoint}` : `${browser}, headless=${headless}`})`,
|
|
151
164
|
);
|
|
165
|
+
if (!endpoint) return;
|
|
166
|
+
// Live handshake with the bundled client version: catches a drifted
|
|
167
|
+
// server (the static check only knows the expected minor).
|
|
168
|
+
const client =
|
|
169
|
+
bundledPlaywrightVersion() ?? `${ENDPOINT_PLAYWRIGHT_MINOR}.0`;
|
|
170
|
+
const probe = await probeEndpoint(endpoint, client);
|
|
171
|
+
switch (probe.state) {
|
|
172
|
+
case "match":
|
|
173
|
+
log("playwright", `Endpoint handshake OK (Playwright ${client})`);
|
|
174
|
+
break;
|
|
175
|
+
case "mismatch":
|
|
176
|
+
log(
|
|
177
|
+
"playwright",
|
|
178
|
+
`ERROR: endpoint ${endpoint} is on Playwright ${probe.server} but the bundled client is ${probe.client} — every browser tool call will fail with 428. Align the python playwright hosting Camoufox with ENDPOINT_PLAYWRIGHT_MINOR (${ENDPOINT_PLAYWRIGHT_MINOR}) or re-pin @playwright/mcp.`,
|
|
179
|
+
);
|
|
180
|
+
break;
|
|
181
|
+
case "unreachable":
|
|
182
|
+
log(
|
|
183
|
+
"playwright",
|
|
184
|
+
`Warning: endpoint ${endpoint} did not answer the handshake probe (${probe.reason}); browser tools will fail until it is up.`,
|
|
185
|
+
);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
152
188
|
},
|
|
153
189
|
};
|
|
154
190
|
}
|
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
type ProvisionOutcome,
|
|
29
29
|
} from "../../core/plugin/provision.js";
|
|
30
30
|
import { dirs } from "../../util/paths.js";
|
|
31
|
+
import {
|
|
32
|
+
ENDPOINT_PLAYWRIGHT_MINOR,
|
|
33
|
+
bundledPlaywrightVersion,
|
|
34
|
+
couplingError,
|
|
35
|
+
} from "./version-coupling.js";
|
|
31
36
|
|
|
32
37
|
/** Engines whose builds Playwright manages (vs system channels). */
|
|
33
38
|
const MANAGED_ENGINES = new Set(["chromium", "firefox", "webkit"]);
|
|
@@ -54,6 +59,8 @@ export interface PlaywrightProvisionDeps {
|
|
|
54
59
|
listDir?: (p: string) => string[];
|
|
55
60
|
/** playwright-core's browser registry (default: browsers.json beside the CLI). */
|
|
56
61
|
registry?: BrowserDescriptor[];
|
|
62
|
+
/** Bundled playwright-core version (default: read from node_modules). */
|
|
63
|
+
bundledPlaywrightVersion?: string;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
/** One entry of playwright-core's browsers.json. */
|
|
@@ -287,12 +294,26 @@ export function inspectPlaywright(
|
|
|
287
294
|
deps: PlaywrightProvisionDeps = {},
|
|
288
295
|
): DoctorCheck[] {
|
|
289
296
|
if (section.endpoint ?? section.endpointFile) {
|
|
297
|
+
const bundled = deps.bundledPlaywrightVersion ?? bundledPlaywrightVersion();
|
|
298
|
+
const coupling = couplingError(bundled);
|
|
290
299
|
return [
|
|
291
300
|
{
|
|
292
301
|
label: "Playwright: remote endpoint mode",
|
|
293
302
|
status: "info",
|
|
294
303
|
detail: "browser lives on the remote end",
|
|
295
304
|
},
|
|
305
|
+
coupling
|
|
306
|
+
? {
|
|
307
|
+
label: "Playwright: client/endpoint version mismatch",
|
|
308
|
+
status: "fail",
|
|
309
|
+
detail: coupling,
|
|
310
|
+
issue: true,
|
|
311
|
+
}
|
|
312
|
+
: {
|
|
313
|
+
label: `Playwright: client on endpoint minor ${ENDPOINT_PLAYWRIGHT_MINOR}`,
|
|
314
|
+
status: "ok",
|
|
315
|
+
detail: bundled ? `playwright-core ${bundled}` : undefined,
|
|
316
|
+
},
|
|
296
317
|
];
|
|
297
318
|
}
|
|
298
319
|
const browser = section.browser ?? "chromium";
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint-mode version coupling — the machinery behind the pin.
|
|
3
|
+
*
|
|
4
|
+
* In endpoint mode the MCP child connects to a remote Playwright server
|
|
5
|
+
* (the python-playwright process hosting Camoufox). Playwright's server
|
|
6
|
+
* refuses the WebSocket upgrade with "428 Precondition Required" when the
|
|
7
|
+
* client's playwright MINOR (sent in the User-Agent, `Playwright/x.y.z`)
|
|
8
|
+
* differs from its own — so a client bump that nobody noticed turns every
|
|
9
|
+
* browser tool call into an opaque error at the worst possible moment.
|
|
10
|
+
*
|
|
11
|
+
* Three guards, all keyed on ENDPOINT_PLAYWRIGHT_MINOR:
|
|
12
|
+
* - a unit test asserts the playwright-core that @playwright/mcp bundles is
|
|
13
|
+
* on that minor, so a dependency bump goes red in CI instead of green;
|
|
14
|
+
* - `validateConfig` refuses to start the plugin on a mismatch, with a
|
|
15
|
+
* message that names both versions and the fix;
|
|
16
|
+
* - `probeEndpoint` performs the real handshake at init and reports the
|
|
17
|
+
* server's verdict, so a drifted *server* is caught too.
|
|
18
|
+
*
|
|
19
|
+
* Bumping: change ENDPOINT_PLAYWRIGHT_MINOR and `@playwright/mcp` in
|
|
20
|
+
* package.json in the same commit, after upgrading the python side
|
|
21
|
+
* (camoufox caps python-playwright, so the node side cannot chase latest).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { request as httpRequest } from "node:http";
|
|
27
|
+
import { request as httpsRequest } from "node:https";
|
|
28
|
+
import { resolve } from "node:path";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Playwright minor of the remote endpoint (python playwright behind
|
|
32
|
+
* Camoufox). Must equal the minor of the playwright-core bundled by the
|
|
33
|
+
* pinned @playwright/mcp — see the header comment before changing it.
|
|
34
|
+
*/
|
|
35
|
+
export const ENDPOINT_PLAYWRIGHT_MINOR = "1.58";
|
|
36
|
+
|
|
37
|
+
/** "1.58.0-alpha-2026-01-16" → "1.58". */
|
|
38
|
+
export function minorOf(version: string): string {
|
|
39
|
+
const m = version.match(/^(\d+)\.(\d+)/);
|
|
40
|
+
return m ? `${m[1]}.${m[2]}` : version;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultModulesRoot(): string {
|
|
44
|
+
return resolve(import.meta.dirname ?? ".", "../../../node_modules");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Version of the playwright-core the MCP child will run with. */
|
|
48
|
+
export function bundledPlaywrightVersion(
|
|
49
|
+
modulesRoot: string = defaultModulesRoot(),
|
|
50
|
+
): string | undefined {
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(
|
|
53
|
+
readFileSync(
|
|
54
|
+
resolve(modulesRoot, "playwright-core/package.json"),
|
|
55
|
+
"utf-8",
|
|
56
|
+
),
|
|
57
|
+
) as { version?: string };
|
|
58
|
+
return pkg.version;
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Static check: does the bundled client sit on the endpoint's minor?
|
|
66
|
+
* Returns an error message, or undefined when coupled (or when the bundle
|
|
67
|
+
* cannot be read — that is reported separately as a missing install).
|
|
68
|
+
*/
|
|
69
|
+
export function couplingError(
|
|
70
|
+
bundled: string | undefined,
|
|
71
|
+
expectedMinor: string = ENDPOINT_PLAYWRIGHT_MINOR,
|
|
72
|
+
): string | undefined {
|
|
73
|
+
if (!bundled) return undefined;
|
|
74
|
+
const got = minorOf(bundled);
|
|
75
|
+
if (got === expectedMinor) return undefined;
|
|
76
|
+
return (
|
|
77
|
+
`@playwright/mcp bundles playwright-core ${bundled} (minor ${got}) but the ` +
|
|
78
|
+
`remote endpoint is on Playwright ${expectedMinor} — every browser tool call ` +
|
|
79
|
+
`would fail with "428 Precondition Required". Pin @playwright/mcp to the ` +
|
|
80
|
+
`release that bundles playwright-core ${expectedMinor}.x, or bump ` +
|
|
81
|
+
`ENDPOINT_PLAYWRIGHT_MINOR together with the python side ` +
|
|
82
|
+
`(src/plugins/playwright/version-coupling.ts).`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type EndpointProbe =
|
|
87
|
+
| { state: "match"; client: string }
|
|
88
|
+
| { state: "mismatch"; client: string; server: string }
|
|
89
|
+
| { state: "unreachable"; client: string; reason: string };
|
|
90
|
+
|
|
91
|
+
/** Parse the body Playwright's server sends with its 428. */
|
|
92
|
+
export function parseMismatch(
|
|
93
|
+
body: string,
|
|
94
|
+
): { server: string; client: string } | undefined {
|
|
95
|
+
const server = body.match(/server version:\s*v?([\d.]+)/);
|
|
96
|
+
const client = body.match(/client version:\s*v?([\d.]+)/);
|
|
97
|
+
return server && client
|
|
98
|
+
? { server: server[1], client: client[1] }
|
|
99
|
+
: undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Perform the WebSocket upgrade the MCP child performs, advertising
|
|
104
|
+
* `clientVersion`, and read the server's verdict. Never throws; never
|
|
105
|
+
* leaves a connection open (a completed upgrade is torn down at once, and
|
|
106
|
+
* Playwright's server treats that as an ordinary client disconnect).
|
|
107
|
+
*/
|
|
108
|
+
export function probeEndpoint(
|
|
109
|
+
endpoint: string,
|
|
110
|
+
clientVersion: string,
|
|
111
|
+
timeoutMs = 3000,
|
|
112
|
+
): Promise<EndpointProbe> {
|
|
113
|
+
return new Promise((settle) => {
|
|
114
|
+
let url: URL;
|
|
115
|
+
try {
|
|
116
|
+
url = new URL(endpoint);
|
|
117
|
+
} catch {
|
|
118
|
+
settle({
|
|
119
|
+
state: "unreachable",
|
|
120
|
+
client: clientVersion,
|
|
121
|
+
reason: `invalid endpoint URL: ${endpoint}`,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const secure = url.protocol === "wss:" || url.protocol === "https:";
|
|
126
|
+
const request = secure ? httpsRequest : httpRequest;
|
|
127
|
+
const req = request({
|
|
128
|
+
host: url.hostname,
|
|
129
|
+
port: url.port || (secure ? 443 : 80),
|
|
130
|
+
path: `${url.pathname}${url.search}`,
|
|
131
|
+
method: "GET",
|
|
132
|
+
headers: {
|
|
133
|
+
Connection: "Upgrade",
|
|
134
|
+
Upgrade: "websocket",
|
|
135
|
+
"Sec-WebSocket-Version": "13",
|
|
136
|
+
"Sec-WebSocket-Key": randomBytes(16).toString("base64"),
|
|
137
|
+
"User-Agent": `Playwright/${clientVersion} (talon endpoint probe)`,
|
|
138
|
+
},
|
|
139
|
+
timeout: timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
let settled = false;
|
|
142
|
+
const done = (result: EndpointProbe) => {
|
|
143
|
+
if (settled) return;
|
|
144
|
+
settled = true;
|
|
145
|
+
req.destroy();
|
|
146
|
+
settle(result);
|
|
147
|
+
};
|
|
148
|
+
req.on("upgrade", (_res, socket) => {
|
|
149
|
+
socket.destroy();
|
|
150
|
+
done({ state: "match", client: clientVersion });
|
|
151
|
+
});
|
|
152
|
+
req.on("response", (res) => {
|
|
153
|
+
let body = "";
|
|
154
|
+
res.setEncoding("utf-8");
|
|
155
|
+
res.on("data", (chunk: string) => {
|
|
156
|
+
body += chunk;
|
|
157
|
+
});
|
|
158
|
+
res.on("end", () => {
|
|
159
|
+
if (res.statusCode === 428) {
|
|
160
|
+
const parsed = parseMismatch(body);
|
|
161
|
+
done(
|
|
162
|
+
parsed
|
|
163
|
+
? {
|
|
164
|
+
state: "mismatch",
|
|
165
|
+
client: clientVersion,
|
|
166
|
+
server: parsed.server,
|
|
167
|
+
}
|
|
168
|
+
: {
|
|
169
|
+
state: "unreachable",
|
|
170
|
+
client: clientVersion,
|
|
171
|
+
reason: "428 without a version box in the body",
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
done({
|
|
177
|
+
state: "unreachable",
|
|
178
|
+
client: clientVersion,
|
|
179
|
+
reason: `HTTP ${res.statusCode ?? "?"} instead of an upgrade`,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
req.on("timeout", () =>
|
|
184
|
+
done({ state: "unreachable", client: clientVersion, reason: "timeout" }),
|
|
185
|
+
);
|
|
186
|
+
req.on("error", (err: Error) =>
|
|
187
|
+
done({
|
|
188
|
+
state: "unreachable",
|
|
189
|
+
client: clientVersion,
|
|
190
|
+
reason: err.message,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
req.end();
|
|
194
|
+
});
|
|
195
|
+
}
|