browser-debugger-cli 0.8.0 → 0.10.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.md +7 -1
- package/dist/cdp/schema.d.ts +4 -1
- package/dist/cdp/schema.js +48 -7
- package/dist/commands/cdp.js +3 -2
- package/dist/commands/cleanup.d.ts +11 -0
- package/dist/commands/cleanup.js +161 -57
- package/dist/commands/console.d.ts +20 -1
- package/dist/commands/console.js +57 -17
- package/dist/commands/details.js +3 -2
- package/dist/commands/dom/DomElementResolver.d.ts +10 -3
- package/dist/commands/dom/DomElementResolver.js +35 -17
- package/dist/commands/dom/a11y.d.ts +10 -0
- package/dist/commands/dom/a11y.js +29 -6
- package/dist/commands/dom/eval.d.ts +3 -1
- package/dist/commands/dom/eval.js +29 -4
- package/dist/commands/dom/form.js +16 -62
- package/dist/commands/dom/formInteraction.js +189 -119
- package/dist/commands/dom/formSummary.d.ts +49 -0
- package/dist/commands/dom/formSummary.js +180 -0
- package/dist/commands/dom/frames.d.ts +2 -1
- package/dist/commands/dom/frames.js +17 -2
- package/dist/commands/dom/get.d.ts +6 -5
- package/dist/commands/dom/get.js +92 -82
- package/dist/commands/dom/helpers/index.d.ts +1 -1
- package/dist/commands/dom/helpers/index.js +1 -1
- package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
- package/dist/commands/dom/helpers/keyAttributes.js +54 -0
- package/dist/commands/dom/helpers/query.d.ts +44 -17
- package/dist/commands/dom/helpers/query.js +300 -106
- package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
- package/dist/commands/dom/helpers/runElementCommand.js +98 -30
- package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
- package/dist/commands/dom/helpers/screenshot.js +239 -51
- package/dist/commands/dom/index.d.ts +4 -1
- package/dist/commands/dom/index.js +22 -7
- package/dist/commands/dom/inspect.d.ts +15 -0
- package/dist/commands/dom/inspect.js +82 -0
- package/dist/commands/dom/layout.d.ts +14 -0
- package/dist/commands/dom/layout.js +54 -0
- package/dist/commands/dom/listeners.d.ts +5 -1
- package/dist/commands/dom/listeners.js +15 -5
- package/dist/commands/dom/query.js +2 -3
- package/dist/commands/dom/screenshot.d.ts +12 -2
- package/dist/commands/dom/screenshot.js +27 -3
- package/dist/commands/dom/semanticUtils.d.ts +16 -10
- package/dist/commands/dom/semanticUtils.js +53 -16
- package/dist/commands/dom/wait.d.ts +13 -0
- package/dist/commands/dom/wait.js +83 -0
- package/dist/commands/helpJson.js +2 -2
- package/dist/commands/network/list.js +17 -13
- package/dist/commands/optionBehaviors.js +154 -21
- package/dist/commands/page.d.ts +3 -2
- package/dist/commands/page.js +100 -5
- package/dist/commands/peek.js +4 -11
- package/dist/commands/sessions.d.ts +8 -0
- package/dist/commands/sessions.js +19 -0
- package/dist/commands/shared/CommandRunner.js +4 -4
- package/dist/commands/shared/commonOptions.d.ts +4 -0
- package/dist/commands/shared/commonOptions.js +9 -0
- package/dist/commands/shared/dataFetcher.js +2 -2
- package/dist/commands/shared/followMode.d.ts +21 -1
- package/dist/commands/shared/followMode.js +29 -2
- package/dist/commands/shared/handleValidationError.d.ts +2 -2
- package/dist/commands/shared/handleValidationError.js +12 -3
- package/dist/commands/shared/optionTypes.d.ts +61 -5
- package/dist/commands/shared/startHelpers.d.ts +66 -0
- package/dist/commands/shared/startHelpers.js +103 -13
- package/dist/commands/shared/validation.d.ts +14 -2
- package/dist/commands/shared/validation.js +20 -3
- package/dist/commands/start.d.ts +63 -0
- package/dist/commands/start.js +115 -15
- package/dist/commands/status.js +29 -7
- package/dist/commands/stop.js +7 -6
- package/dist/commands/tail.js +4 -11
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands.js +2 -0
- package/dist/connection/chromeIdentity.d.ts +65 -0
- package/dist/connection/chromeIdentity.js +143 -0
- package/dist/connection/launcher/profilePreferences.d.ts +47 -0
- package/dist/connection/launcher/profilePreferences.js +151 -0
- package/dist/connection/launcher.d.ts +21 -2
- package/dist/connection/launcher.js +42 -16
- package/dist/connection/portReservation.d.ts +14 -4
- package/dist/connection/portReservation.js +21 -6
- package/dist/connection/startupExit.d.ts +8 -0
- package/dist/connection/startupExit.js +15 -6
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +9 -2
- package/dist/daemon/SessionController.js +23 -7
- package/dist/daemon/errors.d.ts +1 -1
- package/dist/daemon/errors.js +1 -1
- package/dist/daemon/launcher.d.ts +10 -2
- package/dist/daemon/launcher.js +8 -7
- package/dist/daemon/server/SocketServer.js +1 -2
- package/dist/daemon/session/Session.d.ts +20 -0
- package/dist/daemon/session/Session.js +80 -9
- package/dist/daemon/session/chromeConnection.d.ts +9 -0
- package/dist/daemon/session/chromeConnection.js +45 -8
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +113 -67
- package/dist/daemon/session/interactions.d.ts +48 -9
- package/dist/daemon/session/interactions.js +46 -9
- package/dist/daemon/session/triggeredRequests.d.ts +67 -0
- package/dist/daemon/session/triggeredRequests.js +157 -0
- package/dist/daemon/session/types.d.ts +5 -1
- package/dist/daemon.js +10630 -3601
- package/dist/errors/messages.d.ts +456 -24
- package/dist/errors/messages.js +862 -67
- package/dist/index.js +6915 -3401
- package/dist/ipc/client.d.ts +21 -1
- package/dist/ipc/client.js +35 -3
- package/dist/ipc/protocol/commands.d.ts +145 -5
- package/dist/ipc/protocol/commands.js +4 -0
- package/dist/ipc/protocol/domTypes.d.ts +291 -7
- package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
- package/dist/ipc/protocol/inspectTypes.js +10 -0
- package/dist/ipc/session/lifecycle.d.ts +8 -1
- package/dist/ipc/session/queries.d.ts +5 -1
- package/dist/ipc/session/types.d.ts +5 -0
- package/dist/ipc/transport/index.d.ts +2 -1
- package/dist/ipc/transport/index.js +2 -2
- package/dist/runtime/dom/actionEffects.d.ts +185 -0
- package/dist/runtime/dom/actionEffects.js +402 -0
- package/dist/runtime/dom/actionEffectsScripts.d.ts +90 -0
- package/dist/runtime/dom/actionEffectsScripts.js +426 -0
- package/dist/runtime/dom/elementGeometry.d.ts +170 -0
- package/dist/runtime/dom/elementGeometry.js +553 -0
- package/dist/runtime/dom/elementInfo.d.ts +103 -0
- package/dist/runtime/dom/elementInfo.js +256 -0
- package/dist/runtime/dom/evalHelpers.d.ts +51 -6
- package/dist/runtime/dom/evalHelpers.js +136 -26
- package/dist/runtime/dom/eventListeners.d.ts +2 -1
- package/dist/runtime/dom/eventListeners.js +184 -47
- package/dist/runtime/dom/formDiscovery.d.ts +1 -1
- package/dist/runtime/dom/formDiscovery.js +116 -16
- package/dist/runtime/dom/formFillHelpers/fill.d.ts +9 -0
- package/dist/runtime/dom/formFillHelpers/fill.js +178 -14
- package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
- package/dist/runtime/dom/formFillHelpers/index.js +2 -2
- package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
- package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
- package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
- package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
- package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
- package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
- package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
- package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
- package/dist/runtime/dom/formSubmitHelpers.js +89 -15
- package/dist/runtime/dom/frameLayout.d.ts +60 -0
- package/dist/runtime/dom/frameLayout.js +140 -0
- package/dist/runtime/dom/frameOrigin.d.ts +50 -0
- package/dist/runtime/dom/frameOrigin.js +62 -0
- package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
- package/dist/runtime/dom/frameScopedConnection.js +252 -0
- package/dist/runtime/dom/frameSelection.d.ts +12 -1
- package/dist/runtime/dom/frameSelection.js +22 -3
- package/dist/runtime/dom/frames.d.ts +61 -5
- package/dist/runtime/dom/frames.js +329 -75
- package/dist/runtime/dom/inspect.d.ts +28 -0
- package/dist/runtime/dom/inspect.js +557 -0
- package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
- package/dist/runtime/dom/inspectAllStyles.js +385 -0
- package/dist/runtime/dom/inspectCascade.d.ts +94 -0
- package/dist/runtime/dom/inspectCascade.js +371 -0
- package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
- package/dist/runtime/dom/inspectCascadeModel.js +232 -0
- package/dist/runtime/dom/inspectHints.d.ts +62 -0
- package/dist/runtime/dom/inspectHints.js +305 -0
- package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
- package/dist/runtime/dom/inspectLayoutModel.js +346 -0
- package/dist/runtime/dom/inspectModel.d.ts +74 -0
- package/dist/runtime/dom/inspectModel.js +184 -0
- package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
- package/dist/runtime/dom/inspectPaintModel.js +461 -0
- package/dist/runtime/dom/inspectRules.d.ts +37 -0
- package/dist/runtime/dom/inspectRules.js +101 -0
- package/dist/runtime/dom/inspectScripts.d.ts +132 -0
- package/dist/runtime/dom/inspectScripts.js +263 -0
- package/dist/runtime/dom/inspectTree.d.ts +40 -0
- package/dist/runtime/dom/inspectTree.js +134 -0
- package/dist/runtime/dom/inspectVariables.d.ts +33 -0
- package/dist/runtime/dom/inspectVariables.js +94 -0
- package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
- package/dist/runtime/dom/inspectWhyModel.js +134 -0
- package/dist/runtime/dom/layout.d.ts +71 -0
- package/dist/runtime/dom/layout.js +340 -0
- package/dist/runtime/dom/listenerPageScripts.d.ts +72 -0
- package/dist/runtime/dom/listenerPageScripts.js +365 -0
- package/dist/runtime/dom/listenerSummary.d.ts +136 -11
- package/dist/runtime/dom/listenerSummary.js +361 -22
- package/dist/runtime/dom/pageActivity.d.ts +41 -0
- package/dist/runtime/dom/pageActivity.js +123 -0
- package/dist/runtime/dom/reactEventHelpers.d.ts +63 -2
- package/dist/runtime/dom/reactEventHelpers.js +220 -41
- package/dist/runtime/dom/targetNode.d.ts +80 -27
- package/dist/runtime/dom/targetNode.js +249 -33
- package/dist/runtime/dom/wait.d.ts +25 -0
- package/dist/runtime/dom/wait.js +199 -0
- package/dist/runtime/dom/waitCondition.d.ts +71 -0
- package/dist/runtime/dom/waitCondition.js +75 -0
- package/dist/runtime/page/emulation.d.ts +71 -0
- package/dist/runtime/page/emulation.js +117 -0
- package/dist/runtime/page/loadingState.d.ts +36 -0
- package/dist/runtime/page/loadingState.js +86 -0
- package/dist/runtime/page/navigation.d.ts +46 -2
- package/dist/runtime/page/navigation.js +69 -33
- package/dist/session/QueryCacheManager.d.ts +11 -1
- package/dist/session/QueryCacheManager.js +25 -3
- package/dist/session/chromeOwners.d.ts +34 -0
- package/dist/session/chromeOwners.js +51 -0
- package/dist/session/cleanup/staleSession.d.ts +11 -1
- package/dist/session/cleanup/staleSession.js +17 -6
- package/dist/session/cleanup/userCommands.js +2 -4
- package/dist/session/metadata.d.ts +5 -1
- package/dist/session/metadata.js +2 -1
- package/dist/session/paths.d.ts +77 -3
- package/dist/session/paths.js +111 -5
- package/dist/session/port.d.ts +31 -7
- package/dist/session/port.js +50 -43
- package/dist/session/portClaims.d.ts +66 -0
- package/dist/session/portClaims.js +284 -0
- package/dist/session/sessionList.d.ts +58 -0
- package/dist/session/sessionList.js +199 -0
- package/dist/session/sessionName.d.ts +46 -0
- package/dist/session/sessionName.js +97 -0
- package/dist/telemetry/a11y.d.ts +18 -3
- package/dist/telemetry/a11y.js +170 -29
- package/dist/telemetry/console.d.ts +1 -0
- package/dist/telemetry/console.js +100 -5
- package/dist/telemetry/network.js +3 -1
- package/dist/telemetry/requestKinds.d.ts +32 -0
- package/dist/telemetry/requestKinds.js +61 -0
- package/dist/telemetry/requestState.d.ts +31 -0
- package/dist/telemetry/requestState.js +38 -0
- package/dist/types.d.ts +112 -3
- package/dist/ui/formatters/a11y.js +3 -0
- package/dist/ui/formatters/console/chronological.d.ts +8 -0
- package/dist/ui/formatters/console/chronological.js +17 -4
- package/dist/ui/formatters/console/json.js +3 -4
- package/dist/ui/formatters/console/shared.d.ts +12 -0
- package/dist/ui/formatters/console.d.ts +2 -2
- package/dist/ui/formatters/console.js +1 -1
- package/dist/ui/formatters/details.d.ts +8 -0
- package/dist/ui/formatters/details.js +61 -4
- package/dist/ui/formatters/dom.d.ts +27 -14
- package/dist/ui/formatters/dom.js +88 -59
- package/dist/ui/formatters/form.js +29 -18
- package/dist/ui/formatters/inspect.d.ts +39 -0
- package/dist/ui/formatters/inspect.js +596 -0
- package/dist/ui/formatters/keyAttributes.d.ts +19 -0
- package/dist/ui/formatters/keyAttributes.js +84 -0
- package/dist/ui/formatters/layout.d.ts +31 -0
- package/dist/ui/formatters/layout.js +53 -0
- package/dist/ui/formatters/listeners.d.ts +3 -2
- package/dist/ui/formatters/listeners.js +73 -9
- package/dist/ui/formatters/networkHeaders.d.ts +13 -0
- package/dist/ui/formatters/networkHeaders.js +36 -3
- package/dist/ui/formatters/networkList.d.ts +29 -1
- package/dist/ui/formatters/networkList.js +86 -20
- package/dist/ui/formatters/preview.js +2 -1
- package/dist/ui/formatters/requestStatus.d.ts +1 -17
- package/dist/ui/formatters/requestStatus.js +2 -30
- package/dist/ui/formatters/sessions.d.ts +12 -0
- package/dist/ui/formatters/sessions.js +40 -0
- package/dist/ui/formatters/status.d.ts +21 -2
- package/dist/ui/formatters/status.js +47 -10
- package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
- package/dist/ui/formatters/triggeredRequests.js +65 -0
- package/dist/ui/formatting.d.ts +19 -0
- package/dist/ui/formatting.js +31 -36
- package/dist/ui/messages/chrome.d.ts +9 -0
- package/dist/ui/messages/chrome.js +17 -5
- package/dist/ui/messages/commands.d.ts +504 -14
- package/dist/ui/messages/commands.js +835 -21
- package/dist/ui/messages/consoleMessages.d.ts +10 -0
- package/dist/ui/messages/consoleMessages.js +17 -0
- package/dist/ui/messages/hints.js +2 -1
- package/dist/ui/messages/networkMessages.d.ts +14 -0
- package/dist/ui/messages/networkMessages.js +18 -0
- package/dist/ui/messages/preview.js +5 -4
- package/dist/ui/messages/session.d.ts +30 -21
- package/dist/ui/messages/session.js +48 -26
- package/dist/ui/messages/sessionCommand.d.ts +43 -0
- package/dist/ui/messages/sessionCommand.js +52 -0
- package/dist/utils/async.d.ts +17 -0
- package/dist/utils/async.js +36 -0
- package/dist/utils/color.d.ts +84 -0
- package/dist/utils/color.js +376 -0
- package/dist/utils/cssValues.d.ts +109 -0
- package/dist/utils/cssValues.js +236 -0
- package/dist/utils/http.d.ts +22 -1
- package/dist/utils/http.js +28 -9
- package/dist/utils/selectorFilters.d.ts +48 -8
- package/dist/utils/selectorFilters.js +296 -53
- package/dist/utils/shellDetection.d.ts +8 -2
- package/dist/utils/shellDetection.js +120 -33
- package/dist/utils/suggestions.d.ts +26 -0
- package/dist/utils/suggestions.js +73 -0
- package/dist/utils/taskMappings.js +10 -0
- package/dist/utils/url.d.ts +12 -2
- package/dist/utils/url.js +69 -7
- package/package.json +1 -1
- package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
- package/dist/ui/formatters/sessionFormatters.js +0 -121
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of the Chrome bdg launched.
|
|
3
|
+
*
|
|
4
|
+
* chrome-launcher only waits until something answers on 127.0.0.1:<port>. That
|
|
5
|
+
* may be another process: when 127.0.0.1:<port> is taken, Chrome listens on
|
|
6
|
+
* [::1]:<port> instead and the readiness check is answered by the other one.
|
|
7
|
+
*
|
|
8
|
+
* Chrome prints `DevTools listening on ws://<host>:<port>/devtools/browser/<id>`
|
|
9
|
+
* to stderr (chrome-launcher writes it to `chrome-err.log` in the profile).
|
|
10
|
+
* The browser path is unique per browser, so comparing it with the one
|
|
11
|
+
* 127.0.0.1:<port> reports tells whether that is the launched Chrome.
|
|
12
|
+
* (Chrome's `DevToolsActivePort` file holds the same, but Chrome writes it only
|
|
13
|
+
* for `--remote-debugging-port=0`.)
|
|
14
|
+
*
|
|
15
|
+
* A Chrome whose output lacks the line (stderr redirected, a Chromium build
|
|
16
|
+
* that does not print it) is accepted with a warning when its browser answers
|
|
17
|
+
* on 127.0.0.1 and nothing listens on [::1]: the port was free on both right
|
|
18
|
+
* before the launch, and a second listener is how a conflict shows.
|
|
19
|
+
*/
|
|
20
|
+
import { createLogger } from '../ui/logging/index.js';
|
|
21
|
+
import { delay } from '../utils/async.js';
|
|
22
|
+
import { fetchBrowserWsUrl } from '../utils/http.js';
|
|
23
|
+
import { isProcessAlive } from '../utils/process.js';
|
|
24
|
+
import { ChromeLaunchError } from './errors.js';
|
|
25
|
+
import { acceptsConnections } from './portReservation.js';
|
|
26
|
+
import { readStartupLines } from './startupExit.js';
|
|
27
|
+
/** Chrome's stderr line announcing its debugging endpoint */
|
|
28
|
+
const LISTENING_PATTERN = /^DevTools listening on (ws:\/\/\S+)/;
|
|
29
|
+
/** How long to wait for Chrome to announce its endpoint after the port answered */
|
|
30
|
+
const ENDPOINT_WAIT_MS = 10000;
|
|
31
|
+
const ENDPOINT_POLL_MS = 50;
|
|
32
|
+
const log = createLogger('chrome');
|
|
33
|
+
/**
|
|
34
|
+
* Find the endpoint Chrome announced in its output (the last announcement).
|
|
35
|
+
*
|
|
36
|
+
* @param lines - Chrome's output lines
|
|
37
|
+
* @returns Endpoint, or null if there is no complete announcement
|
|
38
|
+
*/
|
|
39
|
+
export function parseDevToolsListening(lines) {
|
|
40
|
+
for (const line of [...lines].reverse()) {
|
|
41
|
+
const match = LISTENING_PATTERN.exec(line.trim());
|
|
42
|
+
if (!match?.[1])
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(match[1]);
|
|
46
|
+
const port = Number(url.port);
|
|
47
|
+
if (!port || !url.pathname.startsWith('/devtools/browser/'))
|
|
48
|
+
continue;
|
|
49
|
+
return { host: url.hostname, port, browserPath: url.pathname };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
log.debug(`Ignoring malformed DevTools announcement: ${String(error)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Wait for Chrome to announce its endpoint.
|
|
59
|
+
*
|
|
60
|
+
* @param logs - Chrome's log positions from before the launch
|
|
61
|
+
* @param isRunning - Whether Chrome is still running (waiting stops when not)
|
|
62
|
+
* @param timeoutMs - Longest wait
|
|
63
|
+
* @returns Endpoint, or null if Chrome exited or announced none in time
|
|
64
|
+
*/
|
|
65
|
+
export async function waitForDevToolsEndpoint(logs, isRunning, timeoutMs = ENDPOINT_WAIT_MS) {
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const endpoint = parseDevToolsListening(readStartupLines(logs));
|
|
69
|
+
if (endpoint || !isRunning() || Date.now() >= deadline)
|
|
70
|
+
return endpoint;
|
|
71
|
+
await delay(ENDPOINT_POLL_MS);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check that the Chrome answering on 127.0.0.1:<port> is the one just
|
|
76
|
+
* launched, so bdg never drives another session's browser.
|
|
77
|
+
*
|
|
78
|
+
* @param options - Chrome's log positions from before the launch, requested
|
|
79
|
+
* port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID and
|
|
80
|
+
* longest wait for its announcement
|
|
81
|
+
* @throws ChromeLaunchError: CHROME_DIED_AFTER_LAUNCH if Chrome exits first;
|
|
82
|
+
* PORT_IN_USE if another browser or process answers on the port;
|
|
83
|
+
* CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers
|
|
84
|
+
*/
|
|
85
|
+
export async function verifyLaunchedChrome(options) {
|
|
86
|
+
const { logs, port, pid, timeoutMs } = options;
|
|
87
|
+
const isRunning = () => isProcessAlive(pid);
|
|
88
|
+
const endpoint = await waitForDevToolsEndpoint(logs, isRunning, timeoutMs);
|
|
89
|
+
if (!endpoint && !isRunning()) {
|
|
90
|
+
throw new ChromeLaunchError(`Chrome died immediately after launch (PID: ${pid})`, {
|
|
91
|
+
issue: { code: 'CHROME_DIED_AFTER_LAUNCH', context: { port, pid } },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (!endpoint)
|
|
95
|
+
return acceptUnannouncedChrome(logs, port);
|
|
96
|
+
if (endpoint.port !== port) {
|
|
97
|
+
throw portTakenError(port, `Chrome listens on port ${endpoint.port} instead`);
|
|
98
|
+
}
|
|
99
|
+
const answering = await fetchBrowserWsUrl(port, log);
|
|
100
|
+
if (!answering || new URL(answering).pathname !== endpoint.browserPath) {
|
|
101
|
+
throw portTakenError(port, `another process answers on 127.0.0.1 (Chrome listens on ${endpoint.host})`);
|
|
102
|
+
}
|
|
103
|
+
log.debug(`Chrome on port ${port} is the launched one (${endpoint.browserPath})`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Accept a Chrome that did not announce its endpoint, if a browser answers on
|
|
107
|
+
* 127.0.0.1:<port> and nothing listens on [::1]:<port> (two listeners mean a
|
|
108
|
+
* conflict).
|
|
109
|
+
*
|
|
110
|
+
* @param logs - Chrome's log positions (the stderr log is named in messages)
|
|
111
|
+
* @param port - Requested port
|
|
112
|
+
* @throws ChromeLaunchError: PORT_IN_USE for a second listener;
|
|
113
|
+
* CHROME_LAUNCH_FAILED if no browser answers
|
|
114
|
+
*/
|
|
115
|
+
async function acceptUnannouncedChrome(logs, port) {
|
|
116
|
+
const errLog = logs.files[0]?.file ?? 'chrome-err.log';
|
|
117
|
+
const missing = `Chrome did not print "DevTools listening on" to ${errLog}`;
|
|
118
|
+
if (!(await fetchBrowserWsUrl(port, log))) {
|
|
119
|
+
throw new ChromeLaunchError(`${missing}, and no browser answers on port ${port}`, {
|
|
120
|
+
issue: {
|
|
121
|
+
code: 'CHROME_LAUNCH_FAILED',
|
|
122
|
+
context: { port, reason: `${missing}, and no browser answers on port ${port}` },
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (await acceptsConnections(port, '::1')) {
|
|
127
|
+
throw portTakenError(port, `${missing}, and both 127.0.0.1 and [::1] answer`);
|
|
128
|
+
}
|
|
129
|
+
log.info(`Warning: ${missing}; using the browser on 127.0.0.1:${port}, which was free before the launch`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The error for a port another process answers on.
|
|
133
|
+
*
|
|
134
|
+
* @param port - Requested port
|
|
135
|
+
* @param reason - What was found
|
|
136
|
+
* @returns Launch error with the PORT_IN_USE issue
|
|
137
|
+
*/
|
|
138
|
+
function portTakenError(port, reason) {
|
|
139
|
+
return new ChromeLaunchError(`Port ${port} is already in use: ${reason}`, {
|
|
140
|
+
issue: { code: 'PORT_IN_USE', context: { port, reason } },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=chromeIdentity.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome profile preferences writer.
|
|
3
|
+
*
|
|
4
|
+
* Chrome reads `<user-data-dir>/Default/Preferences` as nested JSON and looks
|
|
5
|
+
* preferences up by path (`profile.password_manager_enabled` is
|
|
6
|
+
* `{"profile": {"password_manager_enabled": ...}}`). bdg's preferences use
|
|
7
|
+
* dotted names, so they are expanded into that structure and deep-merged into
|
|
8
|
+
* the existing file: persistent profiles reused across sessions get them too,
|
|
9
|
+
* and everything else Chrome stored there is kept.
|
|
10
|
+
*/
|
|
11
|
+
import type { Logger } from '../types.js';
|
|
12
|
+
/** JSON object as stored in the Preferences file */
|
|
13
|
+
type PrefsObject = Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* Merge preferences into an existing Preferences object.
|
|
16
|
+
*
|
|
17
|
+
* Dotted names become nested paths, and a literal dotted top-level key (as
|
|
18
|
+
* earlier bdg versions wrote, which Chrome ignored) is removed.
|
|
19
|
+
*
|
|
20
|
+
* @param existing - Current Preferences content
|
|
21
|
+
* @param prefs - Preferences to apply, dotted or nested
|
|
22
|
+
* @returns Merged Preferences (a new object; `existing` is not modified)
|
|
23
|
+
*/
|
|
24
|
+
export declare function mergePreferences(existing: PrefsObject, prefs: PrefsObject): PrefsObject;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a running Chrome has the profile open, judged by its
|
|
27
|
+
* `SingletonLock` symlink (`<host>-<pid>`, macOS and Linux).
|
|
28
|
+
*
|
|
29
|
+
* @param userDataDir - Chrome user data directory
|
|
30
|
+
* @returns True if the lock names a live process on this host
|
|
31
|
+
*/
|
|
32
|
+
export declare function profileInUse(userDataDir: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Apply preferences to the default profile in a Chrome user data directory.
|
|
35
|
+
*
|
|
36
|
+
* Failures are logged (visible in daemon.log), not thrown: Chrome still
|
|
37
|
+
* starts, only without them. An unreadable Preferences file and a profile
|
|
38
|
+
* that a running Chrome has open are left alone, and the file is not
|
|
39
|
+
* rewritten when nothing changes.
|
|
40
|
+
*
|
|
41
|
+
* @param userDataDir - Chrome user data directory
|
|
42
|
+
* @param prefs - Preferences to apply, dotted or nested
|
|
43
|
+
* @param logger - Logger for failures
|
|
44
|
+
*/
|
|
45
|
+
export declare function writeProfilePreferences(userDataDir: string, prefs: PrefsObject, logger: Logger): void;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=profilePreferences.d.ts.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome profile preferences writer.
|
|
3
|
+
*
|
|
4
|
+
* Chrome reads `<user-data-dir>/Default/Preferences` as nested JSON and looks
|
|
5
|
+
* preferences up by path (`profile.password_manager_enabled` is
|
|
6
|
+
* `{"profile": {"password_manager_enabled": ...}}`). bdg's preferences use
|
|
7
|
+
* dotted names, so they are expanded into that structure and deep-merged into
|
|
8
|
+
* the existing file: persistent profiles reused across sessions get them too,
|
|
9
|
+
* and everything else Chrome stored there is kept.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { chromePrefsNotAppliedMessage } from '../../ui/messages/chrome.js';
|
|
15
|
+
import { getErrorMessage } from '../../utils/errors.js';
|
|
16
|
+
import { isProcessAlive } from '../../utils/process.js';
|
|
17
|
+
/**
|
|
18
|
+
* Whether a value is a plain JSON object (not an array or null).
|
|
19
|
+
*
|
|
20
|
+
* @param value - Value to check
|
|
21
|
+
* @returns True for objects that can be merged into
|
|
22
|
+
*/
|
|
23
|
+
function isPrefsObject(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Set a preference at a nested path, merging objects with what is there.
|
|
28
|
+
*
|
|
29
|
+
* @param target - Preferences object to modify
|
|
30
|
+
* @param keys - Path segments, e.g. `['profile', 'exit_type']`
|
|
31
|
+
* @param value - Value to set
|
|
32
|
+
*/
|
|
33
|
+
function setAtPath(target, keys, value) {
|
|
34
|
+
const [key, ...rest] = keys;
|
|
35
|
+
if (key === undefined)
|
|
36
|
+
return;
|
|
37
|
+
const current = target[key];
|
|
38
|
+
if (rest.length > 0) {
|
|
39
|
+
const child = isPrefsObject(current) ? current : {};
|
|
40
|
+
target[key] = child;
|
|
41
|
+
setAtPath(child, rest, value);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (isPrefsObject(current) && isPrefsObject(value)) {
|
|
45
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
46
|
+
setAtPath(current, [childKey], childValue);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
target[key] = value;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Merge preferences into an existing Preferences object.
|
|
54
|
+
*
|
|
55
|
+
* Dotted names become nested paths, and a literal dotted top-level key (as
|
|
56
|
+
* earlier bdg versions wrote, which Chrome ignored) is removed.
|
|
57
|
+
*
|
|
58
|
+
* @param existing - Current Preferences content
|
|
59
|
+
* @param prefs - Preferences to apply, dotted or nested
|
|
60
|
+
* @returns Merged Preferences (a new object; `existing` is not modified)
|
|
61
|
+
*/
|
|
62
|
+
export function mergePreferences(existing, prefs) {
|
|
63
|
+
const merged = structuredClone(existing);
|
|
64
|
+
for (const [name, value] of Object.entries(prefs)) {
|
|
65
|
+
if (name.includes('.'))
|
|
66
|
+
delete merged[name];
|
|
67
|
+
setAtPath(merged, name.split('.'), value);
|
|
68
|
+
}
|
|
69
|
+
return merged;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read the profile's Preferences file.
|
|
73
|
+
*
|
|
74
|
+
* @param file - Preferences file path
|
|
75
|
+
* @returns Its content, `{}` if missing
|
|
76
|
+
* @throws Error if it is not valid JSON or not a JSON object
|
|
77
|
+
*/
|
|
78
|
+
function readPreferencesFile(file) {
|
|
79
|
+
if (!fs.existsSync(file))
|
|
80
|
+
return {};
|
|
81
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
82
|
+
if (!isPrefsObject(parsed))
|
|
83
|
+
throw new Error('not a JSON object');
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether a running Chrome has the profile open, judged by its
|
|
88
|
+
* `SingletonLock` symlink (`<host>-<pid>`, macOS and Linux).
|
|
89
|
+
*
|
|
90
|
+
* @param userDataDir - Chrome user data directory
|
|
91
|
+
* @returns True if the lock names a live process on this host
|
|
92
|
+
*/
|
|
93
|
+
export function profileInUse(userDataDir) {
|
|
94
|
+
let target;
|
|
95
|
+
try {
|
|
96
|
+
target = fs.readlinkSync(path.join(userDataDir, 'SingletonLock'));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const separator = target.lastIndexOf('-');
|
|
102
|
+
const pid = Number(target.slice(separator + 1));
|
|
103
|
+
return (target.slice(0, separator) === os.hostname() && Number.isInteger(pid) && isProcessAlive(pid));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Replace a file atomically: write a temporary file next to it, then rename.
|
|
107
|
+
*
|
|
108
|
+
* @param file - File to replace
|
|
109
|
+
* @param content - New content
|
|
110
|
+
*/
|
|
111
|
+
function writeFileAtomic(file, content) {
|
|
112
|
+
const temp = `${file}.bdg-${process.pid}.tmp`;
|
|
113
|
+
try {
|
|
114
|
+
fs.writeFileSync(temp, content, 'utf8');
|
|
115
|
+
fs.renameSync(temp, file);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
fs.rmSync(temp, { force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Apply preferences to the default profile in a Chrome user data directory.
|
|
123
|
+
*
|
|
124
|
+
* Failures are logged (visible in daemon.log), not thrown: Chrome still
|
|
125
|
+
* starts, only without them. An unreadable Preferences file and a profile
|
|
126
|
+
* that a running Chrome has open are left alone, and the file is not
|
|
127
|
+
* rewritten when nothing changes.
|
|
128
|
+
*
|
|
129
|
+
* @param userDataDir - Chrome user data directory
|
|
130
|
+
* @param prefs - Preferences to apply, dotted or nested
|
|
131
|
+
* @param logger - Logger for failures
|
|
132
|
+
*/
|
|
133
|
+
export function writeProfilePreferences(userDataDir, prefs, logger) {
|
|
134
|
+
const file = path.join(userDataDir, 'Default', 'Preferences');
|
|
135
|
+
if (profileInUse(userDataDir)) {
|
|
136
|
+
logger.info(chromePrefsNotAppliedMessage(file, 'the profile is in use by a running Chrome'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const existing = readPreferencesFile(file);
|
|
141
|
+
const content = JSON.stringify(mergePreferences(existing, prefs));
|
|
142
|
+
if (content === JSON.stringify(existing))
|
|
143
|
+
return;
|
|
144
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
145
|
+
writeFileAtomic(file, content);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
logger.info(chromePrefsNotAppliedMessage(file, getErrorMessage(error)));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=profilePreferences.js.map
|
|
@@ -7,7 +7,11 @@ import type { Options as ChromeLaunchOptions } from 'chrome-launcher';
|
|
|
7
7
|
export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'connectionPollInterval' | 'maxConnectionRetries' | 'portStrictMode' | 'envVars' | 'handleSIGINT' | 'ignoreDefaultFlags' | 'chromeFlags' | 'chromePath'> {
|
|
8
8
|
/** Remote debugging port (defaults to 9222 when omitted) */
|
|
9
9
|
port?: number;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Directory for Chrome profile data, chosen by the user: bdg's default
|
|
12
|
+
* preferences are not written into it. Falls back to the bdg-managed
|
|
13
|
+
* persistent profile in the session directory (~/.bdg/chrome-profile)
|
|
14
|
+
*/
|
|
11
15
|
userDataDir?: string | undefined;
|
|
12
16
|
/** Base directory for creating user data dir (defaults to OS temp dir, injectable for testing) */
|
|
13
17
|
baseDir?: string | undefined;
|
|
@@ -23,7 +27,7 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
|
|
|
23
27
|
prefsFile?: string | undefined;
|
|
24
28
|
/** Override Chrome binary detection with an explicit path */
|
|
25
29
|
chromePath?: string;
|
|
26
|
-
/** bdg session directory
|
|
30
|
+
/** bdg session directory: recorded as a marker flag on the Chrome command line, and holds the default profile */
|
|
27
31
|
sessionDir?: string | undefined;
|
|
28
32
|
}
|
|
29
33
|
/**
|
|
@@ -46,4 +50,19 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
|
|
|
46
50
|
* Uses chrome-launcher for cross-platform Chrome detection and launching.
|
|
47
51
|
*/
|
|
48
52
|
export declare function launchChrome(options?: LaunchOptions): Promise<LaunchedChrome>;
|
|
53
|
+
/**
|
|
54
|
+
* Preferences for the launched profile: bdg's defaults (no translate prompt,
|
|
55
|
+
* no password manager or leak check, whose bubbles capture input) for
|
|
56
|
+
* profiles bdg manages, with the caller's preferences on top. A profile the
|
|
57
|
+
* user chose (`--user-data-dir`) gets only preferences the caller passed.
|
|
58
|
+
*
|
|
59
|
+
* File-based preferences take precedence over inline preferences because
|
|
60
|
+
* files allow for complex, reusable configurations that can be version
|
|
61
|
+
* controlled and shared across team members or CI environments.
|
|
62
|
+
*
|
|
63
|
+
* @param options - Launch options containing prefs or prefsFile
|
|
64
|
+
* @returns Merged preferences (empty when there is nothing to apply)
|
|
65
|
+
* @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveChromePrefs(options: LaunchOptions): Record<string, unknown>;
|
|
49
68
|
//# sourceMappingURL=launcher.d.ts.map
|
|
@@ -6,10 +6,12 @@ import { BDG_CHROME_PREFS, DEFAULT_CDP_PORT, CHROME_PROFILE_DIR, DEFAULT_CHROME_
|
|
|
6
6
|
import { getErrorMessage } from '../utils/errors.js';
|
|
7
7
|
import { filterDefined } from '../utils/objects.js';
|
|
8
8
|
import { isProcessAlive } from '../utils/process.js';
|
|
9
|
+
import { verifyLaunchedChrome } from './chromeIdentity.js';
|
|
9
10
|
import { ChromeLaunchError } from './errors.js';
|
|
10
11
|
import { resolveChromeBinary } from './launcher/binaryResolver.js';
|
|
11
12
|
import { buildChromeFlags } from './launcher/flagsBuilder.js';
|
|
12
13
|
import { loadChromePrefs, ensureJSONCompatiblePrefs } from './launcher/preferencesLoader.js';
|
|
14
|
+
import { writeProfilePreferences } from './launcher/profilePreferences.js';
|
|
13
15
|
import { reservePort } from './portReservation.js';
|
|
14
16
|
import { markStartupLogs, watchStartupExit } from './startupExit.js';
|
|
15
17
|
/**
|
|
@@ -57,7 +59,7 @@ export async function launchChrome(options = {}) {
|
|
|
57
59
|
}
|
|
58
60
|
const reservation = await reservePort(port);
|
|
59
61
|
reservation.release();
|
|
60
|
-
const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir);
|
|
62
|
+
const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir ?? options.sessionDir);
|
|
61
63
|
if (!fs.existsSync(userDataDir)) {
|
|
62
64
|
try {
|
|
63
65
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
@@ -74,9 +76,11 @@ export async function launchChrome(options = {}) {
|
|
|
74
76
|
}
|
|
75
77
|
logger.info(`Launching Chrome on port ${port}...`);
|
|
76
78
|
logger.debug(`User data directory: ${userDataDir}`);
|
|
77
|
-
|
|
79
|
+
applyProfilePreferences(userDataDir, options, logger);
|
|
80
|
+
const chromeOptions = buildChromeOptions({ ...options, port });
|
|
78
81
|
const launcher = new chromeLauncher.Launcher(chromeOptions);
|
|
79
|
-
const
|
|
82
|
+
const logs = markStartupLogs(userDataDir);
|
|
83
|
+
const startup = watchStartupExit(() => launcher.chromeProcess, logs, userDataDir);
|
|
80
84
|
try {
|
|
81
85
|
const launchStart = Date.now();
|
|
82
86
|
logger.info('Waiting for Chrome to be ready...');
|
|
@@ -96,10 +100,11 @@ export async function launchChrome(options = {}) {
|
|
|
96
100
|
},
|
|
97
101
|
});
|
|
98
102
|
}
|
|
103
|
+
await verifyLaunchedChrome({ logs, port, pid: chromeProcessPid });
|
|
99
104
|
logger.info(`Chrome launched successfully (PID: ${chromeProcessPid}, ${launchDurationMs}ms)`);
|
|
100
105
|
return {
|
|
101
106
|
pid: chromeProcessPid,
|
|
102
|
-
port
|
|
107
|
+
port,
|
|
103
108
|
userDataDir: launcher.userDataDir,
|
|
104
109
|
kill: async () => {
|
|
105
110
|
return Promise.resolve().then(() => {
|
|
@@ -128,7 +133,9 @@ export async function launchChrome(options = {}) {
|
|
|
128
133
|
/**
|
|
129
134
|
* Get the default persistent user-data-dir path.
|
|
130
135
|
*
|
|
131
|
-
* Uses the session directory (
|
|
136
|
+
* Uses the session directory (`~/.bdg`, `$BDG_SESSION_DIR`, or
|
|
137
|
+
* `<base>/sessions/<name>` for a named session, passed as `sessionDir`) to
|
|
138
|
+
* store Chrome profile data,
|
|
132
139
|
* ensuring each session has its own isolated Chrome profile. This prevents
|
|
133
140
|
* SingletonLock conflicts when multiple agents run concurrently.
|
|
134
141
|
*
|
|
@@ -136,7 +143,7 @@ export async function launchChrome(options = {}) {
|
|
|
136
143
|
* For shared login state across sessions, use --user-data-dir to specify a
|
|
137
144
|
* shared location explicitly.
|
|
138
145
|
*
|
|
139
|
-
* @param baseDir - Optional base directory (defaults to
|
|
146
|
+
* @param baseDir - Optional base directory (defaults to `~/.bdg` or `$BDG_SESSION_DIR`). Allows injection for testing or custom locations.
|
|
140
147
|
* @returns Absolute path to session-isolated user-data-dir
|
|
141
148
|
* @throws Error if user data directory cannot be created due to permission issues
|
|
142
149
|
*/
|
|
@@ -168,32 +175,52 @@ function getPersistentUserDataDir(baseDir) {
|
|
|
168
175
|
return userDataDir;
|
|
169
176
|
}
|
|
170
177
|
/**
|
|
171
|
-
*
|
|
178
|
+
* Preferences for the launched profile: bdg's defaults (no translate prompt,
|
|
179
|
+
* no password manager or leak check, whose bubbles capture input) for
|
|
180
|
+
* profiles bdg manages, with the caller's preferences on top. A profile the
|
|
181
|
+
* user chose (`--user-data-dir`) gets only preferences the caller passed.
|
|
172
182
|
*
|
|
173
183
|
* File-based preferences take precedence over inline preferences because
|
|
174
184
|
* files allow for complex, reusable configurations that can be version
|
|
175
185
|
* controlled and shared across team members or CI environments.
|
|
176
186
|
*
|
|
177
187
|
* @param options - Launch options containing prefs or prefsFile
|
|
178
|
-
* @returns
|
|
179
|
-
* @throws ChromeLaunchError if prefs file cannot be read
|
|
188
|
+
* @returns Merged preferences (empty when there is nothing to apply)
|
|
189
|
+
* @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
|
|
180
190
|
*/
|
|
191
|
+
export function resolveChromePrefs(options) {
|
|
192
|
+
const defaults = options.userDataDir === undefined ? BDG_CHROME_PREFS : {};
|
|
193
|
+
const prefs = { ...defaults, ...loadChromePrefs(options) };
|
|
194
|
+
ensureJSONCompatiblePrefs(prefs);
|
|
195
|
+
return prefs;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Write the launch preferences into the profile, if there are any.
|
|
199
|
+
*
|
|
200
|
+
* @param userDataDir - Chrome user data directory
|
|
201
|
+
* @param options - Launch options
|
|
202
|
+
* @param logger - Logger for failures
|
|
203
|
+
* @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
|
|
204
|
+
*/
|
|
205
|
+
function applyProfilePreferences(userDataDir, options, logger) {
|
|
206
|
+
const prefs = resolveChromePrefs(options);
|
|
207
|
+
if (Object.keys(prefs).length > 0)
|
|
208
|
+
writeProfilePreferences(userDataDir, prefs, logger);
|
|
209
|
+
}
|
|
181
210
|
/**
|
|
182
211
|
* Build chrome-launcher options from bdg launch options.
|
|
183
212
|
*
|
|
184
213
|
* Maps LaunchOptions to chrome-launcher API format using a clean utility approach
|
|
185
|
-
* that filters out undefined values automatically.
|
|
186
|
-
*
|
|
214
|
+
* that filters out undefined values automatically. Preferences are not passed:
|
|
215
|
+
* chrome-launcher would replace whole nested sections of an existing profile's
|
|
216
|
+
* Preferences, so `writeProfilePreferences` merges them instead.
|
|
187
217
|
*
|
|
188
218
|
* @param options - bdg launch options to convert
|
|
189
219
|
* @returns chrome-launcher compatible options object
|
|
190
|
-
* @throws ChromeLaunchError if preference loading fails
|
|
191
220
|
*/
|
|
192
221
|
function buildChromeOptions(options) {
|
|
193
|
-
const
|
|
194
|
-
const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir);
|
|
222
|
+
const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir ?? options.sessionDir);
|
|
195
223
|
const chromePathOverride = resolveChromeBinary(options);
|
|
196
|
-
const mergedPrefs = userPrefs ? { ...BDG_CHROME_PREFS, ...userPrefs } : BDG_CHROME_PREFS;
|
|
197
224
|
return {
|
|
198
225
|
logLevel: options.logLevel ?? DEFAULT_CHROME_LOG_LEVEL,
|
|
199
226
|
handleSIGINT: options.handleSIGINT ?? DEFAULT_CHROME_HANDLE_SIGINT,
|
|
@@ -206,7 +233,6 @@ function buildChromeOptions(options) {
|
|
|
206
233
|
connectionPollInterval: options.connectionPollInterval ?? CHROME_READY_POLL_MS,
|
|
207
234
|
maxConnectionRetries: options.maxConnectionRetries ?? CHROME_READY_POLL_ATTEMPTS,
|
|
208
235
|
portStrictMode: options.portStrictMode,
|
|
209
|
-
prefs: ensureJSONCompatiblePrefs(mergedPrefs),
|
|
210
236
|
envVars: options.envVars,
|
|
211
237
|
chromePath: chromePathOverride,
|
|
212
238
|
}),
|
|
@@ -12,14 +12,24 @@ export interface PortReservation {
|
|
|
12
12
|
release: () => void;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Whether something already accepts connections on
|
|
16
|
-
* listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from binding
|
|
17
|
-
* 127.0.0.1 on macOS, but Chrome would then fail to listen, so it is checked
|
|
18
|
-
* by connecting.
|
|
15
|
+
* Whether something already accepts connections on host:port.
|
|
19
16
|
*
|
|
20
17
|
* @param port - Port to check
|
|
18
|
+
* @param host - Address to connect to
|
|
21
19
|
* @returns True if a connection was accepted
|
|
22
20
|
*/
|
|
21
|
+
export declare function acceptsConnections(port: number, host: string): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Whether something already accepts connections on the port, on 127.0.0.1 or
|
|
24
|
+
* ::1. A listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from
|
|
25
|
+
* binding 127.0.0.1 on macOS, but Chrome would then fail to listen; a Chrome
|
|
26
|
+
* on [::1] (one that found 127.0.0.1 taken) does not stop it either, but a
|
|
27
|
+
* second Chrome would be reached through the first one's port. Both are found
|
|
28
|
+
* by connecting.
|
|
29
|
+
*
|
|
30
|
+
* @param port - Port to check
|
|
31
|
+
* @returns True if a connection was accepted on either address
|
|
32
|
+
*/
|
|
23
33
|
export declare function isPortAnswering(port: number): Promise<boolean>;
|
|
24
34
|
/**
|
|
25
35
|
* Atomically reserve a port to prevent race conditions during Chrome launch.
|
|
@@ -8,18 +8,18 @@ import * as net from 'net';
|
|
|
8
8
|
import { ChromeLaunchError } from './errors.js';
|
|
9
9
|
/** How long a connection attempt may take when checking a port */
|
|
10
10
|
const CONNECT_CHECK_MS = 500;
|
|
11
|
+
/** Loopback addresses Chrome may listen on (it falls back to IPv6 when IPv4 is taken) */
|
|
12
|
+
const LOOPBACK_HOSTS = ['127.0.0.1', '::1'];
|
|
11
13
|
/**
|
|
12
|
-
* Whether something already accepts connections on
|
|
13
|
-
* listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from binding
|
|
14
|
-
* 127.0.0.1 on macOS, but Chrome would then fail to listen, so it is checked
|
|
15
|
-
* by connecting.
|
|
14
|
+
* Whether something already accepts connections on host:port.
|
|
16
15
|
*
|
|
17
16
|
* @param port - Port to check
|
|
17
|
+
* @param host - Address to connect to
|
|
18
18
|
* @returns True if a connection was accepted
|
|
19
19
|
*/
|
|
20
|
-
export function
|
|
20
|
+
export function acceptsConnections(port, host) {
|
|
21
21
|
return new Promise((resolve) => {
|
|
22
|
-
const socket = net.connect({ port, host
|
|
22
|
+
const socket = net.connect({ port, host });
|
|
23
23
|
const finish = (answering) => {
|
|
24
24
|
socket.destroy();
|
|
25
25
|
resolve(answering);
|
|
@@ -29,6 +29,21 @@ export function isPortAnswering(port) {
|
|
|
29
29
|
socket.once('error', () => finish(false));
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether something already accepts connections on the port, on 127.0.0.1 or
|
|
34
|
+
* ::1. A listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from
|
|
35
|
+
* binding 127.0.0.1 on macOS, but Chrome would then fail to listen; a Chrome
|
|
36
|
+
* on [::1] (one that found 127.0.0.1 taken) does not stop it either, but a
|
|
37
|
+
* second Chrome would be reached through the first one's port. Both are found
|
|
38
|
+
* by connecting.
|
|
39
|
+
*
|
|
40
|
+
* @param port - Port to check
|
|
41
|
+
* @returns True if a connection was accepted on either address
|
|
42
|
+
*/
|
|
43
|
+
export async function isPortAnswering(port) {
|
|
44
|
+
const answers = await Promise.all(LOOPBACK_HOSTS.map((host) => acceptsConnections(port, host)));
|
|
45
|
+
return answers.some(Boolean);
|
|
46
|
+
}
|
|
32
47
|
/**
|
|
33
48
|
* The error for a port another process uses.
|
|
34
49
|
*
|
|
@@ -22,6 +22,14 @@ export interface StartupLogs {
|
|
|
22
22
|
* @returns Log positions
|
|
23
23
|
*/
|
|
24
24
|
export declare function markStartupLogs(userDataDir: string): StartupLogs;
|
|
25
|
+
/**
|
|
26
|
+
* Chrome's output (stderr, then stdout) since {@link markStartupLogs}. The
|
|
27
|
+
* marked offsets are byte sizes, so the files are cut before decoding.
|
|
28
|
+
*
|
|
29
|
+
* @param logs - Log positions
|
|
30
|
+
* @returns Output lines
|
|
31
|
+
*/
|
|
32
|
+
export declare function readStartupLines(logs: StartupLogs): string[];
|
|
25
33
|
/**
|
|
26
34
|
* Reject when the launched Chrome exits before it is ready.
|
|
27
35
|
*
|
|
@@ -31,18 +31,27 @@ export function markStartupLogs(userDataDir) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Chrome's output since {@link markStartupLogs}
|
|
34
|
+
* Chrome's output (stderr, then stdout) since {@link markStartupLogs}. The
|
|
35
|
+
* marked offsets are byte sizes, so the files are cut before decoding.
|
|
35
36
|
*
|
|
36
37
|
* @param logs - Log positions
|
|
37
|
-
* @returns
|
|
38
|
+
* @returns Output lines
|
|
38
39
|
*/
|
|
39
|
-
function
|
|
40
|
-
|
|
40
|
+
export function readStartupLines(logs) {
|
|
41
|
+
return logs.files.flatMap(({ file, offset }) => {
|
|
41
42
|
if (!fs.existsSync(file))
|
|
42
43
|
return [];
|
|
43
|
-
return fs.readFileSync(file
|
|
44
|
+
return fs.readFileSync(file).subarray(offset).toString('utf8').split('\n');
|
|
44
45
|
});
|
|
45
|
-
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Chrome's output since {@link markStartupLogs}, last lines first trimmed.
|
|
49
|
+
*
|
|
50
|
+
* @param logs - Log positions
|
|
51
|
+
* @returns Up to {@link OUTPUT_LINES} non-empty lines
|
|
52
|
+
*/
|
|
53
|
+
function readStartupOutput(logs) {
|
|
54
|
+
return readStartupLines(logs)
|
|
46
55
|
.map((line) => line.trim())
|
|
47
56
|
.filter(Boolean)
|
|
48
57
|
.slice(-OUTPUT_LINES);
|
package/dist/constants.d.ts
CHANGED
|
@@ -46,8 +46,12 @@ export declare const BDG_CHROME_FLAGS: string[];
|
|
|
46
46
|
export declare const DOCKER_CHROME_FLAGS: string[];
|
|
47
47
|
/**
|
|
48
48
|
* BDG-specific Chrome preferences for automation
|
|
49
|
-
*
|
|
50
|
-
* User preferences take precedence
|
|
49
|
+
* Written into the launched profile's Default/Preferences (dotted names become nested paths)
|
|
50
|
+
* before every launch, so persistent profiles get them too. User preferences take precedence.
|
|
51
|
+
*
|
|
52
|
+
* The password manager and its leak check are off: after a login their bubble
|
|
53
|
+
* (e.g. "Change your password") captures all input in headless Chrome, so
|
|
54
|
+
* later clicks silently never reach the page.
|
|
51
55
|
*
|
|
52
56
|
* Note: Crash/restore popup suppression is handled by Chrome flags (--disable-session-crashed-bubble, --disable-infobars)
|
|
53
57
|
* which are more reliable than preference-based approaches.
|