wsl-chrome-bridge 0.2.0 → 0.3.0
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-zh.md +25 -9
- package/README.md +25 -8
- package/dist/bridge-options.d.ts +2 -0
- package/dist/bridge-options.js +6 -0
- package/dist/bridge-options.js.map +1 -1
- package/dist/bridge-powershell-scripts.d.ts +11 -0
- package/dist/bridge-powershell-scripts.js +475 -0
- package/dist/bridge-powershell-scripts.js.map +1 -0
- package/dist/bridge-runner.js +1006 -369
- package/dist/bridge-runner.js.map +1 -1
- package/dist/bridge-watchdog.d.ts +1 -0
- package/dist/bridge-watchdog.js +189 -0
- package/dist/bridge-watchdog.js.map +1 -0
- package/docs/BRIDGE_CONNECTION_LIFECYCLE-zh.md +199 -0
- package/docs/BRIDGE_CONNECTION_LIFECYCLE.md +198 -0
- package/package.json +4 -3
package/dist/bridge-runner.js
CHANGED
|
@@ -1,206 +1,150 @@
|
|
|
1
1
|
import { appendFileSync, createReadStream, createWriteStream, fstatSync, lstatSync, mkdirSync, readdirSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
-
import { dirname } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { WebSocket, WebSocketServer } from "ws";
|
|
7
8
|
import { buildWindowsChromeArgs, parseBrowserPathFromWsUrl, planBridgeLaunch } from "./bridge-options.js";
|
|
8
9
|
import { resolveChromeCommand } from "./chrome-command.js";
|
|
9
10
|
import { createPowerShellContext, destroyPowerShellContext, runPowerShellFile, writePowerShellScript } from "./powershell.js";
|
|
11
|
+
import { getBridgePowerShellScripts } from "./bridge-powershell-scripts.js";
|
|
10
12
|
const POLL_INTERVAL_MS = 400;
|
|
11
13
|
const CHROME_READY_TIMEOUT_MS = 30_000;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
const LARGE_CDP_STRING_BYTES = 16 * 1024;
|
|
15
|
+
const MAX_TRACKED_REQUEST_METHODS = 100;
|
|
16
|
+
const INTERNAL_CDP_REQUEST_ID_START = 900_000_000;
|
|
17
|
+
const DISCONNECT_EVENT_BUFFER_LIMIT = 10;
|
|
18
|
+
const DISCONNECT_EVENT_TIME_WINDOW_MS = 2_000;
|
|
19
|
+
const IMPORTANT_CDP_METHODS = new Set([
|
|
20
|
+
// Target/session lifecycle (most common failure points for attach/detach/disconnect)
|
|
21
|
+
"Target.setDiscoverTargets",
|
|
22
|
+
"Target.getTargets",
|
|
23
|
+
"Target.getTargetInfo",
|
|
24
|
+
"Target.getDevToolsTarget",
|
|
25
|
+
"Target.setAutoAttach",
|
|
26
|
+
"Target.autoAttachRelated",
|
|
27
|
+
"Target.attachToTarget",
|
|
28
|
+
"Target.attachedToTarget",
|
|
29
|
+
"Target.detachFromTarget",
|
|
30
|
+
"Target.detachedFromTarget",
|
|
31
|
+
"Target.targetCreated",
|
|
32
|
+
"Target.targetInfoChanged",
|
|
33
|
+
"Target.targetDestroyed",
|
|
34
|
+
"Target.activateTarget",
|
|
35
|
+
"Target.createTarget",
|
|
36
|
+
"Target.closeTarget",
|
|
37
|
+
// Browser/page open-close lifecycle
|
|
38
|
+
"Browser.getVersion",
|
|
39
|
+
"Browser.close",
|
|
40
|
+
"Page.navigate",
|
|
41
|
+
"Page.frameStartedLoading",
|
|
42
|
+
// Disconnection/error signals
|
|
43
|
+
"Inspector.detached",
|
|
44
|
+
"Runtime.exceptionThrown",
|
|
45
|
+
"Network.loadingFailed"
|
|
46
|
+
]);
|
|
47
|
+
const IMPORTANT_EXCLUDED_METHODS = new Set([
|
|
48
|
+
// Common benign error during early-frame phases; noisy but usually not a disconnect root cause.
|
|
49
|
+
"Storage.getStorageKey"
|
|
50
|
+
]);
|
|
51
|
+
function sleep(ms) {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
53
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
$raw = $json | ConvertTo-Json -Compress -Depth 12
|
|
42
|
-
Write-Output $raw
|
|
43
|
-
exit 0
|
|
44
|
-
} catch {
|
|
45
|
-
exit 1
|
|
54
|
+
const WEAK_DISCONNECT_SIGNALS = new Set([
|
|
55
|
+
"Inspector.detached",
|
|
56
|
+
"Target.detachedFromTarget"
|
|
57
|
+
]);
|
|
58
|
+
const STRONG_DISCONNECT_SIGNALS = new Set([
|
|
59
|
+
"ConnectionClosedPrematurely",
|
|
60
|
+
"READER_CLOSE"
|
|
61
|
+
]);
|
|
62
|
+
function isWeakDisconnectSignal(signal) {
|
|
63
|
+
return signal === "Inspector.detached" || signal === "Target.detachedFromTarget";
|
|
46
64
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
param(
|
|
50
|
-
[int]$RequestedPort,
|
|
51
|
-
[string]$Mode
|
|
52
|
-
)
|
|
53
|
-
$ErrorActionPreference = 'Stop'
|
|
54
|
-
|
|
55
|
-
function Test-PortAvailable {
|
|
56
|
-
param([int]$Port)
|
|
57
|
-
try {
|
|
58
|
-
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port)
|
|
59
|
-
$listener.Start()
|
|
60
|
-
$listener.Stop()
|
|
61
|
-
return $true
|
|
62
|
-
} catch {
|
|
63
|
-
return $false
|
|
64
|
-
}
|
|
65
|
+
function isStrongDisconnectSignal(signal) {
|
|
66
|
+
return signal === "ConnectionClosedPrematurely" || signal === "READER_CLOSE";
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
function cdpKindLabel(kind) {
|
|
69
|
+
switch (kind) {
|
|
70
|
+
case "request":
|
|
71
|
+
return "Request";
|
|
72
|
+
case "response":
|
|
73
|
+
return "Response";
|
|
74
|
+
case "event":
|
|
75
|
+
return "Event";
|
|
76
|
+
case "invalid-json":
|
|
77
|
+
return "InvalidJson";
|
|
78
|
+
case "unknown":
|
|
79
|
+
return "Unknown";
|
|
80
|
+
}
|
|
73
81
|
}
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
Write-Output $RequestedPort
|
|
81
|
-
exit 0
|
|
82
|
+
function asObject(value) {
|
|
83
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
82
87
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
for ($i = 0; $i -lt 50; $i++) {
|
|
91
|
-
$candidate = Get-RandomFreePort
|
|
92
|
-
if (Test-PortAvailable -Port $candidate) {
|
|
93
|
-
Write-Output $candidate
|
|
94
|
-
exit 0
|
|
88
|
+
function pickFirstString(...values) {
|
|
89
|
+
for (const value of values) {
|
|
90
|
+
if (typeof value === "string" && value.length > 0) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
95
93
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Write-Error "Failed to allocate an available Windows debug port."
|
|
99
|
-
exit 3
|
|
94
|
+
return null;
|
|
100
95
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
public class CDPRelay
|
|
119
|
-
{
|
|
120
|
-
public static void Run(string wsUrl)
|
|
121
|
-
{
|
|
122
|
-
var ws = new ClientWebSocket();
|
|
123
|
-
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
|
|
124
|
-
var cts = new CancellationTokenSource();
|
|
125
|
-
var token = cts.Token;
|
|
126
|
-
|
|
127
|
-
try
|
|
128
|
-
{
|
|
129
|
-
ws.ConnectAsync(new Uri(wsUrl), token).GetAwaiter().GetResult();
|
|
130
|
-
Console.Error.WriteLine("CONNECTED");
|
|
131
|
-
Console.Error.Flush();
|
|
132
|
-
|
|
133
|
-
var reader = Task.Run(() =>
|
|
134
|
-
{
|
|
135
|
-
var buf = new byte[4 * 1024 * 1024];
|
|
136
|
-
try
|
|
137
|
-
{
|
|
138
|
-
while (ws.State == WebSocketState.Open && !token.IsCancellationRequested)
|
|
139
|
-
{
|
|
140
|
-
var sb = new StringBuilder();
|
|
141
|
-
WebSocketReceiveResult recv;
|
|
142
|
-
do
|
|
143
|
-
{
|
|
144
|
-
var seg = new ArraySegment<byte>(buf);
|
|
145
|
-
recv = ws.ReceiveAsync(seg, token).GetAwaiter().GetResult();
|
|
146
|
-
if (recv.MessageType == WebSocketMessageType.Close) return;
|
|
147
|
-
sb.Append(Encoding.UTF8.GetString(buf, 0, recv.Count));
|
|
148
|
-
} while (!recv.EndOfMessage);
|
|
149
|
-
|
|
150
|
-
Console.Out.WriteLine(sb.ToString());
|
|
151
|
-
Console.Out.Flush();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch (OperationCanceledException) { }
|
|
155
|
-
catch (Exception ex)
|
|
156
|
-
{
|
|
157
|
-
Console.Error.WriteLine("READER_ERROR:" + ex.Message);
|
|
158
|
-
Console.Error.Flush();
|
|
159
|
-
}
|
|
160
|
-
}, token);
|
|
161
|
-
|
|
162
|
-
string line;
|
|
163
|
-
while ((line = Console.In.ReadLine()) != null)
|
|
164
|
-
{
|
|
165
|
-
if (ws.State != WebSocketState.Open) break;
|
|
166
|
-
var bytes = Encoding.UTF8.GetBytes(line);
|
|
167
|
-
var seg = new ArraySegment<byte>(bytes);
|
|
168
|
-
ws.SendAsync(seg, WebSocketMessageType.Text, true, token).GetAwaiter().GetResult();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
cts.Cancel();
|
|
172
|
-
reader.Wait(TimeSpan.FromSeconds(2));
|
|
96
|
+
function parseRelayMessage(payload) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(payload);
|
|
99
|
+
const id = typeof parsed.id === "number" || typeof parsed.id === "string" ? parsed.id : null;
|
|
100
|
+
const method = typeof parsed.method === "string" ? parsed.method : null;
|
|
101
|
+
const hasResult = Object.prototype.hasOwnProperty.call(parsed, "result");
|
|
102
|
+
const hasError = Object.prototype.hasOwnProperty.call(parsed, "error");
|
|
103
|
+
const paramsObj = asObject(parsed.params);
|
|
104
|
+
const resultObj = asObject(parsed.result);
|
|
105
|
+
const paramsTargetInfoObj = asObject(paramsObj?.targetInfo);
|
|
106
|
+
const resultTargetInfoObj = asObject(resultObj?.targetInfo);
|
|
107
|
+
const targetId = pickFirstString(parsed.targetId, paramsObj?.targetId, paramsTargetInfoObj?.targetId, resultObj?.targetId, resultTargetInfoObj?.targetId);
|
|
108
|
+
const browserContextId = pickFirstString(parsed.browserContextId, paramsObj?.browserContextId, paramsTargetInfoObj?.browserContextId, resultObj?.browserContextId, resultTargetInfoObj?.browserContextId);
|
|
109
|
+
const sessionId = pickFirstString(parsed.sessionId, paramsObj?.sessionId, resultObj?.sessionId);
|
|
110
|
+
if (method && id !== null) {
|
|
111
|
+
return { kind: "request", id, method, hasError: false, targetId, browserContextId, sessionId };
|
|
173
112
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Console.Error.WriteLine("FATAL:" + ex.Message);
|
|
177
|
-
Console.Error.Flush();
|
|
113
|
+
if (id !== null && (hasResult || hasError || !method)) {
|
|
114
|
+
return { kind: "response", id, method, hasError, targetId, browserContextId, sessionId };
|
|
178
115
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (ws.State == WebSocketState.Open)
|
|
182
|
-
{
|
|
183
|
-
try
|
|
184
|
-
{
|
|
185
|
-
ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).GetAwaiter().GetResult();
|
|
186
|
-
}
|
|
187
|
-
catch { }
|
|
188
|
-
}
|
|
189
|
-
ws.Dispose();
|
|
116
|
+
if (method) {
|
|
117
|
+
return { kind: "event", id: null, method, hasError: false, targetId, browserContextId, sessionId };
|
|
190
118
|
}
|
|
119
|
+
return { kind: "unknown", id, method, hasError, targetId, browserContextId, sessionId };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return {
|
|
123
|
+
kind: "invalid-json",
|
|
124
|
+
id: null,
|
|
125
|
+
method: null,
|
|
126
|
+
hasError: false,
|
|
127
|
+
targetId: null,
|
|
128
|
+
browserContextId: null,
|
|
129
|
+
sessionId: null
|
|
130
|
+
};
|
|
191
131
|
}
|
|
192
132
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
133
|
+
function toSafeRawTimestamp(iso) {
|
|
134
|
+
return iso.replaceAll(":", "-").replaceAll(".", "-");
|
|
135
|
+
}
|
|
136
|
+
function normalizeDebugLevel(raw) {
|
|
137
|
+
const value = raw?.trim().toLowerCase();
|
|
138
|
+
if (!value || value === "important" || value === "important-only") {
|
|
139
|
+
return "important";
|
|
140
|
+
}
|
|
141
|
+
if (value === "all" || value === "full") {
|
|
142
|
+
return "all";
|
|
143
|
+
}
|
|
144
|
+
return "important";
|
|
145
|
+
}
|
|
146
|
+
function requestKey(id, sessionId) {
|
|
147
|
+
return `${sessionId ?? "root"}::${String(id)}`;
|
|
204
148
|
}
|
|
205
149
|
function detectUpstreamHint(chromeArgs) {
|
|
206
150
|
const hasPipeMode = chromeArgs.includes("--remote-debugging-pipe");
|
|
@@ -220,12 +164,22 @@ function collectBridgeEnvSnapshot(env) {
|
|
|
220
164
|
return {
|
|
221
165
|
WSL_CHROME_BRIDGE_DEBUG: env.WSL_CHROME_BRIDGE_DEBUG ?? null,
|
|
222
166
|
WSL_CHROME_BRIDGE_DEBUG_FILE: env.WSL_CHROME_BRIDGE_DEBUG_FILE ?? null,
|
|
167
|
+
WSL_CHROME_BRIDGE_DEBUG_LEVEL: env.WSL_CHROME_BRIDGE_DEBUG_LEVEL ?? null,
|
|
168
|
+
WSL_CHROME_BRIDGE_DEBUG_RAW_DIR: env.WSL_CHROME_BRIDGE_DEBUG_RAW_DIR ?? null,
|
|
223
169
|
WSL_CHROME_BRIDGE_REMOTE_DEBUG_PORT: env.WSL_CHROME_BRIDGE_REMOTE_DEBUG_PORT ?? null,
|
|
224
170
|
WSL_CHROME_BRIDGE_EXECUTABLE_PATH: env.WSL_CHROME_BRIDGE_EXECUTABLE_PATH ?? null,
|
|
225
171
|
WSL_CHROME_BRIDGE_USER_DATA_DIR: env.WSL_CHROME_BRIDGE_USER_DATA_DIR ?? null,
|
|
226
172
|
DISPLAY: env.DISPLAY ?? null
|
|
227
173
|
};
|
|
228
174
|
}
|
|
175
|
+
function hasRequestedHeadlessFlag(args) {
|
|
176
|
+
for (const arg of args) {
|
|
177
|
+
if (arg === "--headless" || arg.startsWith("--headless=")) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
229
183
|
function cleanupEmptyLocalUserDataDirArtifact(userDataDir, writeDebug) {
|
|
230
184
|
if (!userDataDir) {
|
|
231
185
|
return;
|
|
@@ -270,6 +224,104 @@ function hasPipeFds() {
|
|
|
270
224
|
return false;
|
|
271
225
|
}
|
|
272
226
|
}
|
|
227
|
+
function parseExistingChromeMatch(raw) {
|
|
228
|
+
try {
|
|
229
|
+
const value = JSON.parse(raw.trim());
|
|
230
|
+
if (value &&
|
|
231
|
+
Object.prototype.hasOwnProperty.call(value, "found") &&
|
|
232
|
+
value.found === false) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const port = Number.parseInt(String(value.port), 10);
|
|
236
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const pidRaw = value.pid;
|
|
240
|
+
const pid = typeof pidRaw === "number" && Number.isFinite(pidRaw)
|
|
241
|
+
? Math.trunc(pidRaw)
|
|
242
|
+
: Number.parseInt(String(pidRaw), 10);
|
|
243
|
+
const headless = value.headless === true;
|
|
244
|
+
return {
|
|
245
|
+
port,
|
|
246
|
+
pid: Number.isFinite(pid) ? pid : null,
|
|
247
|
+
headless
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function parseRelayField(line, field) {
|
|
255
|
+
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
256
|
+
const pattern = new RegExp(`(?:^|;)${escapedField}=([^;]*)`);
|
|
257
|
+
const match = line.match(pattern);
|
|
258
|
+
if (!match) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return match[1] ?? null;
|
|
262
|
+
}
|
|
263
|
+
function detectStrongDisconnectSignalFromRelayLog(line) {
|
|
264
|
+
if (line.startsWith("READER_CLOSE:")) {
|
|
265
|
+
return "READER_CLOSE";
|
|
266
|
+
}
|
|
267
|
+
if (!line.startsWith("READER_ERROR:")) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const payload = line.slice("READER_ERROR:".length);
|
|
271
|
+
const webSocketErrorCode = parseRelayField(payload, "webSocketErrorCode");
|
|
272
|
+
if (webSocketErrorCode === "ConnectionClosedPrematurely") {
|
|
273
|
+
return "ConnectionClosedPrematurely";
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function detectWeakDisconnectSignalFromCdp(rendered) {
|
|
278
|
+
if (rendered.parsed.kind !== "event" || !rendered.canonicalMethod) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (rendered.canonicalMethod === "Inspector.detached") {
|
|
282
|
+
return "Inspector.detached";
|
|
283
|
+
}
|
|
284
|
+
if (rendered.canonicalMethod === "Target.detachedFromTarget") {
|
|
285
|
+
return "Target.detachedFromTarget";
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function assessDisconnectSignals(signals, recentWeakEvents, disconnectAtMs, windowMs) {
|
|
290
|
+
let weakSignalSeen = false;
|
|
291
|
+
let strongSignalSeen = false;
|
|
292
|
+
for (const signal of signals) {
|
|
293
|
+
if (isWeakDisconnectSignal(signal) && WEAK_DISCONNECT_SIGNALS.has(signal)) {
|
|
294
|
+
weakSignalSeen = true;
|
|
295
|
+
}
|
|
296
|
+
if (isStrongDisconnectSignal(signal) && STRONG_DISCONNECT_SIGNALS.has(signal)) {
|
|
297
|
+
strongSignalSeen = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// We only treat weak disconnect events as actionable when they happen
|
|
301
|
+
// immediately before websocket interruption. This avoids stale-detach false positives.
|
|
302
|
+
const nearbyWeakEvents = [];
|
|
303
|
+
for (const event of recentWeakEvents) {
|
|
304
|
+
const deltaMs = disconnectAtMs - event.observedAtMs;
|
|
305
|
+
if (deltaMs < 0 || deltaMs > windowMs) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
nearbyWeakEvents.push({
|
|
309
|
+
signal: event.signal,
|
|
310
|
+
deltaMs,
|
|
311
|
+
rawJson: event.rawJson
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const weakSignalWithinWindow = nearbyWeakEvents.length > 0;
|
|
315
|
+
return {
|
|
316
|
+
weakSignalSeen,
|
|
317
|
+
strongSignalSeen,
|
|
318
|
+
weakSignalWithinWindow,
|
|
319
|
+
chromeDisconnectedLikely: weakSignalWithinWindow && strongSignalSeen,
|
|
320
|
+
summary: Array.from(signals).join(",") || "none",
|
|
321
|
+
nearbyWeakEventCount: nearbyWeakEvents.length,
|
|
322
|
+
nearbyWeakEvents
|
|
323
|
+
};
|
|
324
|
+
}
|
|
273
325
|
async function startLocalDebugProxy(options) {
|
|
274
326
|
const clients = new Set();
|
|
275
327
|
const wsServer = new WebSocketServer({ noServer: true });
|
|
@@ -354,7 +406,14 @@ export function createBridgeRunner() {
|
|
|
354
406
|
const env = process.env;
|
|
355
407
|
const debugFileFromEnv = env.WSL_CHROME_BRIDGE_DEBUG_FILE?.trim() || null;
|
|
356
408
|
let activeDebugFile = debugFileFromEnv;
|
|
409
|
+
let activeRawDebugDir = env.WSL_CHROME_BRIDGE_DEBUG_RAW_DIR?.trim() || null;
|
|
410
|
+
const debugLevel = normalizeDebugLevel(env.WSL_CHROME_BRIDGE_DEBUG_LEVEL);
|
|
357
411
|
const debug = env.WSL_CHROME_BRIDGE_DEBUG === "1" || Boolean(activeDebugFile);
|
|
412
|
+
const requestMethodById = new Map();
|
|
413
|
+
let internalCdpRequestId = INTERNAL_CDP_REQUEST_ID_START;
|
|
414
|
+
const pendingInternalCdpRequests = new Map();
|
|
415
|
+
let lastRawTimestamp = "";
|
|
416
|
+
let sameTimestampSequence = 0;
|
|
358
417
|
const writeDebug = (message) => {
|
|
359
418
|
if (!debug) {
|
|
360
419
|
return;
|
|
@@ -371,6 +430,145 @@ export function createBridgeRunner() {
|
|
|
371
430
|
}
|
|
372
431
|
}
|
|
373
432
|
};
|
|
433
|
+
const writeRawPayload = (payload) => {
|
|
434
|
+
if (!activeRawDebugDir) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
mkdirSync(activeRawDebugDir, { recursive: true });
|
|
439
|
+
const now = new Date();
|
|
440
|
+
const safeTimestamp = toSafeRawTimestamp(now.toISOString());
|
|
441
|
+
if (safeTimestamp === lastRawTimestamp) {
|
|
442
|
+
sameTimestampSequence += 1;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
lastRawTimestamp = safeTimestamp;
|
|
446
|
+
sameTimestampSequence = 0;
|
|
447
|
+
}
|
|
448
|
+
const suffix = sameTimestampSequence === 0 ? "" : `-${String(sameTimestampSequence).padStart(4, "0")}`;
|
|
449
|
+
const fileName = `raw-${safeTimestamp}${suffix}.log`;
|
|
450
|
+
const path = join(activeRawDebugDir, fileName);
|
|
451
|
+
writeFileSync(path, payload, "utf8");
|
|
452
|
+
return path;
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
456
|
+
writeDebug(`rawPayloadWriteFailed dir=${activeRawDebugDir} error=${message}`);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const writeDebugBlock = (header, jsonPayload) => {
|
|
461
|
+
if (!debug) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const timestamp = new Date().toISOString();
|
|
465
|
+
const body = typeof jsonPayload === "string" ? jsonPayload : JSON.stringify(jsonPayload);
|
|
466
|
+
const block = `[${timestamp}] ${header}\n${body}\n\n`;
|
|
467
|
+
process.stderr.write(`[wsl-chrome-bridge][debug] ${header}\n${body}\n`);
|
|
468
|
+
if (activeDebugFile) {
|
|
469
|
+
try {
|
|
470
|
+
mkdirSync(dirname(activeDebugFile), { recursive: true });
|
|
471
|
+
appendFileSync(activeDebugFile, block, "utf8");
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// keep running even if debug file write fails
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const replaceLargeStringFields = (value, path) => {
|
|
479
|
+
if (typeof value === "string") {
|
|
480
|
+
if (Buffer.byteLength(value, "utf8") < LARGE_CDP_STRING_BYTES) {
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
const rawPath = writeRawPayload(value);
|
|
484
|
+
if (!rawPath) {
|
|
485
|
+
return value;
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
__rawDataPath: rawPath,
|
|
489
|
+
__rawDataBytes: Buffer.byteLength(value, "utf8"),
|
|
490
|
+
__replacedField: path
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (Array.isArray(value)) {
|
|
494
|
+
return value.map((item, index) => replaceLargeStringFields(item, `${path}[${index}]`));
|
|
495
|
+
}
|
|
496
|
+
if (value && typeof value === "object") {
|
|
497
|
+
const record = value;
|
|
498
|
+
const replaced = {};
|
|
499
|
+
for (const [key, child] of Object.entries(record)) {
|
|
500
|
+
replaced[key] = replaceLargeStringFields(child, `${path}.${key}`);
|
|
501
|
+
}
|
|
502
|
+
return replaced;
|
|
503
|
+
}
|
|
504
|
+
return value;
|
|
505
|
+
};
|
|
506
|
+
const hopLabel = (hop) => {
|
|
507
|
+
switch (hop) {
|
|
508
|
+
case "upstream=>relay":
|
|
509
|
+
return "upstream -> relay";
|
|
510
|
+
case "relay=>chrome":
|
|
511
|
+
return "relay -> chrome";
|
|
512
|
+
case "chrome=>relay":
|
|
513
|
+
return "chrome -> relay";
|
|
514
|
+
case "relay=>upstream":
|
|
515
|
+
return "relay -> upstream";
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const renderCdpMessage = (payload) => {
|
|
519
|
+
const parsed = parseRelayMessage(payload);
|
|
520
|
+
let canonicalMethod = parsed.method;
|
|
521
|
+
if (parsed.kind === "request" && parsed.id !== null && parsed.method) {
|
|
522
|
+
if (requestMethodById.size >= MAX_TRACKED_REQUEST_METHODS) {
|
|
523
|
+
const oldest = requestMethodById.keys().next().value;
|
|
524
|
+
if (oldest) {
|
|
525
|
+
requestMethodById.delete(oldest);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
requestMethodById.set(requestKey(parsed.id, parsed.sessionId), parsed.method);
|
|
529
|
+
}
|
|
530
|
+
if (!canonicalMethod && parsed.kind === "response" && parsed.id !== null) {
|
|
531
|
+
const key = requestKey(parsed.id, parsed.sessionId);
|
|
532
|
+
canonicalMethod = requestMethodById.get(key) ?? null;
|
|
533
|
+
requestMethodById.delete(key);
|
|
534
|
+
}
|
|
535
|
+
const title = canonicalMethod ??
|
|
536
|
+
(parsed.kind === "response" && parsed.id !== null
|
|
537
|
+
? `id=${parsed.id}`
|
|
538
|
+
: parsed.kind === "invalid-json"
|
|
539
|
+
? "InvalidJSON"
|
|
540
|
+
: "UnknownMessage");
|
|
541
|
+
try {
|
|
542
|
+
const json = JSON.parse(payload);
|
|
543
|
+
const replaced = replaceLargeStringFields(json, "$");
|
|
544
|
+
if (replaced && typeof replaced === "object" && !Array.isArray(replaced)) {
|
|
545
|
+
const asRecord = { ...replaced };
|
|
546
|
+
delete asRecord.method;
|
|
547
|
+
return { title, payload: asRecord, parsed, canonicalMethod };
|
|
548
|
+
}
|
|
549
|
+
return { title, payload: replaced, parsed, canonicalMethod };
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return { title, payload, parsed, canonicalMethod };
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const isImportantRenderedMessage = (rendered) => {
|
|
556
|
+
const method = rendered.canonicalMethod;
|
|
557
|
+
if (method && IMPORTANT_EXCLUDED_METHODS.has(method)) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if (rendered.parsed.hasError) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
return method !== null && IMPORTANT_CDP_METHODS.has(method);
|
|
564
|
+
};
|
|
565
|
+
const writeCdpHopLog = (hop, rendered) => {
|
|
566
|
+
if (debugLevel === "important" && !isImportantRenderedMessage(rendered)) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const kind = cdpKindLabel(rendered.parsed.kind);
|
|
570
|
+
writeDebugBlock(`CDP(${hopLabel(hop)}) ${kind} ${rendered.title}`, rendered.payload);
|
|
571
|
+
};
|
|
374
572
|
if (activeDebugFile) {
|
|
375
573
|
try {
|
|
376
574
|
mkdirSync(dirname(activeDebugFile), { recursive: true });
|
|
@@ -404,9 +602,28 @@ export function createBridgeRunner() {
|
|
|
404
602
|
process.stderr.write(`[wsl-chrome-bridge] failed to create debug file: ${activeDebugFile}\n`);
|
|
405
603
|
}
|
|
406
604
|
}
|
|
605
|
+
if (!activeRawDebugDir && plan.bridgeDebugRawDir) {
|
|
606
|
+
activeRawDebugDir = plan.bridgeDebugRawDir;
|
|
607
|
+
}
|
|
608
|
+
if (activeRawDebugDir) {
|
|
609
|
+
try {
|
|
610
|
+
mkdirSync(activeRawDebugDir, { recursive: true });
|
|
611
|
+
writeDebug(`rawDebugDir enabled path=${activeRawDebugDir}`);
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
process.stderr.write(`[wsl-chrome-bridge] failed to create raw debug dir: ${activeRawDebugDir}\n`);
|
|
615
|
+
activeRawDebugDir = null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
writeDebug("rawDebugDir disabled");
|
|
620
|
+
}
|
|
621
|
+
writeDebug(`debugLevel=${debugLevel}`);
|
|
407
622
|
writeDebug(`argv=${JSON.stringify(chromeArgs)}`);
|
|
623
|
+
const requestedHeadlessMode = hasRequestedHeadlessFlag(plan.passthroughArgs);
|
|
408
624
|
writeDebug(`launchPlan=${JSON.stringify({
|
|
409
625
|
bridgeDebugFile: plan.bridgeDebugFile,
|
|
626
|
+
bridgeDebugRawDir: plan.bridgeDebugRawDir,
|
|
410
627
|
bridgeChromeExecutablePath: plan.bridgeChromeExecutablePath,
|
|
411
628
|
usePipeTransport: plan.usePipeTransport,
|
|
412
629
|
userDataDir: plan.userDataDir,
|
|
@@ -416,6 +633,7 @@ export function createBridgeRunner() {
|
|
|
416
633
|
localProxyPort: plan.localProxyPort,
|
|
417
634
|
windowsDebugPort: plan.windowsDebugPort,
|
|
418
635
|
windowsDebugPortSource: plan.windowsDebugPortSource,
|
|
636
|
+
requestedHeadlessMode,
|
|
419
637
|
passthroughArgs: plan.passthroughArgs
|
|
420
638
|
})}`);
|
|
421
639
|
cleanupEmptyLocalUserDataDirArtifact(plan.userDataDir, writeDebug);
|
|
@@ -428,127 +646,189 @@ export function createBridgeRunner() {
|
|
|
428
646
|
};
|
|
429
647
|
const powerShell = createPowerShellContext(env);
|
|
430
648
|
writeDebug(`powershellPath=${powerShell.powershellPath}`);
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
cleanupUserDataDirOnExit("resolvePortFailed");
|
|
445
|
-
return 1;
|
|
446
|
-
}
|
|
447
|
-
const resolvedWindowsDebugPort = Number.parseInt(resolvePortResult.stdout.trim(), 10);
|
|
448
|
-
if (!Number.isFinite(resolvedWindowsDebugPort)) {
|
|
449
|
-
writeDebug(`resolvePortParseFailed raw=${resolvePortResult.stdout.trim()} stderr=${resolvePortResult.stderr.trim()}`);
|
|
450
|
-
process.stderr.write("[wsl-chrome-bridge] failed to parse resolved Windows debug port from PowerShell output.\n");
|
|
451
|
-
destroyPowerShellContext(powerShell);
|
|
452
|
-
cleanupUserDataDirOnExit("resolvePortParseFailed");
|
|
453
|
-
return 1;
|
|
454
|
-
}
|
|
455
|
-
writeDebug(`resolvedWindowsDebugPort=${resolvedWindowsDebugPort} source=${plan.windowsDebugPortSource} mode=${resolvePortMode}`);
|
|
456
|
-
const planWithResolvedPort = {
|
|
457
|
-
...plan,
|
|
458
|
-
windowsDebugPort: resolvedWindowsDebugPort
|
|
459
|
-
};
|
|
460
|
-
const chromePath = resolveChromeCommand({
|
|
461
|
-
env,
|
|
462
|
-
bridgeChromeExecutablePath: plan.bridgeChromeExecutablePath
|
|
463
|
-
});
|
|
464
|
-
const windowsArgs = buildWindowsChromeArgs(planWithResolvedPort, env);
|
|
465
|
-
writeDebug(`chromePath=${chromePath}`);
|
|
466
|
-
writeDebug(`windowsArgs=${JSON.stringify(windowsArgs)}`);
|
|
467
|
-
const launchResult = await runPowerShellFile(powerShell, launchScript.windowsPath, [chromePath, ...windowsArgs], { timeoutMs: 15_000 });
|
|
468
|
-
if (launchResult.code !== 0) {
|
|
469
|
-
writeDebug(`launchFailed code=${launchResult.code} stdout=${launchResult.stdout} stderr=${launchResult.stderr}`);
|
|
470
|
-
process.stderr.write(`[wsl-chrome-bridge] failed to launch Windows Chrome: ${launchResult.stderr || launchResult.stdout}\n`);
|
|
649
|
+
const scripts = getBridgePowerShellScripts();
|
|
650
|
+
const launchScript = writePowerShellScript(powerShell, "launch-chrome.ps1", scripts.launchChrome);
|
|
651
|
+
const findExistingChromeScript = writePowerShellScript(powerShell, "find-existing-chrome.ps1", scripts.findExistingChrome);
|
|
652
|
+
const getVersionScript = writePowerShellScript(powerShell, "get-version.ps1", scripts.getVersion);
|
|
653
|
+
const resolvePortScript = writePowerShellScript(powerShell, "resolve-port.ps1", scripts.resolvePort);
|
|
654
|
+
const stopChromeScript = writePowerShellScript(powerShell, "stop-chrome-by-profile-port.ps1", scripts.stopChromeByProfilePort);
|
|
655
|
+
const relayScript = writePowerShellScript(powerShell, "relay.ps1", scripts.relay);
|
|
656
|
+
writeDebug(`scripts={launch:${launchScript.windowsPath},findExisting:${findExistingChromeScript.windowsPath},version:${getVersionScript.windowsPath},resolvePort:${resolvePortScript.windowsPath},stopChrome:${stopChromeScript.windowsPath},relay:${relayScript.windowsPath}}`);
|
|
657
|
+
const usePipeTransport = plan.usePipeTransport && hasPipeFds();
|
|
658
|
+
const useLocalProxyTransport = plan.localProxyPort !== null;
|
|
659
|
+
writeDebug(`transportMode requestedPipe=${plan.usePipeTransport} pipe=${usePipeTransport} localProxy=${useLocalProxyTransport} localProxyPort=${plan.localProxyPort ?? "none"}`);
|
|
660
|
+
if (plan.usePipeTransport && !usePipeTransport) {
|
|
661
|
+
process.stderr.write("[wsl-chrome-bridge] --remote-debugging-pipe was requested but OS pipe fds (3/4) are missing.\n");
|
|
471
662
|
destroyPowerShellContext(powerShell);
|
|
472
|
-
cleanupUserDataDirOnExit("
|
|
663
|
+
cleanupUserDataDirOnExit("pipeFdsMissing");
|
|
473
664
|
return 1;
|
|
474
665
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
writeDebug(`invalidChromePid output=${launchResult.stdout}`);
|
|
479
|
-
process.stderr.write(`[wsl-chrome-bridge] failed to parse Chrome PID from PowerShell output: ${launchResult.stdout}\n`);
|
|
666
|
+
if (!usePipeTransport && !useLocalProxyTransport) {
|
|
667
|
+
process.stderr.write("[wsl-chrome-bridge] missing transport channel. " +
|
|
668
|
+
"Provide --remote-debugging-port (Playwright mode) or launch with OS pipes fd3/fd4.\n");
|
|
480
669
|
destroyPowerShellContext(powerShell);
|
|
481
|
-
cleanupUserDataDirOnExit("
|
|
670
|
+
cleanupUserDataDirOnExit("missingTransportChannel");
|
|
482
671
|
return 1;
|
|
483
672
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
673
|
+
const waitForRemoteVersion = async (windowsDebugPort, phase) => {
|
|
674
|
+
const startAt = Date.now();
|
|
675
|
+
while (Date.now() - startAt < CHROME_READY_TIMEOUT_MS) {
|
|
676
|
+
const result = await runPowerShellFile(powerShell, getVersionScript.windowsPath, [String(windowsDebugPort)], { timeoutMs: 4_000 });
|
|
677
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
678
|
+
try {
|
|
679
|
+
const version = JSON.parse(result.stdout.trim());
|
|
680
|
+
if (typeof version.webSocketDebuggerUrl === "string") {
|
|
681
|
+
writeDebug(`remoteVersion phase=${phase} port=${windowsDebugPort} payload=${JSON.stringify(version)}`);
|
|
682
|
+
return version;
|
|
683
|
+
}
|
|
684
|
+
writeDebug(`remoteVersionMissingWs phase=${phase} port=${windowsDebugPort} payload=${result.stdout.trim()}`);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
writeDebug(`remoteVersionParseFailed phase=${phase} port=${windowsDebugPort} raw=${result.stdout.trim()}`);
|
|
688
|
+
}
|
|
493
689
|
}
|
|
494
|
-
|
|
495
|
-
writeDebug(`
|
|
496
|
-
// ignore malformed JSON and retry
|
|
690
|
+
else if (debug) {
|
|
691
|
+
writeDebug(`waitVersion retry phase=${phase} port=${windowsDebugPort} code=${result.code} stdout=${result.stdout.trim()} stderr=${result.stderr.trim()}`);
|
|
497
692
|
}
|
|
693
|
+
await sleep(POLL_INTERVAL_MS);
|
|
498
694
|
}
|
|
499
|
-
|
|
500
|
-
|
|
695
|
+
return null;
|
|
696
|
+
};
|
|
697
|
+
const tryReuseExistingChromeSession = async (phase) => {
|
|
698
|
+
if (!plan.windowsUserDataDir) {
|
|
699
|
+
writeDebug(`existingChrome skip phase=${phase} reason=no-windows-user-data-dir`);
|
|
700
|
+
return null;
|
|
501
701
|
}
|
|
502
|
-
await
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
702
|
+
const existingResult = await runPowerShellFile(powerShell, findExistingChromeScript.windowsPath, [plan.windowsUserDataDir], { timeoutMs: 8_000 });
|
|
703
|
+
if (existingResult.code !== 0) {
|
|
704
|
+
writeDebug(`existingChrome query failed phase=${phase} code=${existingResult.code} stdout=${existingResult.stdout.trim()} stderr=${existingResult.stderr.trim()}`);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const existingMatch = parseExistingChromeMatch(existingResult.stdout);
|
|
708
|
+
if (!existingMatch) {
|
|
709
|
+
writeDebug(`existingChrome miss phase=${phase}`);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
writeDebug(`existingChrome hit phase=${phase} port=${existingMatch.port} pid=${existingMatch.pid ?? "unknown"} headless=${existingMatch.headless}`);
|
|
713
|
+
const version = await waitForRemoteVersion(existingMatch.port, phase);
|
|
714
|
+
if (!version || typeof version.webSocketDebuggerUrl !== "string") {
|
|
715
|
+
writeDebug(`existingChrome stale phase=${phase} port=${existingMatch.port} pid=${existingMatch.pid ?? "unknown"}`);
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
if (existingMatch.headless !== requestedHeadlessMode) {
|
|
719
|
+
const existingMode = existingMatch.headless ? "headless" : "headed";
|
|
720
|
+
const requestedMode = requestedHeadlessMode ? "headless" : "headed";
|
|
721
|
+
throw new Error(`headless mode mismatch for shared --user-data-dir "${plan.windowsUserDataDir}". Existing Chrome mode is ${existingMode}, requested mode is ${requestedMode}. Use a different --user-data-dir per mode, or close the existing Chrome instance first.`);
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
windowsDebugPort: existingMatch.port,
|
|
725
|
+
remoteVersion: version,
|
|
726
|
+
remoteBrowserWsUrl: version.webSocketDebuggerUrl,
|
|
727
|
+
ownership: "attached",
|
|
728
|
+
chromePid: null,
|
|
729
|
+
launchedExecutablePath: null
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
const launchChromeSession = async (phase) => {
|
|
733
|
+
const resolvePortMode = plan.windowsDebugPortSource === "auto-random" ? "random" : "fixed";
|
|
734
|
+
const resolvePortResult = await runPowerShellFile(powerShell, resolvePortScript.windowsPath, [String(plan.windowsDebugPort), resolvePortMode], { timeoutMs: 8_000 });
|
|
735
|
+
if (resolvePortResult.code !== 0) {
|
|
736
|
+
throw new Error(`failed to resolve an available Windows debug port: ${resolvePortResult.stderr || resolvePortResult.stdout}`);
|
|
737
|
+
}
|
|
738
|
+
const resolvedWindowsDebugPort = Number.parseInt(resolvePortResult.stdout.trim(), 10);
|
|
739
|
+
if (!Number.isFinite(resolvedWindowsDebugPort)) {
|
|
740
|
+
throw new Error("failed to parse resolved Windows debug port from PowerShell output.");
|
|
741
|
+
}
|
|
742
|
+
writeDebug(`resolvedWindowsDebugPort phase=${phase} port=${resolvedWindowsDebugPort} source=${plan.windowsDebugPortSource} mode=${resolvePortMode}`);
|
|
743
|
+
const planWithResolvedPort = {
|
|
744
|
+
...plan,
|
|
745
|
+
windowsDebugPort: resolvedWindowsDebugPort
|
|
746
|
+
};
|
|
747
|
+
const chromePath = resolveChromeCommand({
|
|
748
|
+
env,
|
|
749
|
+
bridgeChromeExecutablePath: plan.bridgeChromeExecutablePath
|
|
509
750
|
});
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
751
|
+
const windowsArgs = buildWindowsChromeArgs(planWithResolvedPort, env);
|
|
752
|
+
writeDebug(`chromePath phase=${phase} path=${chromePath}`);
|
|
753
|
+
writeDebug(`windowsArgs phase=${phase} args=${JSON.stringify(windowsArgs)}`);
|
|
754
|
+
const launchResult = await runPowerShellFile(powerShell, launchScript.windowsPath, [chromePath, ...windowsArgs], { timeoutMs: 15_000 });
|
|
755
|
+
if (launchResult.code !== 0) {
|
|
756
|
+
throw new Error(`failed to launch Windows Chrome: ${launchResult.stderr || launchResult.stdout}`);
|
|
757
|
+
}
|
|
758
|
+
writeDebug(`launchResult phase=${phase} code=${launchResult.code} stdout=${launchResult.stdout.trim()} stderr=${launchResult.stderr.trim()}`);
|
|
759
|
+
const launchedPid = Number.parseInt(launchResult.stdout.trim(), 10);
|
|
760
|
+
if (Number.isFinite(launchedPid)) {
|
|
761
|
+
writeDebug(`launchResult phase=${phase} pid=${launchedPid}`);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
writeDebug(`launchResult phase=${phase} pid=unknown raw=${launchResult.stdout.trim()}`);
|
|
765
|
+
}
|
|
766
|
+
const version = await waitForRemoteVersion(resolvedWindowsDebugPort, phase);
|
|
767
|
+
if (!version || typeof version.webSocketDebuggerUrl !== "string") {
|
|
768
|
+
throw new Error("Chrome debug websocket was not ready in time.");
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
windowsDebugPort: resolvedWindowsDebugPort,
|
|
772
|
+
remoteVersion: version,
|
|
773
|
+
remoteBrowserWsUrl: version.webSocketDebuggerUrl,
|
|
774
|
+
ownership: "launched",
|
|
775
|
+
chromePid: Number.isFinite(launchedPid) ? launchedPid : null,
|
|
776
|
+
launchedExecutablePath: chromePath
|
|
777
|
+
};
|
|
778
|
+
};
|
|
779
|
+
const establishChromeSession = async (phase) => {
|
|
780
|
+
const existing = await tryReuseExistingChromeSession(phase);
|
|
781
|
+
if (existing) {
|
|
782
|
+
return existing;
|
|
783
|
+
}
|
|
784
|
+
return await launchChromeSession(phase);
|
|
785
|
+
};
|
|
786
|
+
let startupSession;
|
|
787
|
+
try {
|
|
788
|
+
startupSession = await establishChromeSession("startup");
|
|
513
789
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
"-NoProfile",
|
|
519
|
-
"-NonInteractive",
|
|
520
|
-
"-ExecutionPolicy",
|
|
521
|
-
"Bypass",
|
|
522
|
-
"-File",
|
|
523
|
-
relayScript.windowsPath,
|
|
524
|
-
remoteBrowserWsUrl
|
|
525
|
-
], {
|
|
526
|
-
env,
|
|
527
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
528
|
-
});
|
|
529
|
-
relayChild.stdout.setEncoding("utf8");
|
|
530
|
-
relayChild.stderr.setEncoding("utf8");
|
|
531
|
-
const usePipeTransport = plan.usePipeTransport && hasPipeFds();
|
|
532
|
-
const useLocalProxyTransport = plan.localProxyPort !== null;
|
|
533
|
-
writeDebug(`transportMode requestedPipe=${plan.usePipeTransport} pipe=${usePipeTransport} localProxy=${useLocalProxyTransport} localProxyPort=${plan.localProxyPort ?? "none"}`);
|
|
534
|
-
if (plan.usePipeTransport && !usePipeTransport) {
|
|
535
|
-
process.stderr.write("[wsl-chrome-bridge] --remote-debugging-pipe was requested but OS pipe fds (3/4) are missing.\n");
|
|
536
|
-
await runPowerShellFile(powerShell, stopProcessScript.windowsPath, [String(chromePid)], {
|
|
537
|
-
timeoutMs: 3_000
|
|
538
|
-
});
|
|
790
|
+
catch (error) {
|
|
791
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
792
|
+
writeDebug(`startupSession failed message=${message}`);
|
|
793
|
+
process.stderr.write(`[wsl-chrome-bridge] ${message}\n`);
|
|
539
794
|
destroyPowerShellContext(powerShell);
|
|
540
|
-
cleanupUserDataDirOnExit("
|
|
795
|
+
cleanupUserDataDirOnExit("startupSessionFailed");
|
|
541
796
|
return 1;
|
|
542
797
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
798
|
+
let activeRemoteBrowserWsUrl = startupSession.remoteBrowserWsUrl;
|
|
799
|
+
let activeWindowsDebugPort = startupSession.windowsDebugPort;
|
|
800
|
+
let activeOwnership = startupSession.ownership;
|
|
801
|
+
let activeChromePid = startupSession.chromePid;
|
|
802
|
+
let activeLaunchedExecutablePath = startupSession.launchedExecutablePath;
|
|
803
|
+
const startupRemoteVersion = startupSession.remoteVersion;
|
|
804
|
+
writeDebug(`startupSession established ownership=${activeOwnership} port=${activeWindowsDebugPort} chromePid=${activeChromePid ?? "unknown"} ws=${activeRemoteBrowserWsUrl}`);
|
|
805
|
+
const startBridgeWatchdog = (chromePid) => {
|
|
806
|
+
const watchdogScriptPath = fileURLToPath(new URL("./bridge-watchdog.js", import.meta.url));
|
|
807
|
+
const watchdogArgs = [
|
|
808
|
+
watchdogScriptPath,
|
|
809
|
+
"--bridge-pid",
|
|
810
|
+
String(process.pid),
|
|
811
|
+
"--chrome-pid",
|
|
812
|
+
String(chromePid),
|
|
813
|
+
"--powershell-path",
|
|
814
|
+
powerShell.powershellPath
|
|
815
|
+
];
|
|
816
|
+
if (activeLaunchedExecutablePath) {
|
|
817
|
+
watchdogArgs.push("--expected-executable-path", activeLaunchedExecutablePath);
|
|
818
|
+
}
|
|
819
|
+
if (activeDebugFile) {
|
|
820
|
+
watchdogArgs.push("--debug-file", activeDebugFile);
|
|
821
|
+
}
|
|
822
|
+
const watchdog = spawn(process.execPath, watchdogArgs, {
|
|
823
|
+
env,
|
|
824
|
+
detached: true,
|
|
825
|
+
stdio: "ignore"
|
|
548
826
|
});
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
827
|
+
watchdog.unref();
|
|
828
|
+
writeDebug(`watchdog started pid=${watchdog.pid ?? "unknown"} bridgePid=${process.pid} chromePid=${chromePid}`);
|
|
829
|
+
};
|
|
830
|
+
if (usePipeTransport && requestedHeadlessMode && activeOwnership === "launched" && activeChromePid) {
|
|
831
|
+
startBridgeWatchdog(activeChromePid);
|
|
552
832
|
}
|
|
553
833
|
let pipeIn = null;
|
|
554
834
|
let pipeOut = null;
|
|
@@ -556,14 +836,18 @@ export function createBridgeRunner() {
|
|
|
556
836
|
pipeIn = createReadStream("", { fd: 3, autoClose: false });
|
|
557
837
|
pipeOut = createWriteStream("", { fd: 4, autoClose: false });
|
|
558
838
|
}
|
|
559
|
-
const remoteBrowserPath = parseBrowserPathFromWsUrl(
|
|
839
|
+
const remoteBrowserPath = parseBrowserPathFromWsUrl(activeRemoteBrowserWsUrl);
|
|
560
840
|
const localBrowserWsUrl = plan.localProxyPort === null
|
|
561
841
|
? null
|
|
562
842
|
: `ws://127.0.0.1:${plan.localProxyPort}${remoteBrowserPath}`;
|
|
563
843
|
let localProxy = null;
|
|
844
|
+
const earlyWsMessages = [];
|
|
845
|
+
let handleLocalProxyMessage = (message) => {
|
|
846
|
+
earlyWsMessages.push(message);
|
|
847
|
+
};
|
|
564
848
|
if (plan.localProxyPort !== null && localBrowserWsUrl) {
|
|
565
849
|
const localVersionPayload = {
|
|
566
|
-
...
|
|
850
|
+
...startupRemoteVersion,
|
|
567
851
|
webSocketDebuggerUrl: localBrowserWsUrl
|
|
568
852
|
};
|
|
569
853
|
try {
|
|
@@ -574,12 +858,7 @@ export function createBridgeRunner() {
|
|
|
574
858
|
localVersionPayload,
|
|
575
859
|
writeDebug,
|
|
576
860
|
onClientMessage: (message) => {
|
|
577
|
-
|
|
578
|
-
relayChild.stdin.write(`${message}\n`);
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
queuedForRelay.push(message);
|
|
582
|
-
}
|
|
861
|
+
handleLocalProxyMessage(message);
|
|
583
862
|
}
|
|
584
863
|
});
|
|
585
864
|
writeDebug(`localProxy listening ws=${localBrowserWsUrl}`);
|
|
@@ -592,25 +871,27 @@ export function createBridgeRunner() {
|
|
|
592
871
|
const message = error instanceof Error ? error.message : String(error);
|
|
593
872
|
writeDebug(`localProxy start failed: ${message}`);
|
|
594
873
|
process.stderr.write(`[wsl-chrome-bridge] failed to start local websocket proxy on 127.0.0.1:${plan.localProxyPort}: ${message}\n`);
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
catch {
|
|
599
|
-
// ignore
|
|
600
|
-
}
|
|
601
|
-
await runPowerShellFile(powerShell, stopProcessScript.windowsPath, [String(chromePid)], {
|
|
602
|
-
timeoutMs: 3_000
|
|
603
|
-
});
|
|
874
|
+
pipeIn?.destroy();
|
|
875
|
+
pipeOut?.destroy();
|
|
604
876
|
destroyPowerShellContext(powerShell);
|
|
605
877
|
cleanupUserDataDirOnExit("localProxyStartFailed");
|
|
606
878
|
return 1;
|
|
607
879
|
}
|
|
608
880
|
}
|
|
881
|
+
let relayState = "connecting";
|
|
609
882
|
let relayConnected = false;
|
|
610
883
|
let relayStdoutBuffer = "";
|
|
611
884
|
let relayStderrBuffer = "";
|
|
885
|
+
let relayGeneration = 0;
|
|
886
|
+
let relayChild = null;
|
|
612
887
|
let pendingFromPipe = Buffer.alloc(0);
|
|
613
888
|
const queuedForRelay = [];
|
|
889
|
+
let relayBootstrapPending = false;
|
|
890
|
+
let recoveryPromise = null;
|
|
891
|
+
const disconnectSignals = new Set();
|
|
892
|
+
const recentWeakDisconnectEvents = [];
|
|
893
|
+
let strongDisconnectAtMs = null;
|
|
894
|
+
let latestDisconnectAssessment = null;
|
|
614
895
|
let closed = false;
|
|
615
896
|
const cleanup = async (code) => {
|
|
616
897
|
if (closed) {
|
|
@@ -624,23 +905,27 @@ export function createBridgeRunner() {
|
|
|
624
905
|
await localProxy.close();
|
|
625
906
|
writeDebug("cleanup localProxy closed");
|
|
626
907
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
908
|
+
const child = relayChild;
|
|
909
|
+
relayChild = null;
|
|
910
|
+
if (child) {
|
|
911
|
+
try {
|
|
912
|
+
child.stdin?.end();
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
// ignore
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
child.kill("SIGTERM");
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
// ignore
|
|
922
|
+
}
|
|
923
|
+
await Promise.race([once(child, "exit"), sleep(1_000)]);
|
|
635
924
|
}
|
|
636
|
-
|
|
637
|
-
|
|
925
|
+
if (usePipeTransport && requestedHeadlessMode && plan.windowsUserDataDir) {
|
|
926
|
+
const stopResult = await runPowerShellFile(powerShell, stopChromeScript.windowsPath, [plan.windowsUserDataDir, String(activeWindowsDebugPort)], { timeoutMs: 8_000 });
|
|
927
|
+
writeDebug(`cleanup stopHeadlessChrome code=${stopResult.code} port=${activeWindowsDebugPort} ownership=${activeOwnership} stdout=${stopResult.stdout.trim()} stderr=${stopResult.stderr.trim()}`);
|
|
638
928
|
}
|
|
639
|
-
await Promise.race([once(relayChild, "exit"), sleep(1_000)]);
|
|
640
|
-
await runPowerShellFile(powerShell, stopProcessScript.windowsPath, [String(chromePid)], {
|
|
641
|
-
timeoutMs: 3_000
|
|
642
|
-
});
|
|
643
|
-
writeDebug("cleanup stopProcess done");
|
|
644
929
|
destroyPowerShellContext(powerShell);
|
|
645
930
|
writeDebug("cleanup powershell context destroyed");
|
|
646
931
|
cleanupUserDataDirOnExit("cleanup");
|
|
@@ -651,6 +936,7 @@ export function createBridgeRunner() {
|
|
|
651
936
|
const signalHandlers = new Map();
|
|
652
937
|
return await new Promise((resolvePromise) => {
|
|
653
938
|
let resolving = false;
|
|
939
|
+
let shuttingDown = false;
|
|
654
940
|
const clear = () => {
|
|
655
941
|
for (const [signal, handler] of signalHandlers) {
|
|
656
942
|
process.off(signal, handler);
|
|
@@ -661,66 +947,422 @@ export function createBridgeRunner() {
|
|
|
661
947
|
return;
|
|
662
948
|
}
|
|
663
949
|
resolving = true;
|
|
950
|
+
shuttingDown = true;
|
|
664
951
|
clear();
|
|
665
952
|
void cleanup(code).then(resolvePromise);
|
|
666
953
|
};
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
954
|
+
const noteDisconnectSignal = (signal, source) => {
|
|
955
|
+
if (disconnectSignals.has(signal)) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
disconnectSignals.add(signal);
|
|
959
|
+
writeDebug(`disconnectSignal observed signal=${signal} source=${source}`);
|
|
960
|
+
};
|
|
961
|
+
const rememberWeakDisconnectEvent = (signal, rawJson, observedAtMs) => {
|
|
962
|
+
recentWeakDisconnectEvents.push({ signal, rawJson, observedAtMs });
|
|
963
|
+
if (recentWeakDisconnectEvents.length > DISCONNECT_EVENT_BUFFER_LIMIT) {
|
|
964
|
+
recentWeakDisconnectEvents.splice(0, recentWeakDisconnectEvents.length - DISCONNECT_EVENT_BUFFER_LIMIT);
|
|
965
|
+
}
|
|
966
|
+
writeDebug(`disconnectEvent cached signal=${signal} cacheSize=${recentWeakDisconnectEvents.length}`);
|
|
967
|
+
};
|
|
968
|
+
const writeDisconnectAssessment = (trigger, disconnectAtMs) => {
|
|
969
|
+
const effectiveDisconnectAtMs = strongDisconnectAtMs ?? disconnectAtMs ?? Date.now();
|
|
970
|
+
const assessment = assessDisconnectSignals(disconnectSignals, recentWeakDisconnectEvents, effectiveDisconnectAtMs, DISCONNECT_EVENT_TIME_WINDOW_MS);
|
|
971
|
+
writeDebug(`disconnectAssessment trigger=${trigger} weakSignalSeen=${assessment.weakSignalSeen} strongSignalSeen=${assessment.strongSignalSeen} weakSignalWithinWindow=${assessment.weakSignalWithinWindow} windowMs=${DISCONNECT_EVENT_TIME_WINDOW_MS} disconnectAtMs=${effectiveDisconnectAtMs} chromeDisconnectedLikely=${assessment.chromeDisconnectedLikely} nearbyWeakEventCount=${assessment.nearbyWeakEventCount} disconnectSignals=${assessment.summary}`);
|
|
972
|
+
if (assessment.nearbyWeakEvents.length > 0) {
|
|
973
|
+
writeDebugBlock(`disconnectAssessment nearbyWeakEvents trigger=${trigger}`, assessment.nearbyWeakEvents);
|
|
974
|
+
}
|
|
975
|
+
return assessment;
|
|
976
|
+
};
|
|
977
|
+
const markRelayState = (nextState, reason, disconnectAssessment = null) => {
|
|
978
|
+
if (nextState !== "degraded") {
|
|
979
|
+
latestDisconnectAssessment = null;
|
|
980
|
+
}
|
|
981
|
+
else if (disconnectAssessment) {
|
|
982
|
+
latestDisconnectAssessment = disconnectAssessment;
|
|
983
|
+
}
|
|
984
|
+
if (relayState === nextState) {
|
|
985
|
+
writeDebug(`relayState unchanged state=${nextState} reason=${reason}`);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
relayState = nextState;
|
|
989
|
+
const assessmentSuffix = disconnectAssessment
|
|
990
|
+
? ` disconnectSignals=${disconnectAssessment.summary} weakSignalWithinWindow=${disconnectAssessment.weakSignalWithinWindow} nearbyWeakEventCount=${disconnectAssessment.nearbyWeakEventCount} chromeDisconnectedLikely=${disconnectAssessment.chromeDisconnectedLikely}`
|
|
991
|
+
: "";
|
|
992
|
+
writeDebug(`relayState changed state=${relayState} reason=${reason} port=${activeWindowsDebugPort} ownership=${activeOwnership}${assessmentSuffix}`);
|
|
993
|
+
};
|
|
994
|
+
const nextInternalCdpRequestId = () => {
|
|
995
|
+
internalCdpRequestId += 1;
|
|
996
|
+
return internalCdpRequestId;
|
|
997
|
+
};
|
|
998
|
+
const sendRecoveryAutoAttachBootstrap = (child, generation) => {
|
|
999
|
+
if (!child.stdin) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
const payload = {
|
|
1003
|
+
id: nextInternalCdpRequestId(),
|
|
1004
|
+
method: "Target.setAutoAttach",
|
|
1005
|
+
params: {
|
|
1006
|
+
autoAttach: true,
|
|
1007
|
+
waitForDebuggerOnStart: true,
|
|
1008
|
+
flatten: true
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
const message = JSON.stringify(payload);
|
|
1012
|
+
const rendered = renderCdpMessage(message);
|
|
1013
|
+
pendingInternalCdpRequests.set(requestKey(payload.id, null), {
|
|
1014
|
+
generation,
|
|
1015
|
+
method: payload.method,
|
|
1016
|
+
purpose: "recovery-auto-attach"
|
|
1017
|
+
});
|
|
1018
|
+
writeDebugBlock("CDP(bridge-internal -> chrome) Request Target.setAutoAttach", rendered.payload);
|
|
1019
|
+
child.stdin.write(`${message}\n`);
|
|
1020
|
+
return true;
|
|
1021
|
+
};
|
|
1022
|
+
const tryHandleInternalCdpResponse = (generation, rendered, child) => {
|
|
1023
|
+
if (rendered.parsed.kind !== "response" || rendered.parsed.id === null) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
const key = requestKey(rendered.parsed.id, rendered.parsed.sessionId);
|
|
1027
|
+
const pending = pendingInternalCdpRequests.get(key);
|
|
1028
|
+
if (!pending) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
pendingInternalCdpRequests.delete(key);
|
|
1032
|
+
writeDebugBlock(`CDP(chrome -> bridge-internal) Response ${pending.method}`, rendered.payload);
|
|
1033
|
+
if (pending.generation !== generation) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
if (pending.purpose === "recovery-auto-attach") {
|
|
1037
|
+
relayBootstrapPending = false;
|
|
1038
|
+
if (rendered.parsed.hasError) {
|
|
1039
|
+
relayConnected = false;
|
|
1040
|
+
const assessment = writeDisconnectAssessment("recoveryBootstrapAutoAttachError");
|
|
1041
|
+
markRelayState("degraded", "recoveryBootstrapAutoAttachError", assessment);
|
|
1042
|
+
try {
|
|
1043
|
+
child.kill("SIGTERM");
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
// ignore
|
|
1047
|
+
}
|
|
1048
|
+
maybeRecoverRelayForWs();
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
writeDebug("recoveryBootstrap autoAttach acknowledged");
|
|
1052
|
+
flushQueuedForRelay();
|
|
1053
|
+
}
|
|
1054
|
+
return true;
|
|
1055
|
+
};
|
|
1056
|
+
const flushQueuedForRelay = () => {
|
|
1057
|
+
if (!relayChild || !relayConnected || !relayChild.stdin || relayBootstrapPending) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const relayStdin = relayChild.stdin;
|
|
1061
|
+
while (queuedForRelay.length > 0) {
|
|
1062
|
+
const queued = queuedForRelay.shift();
|
|
1063
|
+
if (!queued) {
|
|
677
1064
|
break;
|
|
678
1065
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1066
|
+
relayStdin.write(`${queued.message}\n`);
|
|
1067
|
+
writeCdpHopLog("relay=>chrome", queued.rendered);
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
const forwardToUpstream = (message, rendered) => {
|
|
1071
|
+
if (pipeOut) {
|
|
1072
|
+
pipeOut.write(message);
|
|
1073
|
+
pipeOut.write("\0");
|
|
1074
|
+
writeCdpHopLog("relay=>upstream", rendered);
|
|
1075
|
+
}
|
|
1076
|
+
if (localProxy) {
|
|
1077
|
+
localProxy.broadcast(message);
|
|
1078
|
+
writeCdpHopLog("relay=>upstream", rendered);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
const sendToRelayOrQueue = (message, rendered) => {
|
|
1082
|
+
if (relayState === "connected" &&
|
|
1083
|
+
relayChild &&
|
|
1084
|
+
relayConnected &&
|
|
1085
|
+
relayChild.stdin &&
|
|
1086
|
+
!relayBootstrapPending) {
|
|
1087
|
+
relayChild.stdin.write(`${message}\n`);
|
|
1088
|
+
writeCdpHopLog("relay=>chrome", rendered);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
queuedForRelay.push({ message, rendered });
|
|
1092
|
+
};
|
|
1093
|
+
const startRelay = (remoteBrowserWsUrl, reason) => {
|
|
1094
|
+
if (shuttingDown) {
|
|
1095
|
+
writeDebug(`startRelay skipped reason=shuttingDown trigger=${reason}`);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
disconnectSignals.clear();
|
|
1099
|
+
recentWeakDisconnectEvents.length = 0;
|
|
1100
|
+
strongDisconnectAtMs = null;
|
|
1101
|
+
relayBootstrapPending = false;
|
|
1102
|
+
pendingInternalCdpRequests.clear();
|
|
1103
|
+
relayGeneration += 1;
|
|
1104
|
+
const generation = relayGeneration;
|
|
1105
|
+
markRelayState("connecting", reason);
|
|
1106
|
+
relayConnected = false;
|
|
1107
|
+
relayStdoutBuffer = "";
|
|
1108
|
+
relayStderrBuffer = "";
|
|
1109
|
+
const previousRelayChild = relayChild;
|
|
1110
|
+
const child = spawn(powerShell.powershellPath, [
|
|
1111
|
+
"-NoLogo",
|
|
1112
|
+
"-NoProfile",
|
|
1113
|
+
"-NonInteractive",
|
|
1114
|
+
"-ExecutionPolicy",
|
|
1115
|
+
"Bypass",
|
|
1116
|
+
"-File",
|
|
1117
|
+
relayScript.windowsPath,
|
|
1118
|
+
remoteBrowserWsUrl
|
|
1119
|
+
], {
|
|
1120
|
+
env,
|
|
1121
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1122
|
+
});
|
|
1123
|
+
child.stdout.setEncoding("utf8");
|
|
1124
|
+
child.stderr.setEncoding("utf8");
|
|
1125
|
+
relayChild = child;
|
|
1126
|
+
if (previousRelayChild) {
|
|
1127
|
+
try {
|
|
1128
|
+
previousRelayChild.stdin?.end();
|
|
1129
|
+
}
|
|
1130
|
+
catch {
|
|
1131
|
+
// ignore
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
previousRelayChild.kill("SIGTERM");
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
// ignore
|
|
684
1138
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1139
|
+
}
|
|
1140
|
+
child.stderr.on("data", (chunk) => {
|
|
1141
|
+
if (generation !== relayGeneration) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
relayStderrBuffer += chunk;
|
|
1145
|
+
while (true) {
|
|
1146
|
+
const newlineIndex = relayStderrBuffer.indexOf("\n");
|
|
1147
|
+
if (newlineIndex === -1) {
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
const rawLine = relayStderrBuffer.slice(0, newlineIndex);
|
|
1151
|
+
relayStderrBuffer = relayStderrBuffer.slice(newlineIndex + 1);
|
|
1152
|
+
const line = rawLine.replace(/\r$/, "");
|
|
1153
|
+
if (!line) {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
writeDebug(`relayStderr ${line}`);
|
|
1157
|
+
const strongDisconnectSignal = detectStrongDisconnectSignalFromRelayLog(line);
|
|
1158
|
+
if (strongDisconnectSignal) {
|
|
1159
|
+
noteDisconnectSignal(strongDisconnectSignal, "relayStderr");
|
|
1160
|
+
const observedAtMs = Date.now();
|
|
1161
|
+
strongDisconnectAtMs = observedAtMs;
|
|
1162
|
+
relayConnected = false;
|
|
1163
|
+
const assessment = writeDisconnectAssessment(`relayStrongDisconnect:${strongDisconnectSignal}`, observedAtMs);
|
|
1164
|
+
if (usePipeTransport) {
|
|
1165
|
+
finalize(1);
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
relayBootstrapPending = false;
|
|
1169
|
+
markRelayState("degraded", `relayStrongDisconnect:${strongDisconnectSignal}`, assessment);
|
|
1170
|
+
try {
|
|
1171
|
+
child.kill("SIGTERM");
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
// ignore
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (line.includes("CONNECTED")) {
|
|
1180
|
+
relayConnected = true;
|
|
1181
|
+
markRelayState("connected", "relayConnected");
|
|
1182
|
+
if (!usePipeTransport && reason === "recovery") {
|
|
1183
|
+
relayBootstrapPending = true;
|
|
1184
|
+
const sent = sendRecoveryAutoAttachBootstrap(child, generation);
|
|
1185
|
+
if (!sent) {
|
|
1186
|
+
relayBootstrapPending = false;
|
|
1187
|
+
relayConnected = false;
|
|
1188
|
+
const assessment = writeDisconnectAssessment("recoveryBootstrapSendFailed");
|
|
1189
|
+
markRelayState("degraded", "recoveryBootstrapSendFailed", assessment);
|
|
1190
|
+
try {
|
|
1191
|
+
child.kill("SIGTERM");
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
// ignore
|
|
1195
|
+
}
|
|
1196
|
+
maybeRecoverRelayForWs();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
flushQueuedForRelay();
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (line.startsWith("FATAL:")) {
|
|
1205
|
+
const assessment = writeDisconnectAssessment(`relayFatal:${line}`);
|
|
1206
|
+
if (usePipeTransport) {
|
|
1207
|
+
finalize(1);
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
relayConnected = false;
|
|
1211
|
+
relayBootstrapPending = false;
|
|
1212
|
+
markRelayState("degraded", `relayFatal:${line}`, assessment);
|
|
1213
|
+
}
|
|
1214
|
+
return;
|
|
690
1215
|
}
|
|
691
|
-
queuedForRelay.length = 0;
|
|
692
|
-
continue;
|
|
693
1216
|
}
|
|
694
|
-
|
|
695
|
-
|
|
1217
|
+
});
|
|
1218
|
+
child.stdout.on("data", (chunk) => {
|
|
1219
|
+
if (generation !== relayGeneration) {
|
|
696
1220
|
return;
|
|
697
1221
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1222
|
+
relayStdoutBuffer += chunk;
|
|
1223
|
+
while (true) {
|
|
1224
|
+
const newlineIndex = relayStdoutBuffer.indexOf("\n");
|
|
1225
|
+
if (newlineIndex === -1) {
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
const rawLine = relayStdoutBuffer.slice(0, newlineIndex);
|
|
1229
|
+
relayStdoutBuffer = relayStdoutBuffer.slice(newlineIndex + 1);
|
|
1230
|
+
const line = rawLine.replace(/\r$/, "");
|
|
1231
|
+
if (!line) {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
const rendered = renderCdpMessage(line);
|
|
1235
|
+
if (tryHandleInternalCdpResponse(generation, rendered, child)) {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
const weakDisconnectSignal = detectWeakDisconnectSignalFromCdp(rendered);
|
|
1239
|
+
if (weakDisconnectSignal) {
|
|
1240
|
+
noteDisconnectSignal(weakDisconnectSignal, "chromeEvent");
|
|
1241
|
+
rememberWeakDisconnectEvent(weakDisconnectSignal, line, Date.now());
|
|
1242
|
+
}
|
|
1243
|
+
writeCdpHopLog("chrome=>relay", rendered);
|
|
1244
|
+
forwardToUpstream(line, rendered);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
child.once("error", () => {
|
|
1248
|
+
if (generation !== relayGeneration) {
|
|
1249
|
+
return;
|
|
706
1250
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
if (
|
|
711
|
-
|
|
1251
|
+
relayConnected = false;
|
|
1252
|
+
relayBootstrapPending = false;
|
|
1253
|
+
const assessment = writeDisconnectAssessment("relayError");
|
|
1254
|
+
if (usePipeTransport) {
|
|
1255
|
+
finalize(1);
|
|
712
1256
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1257
|
+
else {
|
|
1258
|
+
markRelayState("degraded", "relayError", assessment);
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
child.once("exit", (code) => {
|
|
1262
|
+
if (generation !== relayGeneration) {
|
|
1263
|
+
return;
|
|
716
1264
|
}
|
|
717
|
-
|
|
718
|
-
|
|
1265
|
+
relayConnected = false;
|
|
1266
|
+
relayBootstrapPending = false;
|
|
1267
|
+
relayChild = null;
|
|
1268
|
+
const assessment = writeDisconnectAssessment(`relayExit:${code ?? "null"}`);
|
|
1269
|
+
if (usePipeTransport) {
|
|
1270
|
+
finalize(code === 0 ? 0 : 1);
|
|
1271
|
+
return;
|
|
719
1272
|
}
|
|
1273
|
+
markRelayState("degraded", `relayExit:${code ?? "null"}`, assessment);
|
|
1274
|
+
});
|
|
1275
|
+
};
|
|
1276
|
+
const maybeRecoverRelayForWs = () => {
|
|
1277
|
+
if (usePipeTransport || relayState !== "degraded") {
|
|
1278
|
+
return;
|
|
720
1279
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1280
|
+
if (shuttingDown || closed || resolving) {
|
|
1281
|
+
writeDebug("recovery skipped reason=shuttingDown");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (recoveryPromise) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
recoveryPromise = (async () => {
|
|
1288
|
+
writeDebug("recovery start trigger=upstreamRequest");
|
|
1289
|
+
const recoverySession = await establishChromeSession("recovery");
|
|
1290
|
+
activeRemoteBrowserWsUrl = recoverySession.remoteBrowserWsUrl;
|
|
1291
|
+
activeWindowsDebugPort = recoverySession.windowsDebugPort;
|
|
1292
|
+
activeOwnership = recoverySession.ownership;
|
|
1293
|
+
activeChromePid = recoverySession.chromePid;
|
|
1294
|
+
activeLaunchedExecutablePath = recoverySession.launchedExecutablePath;
|
|
1295
|
+
writeDebug(`recovery success ownership=${activeOwnership} port=${activeWindowsDebugPort} chromePid=${activeChromePid ?? "unknown"} ws=${activeRemoteBrowserWsUrl}`);
|
|
1296
|
+
startRelay(activeRemoteBrowserWsUrl, "recovery");
|
|
1297
|
+
})()
|
|
1298
|
+
.catch((error) => {
|
|
1299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1300
|
+
writeDebug(`recovery failed message=${message}`);
|
|
1301
|
+
process.stderr.write(`[wsl-chrome-bridge] recovery failed: ${message}\n`);
|
|
1302
|
+
finalize(1);
|
|
1303
|
+
})
|
|
1304
|
+
.finally(() => {
|
|
1305
|
+
recoveryPromise = null;
|
|
1306
|
+
});
|
|
1307
|
+
};
|
|
1308
|
+
const processUpstreamMessage = (message) => {
|
|
1309
|
+
if (shuttingDown || closed || resolving) {
|
|
1310
|
+
writeDebug("upstream message ignored reason=shuttingDown");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const rendered = renderCdpMessage(message);
|
|
1314
|
+
writeCdpHopLog("upstream=>relay", rendered);
|
|
1315
|
+
const shouldShortCircuitKnownClosedBrowserClose = !usePipeTransport &&
|
|
1316
|
+
relayState !== "connected" &&
|
|
1317
|
+
rendered.parsed.kind === "request" &&
|
|
1318
|
+
rendered.canonicalMethod === "Browser.close" &&
|
|
1319
|
+
rendered.parsed.id !== null &&
|
|
1320
|
+
Boolean(latestDisconnectAssessment?.chromeDisconnectedLikely);
|
|
1321
|
+
if (shouldShortCircuitKnownClosedBrowserClose) {
|
|
1322
|
+
const syntheticResponse = {
|
|
1323
|
+
id: rendered.parsed.id,
|
|
1324
|
+
result: {}
|
|
1325
|
+
};
|
|
1326
|
+
if (rendered.parsed.sessionId) {
|
|
1327
|
+
syntheticResponse.sessionId = rendered.parsed.sessionId;
|
|
1328
|
+
}
|
|
1329
|
+
const syntheticMessage = JSON.stringify(syntheticResponse);
|
|
1330
|
+
const syntheticRendered = renderCdpMessage(syntheticMessage);
|
|
1331
|
+
writeDebug(`shortCircuit Browser.close reason=chromeAlreadyClosed relayState=${relayState} id=${String(rendered.parsed.id)} session=${rendered.parsed.sessionId ?? "root"}`);
|
|
1332
|
+
forwardToUpstream(syntheticMessage, syntheticRendered);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (!usePipeTransport && relayState === "degraded") {
|
|
1336
|
+
maybeRecoverRelayForWs();
|
|
1337
|
+
sendToRelayOrQueue(message, rendered);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (!usePipeTransport && relayState === "connecting") {
|
|
1341
|
+
sendToRelayOrQueue(message, rendered);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (!usePipeTransport && (!relayConnected || relayState !== "connected")) {
|
|
1345
|
+
const assessment = writeDisconnectAssessment("upstreamWhileRelayUnavailable");
|
|
1346
|
+
markRelayState("degraded", "upstreamWhileRelayUnavailable", assessment);
|
|
1347
|
+
maybeRecoverRelayForWs();
|
|
1348
|
+
sendToRelayOrQueue(message, rendered);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
sendToRelayOrQueue(message, rendered);
|
|
1352
|
+
};
|
|
1353
|
+
handleLocalProxyMessage = (message) => {
|
|
1354
|
+
processUpstreamMessage(message);
|
|
1355
|
+
};
|
|
1356
|
+
for (const earlyMessage of earlyWsMessages) {
|
|
1357
|
+
processUpstreamMessage(earlyMessage);
|
|
1358
|
+
}
|
|
1359
|
+
earlyWsMessages.length = 0;
|
|
1360
|
+
for (const signal of signals) {
|
|
1361
|
+
const handler = () => finalize(0);
|
|
1362
|
+
signalHandlers.set(signal, handler);
|
|
1363
|
+
process.on(signal, handler);
|
|
1364
|
+
}
|
|
1365
|
+
startRelay(activeRemoteBrowserWsUrl, "initial");
|
|
724
1366
|
if (pipeIn) {
|
|
725
1367
|
pipeIn.on("data", (chunk) => {
|
|
726
1368
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8");
|
|
@@ -735,12 +1377,7 @@ export function createBridgeRunner() {
|
|
|
735
1377
|
if (!message) {
|
|
736
1378
|
continue;
|
|
737
1379
|
}
|
|
738
|
-
|
|
739
|
-
relayChild.stdin.write(`${message}\n`);
|
|
740
|
-
}
|
|
741
|
-
else {
|
|
742
|
-
queuedForRelay.push(message);
|
|
743
|
-
}
|
|
1380
|
+
processUpstreamMessage(message);
|
|
744
1381
|
}
|
|
745
1382
|
});
|
|
746
1383
|
pipeIn.once("end", () => finalize(0));
|