browser-debugger-cli 0.12.0 → 0.13.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/.claude/skills/bdg/SKILL.md +4 -4
- package/README.md +1 -0
- package/dist/commands/cdp.d.ts +22 -1
- package/dist/commands/cdp.js +100 -43
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/console.js +62 -12
- package/dist/commands/dom/DomElementResolver.d.ts +3 -1
- package/dist/commands/dom/DomElementResolver.js +10 -3
- package/dist/commands/dom/a11y.js +3 -2
- package/dist/commands/dom/eval.d.ts +3 -2
- package/dist/commands/dom/eval.js +11 -5
- package/dist/commands/dom/form.js +10 -9
- package/dist/commands/dom/formInteraction.js +8 -7
- package/dist/commands/dom/get.js +8 -8
- package/dist/commands/dom/helpers/index.d.ts +1 -1
- package/dist/commands/dom/helpers/index.js +1 -1
- package/dist/commands/dom/helpers/query.d.ts +27 -3
- package/dist/commands/dom/helpers/query.js +152 -64
- package/dist/commands/dom/helpers/screenshot.js +13 -13
- package/dist/commands/dom/index.js +4 -2
- package/dist/commands/dom/query.d.ts +19 -2
- package/dist/commands/dom/query.js +37 -6
- package/dist/commands/dom/screenshot.js +2 -1
- package/dist/commands/dom/semanticUtils.d.ts +3 -2
- package/dist/commands/dom/semanticUtils.js +40 -9
- package/dist/commands/helpJson.d.ts +82 -19
- package/dist/commands/helpJson.js +111 -40
- package/dist/commands/helpTopic.d.ts +16 -1
- package/dist/commands/helpTopic.js +59 -1
- package/dist/commands/installSkill.d.ts +15 -5
- package/dist/commands/installSkill.js +86 -16
- package/dist/commands/network/list.js +22 -12
- package/dist/commands/optionBehaviors.js +33 -11
- package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
- package/dist/commands/shared/daemonErrorHandler.js +20 -9
- package/dist/commands/shared/dataFetcher.d.ts +12 -4
- package/dist/commands/shared/dataFetcher.js +12 -4
- package/dist/commands/shared/followMode.d.ts +9 -1
- package/dist/commands/shared/followMode.js +22 -4
- package/dist/commands/shared/optionTypes.d.ts +4 -1
- package/dist/commands/shared/outputFile.js +6 -1
- package/dist/commands/start.d.ts +7 -5
- package/dist/commands/start.js +65 -21
- package/dist/commands/stop.d.ts +11 -0
- package/dist/commands/stop.js +24 -1
- package/dist/commands.js +1 -1
- package/dist/connection/cdp.d.ts +7 -0
- package/dist/connection/cdp.js +9 -0
- package/dist/connection/launcher.js +3 -2
- package/dist/daemon/SessionController.js +6 -1
- package/dist/daemon/launcher.d.ts +3 -2
- package/dist/daemon/launcher.js +47 -3
- package/dist/daemon/session/Session.d.ts +4 -1
- package/dist/daemon/session/Session.js +33 -2
- package/dist/daemon/session/TelemetryStore.d.ts +8 -1
- package/dist/daemon/session/TelemetryStore.js +13 -1
- package/dist/daemon/session/commandRegistry.js +29 -13
- package/dist/daemon/session/interactions.d.ts +2 -1
- package/dist/daemon/session/interactions.js +13 -1
- package/dist/daemon/session/plugins.js +16 -2
- package/dist/daemon/session/teardown.js +1 -1
- package/dist/daemon.js +1622 -748
- package/dist/errors/messages.d.ts +54 -11
- package/dist/errors/messages.js +109 -22
- package/dist/index.js +13733 -8796
- package/dist/ipc/client.d.ts +18 -2
- package/dist/ipc/client.js +26 -5
- package/dist/ipc/protocol/auditTypes.d.ts +8 -2
- package/dist/ipc/protocol/commands.d.ts +12 -0
- package/dist/ipc/protocol/domTypes.d.ts +12 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +2 -0
- package/dist/ipc/session/types.d.ts +2 -0
- package/dist/runtime/dom/actionEffects.d.ts +5 -1
- package/dist/runtime/dom/actionEffects.js +26 -14
- package/dist/runtime/dom/audit.js +3 -2
- package/dist/runtime/dom/auditModel.js +6 -1
- package/dist/runtime/dom/auditScripts.d.ts +9 -3
- package/dist/runtime/dom/auditScripts.js +41 -5
- package/dist/runtime/dom/elementGeometry.d.ts +10 -3
- package/dist/runtime/dom/elementGeometry.js +27 -4
- package/dist/runtime/dom/elementInfo.d.ts +74 -18
- package/dist/runtime/dom/elementInfo.js +187 -40
- package/dist/runtime/dom/evalHelpers.d.ts +12 -2
- package/dist/runtime/dom/evalHelpers.js +67 -7
- package/dist/runtime/dom/formDiscovery.d.ts +6 -2
- package/dist/runtime/dom/formDiscovery.js +20 -3
- package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
- package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
- package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
- package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
- package/dist/runtime/dom/formSubmitHelpers.js +4 -3
- package/dist/runtime/dom/frameLayout.js +1 -0
- package/dist/runtime/dom/inspect.js +5 -6
- package/dist/runtime/dom/inspectAllStyles.js +1 -0
- package/dist/runtime/dom/inspectHints.d.ts +1 -1
- package/dist/runtime/dom/inspectModel.d.ts +2 -1
- package/dist/runtime/dom/inspectModel.js +7 -3
- package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
- package/dist/runtime/dom/inspectPaintModel.js +3 -1
- package/dist/runtime/dom/inspectScripts.d.ts +29 -2
- package/dist/runtime/dom/inspectScripts.js +49 -10
- package/dist/runtime/dom/layout.js +9 -7
- package/dist/runtime/dom/reactEventHelpers.d.ts +14 -4
- package/dist/runtime/dom/reactEventHelpers.js +63 -27
- package/dist/runtime/dom/targetNode.d.ts +18 -5
- package/dist/runtime/dom/targetNode.js +268 -8
- package/dist/runtime/dom/wait.js +2 -1
- package/dist/runtime/page/bdgWorld.d.ts +57 -0
- package/dist/runtime/page/bdgWorld.js +180 -0
- package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
- package/dist/runtime/page/replacedBuiltins.js +136 -0
- package/dist/session/QueryCacheManager.d.ts +4 -1
- package/dist/session/QueryCacheManager.js +5 -2
- package/dist/session/chrome.d.ts +4 -1
- package/dist/session/chrome.js +7 -1
- package/dist/session/cleanup/staleSession.d.ts +21 -4
- package/dist/session/cleanup/staleSession.js +79 -9
- package/dist/session/cleanup/userCommands.d.ts +4 -1
- package/dist/session/cleanup/userCommands.js +10 -5
- package/dist/session/daemonSocket.d.ts +10 -0
- package/dist/session/daemonSocket.js +22 -0
- package/dist/session/lastSession.d.ts +6 -3
- package/dist/session/lastSession.js +11 -5
- package/dist/session/paths.d.ts +3 -1
- package/dist/session/paths.js +5 -5
- package/dist/session/portClaims.js +4 -3
- package/dist/session/sessionList.d.ts +13 -5
- package/dist/session/sessionList.js +31 -7
- package/dist/telemetry/a11y.js +2 -2
- package/dist/telemetry/console.d.ts +2 -1
- package/dist/telemetry/console.js +30 -21
- package/dist/telemetry/pageCrash.d.ts +26 -0
- package/dist/telemetry/pageCrash.js +53 -0
- package/dist/types.d.ts +16 -0
- package/dist/ui/formatters/audit.js +14 -5
- package/dist/ui/formatters/cdp.d.ts +138 -0
- package/dist/ui/formatters/cdp.js +131 -0
- package/dist/ui/formatters/console/chronological.js +3 -1
- package/dist/ui/formatters/console/follow.d.ts +2 -1
- package/dist/ui/formatters/console/follow.js +2 -2
- package/dist/ui/formatters/console/json.d.ts +2 -2
- package/dist/ui/formatters/console/json.js +11 -5
- package/dist/ui/formatters/console/shared.d.ts +30 -0
- package/dist/ui/formatters/console/shared.js +16 -0
- package/dist/ui/formatters/console/summarize.d.ts +9 -2
- package/dist/ui/formatters/console/summarize.js +40 -9
- package/dist/ui/formatters/console.d.ts +2 -1
- package/dist/ui/formatters/console.js +7 -5
- package/dist/ui/formatters/details.js +3 -1
- package/dist/ui/formatters/dom.d.ts +1 -1
- package/dist/ui/formatters/dom.js +5 -6
- package/dist/ui/formatters/helpFormatters.js +1 -1
- package/dist/ui/formatters/inspect.js +9 -3
- package/dist/ui/formatters/installSkill.d.ts +9 -1
- package/dist/ui/formatters/installSkill.js +32 -6
- package/dist/ui/formatters/layout.js +2 -1
- package/dist/ui/formatters/networkList.d.ts +1 -1
- package/dist/ui/formatters/networkList.js +1 -2
- package/dist/ui/formatters/preview.d.ts +2 -0
- package/dist/ui/formatters/preview.js +17 -7
- package/dist/ui/formatters/sessions.d.ts +2 -2
- package/dist/ui/formatters/sessions.js +9 -2
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/commands.d.ts +124 -4
- package/dist/ui/messages/commands.js +162 -7
- package/dist/ui/messages/consoleMessages.d.ts +24 -0
- package/dist/ui/messages/consoleMessages.js +32 -0
- package/dist/ui/messages/preview.d.ts +6 -0
- package/dist/ui/messages/preview.js +9 -1
- package/dist/ui/messages/session.d.ts +13 -2
- package/dist/ui/messages/session.js +22 -3
- package/dist/utils/directories.d.ts +34 -0
- package/dist/utils/directories.js +88 -0
- package/dist/utils/display.d.ts +16 -0
- package/dist/utils/display.js +42 -0
- package/dist/utils/exitCodes.d.ts +1 -0
- package/dist/utils/exitCodes.js +6 -0
- package/dist/utils/process.d.ts +12 -0
- package/dist/utils/process.js +25 -0
- package/package.json +1 -1
|
@@ -90,19 +90,25 @@ export async function fetchPreviewData(query = {}) {
|
|
|
90
90
|
* Fetch all captured network requests from daemon.
|
|
91
91
|
*
|
|
92
92
|
* @param withHeaders - Include request/response headers (needed by header filters)
|
|
93
|
-
* @returns Requests
|
|
93
|
+
* @returns Requests, and when the page crashed (while it is not loaded
|
|
94
|
+
* again), or a fetch error
|
|
94
95
|
*/
|
|
95
96
|
export async function fetchNetworkRequests(withHeaders = false) {
|
|
96
97
|
const result = await fetchPreviewData({ lastN: 0, only: 'network', withHeaders });
|
|
97
98
|
if (!result.success)
|
|
98
99
|
return result;
|
|
99
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
data: { requests: result.data.network, pageCrashedAt: result.data.output.pageCrashedAt },
|
|
103
|
+
};
|
|
100
104
|
}
|
|
101
105
|
/**
|
|
102
106
|
* Fetch all console messages from daemon.
|
|
103
107
|
*
|
|
104
|
-
* @returns Messages (with their session-wide index)
|
|
105
|
-
* the page currently loaded
|
|
108
|
+
* @returns Messages (with their session-wide index), the navigation id of
|
|
109
|
+
* the page currently loaded, how many of the oldest messages the session
|
|
110
|
+
* dropped at its limit and when the page crashed (while it is not loaded
|
|
111
|
+
* again)
|
|
106
112
|
*/
|
|
107
113
|
export async function fetchConsoleMessages() {
|
|
108
114
|
const result = await fetchPreviewData({ lastN: 0, only: 'console' });
|
|
@@ -113,6 +119,8 @@ export async function fetchConsoleMessages() {
|
|
|
113
119
|
data: {
|
|
114
120
|
messages: result.data.console,
|
|
115
121
|
currentNavigationId: result.data.output.currentNavigationId,
|
|
122
|
+
dropped: result.data.output.totals?.consoleDropped ?? 0,
|
|
123
|
+
pageCrashedAt: result.data.output.pageCrashedAt,
|
|
116
124
|
},
|
|
117
125
|
};
|
|
118
126
|
}
|
|
@@ -23,6 +23,14 @@ export declare function followFetchFailure(failure: {
|
|
|
23
23
|
json?: boolean | undefined;
|
|
24
24
|
retryIntervalMs: number;
|
|
25
25
|
}): FollowPoll;
|
|
26
|
+
/**
|
|
27
|
+
* Report each page crash of a stream once: a page loaded again that crashes
|
|
28
|
+
* again is a new crash.
|
|
29
|
+
*
|
|
30
|
+
* @returns Function taking the crash time a refresh fetched, returning it
|
|
31
|
+
* when that crash was not reported yet
|
|
32
|
+
*/
|
|
33
|
+
export declare function newPageCrashes(): (crashedAt: number | undefined) => number | undefined;
|
|
26
34
|
/**
|
|
27
35
|
* Options for configuring follow mode behavior.
|
|
28
36
|
*/
|
|
@@ -42,7 +50,7 @@ export interface FollowModeOptions {
|
|
|
42
50
|
* - Initial display of start message
|
|
43
51
|
* - First refresh call (awaited)
|
|
44
52
|
* - Periodic interval-based refresh
|
|
45
|
-
* - SIGINT
|
|
53
|
+
* - SIGINT/SIGTERM handlers that stop with 130/143, as shells expect
|
|
46
54
|
* - Stopping with the exit code a refresh returns (e.g. the session is gone)
|
|
47
55
|
*
|
|
48
56
|
* @param refreshFn - Async function to call on each refresh cycle
|
|
@@ -27,6 +27,22 @@ export function followFetchFailure(failure, options) {
|
|
|
27
27
|
? { exitCode: result.exitCode ?? EXIT_CODES.RESOURCE_NOT_FOUND }
|
|
28
28
|
: undefined;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Report each page crash of a stream once: a page loaded again that crashes
|
|
32
|
+
* again is a new crash.
|
|
33
|
+
*
|
|
34
|
+
* @returns Function taking the crash time a refresh fetched, returning it
|
|
35
|
+
* when that crash was not reported yet
|
|
36
|
+
*/
|
|
37
|
+
export function newPageCrashes() {
|
|
38
|
+
let reported;
|
|
39
|
+
return (crashedAt) => {
|
|
40
|
+
if (crashedAt === undefined || crashedAt === reported)
|
|
41
|
+
return undefined;
|
|
42
|
+
reported = crashedAt;
|
|
43
|
+
return crashedAt;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
30
46
|
/**
|
|
31
47
|
* Sets up follow mode with periodic refresh and graceful shutdown.
|
|
32
48
|
*
|
|
@@ -35,7 +51,7 @@ export function followFetchFailure(failure, options) {
|
|
|
35
51
|
* - Initial display of start message
|
|
36
52
|
* - First refresh call (awaited)
|
|
37
53
|
* - Periodic interval-based refresh
|
|
38
|
-
* - SIGINT
|
|
54
|
+
* - SIGINT/SIGTERM handlers that stop with 130/143, as shells expect
|
|
39
55
|
* - Stopping with the exit code a refresh returns (e.g. the session is gone)
|
|
40
56
|
*
|
|
41
57
|
* @param refreshFn - Async function to call on each refresh cycle
|
|
@@ -71,10 +87,12 @@ export async function setupFollowMode(refreshFn, options) {
|
|
|
71
87
|
console.error(genericError(getErrorMessage(error)));
|
|
72
88
|
});
|
|
73
89
|
}, intervalMs);
|
|
74
|
-
|
|
90
|
+
const stop = (exitCode) => {
|
|
75
91
|
clearInterval(intervalId);
|
|
76
92
|
console.error(stopMessage());
|
|
77
|
-
process.exit(
|
|
78
|
-
}
|
|
93
|
+
process.exit(exitCode);
|
|
94
|
+
};
|
|
95
|
+
process.on('SIGINT', () => stop(EXIT_CODES.INTERRUPTED));
|
|
96
|
+
process.on('SIGTERM', () => stop(EXIT_CODES.TERMINATED));
|
|
79
97
|
}
|
|
80
98
|
//# sourceMappingURL=followMode.js.map
|
|
@@ -142,7 +142,10 @@ export type DetailsCommandOptions = BaseOptions & {
|
|
|
142
142
|
id: string;
|
|
143
143
|
};
|
|
144
144
|
/** Options for DOM query command */
|
|
145
|
-
export type DomQueryCommandOptions = BaseOptions
|
|
145
|
+
export type DomQueryCommandOptions = BaseOptions & {
|
|
146
|
+
/** Matches listed (0 = all); default 50, or 1000 with --json */
|
|
147
|
+
limit?: number;
|
|
148
|
+
};
|
|
146
149
|
/** Options for DOM get command */
|
|
147
150
|
export type DomGetCommandOptions = BaseOptions & RawOptions & SelectionOptions & {
|
|
148
151
|
/** All of the element's text instead of its first 500 characters (semantic output) */
|
|
@@ -7,6 +7,7 @@ import * as path from 'path';
|
|
|
7
7
|
import { CommandError } from '../../errors/index.js';
|
|
8
8
|
import { emptyOutputPathError, outputFileError } from '../../errors/messages.js';
|
|
9
9
|
import { AtomicFileWriter } from '../../utils/atomicFile.js';
|
|
10
|
+
import { makeDirectory } from '../../utils/directories.js';
|
|
10
11
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
11
12
|
/** What went wrong with a path, by error code */
|
|
12
13
|
const PATH_PROBLEMS = {
|
|
@@ -19,6 +20,10 @@ const PATH_PROBLEMS = {
|
|
|
19
20
|
ENAMETOOLONG: { reason: 'the name is too long', exitCode: EXIT_CODES.INVALID_ARGUMENTS },
|
|
20
21
|
ENOENT: { reason: 'the directory cannot be created', exitCode: EXIT_CODES.INVALID_ARGUMENTS },
|
|
21
22
|
ENOSPC: { reason: 'no space left on the device', exitCode: EXIT_CODES.SESSION_FILE_ERROR },
|
|
23
|
+
EPSEUDOFS: {
|
|
24
|
+
reason: 'it is on a pseudo-filesystem (/proc, /sys)',
|
|
25
|
+
exitCode: EXIT_CODES.INVALID_ARGUMENTS,
|
|
26
|
+
},
|
|
22
27
|
};
|
|
23
28
|
/**
|
|
24
29
|
* A file-system error as a user-facing error about the given path.
|
|
@@ -64,7 +69,7 @@ export async function writeOutputFile(filePath, data, extension) {
|
|
|
64
69
|
assertFilePath(filePath, extension);
|
|
65
70
|
const absolutePath = path.resolve(filePath);
|
|
66
71
|
try {
|
|
67
|
-
|
|
72
|
+
makeDirectory(path.dirname(absolutePath));
|
|
68
73
|
if (typeof data === 'string')
|
|
69
74
|
await AtomicFileWriter.writeAsync(absolutePath, data);
|
|
70
75
|
else
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -84,15 +84,17 @@ export declare function parseViewport(value: string): ViewportSize;
|
|
|
84
84
|
*/
|
|
85
85
|
export declare function parseColorScheme(value: string): ColorScheme;
|
|
86
86
|
/**
|
|
87
|
-
* Reject a
|
|
88
|
-
* `
|
|
89
|
-
*
|
|
87
|
+
* Reject a mistyped command before Commander reads it as a start URL with
|
|
88
|
+
* extra arguments (`too many arguments`): a subcommand typed without its
|
|
89
|
+
* group (`bdg query x` for `bdg dom query x`), or a word close to a command
|
|
90
|
+
* followed by more words (`bdg netwrk list`). A single word is left to the
|
|
91
|
+
* start command, which checks it the same way.
|
|
90
92
|
*
|
|
91
93
|
* @param program - Root command with all commands registered
|
|
92
94
|
* @param argv - Process arguments
|
|
93
|
-
* @throws CommandError (81) naming the
|
|
95
|
+
* @throws CommandError (81) naming the command meant
|
|
94
96
|
*/
|
|
95
|
-
export declare function
|
|
97
|
+
export declare function assertNotMistypedCommand(program: Command, argv: string[]): void;
|
|
96
98
|
/**
|
|
97
99
|
* Register the start command
|
|
98
100
|
*
|
package/dist/commands/start.js
CHANGED
|
@@ -9,19 +9,12 @@ import { PORT_OPTION_DESCRIPTION } from '../constants.js';
|
|
|
9
9
|
import { CommandError } from '../errors/index.js';
|
|
10
10
|
import { chromeWsUrlConflictError, externalChromeUnreachableError, invalidChromeFlagError, notDevToolsEndpointError, invalidColorSchemeError, invalidUserDataDirError, invalidViewportError, missingStartUrlError, unknownCommandError, } from '../errors/messages.js';
|
|
11
11
|
import { startCommandHelpMessage } from '../ui/messages/commands.js';
|
|
12
|
+
import { directoryProblem } from '../utils/directories.js';
|
|
13
|
+
import { hasDisplay } from '../utils/display.js';
|
|
12
14
|
import { EXIT_CODES } from '../utils/exitCodes.js';
|
|
13
15
|
import { probeDevToolsEndpoint } from '../utils/http.js';
|
|
14
16
|
import { findSimilar } from '../utils/suggestions.js';
|
|
15
17
|
import { devToolsHttpEndpoint, validateChromeWsUrl, validateUrl } from '../utils/url.js';
|
|
16
|
-
/**
|
|
17
|
-
* Check if a display server (X11 or Wayland) is available.
|
|
18
|
-
* Used to determine default headless mode.
|
|
19
|
-
*/
|
|
20
|
-
function hasDisplay() {
|
|
21
|
-
const display = process.env['DISPLAY'];
|
|
22
|
-
const wayland = process.env['WAYLAND_DISPLAY'];
|
|
23
|
-
return (display !== undefined && display !== '') || (wayland !== undefined && wayland !== '');
|
|
24
|
-
}
|
|
25
18
|
/**
|
|
26
19
|
* Expand a leading `~/` in a path to the user's home directory.
|
|
27
20
|
* Chrome itself does not expand `~`, so bdg normalizes it for users.
|
|
@@ -74,7 +67,7 @@ export function applyCollectorOptions(command) {
|
|
|
74
67
|
.option('-a, --all', 'Include all data: no tracking/analytics filtering, and capture every response body (incl. binary)', false)
|
|
75
68
|
.option('-m, --max-body-size <megabytes>', 'Maximum response body size in MB', '5')
|
|
76
69
|
.addOption(new Option('--compact', 'No effect; kept for compatibility').hideHelp())
|
|
77
|
-
.option('--headless', 'Run Chrome without a window (default
|
|
70
|
+
.option('--headless', 'Run Chrome without a window (default without a display: Linux without DISPLAY or WAYLAND_DISPLAY, macOS over SSH or in CI)', defaultHeadless)
|
|
78
71
|
.option('--no-headless', 'Show browser window')
|
|
79
72
|
.option('--chrome-ws-url <url>', 'Connect to an existing Chrome: its DevTools port (9222, host:port, http://host:port), or a WebSocket URL: browser (ws://host:port/devtools/browser/<id>, uses the first tab) or page (.../devtools/page/<id>)')
|
|
80
73
|
.option('-q, --quiet', 'Quiet mode - minimal output for AI agents', false)
|
|
@@ -180,6 +173,8 @@ function buildSessionOptions(options) {
|
|
|
180
173
|
}
|
|
181
174
|
/** Telemetry collected by every session. */
|
|
182
175
|
const SESSION_TELEMETRY = ['dom', 'network', 'console'];
|
|
176
|
+
/** A word that cannot be a URL: no dot, colon or slash */
|
|
177
|
+
const BARE_WORD = /^[a-z][a-z0-9_-]*$/i;
|
|
183
178
|
/** Flags users type as commands (`bdg version`). */
|
|
184
179
|
const FLAG_WORDS = { version: '--version', help: '--help' };
|
|
185
180
|
/**
|
|
@@ -194,29 +189,59 @@ const FLAG_WORDS = { version: '--version', help: '--help' };
|
|
|
194
189
|
* @throws CommandError (81) for a bare word other than `localhost`
|
|
195
190
|
*/
|
|
196
191
|
function assertNotCommandTypo(arg, commandNames) {
|
|
197
|
-
if (
|
|
192
|
+
if (!BARE_WORD.test(arg) || arg.toLowerCase() === 'localhost')
|
|
198
193
|
return;
|
|
199
194
|
const flag = FLAG_WORDS[arg.toLowerCase()];
|
|
200
195
|
const err = unknownCommandError(arg, flag ? [flag] : findSimilar(arg, commandNames));
|
|
201
196
|
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
202
197
|
}
|
|
203
198
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
199
|
+
* The words of the command line that are not options or option values of
|
|
200
|
+
* the root command (`bdg --session a netwrk list` gives `netwrk`, `list`).
|
|
201
|
+
*
|
|
202
|
+
* @param program - Root command
|
|
203
|
+
* @param args - Arguments after the executable and script
|
|
204
|
+
* @returns Positional words, in order
|
|
205
|
+
*/
|
|
206
|
+
function positionalWords(program, args) {
|
|
207
|
+
const words = [];
|
|
208
|
+
for (let i = 0; i < args.length; i++) {
|
|
209
|
+
const arg = args[i] ?? '';
|
|
210
|
+
if (!arg.startsWith('-')) {
|
|
211
|
+
words.push(arg);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const option = program.options.find((o) => o.long === arg || o.short === arg);
|
|
215
|
+
if (option && (option.required || option.optional))
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
return words;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Reject a mistyped command before Commander reads it as a start URL with
|
|
222
|
+
* extra arguments (`too many arguments`): a subcommand typed without its
|
|
223
|
+
* group (`bdg query x` for `bdg dom query x`), or a word close to a command
|
|
224
|
+
* followed by more words (`bdg netwrk list`). A single word is left to the
|
|
225
|
+
* start command, which checks it the same way.
|
|
207
226
|
*
|
|
208
227
|
* @param program - Root command with all commands registered
|
|
209
228
|
* @param argv - Process arguments
|
|
210
|
-
* @throws CommandError (81) naming the
|
|
229
|
+
* @throws CommandError (81) naming the command meant
|
|
211
230
|
*/
|
|
212
|
-
export function
|
|
213
|
-
const [first] = argv.slice(2)
|
|
214
|
-
|
|
231
|
+
export function assertNotMistypedCommand(program, argv) {
|
|
232
|
+
const [first, ...rest] = positionalWords(program, argv.slice(2));
|
|
233
|
+
const commandNames = program.commands.map((command) => command.name());
|
|
234
|
+
if (first === undefined || commandNames.includes(first))
|
|
215
235
|
return;
|
|
216
236
|
const group = program.commands.find((command) => command.commands.some((sub) => sub.name() === first));
|
|
217
|
-
|
|
237
|
+
const similar = group
|
|
238
|
+
? [`${group.name()} ${first}`]
|
|
239
|
+
: rest.length > 0 && BARE_WORD.test(first)
|
|
240
|
+
? findSimilar(first, commandNames)
|
|
241
|
+
: [];
|
|
242
|
+
if (similar.length === 0)
|
|
218
243
|
return;
|
|
219
|
-
const err = unknownCommandError(first,
|
|
244
|
+
const err = unknownCommandError(first, similar);
|
|
220
245
|
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
221
246
|
}
|
|
222
247
|
/**
|
|
@@ -247,7 +272,10 @@ function validateStartInput(url, options, program) {
|
|
|
247
272
|
...(process.env['BDG_CHROME_FLAGS']?.split(' ') ?? []),
|
|
248
273
|
...(options.chromeFlags?.split(' ') ?? []),
|
|
249
274
|
]);
|
|
250
|
-
|
|
275
|
+
const sessionOptions = buildSessionOptions(options);
|
|
276
|
+
if (sessionOptions.userDataDir !== undefined)
|
|
277
|
+
assertUsableProfile(sessionOptions.userDataDir);
|
|
278
|
+
return { url, sessionOptions };
|
|
251
279
|
}
|
|
252
280
|
/**
|
|
253
281
|
* Register the start command
|
|
@@ -346,6 +374,22 @@ function assertUserDataDir(value) {
|
|
|
346
374
|
const err = invalidUserDataDirError(value, reason);
|
|
347
375
|
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
348
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Check that Chrome can create and write its profile directory (given with
|
|
379
|
+
* `-u` or in `--chrome-flags`), before a daemon is spawned: a profile under
|
|
380
|
+
* `/proc` spun the daemon at full CPU and wedged the session.
|
|
381
|
+
*
|
|
382
|
+
* @param dir - Profile directory, `~/` expanded
|
|
383
|
+
* @throws CommandError (81) for a path that cannot hold a directory, (82)
|
|
384
|
+
* when its nearest existing directory is not writable
|
|
385
|
+
*/
|
|
386
|
+
function assertUsableProfile(dir) {
|
|
387
|
+
const problem = directoryProblem(dir);
|
|
388
|
+
if (!problem)
|
|
389
|
+
return;
|
|
390
|
+
const err = invalidUserDataDirError(dir, problem.reason);
|
|
391
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, problem.denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.INVALID_ARGUMENTS);
|
|
392
|
+
}
|
|
349
393
|
/**
|
|
350
394
|
* Options given that only apply to a Chrome bdg launches. `--headless` has a
|
|
351
395
|
* default, so it counts only when given on the command line.
|
package/dist/commands/stop.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Wait until the stopped session's daemon has exited: it removes the
|
|
4
|
+
* session's files on the way out, and a command run right after (`bdg
|
|
5
|
+
* sessions`, a new start) would otherwise still see the session. The wait is
|
|
6
|
+
* bounded, as after a failed start.
|
|
7
|
+
*
|
|
8
|
+
* @param pid - Daemon PID read before the stop, or null when unknown
|
|
9
|
+
* @param waitMs - Milliseconds to wait at most
|
|
10
|
+
* @returns Warning when the daemon still runs after the wait
|
|
11
|
+
*/
|
|
12
|
+
export declare function waitForDaemonExit(pid: number | null, waitMs?: number): Promise<string | undefined>;
|
|
2
13
|
/**
|
|
3
14
|
* Register stop command
|
|
4
15
|
*
|
package/dist/commands/stop.js
CHANGED
|
@@ -3,13 +3,33 @@ import { jsonOption } from './shared/commonOptions.js';
|
|
|
3
3
|
import { stopSession } from '../ipc/client.js';
|
|
4
4
|
import { IPCErrorCode } from '../ipc/index.js';
|
|
5
5
|
import { IPCTimeoutError } from '../ipc/transport/index.js';
|
|
6
|
+
import { readLiveDaemonPid } from '../session/cleanup/staleSession.js';
|
|
6
7
|
import { joinLines } from '../ui/formatting.js';
|
|
7
8
|
import { chromeClosedMessage, orphanedDaemonsCleanedMessage, warningMessage, } from '../ui/messages/commands.js';
|
|
8
|
-
import { sessionStopped, STOP_MESSAGES, stopFailedError } from '../ui/messages/session.js';
|
|
9
|
+
import { daemonStillExitingHint, daemonStillExitingSuggestion, sessionStopped, STOP_MESSAGES, stopFailedError, } from '../ui/messages/session.js';
|
|
9
10
|
import { noActiveSessionMessage, sessionCommand, startSessionSuggestion, } from '../ui/messages/sessionCommand.js';
|
|
11
|
+
import { waitUntil } from '../utils/async.js';
|
|
10
12
|
import { getExitCodeForIPCError, isDaemonNotRunningError } from '../utils/errorMapping.js';
|
|
11
13
|
import { getErrorMessage } from '../utils/errors.js';
|
|
12
14
|
import { EXIT_CODES } from '../utils/exitCodes.js';
|
|
15
|
+
import { isProcessAlive } from '../utils/process.js';
|
|
16
|
+
/** How long `bdg stop` waits for the session's daemon to exit */
|
|
17
|
+
const DAEMON_EXIT_WAIT_MS = 3000;
|
|
18
|
+
/**
|
|
19
|
+
* Wait until the stopped session's daemon has exited: it removes the
|
|
20
|
+
* session's files on the way out, and a command run right after (`bdg
|
|
21
|
+
* sessions`, a new start) would otherwise still see the session. The wait is
|
|
22
|
+
* bounded, as after a failed start.
|
|
23
|
+
*
|
|
24
|
+
* @param pid - Daemon PID read before the stop, or null when unknown
|
|
25
|
+
* @param waitMs - Milliseconds to wait at most
|
|
26
|
+
* @returns Warning when the daemon still runs after the wait
|
|
27
|
+
*/
|
|
28
|
+
export async function waitForDaemonExit(pid, waitMs = DAEMON_EXIT_WAIT_MS) {
|
|
29
|
+
if (pid === null || (await waitUntil(() => !isProcessAlive(pid), waitMs)))
|
|
30
|
+
return undefined;
|
|
31
|
+
return `${daemonStillExitingHint(pid, waitMs)}; ${daemonStillExitingSuggestion()}`;
|
|
32
|
+
}
|
|
13
33
|
/**
|
|
14
34
|
* Format stop result for human-readable output.
|
|
15
35
|
*
|
|
@@ -36,8 +56,10 @@ export function registerStopCommand(program) {
|
|
|
36
56
|
.action(async (options) => {
|
|
37
57
|
await runCommand(async () => {
|
|
38
58
|
try {
|
|
59
|
+
const daemonPid = readLiveDaemonPid();
|
|
39
60
|
const response = await stopSession();
|
|
40
61
|
if (response.status === 'ok') {
|
|
62
|
+
const warning = await waitForDaemonExit(daemonPid);
|
|
41
63
|
return {
|
|
42
64
|
success: true,
|
|
43
65
|
data: {
|
|
@@ -48,6 +70,7 @@ export function registerStopCommand(program) {
|
|
|
48
70
|
},
|
|
49
71
|
orphanedDaemonsCount: 0,
|
|
50
72
|
message: response.message ?? STOP_MESSAGES.SUCCESS,
|
|
73
|
+
...(warning && { warnings: [warning] }),
|
|
51
74
|
},
|
|
52
75
|
};
|
|
53
76
|
}
|
package/dist/commands.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { registerCdpCommand } from './commands/cdp.js';
|
|
2
2
|
import { registerCleanupCommand } from './commands/cleanup.js';
|
|
3
3
|
import { registerConsoleCommand } from './commands/console.js';
|
|
4
|
+
import { registerCssCommands } from './commands/css.js';
|
|
4
5
|
import { registerDetailsCommand } from './commands/details.js';
|
|
5
6
|
import { registerFormInteractionCommands } from './commands/dom/formInteraction.js';
|
|
6
7
|
import { registerDomCommands } from './commands/dom/index.js';
|
|
7
8
|
import { registerInstallSkillCommand } from './commands/installSkill.js';
|
|
8
9
|
import { registerNetworkCommands } from './commands/network/index.js';
|
|
9
|
-
import { registerCssCommands } from './commands/css.js';
|
|
10
10
|
import { registerPageCommands } from './commands/page.js';
|
|
11
11
|
import { registerPeekCommand } from './commands/peek.js';
|
|
12
12
|
import { registerSessionsCommand } from './commands/sessions.js';
|
package/dist/connection/cdp.d.ts
CHANGED
|
@@ -181,6 +181,13 @@ export declare class CDPConnection implements CDPEventSource {
|
|
|
181
181
|
* @returns Calculated delay in milliseconds (base delay * 2^attempt, capped at maxDelay)
|
|
182
182
|
*/
|
|
183
183
|
private calculateBackoffDelay;
|
|
184
|
+
/**
|
|
185
|
+
* Fail every command still waiting for an answer, e.g. after the page's
|
|
186
|
+
* renderer crashed: commands it was running are never answered.
|
|
187
|
+
*
|
|
188
|
+
* @param error - Error the waiting commands fail with
|
|
189
|
+
*/
|
|
190
|
+
rejectPending(error: Error): void;
|
|
184
191
|
/**
|
|
185
192
|
* Clear all pending command promises with the given error.
|
|
186
193
|
*
|
package/dist/connection/cdp.js
CHANGED
|
@@ -390,6 +390,15 @@ export class CDPConnection {
|
|
|
390
390
|
calculateBackoffDelay(attempt, maxDelay) {
|
|
391
391
|
return Math.min(this.config.baseRetryDelay * Math.pow(2, attempt), maxDelay);
|
|
392
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Fail every command still waiting for an answer, e.g. after the page's
|
|
395
|
+
* renderer crashed: commands it was running are never answered.
|
|
396
|
+
*
|
|
397
|
+
* @param error - Error the waiting commands fail with
|
|
398
|
+
*/
|
|
399
|
+
rejectPending(error) {
|
|
400
|
+
this.clearPendingMessages(error);
|
|
401
|
+
}
|
|
393
402
|
/**
|
|
394
403
|
* Clear all pending command promises with the given error.
|
|
395
404
|
*
|
|
@@ -3,6 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as chromeLauncher from 'chrome-launcher';
|
|
5
5
|
import { BDG_CHROME_PREFS, DEFAULT_CDP_PORT, CHROME_PROFILE_DIR, DEFAULT_CHROME_LOG_LEVEL, DEFAULT_CHROME_HANDLE_SIGINT, } from '../constants.js';
|
|
6
|
+
import { makeDirectory } from '../utils/directories.js';
|
|
6
7
|
import { getErrorMessage } from '../utils/errors.js';
|
|
7
8
|
import { filterDefined } from '../utils/objects.js';
|
|
8
9
|
import { isProcessAlive } from '../utils/process.js';
|
|
@@ -62,7 +63,7 @@ export async function launchChrome(options = {}) {
|
|
|
62
63
|
const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir ?? options.sessionDir);
|
|
63
64
|
if (!fs.existsSync(userDataDir)) {
|
|
64
65
|
try {
|
|
65
|
-
|
|
66
|
+
makeDirectory(userDataDir);
|
|
66
67
|
}
|
|
67
68
|
catch (error) {
|
|
68
69
|
throw new ChromeLaunchError(`Failed to create user data directory`, {
|
|
@@ -160,7 +161,7 @@ function getPersistentUserDataDir(baseDir) {
|
|
|
160
161
|
const userDataDir = path.join(dir, CHROME_PROFILE_DIR);
|
|
161
162
|
if (!fs.existsSync(userDataDir)) {
|
|
162
163
|
try {
|
|
163
|
-
|
|
164
|
+
makeDirectory(userDataDir);
|
|
164
165
|
}
|
|
165
166
|
catch (error) {
|
|
166
167
|
throw new ChromeLaunchError(`Failed to create user data directory`, {
|
|
@@ -241,8 +241,13 @@ export class SessionController {
|
|
|
241
241
|
duration: data.duration,
|
|
242
242
|
target: data.target,
|
|
243
243
|
data: { network: data.network, console: data.console },
|
|
244
|
-
totals: {
|
|
244
|
+
totals: {
|
|
245
|
+
network: data.totalNetwork,
|
|
246
|
+
console: data.totalConsole,
|
|
247
|
+
...(data.droppedConsole && { consoleDropped: data.droppedConsole }),
|
|
248
|
+
},
|
|
245
249
|
currentNavigationId: data.currentNavigationId,
|
|
250
|
+
...(data.pageCrashedAt !== undefined && { pageCrashedAt: data.pageCrashedAt }),
|
|
246
251
|
partial: true,
|
|
247
252
|
},
|
|
248
253
|
},
|
|
@@ -24,8 +24,9 @@ export declare function launchDaemon(): Promise<SpawnedDaemon | undefined>;
|
|
|
24
24
|
* Check that the session directory can hold the daemon's files before
|
|
25
25
|
* spawning it (otherwise the daemon dies and only its log says why).
|
|
26
26
|
*
|
|
27
|
-
* @throws SessionDirError (103) for a file
|
|
28
|
-
*
|
|
27
|
+
* @throws SessionDirError (103) for a file or a path that cannot hold a
|
|
28
|
+
* directory (a pseudo-filesystem like `/proc`), (81) for a too-long path
|
|
29
|
+
* like a named session's, (82) when not writable
|
|
29
30
|
*/
|
|
30
31
|
export declare function assertUsableSessionDir(): void;
|
|
31
32
|
//# sourceMappingURL=launcher.d.ts.map
|
package/dist/daemon/launcher.js
CHANGED
|
@@ -11,13 +11,16 @@ import fs from 'fs';
|
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
import { DaemonStartupError, SessionDirError } from './errors.js';
|
|
13
13
|
import { sessionDirIsFileError, sessionDirNotWritableError, socketPathTooLongError, } from '../errors/messages.js';
|
|
14
|
-
import {
|
|
14
|
+
import { killSessionChromes, readLiveDaemonPid } from '../session/cleanup/staleSession.js';
|
|
15
|
+
import { isDaemonAlive, isDaemonSocketGone } from '../session/daemonSocket.js';
|
|
15
16
|
import { MAX_DAEMON_SOCKET_PATH_BYTES, ensureSessionDir, getSessionDir, getSessionFilePath, } from '../session/paths.js';
|
|
16
17
|
import { createLogger } from '../ui/logging/index.js';
|
|
17
18
|
import { delay } from '../utils/async.js';
|
|
19
|
+
import { directoryProblem } from '../utils/directories.js';
|
|
18
20
|
import { getErrorMessage } from '../utils/errors.js';
|
|
19
21
|
import { EXIT_CODES } from '../utils/exitCodes.js';
|
|
20
22
|
import { DAEMON_SCRIPT_PATH } from '../utils/packageRoot.js';
|
|
23
|
+
import { isProcessAlive } from '../utils/process.js';
|
|
21
24
|
const log = createLogger('launcher');
|
|
22
25
|
const DAEMON_READY_TIMEOUT_MS = 5000;
|
|
23
26
|
const DAEMON_READY_POLL_MS = 20;
|
|
@@ -37,6 +40,7 @@ export async function launchDaemon() {
|
|
|
37
40
|
throw new DaemonStartupError(`Daemon script not found at ${DAEMON_SCRIPT_PATH}. Did you run 'npm run build'?`, 'DAEMON_SCRIPT_NOT_FOUND');
|
|
38
41
|
}
|
|
39
42
|
assertUsableSessionDir();
|
|
43
|
+
await stopUnreachableDaemon();
|
|
40
44
|
const logPath = join(getSessionDir(), 'daemon.log');
|
|
41
45
|
rotateLog(logPath);
|
|
42
46
|
const logFd = fs.openSync(logPath, 'a');
|
|
@@ -54,12 +58,48 @@ export async function launchDaemon() {
|
|
|
54
58
|
await waitForDaemonReady(() => exited);
|
|
55
59
|
return { pid: daemon.pid, hasExited: () => exited };
|
|
56
60
|
}
|
|
61
|
+
/** How long a daemon that lost its socket gets to end its session before it is killed */
|
|
62
|
+
const UNREACHABLE_DAEMON_EXIT_MS = 8000;
|
|
63
|
+
/**
|
|
64
|
+
* Stop a daemon of this session directory that still runs but can no longer
|
|
65
|
+
* be reached (its socket was removed, or refuses connections; one that is
|
|
66
|
+
* only slow to answer is left alone), before a new one starts: two daemons
|
|
67
|
+
* would launch two Chromes and remove each other's files. It is asked to end
|
|
68
|
+
* its session (closing its Chrome), and killed with what it launched if it
|
|
69
|
+
* does not exit in time.
|
|
70
|
+
*/
|
|
71
|
+
async function stopUnreachableDaemon() {
|
|
72
|
+
const pid = readLiveDaemonPid();
|
|
73
|
+
if (pid === null || !(await isDaemonSocketGone()))
|
|
74
|
+
return;
|
|
75
|
+
log.info(`Stopping daemon ${pid}, which can no longer be reached (its socket is gone)`);
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, 'SIGTERM');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
log.debug(`Daemon ${pid} not signalled: ${getErrorMessage(error)}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const deadline = Date.now() + UNREACHABLE_DAEMON_EXIT_MS;
|
|
84
|
+
while (isProcessAlive(pid) && Date.now() < deadline)
|
|
85
|
+
await delay(100);
|
|
86
|
+
if (!isProcessAlive(pid))
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
process.kill(pid, 'SIGKILL');
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
log.debug(`Daemon ${pid} not killed: ${getErrorMessage(error)}`);
|
|
93
|
+
}
|
|
94
|
+
killSessionChromes();
|
|
95
|
+
}
|
|
57
96
|
/**
|
|
58
97
|
* Check that the session directory can hold the daemon's files before
|
|
59
98
|
* spawning it (otherwise the daemon dies and only its log says why).
|
|
60
99
|
*
|
|
61
|
-
* @throws SessionDirError (103) for a file
|
|
62
|
-
*
|
|
100
|
+
* @throws SessionDirError (103) for a file or a path that cannot hold a
|
|
101
|
+
* directory (a pseudo-filesystem like `/proc`), (81) for a too-long path
|
|
102
|
+
* like a named session's, (82) when not writable
|
|
63
103
|
*/
|
|
64
104
|
export function assertUsableSessionDir() {
|
|
65
105
|
const dir = getSessionDir();
|
|
@@ -72,6 +112,10 @@ export function assertUsableSessionDir() {
|
|
|
72
112
|
if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
|
|
73
113
|
fail(socketPathTooLongError(socketPath, MAX_DAEMON_SOCKET_PATH_BYTES), EXIT_CODES.INVALID_ARGUMENTS);
|
|
74
114
|
}
|
|
115
|
+
const problem = directoryProblem(dir);
|
|
116
|
+
if (problem) {
|
|
117
|
+
fail(sessionDirNotWritableError(dir, problem.reason), problem.denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR);
|
|
118
|
+
}
|
|
75
119
|
try {
|
|
76
120
|
ensureSessionDir();
|
|
77
121
|
fs.accessSync(dir, fs.constants.W_OK);
|
|
@@ -80,7 +80,10 @@ export declare class Session {
|
|
|
80
80
|
*/
|
|
81
81
|
launch(): Promise<void>;
|
|
82
82
|
/**
|
|
83
|
-
* Execute a registered command against this session.
|
|
83
|
+
* Execute a registered command against this session. After a renderer
|
|
84
|
+
* crash only {@link RUN_ON_CRASHED_PAGE} commands run (`bdg cdp` only for
|
|
85
|
+
* {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
|
|
86
|
+
* waiting for a page that cannot answer.
|
|
84
87
|
*
|
|
85
88
|
* @param name - Command name
|
|
86
89
|
* @param params - Command parameters
|