├── .eslintrc.json ├── .github └── dependabot.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── @tracerbench ├── find-chrome │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── message-transport │ ├── .eslintrc.js │ ├── README.md │ ├── index.d.ts │ ├── index.js │ ├── package.json │ └── tsconfig.json ├── protocol-connection │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── newEventHook.ts │ │ └── newProtocolConnection.ts │ ├── tsconfig.json │ └── types.d.ts ├── protocol-transport │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── newAttachJsonRpcTransport.ts │ │ ├── newAttachProtocolTransport.ts │ │ ├── newProtocolError.ts │ │ ├── newResponses.ts │ │ └── newSessions.ts │ ├── tsconfig.json │ └── types.d.ts ├── spawn-chrome │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── canonicalizeOptions.ts │ │ ├── createTmpDir.ts │ │ ├── defaultFlags.ts │ │ ├── getArguments.ts │ │ ├── index.ts │ │ └── spawnChrome.ts │ ├── tsconfig.json │ └── types.d.ts ├── spawn │ ├── .eslintrc.js │ ├── README.md │ ├── examples │ │ ├── pipe.js │ │ └── websocket.js │ ├── package.json │ ├── src │ │ ├── execa.ts │ │ ├── index.ts │ │ ├── newBufferSplitter.ts │ │ ├── newPipeMessageTransport.ts │ │ ├── newProcess.ts │ │ ├── newProcessWithPipeMessageTransport.ts │ │ ├── newProcessWithWebSocketUrl.ts │ │ ├── newTaskQueue.ts │ │ ├── newWebSocketUrlParser.ts │ │ └── spawn.ts │ ├── tsconfig.json │ └── types.d.ts └── websocket-message-transport │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── chrome-debugging-client.code-workspace ├── chrome-debugging-client ├── .eslintrc.js ├── README.md ├── package.json ├── src │ └── index.ts └── tsconfig.json ├── examples ├── .eslintrc.js ├── nodeDebug.js ├── package.json ├── printToPDF.js └── tsconfig.json ├── lerna.json ├── package.json ├── scripts ├── .eslintrc.js ├── import-code │ └── index.js ├── package.json ├── readme.js ├── tsconfig.json └── types.d.ts ├── test ├── .eslintrc.js ├── examplesTest.js ├── getArgumentsTest.js ├── package.json ├── spawnChromeTest.js ├── spawnWithWebSocketTest.js └── tsconfig.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "import", "simple-import-sort", "prettier"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:import/errors", 7 | "plugin:import/warnings", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "plugin:prettier/recommended", 12 | "prettier" 13 | ], 14 | "rules": { 15 | "sort-imports": "off", 16 | "import/order": "off", 17 | "import/no-extraneous-dependencies": "error", 18 | "import/no-unassigned-import": "error", 19 | "import/no-duplicates": "error", 20 | "import/no-unresolved": "off", 21 | "@typescript-eslint/no-use-before-define": [ 22 | "error", 23 | { "functions": false } 24 | ], 25 | "@typescript-eslint/explicit-function-return-type": [ 26 | "error", 27 | { "allowExpressions": true } 28 | ], 29 | "@typescript-eslint/explicit-module-boundary-types": "off", 30 | "@typescript-eslint/ban-types": "off", 31 | "simple-import-sort/imports": "error" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "@types/node" 10 | versions: 11 | - 14.14.17 12 | - 14.14.22 13 | - dependency-name: devtools-protocol 14 | versions: 15 | - 0.0.839267 16 | - 0.0.848227 17 | - dependency-name: eslint 18 | versions: 19 | - 7.16.0 20 | - dependency-name: typescript 21 | versions: 22 | - 4.0.5 23 | - dependency-name: "@typescript-eslint/eslint-plugin" 24 | versions: 25 | - 4.3.0 26 | - dependency-name: eslint-plugin-import 27 | versions: 28 | - 2.22.0 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | /package/ 4 | *.log 5 | *.tgz 6 | *.tsbuildinfo 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.gitignore 3 | /.npmignore 4 | /dist/test* 5 | /test* 6 | /*.tgz 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: node_js 3 | node_js: 4 | - "stable" 5 | addons: 6 | apt: 7 | sources: 8 | - sourceline: "deb http://dl.google.com/linux/chrome/deb/ stable main" 9 | key_url: "https://dl-ssl.google.com/linux/linux_signing_key.pub" 10 | packages: 11 | - google-chrome-unstable 12 | env: 13 | - CHROME_PATH=/usr/bin/google-chrome-unstable 14 | script: 15 | - yarn checkjs 16 | - yarn lint 17 | - yarn test 18 | -------------------------------------------------------------------------------- /@tracerbench/find-chrome/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | project: "./tsconfig.json", 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module", 7 | }, 8 | ignorePatterns: ["dist/", ".eslintrc.js"], 9 | extends: ["../../.eslintrc"], 10 | }; 11 | -------------------------------------------------------------------------------- /@tracerbench/find-chrome/README.md: -------------------------------------------------------------------------------- 1 | # @tracerbench/find-chrome 2 | 3 | Small wrapper around [chrome-launcher](https://github.com/GoogleChrome/chrome-launcher#readme) to extract just the chrome executable finding part. 4 | 5 | ## Usage 6 | 7 | ```js 8 | import findChrome from "@tracerbench/find-chrome"; 9 | 10 | const executablePath = findChrome(); 11 | ``` 12 | -------------------------------------------------------------------------------- /@tracerbench/find-chrome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tracerbench/find-chrome", 3 | "version": "2.0.0", 4 | "description": "Small wrapper for chrome-launcher to extract just finding chrome.", 5 | "license": "BSD-2-Clause", 6 | "author": "Kris Selden ", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 14 | "directory": "@tracerbench/find-chrome" 15 | }, 16 | "main": "dist/index.js", 17 | "types": "dist/index.d.ts", 18 | "scripts": { 19 | "build": "tsc -b", 20 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 21 | "fixlint": "eslint --ext .ts src --fix", 22 | "lint": "eslint --ext .ts src", 23 | "prepare": "yarn run build" 24 | }, 25 | "dependencies": { 26 | "chrome-launcher": "^0.15.2" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^18.16.0", 30 | "@typescript-eslint/eslint-plugin": "^5.59.1", 31 | "@typescript-eslint/parser": "^5.59.1", 32 | "eslint": "^8.39.0", 33 | "eslint-config-prettier": "^8.8.0", 34 | "eslint-plugin-import": "^2.18.2", 35 | "eslint-plugin-prettier": "^4.2.1", 36 | "eslint-plugin-simple-import-sort": "^10.0.0", 37 | "prettier": "^2.0.5", 38 | "typescript": "^5.0.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /@tracerbench/find-chrome/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Launcher } from "chrome-launcher"; 2 | import * as fs from "fs"; 3 | 4 | const darwinWellKnown = [ 5 | "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", 6 | "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 7 | ] as const; 8 | 9 | let cache: string | undefined; 10 | 11 | export default function findChrome(): string { 12 | if (cache !== undefined) { 13 | return cache; 14 | } 15 | 16 | let path = checkEnv(); 17 | if (!path) { 18 | if (process.platform === "darwin") { 19 | path = darwinQuickFind(); 20 | } else { 21 | path = findInstallation(); 22 | } 23 | } 24 | 25 | if (!path) { 26 | throw new Error(`Failed to find a Chrome installation`); 27 | } 28 | 29 | cache = path; 30 | 31 | return path; 32 | } 33 | 34 | function checkEnv(): string | undefined { 35 | const path = process.env.CHROME_PATH; 36 | if (path) { 37 | return path; 38 | } 39 | } 40 | 41 | function findInstallation(): string | undefined { 42 | const paths = Launcher.getInstallations(); 43 | if (paths.length > 0) { 44 | return paths[0]; 45 | } 46 | } 47 | 48 | function darwinQuickFind(): string | undefined { 49 | // lsregister is super slow, check some well know paths first 50 | for (const path of darwinWellKnown) { 51 | if (checkAccess(path)) { 52 | return path; 53 | } 54 | } 55 | return findInstallation(); 56 | } 57 | 58 | function checkAccess(path: string): boolean { 59 | try { 60 | fs.accessSync(path, fs.constants.X_OK); 61 | return true; 62 | } catch { 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /@tracerbench/find-chrome/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["scripthost", "es2017"], 5 | "types": ["node"], 6 | 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "newLine": "LF", 20 | 21 | "moduleResolution": "node", 22 | "module": "commonjs", 23 | "target": "ES2017" 24 | }, 25 | "files": ["src/index.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /@tracerbench/message-transport/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: "./tsconfig.json", 4 | tsconfigRootDir: __dirname, 5 | sourceType: "module", 6 | }, 7 | ignorePatterns: ["dist/", ".eslintrc.js"], 8 | extends: ["../../.eslintrc"], 9 | }; 10 | -------------------------------------------------------------------------------- /@tracerbench/message-transport/README.md: -------------------------------------------------------------------------------- 1 | # @tracerbench/message-transport 2 | 3 | Types only package that has the interfaces for connecting a message transport. 4 | 5 | -------------------------------------------------------------------------------- /@tracerbench/message-transport/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Attaches a message transport with the specified message callback and close callback. 3 | * 4 | * If an error occurs in the underlying transport it is expected the transport closes and calls the 5 | * close callback with the error, otherwise if the transport is closed normally the 6 | * callback will be called with no args. 7 | * 8 | * Each message should be received with its own event frame with its own microtask queue. 9 | * 10 | * @param onMessage receives messages 11 | * @param onClose called when the tranport closes, in an abnormal close it is called with the error 12 | */ 13 | export type AttachMessageTransport = ( 14 | onMessage: OnMessage, 15 | onClose: OnClose, 16 | ) => SendMessage; 17 | 18 | export type OnMessage = (message: string) => void; 19 | export type OnClose = (error?: Error) => void; 20 | export type SendMessage = (message: string) => void; 21 | -------------------------------------------------------------------------------- /@tracerbench/message-transport/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TracerBench/chrome-debugging-client/36cbe71c62b918173c82476420c8a197ff93e4ae/@tracerbench/message-transport/index.js -------------------------------------------------------------------------------- /@tracerbench/message-transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tracerbench/message-transport", 3 | "version": "2.0.0", 4 | "license": "BSD-2-Clause", 5 | "author": "Kris Selden ", 6 | "files": [ 7 | "index.d.ts" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 12 | "directory": "@tracerbench/message-transport" 13 | }, 14 | "types": "index.d.ts", 15 | "main": "index.js", 16 | "devDependencies": { 17 | "@typescript-eslint/eslint-plugin": "^5.59.1", 18 | "@typescript-eslint/parser": "^5.59.1", 19 | "eslint": "^8.39.0", 20 | "eslint-config-prettier": "^8.8.0", 21 | "eslint-plugin-import": "^2.18.2", 22 | "eslint-plugin-prettier": "^4.2.1", 23 | "eslint-plugin-simple-import-sort": "^10.0.0", 24 | "prettier": "^2.0.5", 25 | "typescript": "^5.0.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /@tracerbench/message-transport/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["scripthost", "es2017"], 4 | "typeRoots": [], 5 | "types": [], 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "target": "ES2017", 9 | "noEmit": true 10 | }, 11 | "files": ["index.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: "./tsconfig.json", 4 | tsconfigRootDir: __dirname, 5 | sourceType: "module", 6 | }, 7 | ignorePatterns: ["dist/", ".eslintrc.js"], 8 | extends: ["../../.eslintrc"], 9 | }; 10 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/README.md: -------------------------------------------------------------------------------- 1 | # @tracerbench/protocol-connection 2 | 3 | Adapts a message transport into a [devtools protocol](https://chromedevtools.github.io/devtools-protocol/) connection. 4 | 5 | This peer depends on `devtools-protocol` which defines the typing of events, requests, and responses of the devtools protocol. 6 | 7 | ## API 8 | 9 | ```ts 10 | export default function newProtocolConnection( 11 | attach: AttachMessageTransport, 12 | newEventEmitter: NewEventEmitter, 13 | debug: DebugCallback, 14 | ): ProtocolConnection; 15 | ``` 16 | 17 | The types are declared in [types.d.ts](types.d.ts) 18 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tracerbench/protocol-connection", 3 | "version": "2.0.0", 4 | "license": "BSD-2-Clause", 5 | "author": "Kris Selden ", 6 | "files": [ 7 | "dist", 8 | "src", 9 | "types.d.ts" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 14 | "directory": "@tracerbench/protocol-connection" 15 | }, 16 | "main": "dist/index.umd.js", 17 | "module": "dist/index.js", 18 | "types": "dist/index.d.ts", 19 | "scripts": { 20 | "build": "tsc -b && rollup -c", 21 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 22 | "fixlint": "eslint --ext .ts src --fix", 23 | "lint": "eslint --ext .ts src", 24 | "prepare": "yarn run build" 25 | }, 26 | "dependencies": { 27 | "@tracerbench/message-transport": "^2.0.0", 28 | "@tracerbench/protocol-transport": "^2.0.0", 29 | "race-cancellation": "^0.4.1" 30 | }, 31 | "devDependencies": { 32 | "@typescript-eslint/eslint-plugin": "^5.59.1", 33 | "@typescript-eslint/parser": "^5.59.1", 34 | "eslint": "^8.39.0", 35 | "eslint-config-prettier": "^8.8.0", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-prettier": "^4.2.1", 38 | "eslint-plugin-simple-import-sort": "^10.0.0", 39 | "prettier": "^2.0.5", 40 | "rollup": "^2.11.2", 41 | "rollup-plugin-commonjs": "^10.1.0", 42 | "rollup-plugin-sourcemaps": "^0.6.2", 43 | "rollup-plugin-terser": "^6.1.0", 44 | "typescript": "^5.0.4" 45 | }, 46 | "peerDependencies": { 47 | "devtools-protocol": "*" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/rollup.config.js: -------------------------------------------------------------------------------- 1 | import sourcemaps from "rollup-plugin-sourcemaps"; 2 | import { terser } from "rollup-plugin-terser"; 3 | 4 | const plugins = [sourcemaps()]; 5 | 6 | if (process.env.NODE_ENV === "production") { 7 | plugins.push( 8 | terser({ 9 | compress: { 10 | negate_iife: false, 11 | sequences: 0, 12 | }, 13 | mangle: { 14 | safari10: true, 15 | }, 16 | }), 17 | ); 18 | } 19 | 20 | export default { 21 | input: "dist/index.js", 22 | plugins, 23 | external: ["race-cancellation", "@tracerbench/protocol-transport"], 24 | output: [ 25 | { 26 | exports: "named", 27 | file: "dist/index.umd.js", 28 | format: "umd", 29 | globals: { 30 | "race-cancellation": "RaceCancellation", 31 | "@tracerbench/protocol-transport": "TBProtocolTransport", 32 | }, 33 | name: "TBProtocolConnection", 34 | sourcemap: true, 35 | sourcemapExcludeSources: true, 36 | }, 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/src/index.ts: -------------------------------------------------------------------------------- 1 | import _newAttachProtocolTransport from "@tracerbench/protocol-transport"; 2 | 3 | import type { 4 | AttachMessageTransport, 5 | DebugCallback, 6 | EventEmitter, 7 | RaceCancellation, 8 | RootConnection, 9 | } from "../types"; 10 | import _newProtocolConnection from "./newProtocolConnection"; 11 | 12 | /** 13 | * Creates a ProtocolConnection to the DevTools API from a MessageTransport. 14 | */ 15 | export default function newProtocolConnection( 16 | attach: AttachMessageTransport, 17 | newEventEmitter: () => EventEmitter, 18 | debug: DebugCallback = () => void 0, 19 | raceCancellation?: RaceCancellation, 20 | ): RootConnection { 21 | return _newProtocolConnection( 22 | _newAttachProtocolTransport(attach, debug, raceCancellation), 23 | newEventEmitter, 24 | ); 25 | } 26 | 27 | export { default as newProtocolConnection } from "./newProtocolConnection"; 28 | export type { 29 | AttachJsonRpcTransport, 30 | AttachMessageTransport, 31 | AttachProtocolTransport, 32 | AttachSession, 33 | Cancellation, 34 | DebugCallback, 35 | DetachSession, 36 | ErrorResponse, 37 | Notification, 38 | OnClose, 39 | OnError, 40 | OnEvent, 41 | OnMessage, 42 | OnNotification, 43 | Protocol, 44 | ProtocolMapping, 45 | ProtocolError, 46 | ProtocolTransport, 47 | RaceCancellation, 48 | Request, 49 | Response, 50 | ResponseError, 51 | SendMessage, 52 | SendMethod, 53 | SendRequest, 54 | SuccessResponse, 55 | Task, 56 | ProtocolConnection, 57 | SessionConnection, 58 | RootConnection, 59 | ProtocolConnectionBase, 60 | EventListener, 61 | EventEmitter, 62 | EventPredicate, 63 | NewEventEmitter, 64 | TargetID, 65 | TargetInfo, 66 | SessionID, 67 | Method, 68 | Event, 69 | EventMapping, 70 | RequestMapping, 71 | ResponseMapping, 72 | VoidRequestMethod, 73 | MappedRequestMethod, 74 | MaybeMappedRequestMethod, 75 | VoidResponseMethod, 76 | MappedResponseMethod, 77 | VoidRequestVoidResponseMethod, 78 | VoidRequestMappedResponseMethod, 79 | VoidEvent, 80 | MappedEvent, 81 | SessionIdentifier, 82 | } from "../types"; 83 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/src/newEventHook.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Protocol, 3 | SessionConnection, 4 | SessionID, 5 | SessionIdentifier, 6 | TargetID, 7 | TargetInfo, 8 | } from "../types"; 9 | 10 | const CONNECTION = Symbol("connection"); 11 | 12 | export type NewConnection = (session: Session) => SessionConnection; 13 | export type DestroyConnection = (sessionId: SessionID) => void; 14 | 15 | export type EventHook = (event: string, params?: object) => void; 16 | export interface GetConnection { 17 | (session: SessionIdentifier, throwIfNotAttached?: true): SessionConnection; 18 | (session: SessionIdentifier, throwIfNotAttached: boolean | undefined): 19 | | SessionConnection 20 | | undefined; 21 | } 22 | export type ClearSessions = () => void; 23 | 24 | export interface Session { 25 | sessionId: SessionID; 26 | targetId: TargetID; 27 | targetInfo: TargetInfo; 28 | } 29 | 30 | interface Attachment { 31 | sessionId: SessionID; 32 | targetId: TargetID; 33 | targetInfo: TargetInfo; 34 | [CONNECTION]: SessionConnection | undefined; 35 | } 36 | 37 | export default function newEventHook( 38 | newConnection: NewConnection, 39 | detachConnection: DestroyConnection, 40 | ): [EventHook, GetConnection, ClearSessions] { 41 | let attachments: Map | undefined; 42 | let sessionIds: Map | undefined; 43 | 44 | return [eventHook, getConnection, clearSessions]; 45 | 46 | function eventHook(event: string, params?: object): void { 47 | if (params) { 48 | switch (event) { 49 | case "Target.attachedToTarget": 50 | attachedToTarget(params as Protocol.Target.AttachedToTargetEvent); 51 | break; 52 | case "Target.detachedFromTarget": 53 | detachedFromTarget(params as Protocol.Target.DetachedFromTargetEvent); 54 | break; 55 | case "Target.targetInfoChanged": 56 | targetInfoChanged(params as Protocol.Target.TargetInfoChangedEvent); 57 | break; 58 | } 59 | } 60 | } 61 | 62 | function getSessionId( 63 | session: SessionIdentifier, 64 | throwIfNotAttached = true, 65 | ): SessionID | undefined { 66 | let sessionId: SessionID | undefined; 67 | if (typeof session === "string") { 68 | sessionId = session; 69 | } else if ( 70 | sessionIds !== undefined && 71 | session !== null && 72 | typeof session === "object" 73 | ) { 74 | if ("sessionId" in session) { 75 | sessionId = session.sessionId; 76 | } else if ("targetId" in session) { 77 | const { targetId } = session; 78 | sessionId = sessionIds.get(session.targetId); 79 | if (!sessionId && throwIfNotAttached) { 80 | throw new Error(`Target ${targetId} is not attached.`); 81 | } 82 | } 83 | } 84 | return sessionId; 85 | } 86 | 87 | function getSession( 88 | session: SessionIdentifier, 89 | throwIfNotAttached = true, 90 | ): Attachment | undefined { 91 | const sessionId = getSessionId(session, throwIfNotAttached); 92 | if (sessionId === undefined) { 93 | return; 94 | } 95 | 96 | if (attachments !== undefined) { 97 | const attachment = attachments.get(sessionId); 98 | if (attachment !== undefined) { 99 | return attachment; 100 | } 101 | } 102 | 103 | if (throwIfNotAttached) { 104 | throw new Error(`Session ${sessionId} is no longer attached.`); 105 | } 106 | } 107 | 108 | function attachedToTarget({ 109 | sessionId, 110 | targetInfo, 111 | }: Protocol.Target.AttachedToTargetEvent): void { 112 | const { targetId } = targetInfo; 113 | if (attachments === undefined) { 114 | attachments = new Map(); 115 | } 116 | // we make the connection lazily 117 | attachments.set(sessionId, { 118 | [CONNECTION]: undefined, 119 | sessionId, 120 | targetId, 121 | targetInfo, 122 | }); 123 | if (sessionIds === undefined) { 124 | sessionIds = new Map(); 125 | } 126 | sessionIds.set(targetId, sessionId); 127 | } 128 | 129 | function detachedFromTarget({ 130 | sessionId, 131 | }: Protocol.Target.DetachedFromTargetEvent): void { 132 | if (attachments === undefined) { 133 | return; 134 | } 135 | const attachment = attachments.get(sessionId); 136 | if (attachment !== undefined) { 137 | attachments.delete(sessionId); 138 | if (sessionIds !== undefined) { 139 | sessionIds.delete(attachment.targetId); 140 | } 141 | if (attachment[CONNECTION] !== undefined) { 142 | attachment[CONNECTION] = undefined; 143 | detachConnection(sessionId); 144 | } 145 | } 146 | } 147 | 148 | function targetInfoChanged({ 149 | targetInfo, 150 | }: Protocol.Target.TargetInfoChangedEvent): void { 151 | const attachment = getSession(targetInfo, false); 152 | if (attachment !== undefined) { 153 | attachment.targetInfo = targetInfo; 154 | } 155 | } 156 | 157 | function getConnection(session: SessionIdentifier): SessionConnection; 158 | function getConnection( 159 | session: SessionIdentifier, 160 | throwIfNotAttached?: true, 161 | ): SessionConnection; 162 | function getConnection( 163 | session: SessionIdentifier, 164 | throwIfNotAttached: boolean | undefined, 165 | ): SessionConnection | undefined; 166 | function getConnection( 167 | session: SessionIdentifier, 168 | throwIfNotAttached = true, 169 | ): SessionConnection | undefined { 170 | const attachment = getSession(session, throwIfNotAttached); 171 | if (attachment === undefined) { 172 | return; 173 | } 174 | let connection = attachment[CONNECTION]; 175 | if (connection === undefined) { 176 | connection = newConnection(attachment); 177 | attachment[CONNECTION] = connection; 178 | } 179 | return connection; 180 | } 181 | 182 | function clearSessions(): void { 183 | if (attachments !== undefined) { 184 | for (const attachment of attachments.values()) { 185 | if (attachment[CONNECTION] !== undefined) { 186 | attachment[CONNECTION] = undefined; 187 | detachConnection(attachment.sessionId); 188 | } 189 | } 190 | attachments.clear(); 191 | } 192 | if (sessionIds !== undefined) { 193 | sessionIds.clear(); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/src/newProtocolConnection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | combineRaceCancellation, 3 | disposablePromise, 4 | throwIfCancelled, 5 | } from "race-cancellation"; 6 | 7 | import type { 8 | AttachProtocolTransport, 9 | AttachSession, 10 | NewEventEmitter, 11 | Protocol, 12 | ProtocolConnection, 13 | RaceCancellation, 14 | RootConnection, 15 | SessionConnection, 16 | SessionID, 17 | TargetID, 18 | } from "../types"; 19 | import type { Session } from "./newEventHook"; 20 | import newEventHook from "./newEventHook"; 21 | 22 | /** 23 | * This method adapts a AttachProtocolTransport into higher level 24 | * ProtocolConnection. 25 | * 26 | * @param connect 27 | * @param newEventEmitter 28 | */ 29 | export default function newRootConnection( 30 | attach: AttachProtocolTransport, 31 | newEventEmitter: NewEventEmitter, 32 | ): RootConnection { 33 | return newProtocolConnection(attach, newEventEmitter); 34 | } 35 | 36 | function newSessionConnection( 37 | attachSession: AttachSession, 38 | newEventEmitter: NewEventEmitter, 39 | session: Session, 40 | ): SessionConnection { 41 | return newProtocolConnection( 42 | attachSession(session.sessionId), 43 | newEventEmitter, 44 | session, 45 | ); 46 | } 47 | 48 | function newProtocolConnection( 49 | attachTransport: AttachProtocolTransport, 50 | newEventEmitter: NewEventEmitter, 51 | session: Session, 52 | ): SessionConnection; 53 | function newProtocolConnection( 54 | attachTransport: AttachProtocolTransport, 55 | newEventEmitter: NewEventEmitter, 56 | ): RootConnection; 57 | function newProtocolConnection( 58 | attachTransport: AttachProtocolTransport, 59 | newEventEmitter: NewEventEmitter, 60 | session?: Session, 61 | ): ProtocolConnection { 62 | const emitter = newEventEmitter(); 63 | 64 | let isDetached = false; 65 | 66 | const [onTargetAttached, onTargetDetached, send, raceDetached] = 67 | attachTransport(onEvent, onError, onDetached); 68 | 69 | const [eventHook, connection, clearSessions] = newEventHook( 70 | newSessionConnection.bind(null, onTargetAttached, newEventEmitter), 71 | onTargetDetached, 72 | ); 73 | 74 | const base: RootConnection = { 75 | attachToTarget, 76 | connection, 77 | off: emitter.removeListener.bind(emitter), 78 | on: emitter.on.bind(emitter), 79 | once: emitter.once.bind(emitter), 80 | raceDetached, 81 | removeAllListeners: emitter.removeAllListeners.bind(emitter), 82 | removeListener: emitter.removeListener.bind(emitter), 83 | send, 84 | setAutoAttach, 85 | until, 86 | get isDetached() { 87 | return isDetached; 88 | }, 89 | }; 90 | 91 | if (session !== undefined) { 92 | return Object.create(base, { 93 | sessionId: { 94 | get: () => session.sessionId, 95 | }, 96 | targetId: { 97 | get: () => session.targetId, 98 | }, 99 | targetInfo: { 100 | get: () => session.targetInfo, 101 | }, 102 | }) as SessionConnection; 103 | } 104 | 105 | return base; 106 | 107 | async function attachToTarget( 108 | targetId: TargetID | { targetId: TargetID }, 109 | raceCancellation?: RaceCancellation, 110 | ): Promise { 111 | if (typeof targetId === "object" && targetId !== null) { 112 | targetId = targetId.targetId; 113 | } 114 | const request = { flatten: true, targetId }; 115 | const conn = connection(request, false); 116 | if (conn !== undefined) { 117 | return conn; 118 | } 119 | const resp: Protocol.Target.AttachToTargetResponse = await send( 120 | "Target.attachToTarget", 121 | request, 122 | raceCancellation, 123 | ); 124 | return connection(resp); 125 | } 126 | 127 | async function setAutoAttach( 128 | autoAttach: boolean, 129 | waitForDebuggerOnStart = false, 130 | raceCancellation?: RaceCancellation, 131 | ): Promise { 132 | const request: Protocol.Target.SetAutoAttachRequest = { 133 | autoAttach, 134 | flatten: true, 135 | waitForDebuggerOnStart, 136 | }; 137 | await send("Target.setAutoAttach", request, raceCancellation); 138 | } 139 | 140 | function onEvent(event: string, params?: object): void { 141 | eventHook(event, params); 142 | emitter.emit(event, params); 143 | } 144 | 145 | function onError(error: Error): void { 146 | emitter.emit("error", error); 147 | } 148 | 149 | function onDetached(): void { 150 | if (isDetached) { 151 | return; 152 | } 153 | isDetached = true; 154 | 155 | // in practice it chrome notifies child sessions before 156 | // parent session but just in case we clear here 157 | clearSessions(); 158 | 159 | emitter.emit("detached"); 160 | } 161 | 162 | async function until( 163 | eventName: string, 164 | predicate?: (event: Event) => boolean, 165 | raceCancellation?: RaceCancellation, 166 | ): Promise { 167 | return throwIfCancelled( 168 | await disposablePromise((resolve, reject) => { 169 | const listener = 170 | predicate === undefined 171 | ? resolve 172 | : (event: Event) => { 173 | try { 174 | if (predicate(event)) { 175 | resolve(event); 176 | } 177 | } catch (e) { 178 | reject(e); 179 | } 180 | }; 181 | emitter.on(eventName, listener); 182 | return () => { 183 | emitter.removeListener(eventName, listener); 184 | }; 185 | }, combineRaceCancellation(raceDetached, raceCancellation)), 186 | ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["scripthost", "es2017"], 5 | "typeRoots": [], 6 | "types": [], 7 | 8 | "outDir": "dist", 9 | "rootDir": "src", 10 | 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | 20 | "newLine": "LF", 21 | 22 | "moduleResolution": "node", 23 | "module": "es2015", 24 | "target": "es2017" 25 | }, 26 | "includes": ["src/*.ts"], 27 | "references": [ 28 | { 29 | "path": "../protocol-transport" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /@tracerbench/protocol-connection/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttachJsonRpcTransport, 3 | AttachMessageTransport, 4 | AttachProtocolTransport, 5 | AttachSession, 6 | Cancellation, 7 | DebugCallback, 8 | DetachSession, 9 | ErrorResponse, 10 | Notification, 11 | OnClose, 12 | OnError, 13 | OnEvent, 14 | OnMessage, 15 | OnNotification, 16 | ProtocolError, 17 | ProtocolTransport, 18 | RaceCancellation, 19 | Request, 20 | Response, 21 | ResponseError, 22 | SendMessage, 23 | SendMethod, 24 | SendRequest, 25 | SuccessResponse, 26 | Task, 27 | } from "@tracerbench/protocol-transport"; 28 | import type { Protocol } from "devtools-protocol"; 29 | import type { ProtocolMapping } from "devtools-protocol/types/protocol-mapping"; 30 | 31 | export type { 32 | AttachJsonRpcTransport, 33 | AttachMessageTransport, 34 | AttachProtocolTransport, 35 | AttachSession, 36 | Cancellation, 37 | DebugCallback, 38 | DetachSession, 39 | ErrorResponse, 40 | Notification, 41 | OnClose, 42 | OnError, 43 | OnEvent, 44 | OnMessage, 45 | OnNotification, 46 | Protocol, 47 | ProtocolMapping, 48 | ProtocolError, 49 | ProtocolTransport, 50 | RaceCancellation, 51 | Request, 52 | Response, 53 | ResponseError, 54 | SendMessage, 55 | SendMethod, 56 | SendRequest, 57 | SuccessResponse, 58 | Task, 59 | }; 60 | 61 | export type ProtocolConnection = SessionConnection | RootConnection; 62 | 63 | export interface SessionConnection extends ProtocolConnectionBase { 64 | readonly sessionId: SessionID; 65 | readonly targetId: TargetID; 66 | readonly targetInfo: TargetInfo; 67 | } 68 | 69 | export interface RootConnection extends ProtocolConnectionBase { 70 | readonly sessionId?: undefined; 71 | readonly targetId?: undefined; 72 | readonly targetInfo?: undefined; 73 | } 74 | 75 | export interface ProtocolConnectionBase { 76 | /** 77 | * Whether the connection is still attached. 78 | */ 79 | readonly isDetached: boolean; 80 | 81 | /** 82 | * Use to race an async task against the connection detaching. 83 | */ 84 | readonly raceDetached: RaceCancellation; 85 | 86 | /** 87 | * Get a connection for a currently attached session. 88 | * 89 | * If the session is not attached this will throw. 90 | * 91 | * Should be used with Target.attachToTarget or Target.createTarget 92 | * 93 | * @param sessionId the session id or a obj with a sessionId or a targetId 94 | */ 95 | connection( 96 | sessionId: SessionIdentifier, 97 | throwIfNotAttached?: true, 98 | ): SessionConnection; 99 | 100 | /** 101 | * Get a connection for a currently attached session. 102 | * 103 | * If throwIfNotAttached is undefined or true, it will throw if session is not attached, 104 | * otherwise it returns undefined. 105 | * 106 | * @param sessionId the session id or a obj with a sessionId or a targetId 107 | * @param throwIfNotAttached whether to throw if session is not attached, defaults to true. 108 | */ 109 | connection( 110 | sessionId: SessionIdentifier, 111 | throwIfNotAttached: boolean | undefined, 112 | ): SessionConnection | undefined; 113 | 114 | /** 115 | * Attaches to a target and returns the SessionConnection, if the target is already attached, 116 | * it returns the existing SessionConnection. 117 | * 118 | * This is a convenience for ensuring that flattened sessions are used it is the same as 119 | * ```js 120 | * conn.connection(await conn.send("Target.attachToTarget", { targetId, flatten: true })); 121 | * ``` 122 | * 123 | * You can either use with Target.createTarget or the Target.setDiscoverTargets method and 124 | * Target.targetCreated events. 125 | * 126 | * https://chromedevtools.github.io/devtools-protocol/tot/Target#method-attachToTarget 127 | */ 128 | attachToTarget( 129 | targetId: TargetID | { targetId: TargetID }, 130 | raceCancellation?: RaceCancellation, 131 | ): Promise; 132 | 133 | /** 134 | * This will cause Target.attachedToTarget events to fire for related targets. 135 | * 136 | * This is a convenience for ensuring that flattened sessions are used it is the same as 137 | * 138 | * ```js 139 | * await conn.send("Target.setAutoAttach", { autoAttach, waitForDebuggerOnStart, flatten: true }); 140 | * ``` 141 | * https://chromedevtools.github.io/devtools-protocol/tot/Target#method-setAutoAttach 142 | * 143 | * You suscribe to the Target.attachedToTarget event and use the connection method to get 144 | * the connection for the attached session. If it isn't a target you are interested in 145 | * you must detach from it or it will stay alive until you set auto attach to false. This 146 | * can be an issue with testing service workers. 147 | * 148 | * ```js 149 | * conn.on("Target.attachedToTarget", event => { 150 | * const session = conn.connection(event); 151 | * }) 152 | * ``` 153 | * 154 | * @param autoAttach auto attach flag 155 | * @param waitForDebuggerOnStart whether the debugger should wait target to be attached. 156 | */ 157 | setAutoAttach( 158 | autoAttach: boolean, 159 | waitForDebuggerOnStart?: boolean, 160 | raceCancellation?: RaceCancellation, 161 | ): Promise; 162 | 163 | /** 164 | * Cancellable send of a request and wait for response. This 165 | * already races against closing the connection but you can 166 | * pass in another raceCancellation concern like a timeout. 167 | * 168 | * See documentation of DevTools API for documentation of methods 169 | * https://chromedevtools.github.io/devtools-protocol/tot 170 | * 171 | * The request and reponse types are provide by the devtools-protocol 172 | * peer dependency. 173 | * 174 | * @param method the DevTools API method 175 | * @param request required request parameters 176 | * @param raceCancellation 177 | * @returns a promise of the method's result. 178 | */ 179 | send( 180 | method: M, 181 | request: RequestMapping[M], 182 | raceCancellation?: RaceCancellation, 183 | ): Promise; 184 | 185 | /** 186 | * Cancellable send of a request and wait for response. This 187 | * already races against closing the connection but you can 188 | * pass in another raceCancellation concern like a timeout. 189 | * 190 | * See documentation of DevTools API for documentation of methods 191 | * https://chromedevtools.github.io/devtools-protocol/tot 192 | * 193 | * The request and reponse types are provide by the devtools-protocol 194 | * peer dependency. 195 | * 196 | * @param method the DevTools API method 197 | * @param request optional request parameters 198 | * @param raceCancellation 199 | * @returns a promise of the method's result. 200 | */ 201 | send( 202 | method: M, 203 | request?: RequestMapping[M], 204 | raceCancellation?: RaceCancellation, 205 | ): Promise; 206 | 207 | /** 208 | * Cancellable send of a request and wait for response. This 209 | * already races against session detached/transport close 210 | * but you can pass in another raceCancellation concern 211 | * like a timeout. 212 | * 213 | * See documentation of DevTools API for documentation of methods 214 | * https://chromedevtools.github.io/devtools-protocol/tot 215 | * 216 | * The request and reponse types are provide by the devtools-protocol 217 | * peer dependency. 218 | * 219 | * @param method the DevTools API method 220 | * @param request this request takes no params or empty pojo 221 | * @param raceCancellation 222 | * @returns a promise of the method's result. 223 | */ 224 | send( 225 | method: M, 226 | request?: object, 227 | raceCancellation?: RaceCancellation, 228 | ): Promise; 229 | 230 | /** 231 | * Cancellable send of a request and wait for response. This 232 | * already races against session detached/transport close 233 | * but you can pass in another raceCancellation concern 234 | * like a timeout. 235 | * 236 | * See documentation of DevTools API for documentation of methods 237 | * https://chromedevtools.github.io/devtools-protocol/tot 238 | * 239 | * The request and reponse types are provide by the devtools-protocol 240 | * peer dependency. 241 | * 242 | * @param method the DevTools API method 243 | * @param request this request takes no params or empty pojo 244 | * @param raceCancellation 245 | * @returns a promise of the method completion. 246 | */ 247 | send( 248 | method: VoidRequestVoidResponseMethod, 249 | request?: undefined, 250 | raceCancellation?: RaceCancellation, 251 | ): Promise; 252 | 253 | /** 254 | * Subscribe to an event. 255 | * @param event name of event 256 | * @param listener callback with event object 257 | */ 258 | on( 259 | event: E, 260 | listener: (event: EventMapping[E]) => void, 261 | ): void; 262 | 263 | /** 264 | * Subscribe to an event. 265 | * @param event name of event 266 | * @param listener void callback 267 | */ 268 | on(event: VoidEvent, listener: () => void): void; 269 | 270 | off( 271 | event: E, 272 | listener: (event: EventMapping[E]) => void, 273 | ): void; 274 | off(event: VoidEvent, listener: () => void): void; 275 | 276 | once( 277 | event: E, 278 | listener: (event: EventMapping[E]) => void, 279 | ): void; 280 | once(event: VoidEvent, listener: () => void): void; 281 | 282 | removeListener( 283 | event: E, 284 | listener: (event: EventMapping[E]) => void, 285 | ): void; 286 | removeListener(event: VoidEvent, listener: () => void): void; 287 | 288 | removeAllListeners(event?: Event): void; 289 | 290 | /** 291 | * Cancellable promise of an event with an optional predicate function 292 | * to check whether the event is the one you are waiting for. 293 | * 294 | * See documentation of DevTools API for documentation of events 295 | * https://chromedevtools.github.io/devtools-protocol/tot 296 | * 297 | * Event types are provided by the devtools-protocol peer dependency. 298 | * 299 | * @param event the name of the event 300 | * @param predicate optional callback to test event object whether to resolve the until 301 | * @param raceCancellation additional cancellation concern, until already races against session detached/transport close. 302 | */ 303 | until( 304 | event: E, 305 | predicate?: EventPredicate, 306 | raceCancellation?: RaceCancellation, 307 | ): Promise; 308 | 309 | /** 310 | * Cancellable promise of an event. 311 | * 312 | * See documentation of DevTools API for documentation of events 313 | * https://chromedevtools.github.io/devtools-protocol/tot 314 | * 315 | * @param event the name of the event 316 | * @param predicate this event doesn't have an object so this will be undefined 317 | * @param raceCancellation additional cancellation concern, until already races against detachment. 318 | */ 319 | until( 320 | event: VoidEvent, 321 | predicate?: () => boolean, 322 | raceCancellation?: RaceCancellation, 323 | ): Promise; 324 | } 325 | 326 | export type EventListener = (...args: any[]) => void; 327 | 328 | export interface EventEmitter { 329 | on(event: string, listener: EventListener): void; 330 | once(event: string, listener: EventListener): void; 331 | removeListener(event: string, listener: EventListener): void; 332 | removeAllListeners(event?: string): void; 333 | emit(event: string, ...args: any[]): void; 334 | } 335 | 336 | export type EventPredicate = (event: Event) => boolean; 337 | 338 | export type NewEventEmitter = () => EventEmitter; 339 | 340 | export type TargetID = Protocol.Target.TargetID; 341 | export type TargetInfo = Protocol.Target.TargetInfo; 342 | export type SessionID = Protocol.Target.SessionID; 343 | 344 | export type Method = keyof ProtocolMapping.Commands; 345 | export type Event = keyof ProtocolMapping.Events | "error" | "detached"; 346 | 347 | export type EventMapping = { 348 | [E in keyof ProtocolMapping.Events]: ProtocolMapping.Events[E] extends [ 349 | (infer T)?, 350 | ] 351 | ? T 352 | : never; 353 | } & { 354 | error: Error; 355 | }; 356 | 357 | export type RequestMapping = { 358 | [M in Method]: ProtocolMapping.Commands[M]["paramsType"] extends [(infer T)?] 359 | ? T 360 | : never; 361 | }; 362 | 363 | export type ResponseMapping = { 364 | [M in Method]: ProtocolMapping.Commands[M]["returnType"]; 365 | }; 366 | 367 | export type VoidRequestMethod = { 368 | [M in Method]: ProtocolMapping.Commands[M]["paramsType"] extends [] 369 | ? M 370 | : never; 371 | }[Method]; 372 | 373 | export type MappedRequestMethod = { 374 | [M in Method]: ProtocolMapping.Commands[M]["paramsType"] extends [infer T] 375 | ? M 376 | : never; 377 | }[Method]; 378 | 379 | export type MaybeMappedRequestMethod = Exclude< 380 | Method, 381 | VoidRequestMethod | MappedRequestMethod 382 | >; 383 | 384 | export type VoidResponseMethod = { 385 | [M in Method]: ProtocolMapping.Commands[M]["returnType"] extends void 386 | ? M 387 | : never; 388 | }[Method]; 389 | 390 | export type MappedResponseMethod = Exclude; 391 | 392 | export type VoidRequestVoidResponseMethod = Extract< 393 | MaybeMappedRequestMethod | VoidRequestMethod, 394 | VoidResponseMethod 395 | >; 396 | export type VoidRequestMappedResponseMethod = Exclude< 397 | MaybeMappedRequestMethod | VoidRequestMethod, 398 | VoidRequestVoidResponseMethod 399 | >; 400 | 401 | export type VoidEvent = 402 | | { 403 | [E in keyof ProtocolMapping.Events]: ProtocolMapping.Events[E] extends [] 404 | ? E 405 | : never; 406 | }[keyof ProtocolMapping.Events] 407 | | "detached"; 408 | 409 | export type MappedEvent = Exclude | "error"; 410 | export type SessionIdentifier = 411 | | SessionID 412 | | { 413 | targetId: TargetID; 414 | } 415 | | { 416 | sessionId: SessionID; 417 | }; 418 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | project: "./tsconfig.json", 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module", 7 | }, 8 | ignorePatterns: ["dist/", ".eslintrc.js"], 9 | extends: ["../../.eslintrc"], 10 | }; 11 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/README.md: -------------------------------------------------------------------------------- 1 | # @tracerbench/protocol-transport 2 | 3 | Adapts a `AttachMessageTransport` function defined in `@tracerbench/message-transport` into 4 | an `AttachProtocolTransport` function which is used to create a send function. 5 | 6 | ```ts 7 | export type AttachProtocolTransport = ( 8 | onEvent: OnEvent, 9 | onError: OnError, 10 | onClose: OnClose, 11 | ) => [ 12 | AttachSession, 13 | DetachSession, 14 | SendMethod, 15 | RaceCancellation 16 | ]; 17 | 18 | export type AttachSession = ( 19 | sessionId: SessionId, 20 | ) => AttachProtocolTransport; 21 | 22 | export type DetachSession = (sessionId: SessionId) => void; 23 | 24 | export type SendMethod = < 25 | Method extends string, 26 | Params extends object, 27 | Result extends object 28 | >( 29 | method: Method, 30 | params?: Params, 31 | raceCancellation?: RaceCancellation, 32 | ) => Promise; 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tracerbench/protocol-transport", 3 | "version": "2.0.0", 4 | "license": "BSD-2-Clause", 5 | "author": "Kris Selden ", 6 | "files": [ 7 | "dist", 8 | "src", 9 | "types.d.ts" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 14 | "directory": "@tracerbench/protocol-transport" 15 | }, 16 | "main": "dist/index.umd.js", 17 | "module": "dist/index.js", 18 | "types": "dist/index.d.ts", 19 | "scripts": { 20 | "build": "tsc -b && rollup -c", 21 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 22 | "fixlint": "eslint --ext .ts src --fix", 23 | "lint": "eslint --ext .ts src", 24 | "prepare": "yarn run build" 25 | }, 26 | "dependencies": { 27 | "@tracerbench/message-transport": "^2.0.0", 28 | "race-cancellation": "^0.4.1" 29 | }, 30 | "devDependencies": { 31 | "@typescript-eslint/eslint-plugin": "^5.59.1", 32 | "@typescript-eslint/parser": "^5.59.1", 33 | "eslint": "^8.39.0", 34 | "eslint-config-prettier": "^8.8.0", 35 | "eslint-plugin-import": "^2.18.2", 36 | "eslint-plugin-prettier": "^4.2.1", 37 | "eslint-plugin-simple-import-sort": "^10.0.0", 38 | "prettier": "^2.0.5", 39 | "rollup": "^2.11.2", 40 | "rollup-plugin-commonjs": "^10.1.0", 41 | "rollup-plugin-sourcemaps": "^0.6.2", 42 | "rollup-plugin-terser": "^6.1.0", 43 | "typescript": "^5.0.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/rollup.config.js: -------------------------------------------------------------------------------- 1 | import sourcemaps from "rollup-plugin-sourcemaps"; 2 | import { terser } from "rollup-plugin-terser"; 3 | 4 | const plugins = [sourcemaps()]; 5 | 6 | if (process.env.NODE_ENV === "production") { 7 | plugins.push( 8 | terser({ 9 | compress: { 10 | negate_iife: false, 11 | sequences: 0, 12 | }, 13 | mangle: { 14 | safari10: true, 15 | }, 16 | }), 17 | ); 18 | } 19 | 20 | export default { 21 | input: "dist/index.js", 22 | plugins, 23 | external: ["race-cancellation"], 24 | output: [ 25 | { 26 | exports: "named", 27 | file: "dist/index.umd.js", 28 | format: "umd", 29 | globals: { 30 | "race-cancellation": "RaceCancellation", 31 | }, 32 | name: "TBProtocolTransport", 33 | sourcemap: true, 34 | sourcemapExcludeSources: true, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttachMessageTransport, 3 | AttachProtocolTransport, 4 | DebugCallback, 5 | RaceCancellation, 6 | } from "../types"; 7 | import newAttachJsonRpcTransport from "./newAttachJsonRpcTransport"; 8 | import _newAttachProtocolTransport from "./newAttachProtocolTransport"; 9 | 10 | export default function newAttachProtocolTransport( 11 | attach: AttachMessageTransport, 12 | debug: DebugCallback = () => void 0, 13 | raceCancellation?: RaceCancellation, 14 | ): AttachProtocolTransport { 15 | return _newAttachProtocolTransport( 16 | newAttachJsonRpcTransport(attach, debug, raceCancellation), 17 | ); 18 | } 19 | 20 | export { default as newAttachJsonRpcTransport } from "./newAttachJsonRpcTransport"; 21 | export { default as newAttachProtocolTransport } from "./newAttachProtocolTransport"; 22 | export { isProtocolError } from "./newProtocolError"; 23 | export type { 24 | AttachMessageTransport, 25 | OnClose, 26 | OnMessage, 27 | SendMessage, 28 | Cancellation, 29 | RaceCancellation, 30 | Task, 31 | AttachJsonRpcTransport, 32 | SendRequest, 33 | AttachProtocolTransport, 34 | ProtocolTransport, 35 | AttachSession, 36 | DetachSession, 37 | SendMethod, 38 | SuccessResponse, 39 | ErrorResponse, 40 | ResponseError, 41 | Response, 42 | Request, 43 | Notification, 44 | DebugCallback, 45 | ProtocolError, 46 | OnNotification, 47 | OnError, 48 | OnEvent, 49 | } from "../types"; 50 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/src/newAttachJsonRpcTransport.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cancellableRace, 3 | combineRaceCancellation, 4 | throwIfCancelled, 5 | } from "race-cancellation"; 6 | 7 | import type { 8 | AttachJsonRpcTransport, 9 | AttachMessageTransport, 10 | DebugCallback, 11 | Notification, 12 | RaceCancellation, 13 | Request, 14 | Response, 15 | } from "../types"; 16 | import newResponses from "./newResponses"; 17 | 18 | /** 19 | * Creates a AttachJsonRpcTransport function from the specified 20 | * AttachMessageTransport function. 21 | * 22 | * This just handles the JSON RPC part of adapting the message transport. 23 | * 24 | * It does not support all of JSON RPC, only whats needed for the DevTools API. 25 | * The client can only receive notifications and send requests. 26 | * The client cannot receive requests or send notifications. 27 | * 28 | * @param attach a function that attaches the message transport 29 | * @param debug an optional debug function, should support format string + args like npm debug 30 | * @param raceCancellation a raceCancellation that is scoped to the transport like Chrome exited 31 | */ 32 | export default function newAttachJsonRpcTransport( 33 | attach: AttachMessageTransport, 34 | debug: DebugCallback = () => void 0, 35 | raceCancellation?: RaceCancellation, 36 | ): AttachJsonRpcTransport { 37 | return (onNotifiction, emitError, emitClose) => { 38 | const [_raceClose, cancel] = cancellableRace(); 39 | 40 | const raceClose = combineRaceCancellation(raceCancellation, _raceClose); 41 | 42 | const [usingResponse, resolveResponse] = newResponses(); 43 | const sendMessage = attach(onMessage, onClose); 44 | return [sendRequest, raceClose]; 45 | 46 | function onMessage(message: string): void { 47 | try { 48 | const notification = JSON.parse(message) as Notification | Response; 49 | debug("RECV %O", notification); 50 | if (notification !== undefined) { 51 | if ("id" in notification) { 52 | resolveResponse(notification); 53 | } else { 54 | onNotifiction(notification); 55 | } 56 | } 57 | } catch (e) { 58 | let err: Error & { cause?: unknown }; 59 | if (e instanceof Error) { 60 | err = e; 61 | } else { 62 | err = new Error("error dispatching message"); 63 | err.cause = e; 64 | } 65 | debug("ERROR %O", err); 66 | emitError(err); 67 | } 68 | } 69 | 70 | function onClose(error?: Error): void { 71 | if (error) { 72 | debug("CLOSE %O", error); 73 | cancel(`transport closed: ${error.message}`); 74 | } else { 75 | debug("CLOSE"); 76 | cancel("transport closed"); 77 | } 78 | emitClose(); 79 | } 80 | 81 | async function sendRequest< 82 | Method extends string, 83 | Params extends object, 84 | Result extends object, 85 | >( 86 | request: Request, 87 | sendRaceCancellation?: RaceCancellation, 88 | ): Promise> { 89 | const combinedRaceCancellation = combineRaceCancellation( 90 | raceClose, 91 | sendRaceCancellation, 92 | ); 93 | return await usingResponse(async (id, response) => { 94 | request.id = id; 95 | debug("SEND %O", request); 96 | sendMessage(JSON.stringify(request)); 97 | return throwIfCancelled(await combinedRaceCancellation(response)); 98 | }); 99 | } 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/src/newAttachProtocolTransport.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttachJsonRpcTransport, 3 | AttachProtocolTransport, 4 | Notification, 5 | RaceCancellation, 6 | } from "../types"; 7 | import newProtocolError from "./newProtocolError"; 8 | import newSessions from "./newSessions"; 9 | 10 | /** 11 | * Adapts a AttachJsonRpcTransport function to a AttachProtocolTransport function. 12 | * 13 | * Adds support for flattened sessions and creates JSON RPC Request object and unwraps Response. 14 | * 15 | * @param attach 16 | */ 17 | export default function newAttachProtocolTransport( 18 | attach: AttachJsonRpcTransport, 19 | ): AttachProtocolTransport { 20 | return (onEvent, onError, onClose) => { 21 | const [sendRequest, raceClose] = attach(onNotification, onError, onClose); 22 | const [attachSession, detachSession, dispatchEvent] = 23 | newSessions(send, raceClose); 24 | 25 | return [attachSession, detachSession, send, raceClose]; 26 | 27 | function onNotification(notification: Notification): void { 28 | const { method, params, sessionId } = notification; 29 | if (sessionId === undefined) { 30 | onEvent(method, params); 31 | } else { 32 | dispatchEvent(sessionId as SessionId, method, params); 33 | } 34 | } 35 | 36 | async function send< 37 | Method extends string, 38 | Params extends object, 39 | Result extends object, 40 | >( 41 | method: Method, 42 | params?: Params, 43 | raceCancellation?: RaceCancellation, 44 | sessionId?: SessionId, 45 | ): Promise { 46 | const request = { 47 | method, 48 | params, 49 | sessionId, 50 | }; 51 | const response = await sendRequest( 52 | request, 53 | raceCancellation, 54 | ); 55 | if ("error" in response) { 56 | throw newProtocolError(request, response); 57 | } 58 | return response.result; 59 | } 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/src/newProtocolError.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorResponse, ProtocolError, Request } from "../types"; 2 | 3 | export default function newProtocolError( 4 | request: Request, 5 | response: ErrorResponse, 6 | ): ProtocolError { 7 | const error = new Error(response.error.message) as ProtocolError; 8 | error.name = "ProtocolError"; 9 | error.request = request; 10 | error.response = response; 11 | return error; 12 | } 13 | 14 | export function isProtocolError(error: Error): error is ProtocolError { 15 | return ( 16 | error.name === "ProtocolError" && "request" in error && "response" in error 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/src/newResponses.ts: -------------------------------------------------------------------------------- 1 | import type { Complete } from "race-cancellation"; 2 | import { oneshot } from "race-cancellation"; 3 | 4 | import type { Response } from "../types"; 5 | 6 | export default function newResponses(): [UsingResponse, ResolveResponse] { 7 | let seq = 0; 8 | const pending = new Map void>(); 9 | 10 | return [usingResponse, resolveResponse]; 11 | 12 | function resolveResponse(response: Response): void { 13 | const resolve = pending.get(response.id); 14 | if (resolve !== undefined) { 15 | resolve(response); 16 | } 17 | } 18 | 19 | async function usingResponse( 20 | using: UsingResponseCallback, 21 | ): Promise> { 22 | const id = seq++; 23 | try { 24 | const [response, resolve] = oneshot>(); 25 | pending.set(id, resolve as Complete); 26 | return await using(id, response); 27 | } finally { 28 | pending.delete(id); 29 | } 30 | } 31 | } 32 | 33 | export type ResolveResponse = (response: Response) => void; 34 | 35 | export type UsingResponse = ( 36 | using: UsingResponseCallback, 37 | ) => Promise>; 38 | 39 | export type UsingResponseCallback = ( 40 | id: number, 41 | response: () => Promise>, 42 | ) => Promise>; 43 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/src/newSessions.ts: -------------------------------------------------------------------------------- 1 | import { cancellableRace, combineRaceCancellation } from "race-cancellation"; 2 | 3 | import { 4 | AttachProtocolTransport, 5 | AttachSession, 6 | DetachSession, 7 | RaceCancellation, 8 | } from "../types"; 9 | 10 | export type DispatchEvent = ( 11 | sessionId: SessionId, 12 | event: string, 13 | params?: object, 14 | ) => void; 15 | 16 | interface Session { 17 | onEvent: (event: string, params?: object) => void; 18 | onError: (error: Error) => void; 19 | onDetach: () => void; 20 | } 21 | 22 | export type Send = < 23 | Method extends string, 24 | Params extends object, 25 | Result extends object, 26 | >( 27 | method: Method, 28 | params?: Params, 29 | raceCancellation?: RaceCancellation, 30 | sessionId?: SessionId, 31 | ) => Promise; 32 | 33 | export default function newSessions( 34 | send: Send, 35 | raceClose: RaceCancellation, 36 | ): [ 37 | AttachSession, 38 | DetachSession, 39 | DispatchEvent, 40 | ] { 41 | let sessions: Map | undefined; 42 | 43 | return [attachSession, detachSession, dispatchEvent]; 44 | 45 | function attachSession( 46 | sessionId: SessionId, 47 | ): AttachProtocolTransport { 48 | return ( 49 | onEvent: (event: string, params?: object) => void, 50 | onError: (err: Error) => void, 51 | onDetach: () => void, 52 | ) => { 53 | if (sessions === undefined) { 54 | sessions = new Map(); 55 | } 56 | const [raceDetach, cancel] = cancellableRace(); 57 | sessions.set(sessionId, { 58 | onDetach() { 59 | cancel(`session detached ${String(sessionId)}`); 60 | onDetach(); 61 | }, 62 | onError, 63 | onEvent, 64 | }); 65 | return [ 66 | attachSession, 67 | detachSession, 68 | (method, params, raceCancellation) => 69 | send( 70 | method, 71 | params, 72 | // send is already raced against close 73 | combineRaceCancellation(raceDetach, raceCancellation), 74 | sessionId, 75 | ), 76 | combineRaceCancellation(raceClose, raceDetach), 77 | ]; 78 | }; 79 | } 80 | 81 | function detachSession(sessionId: SessionId): void { 82 | if (sessions === undefined) { 83 | return; 84 | } 85 | const session = sessions.get(sessionId); 86 | if (session !== undefined) { 87 | sessions.delete(sessionId); 88 | session.onDetach(); 89 | } 90 | } 91 | 92 | function dispatchEvent( 93 | sessionId: SessionId, 94 | event: string, 95 | params?: object, 96 | ): void { 97 | if (sessions === undefined) { 98 | return; 99 | } 100 | const session = sessions.get(sessionId); 101 | if (session !== undefined) { 102 | try { 103 | session.onEvent(event, params); 104 | } catch (e) { 105 | let err: Error & { cause?: unknown }; 106 | if (e instanceof Error) { 107 | err = e; 108 | } else { 109 | err = new Error("error dispatching event"); 110 | err.cause = e; 111 | } 112 | session.onError(err); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["scripthost", "es2017"], 5 | "typeRoots": [], 6 | "types": [], 7 | 8 | "outDir": "dist", 9 | "rootDir": "src", 10 | 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | 20 | "newLine": "LF", 21 | 22 | "moduleResolution": "node", 23 | "module": "es2015", 24 | "target": "es2017" 25 | }, 26 | "include": ["src/*.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /@tracerbench/protocol-transport/types.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AttachMessageTransport, 3 | OnClose, 4 | OnMessage, 5 | SendMessage, 6 | } from "@tracerbench/message-transport"; 7 | import { Cancellation, RaceCancellation, Task } from "race-cancellation"; 8 | 9 | export type { 10 | AttachMessageTransport, 11 | OnClose, 12 | OnMessage, 13 | SendMessage, 14 | Cancellation, 15 | RaceCancellation, 16 | Task, 17 | }; 18 | 19 | export type AttachJsonRpcTransport = ( 20 | onNotification: OnNotification, 21 | onError: OnError, 22 | onClose: OnClose, 23 | ) => [SendRequest, RaceCancellation]; 24 | 25 | export type SendRequest = < 26 | Method extends string, 27 | Params extends object, 28 | Result extends object, 29 | >( 30 | request: Request, 31 | raceCancellation?: RaceCancellation, 32 | ) => Promise>; 33 | 34 | export type AttachProtocolTransport = ( 35 | onEvent: OnEvent, 36 | onError: OnError, 37 | onClose: OnClose, 38 | ) => ProtocolTransport; 39 | 40 | export type ProtocolTransport = [ 41 | AttachSession, 42 | DetachSession, 43 | SendMethod, 44 | RaceCancellation, 45 | ]; 46 | 47 | export type AttachSession = ( 48 | sessionId: SessionId, 49 | ) => AttachProtocolTransport; 50 | 51 | export type DetachSession = (sessionId: SessionId) => void; 52 | 53 | export type SendMethod = < 54 | Method extends string, 55 | Params extends object, 56 | Result extends object, 57 | >( 58 | method: Method, 59 | params?: Params, 60 | raceCancellation?: RaceCancellation, 61 | ) => Promise; 62 | 63 | export interface SuccessResponse { 64 | id: number; 65 | result: Result; 66 | } 67 | 68 | export interface ErrorResponse { 69 | id: number; 70 | error: ResponseError; 71 | } 72 | 73 | export interface ResponseError { 74 | code: number; 75 | message: string; 76 | data?: unknown; 77 | } 78 | 79 | export type Response = 80 | | SuccessResponse 81 | | ErrorResponse; 82 | 83 | export interface Request< 84 | Method extends string = string, 85 | Params extends object = object, 86 | SessionID = unknown, 87 | > { 88 | /** 89 | * The request gets assigned an id when it is sent. 90 | */ 91 | id?: number; 92 | method: Method; 93 | params?: Params; 94 | 95 | /** 96 | * Flattened sessionId 97 | */ 98 | sessionId?: SessionID; 99 | } 100 | 101 | export interface Notification< 102 | Method extends string = string, 103 | Params extends object = object, 104 | SessionID = unknown, 105 | > { 106 | method: Method; 107 | params?: Params; 108 | sessionId?: SessionID; 109 | } 110 | 111 | export type DebugCallback = (formatter: unknown, ...args: unknown[]) => void; 112 | 113 | export interface ProtocolError< 114 | Method extends string = string, 115 | Params extends object = object, 116 | > extends Error { 117 | name: "ProtocolError"; 118 | request: Request; 119 | response: ErrorResponse; 120 | } 121 | 122 | export type OnNotification = < 123 | Method extends string = string, 124 | Params extends object = object, 125 | >( 126 | notification: Notification, 127 | ) => void; 128 | export type OnError = (error: Error) => void; 129 | export type OnEvent = < 130 | Event extends string = string, 131 | Params extends object = object, 132 | >( 133 | event: Event, 134 | params?: Params, 135 | ) => void; 136 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | parserOptions: { 6 | project: "./tsconfig.json", 7 | tsconfigRootDir: __dirname, 8 | sourceType: "module", 9 | }, 10 | ignorePatterns: ["dist/", ".eslintrc.js"], 11 | extends: ["../../.eslintrc"], 12 | }; 13 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/README.md: -------------------------------------------------------------------------------- 1 | # @tracerbench/spawn-chrome 2 | 3 | Spawn api for chrome that combines `@tracerbench/find-chrome` with the pipe 4 | message transport of `@tracerbench/spawn` and a tmp user data directory and some 5 | default chrome flags for automation. 6 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tracerbench/spawn-chrome", 3 | "version": "2.0.0", 4 | "license": "BSD-2-Clause", 5 | "author": "Kris Selden ", 6 | "files": [ 7 | "dist", 8 | "src", 9 | "types.d.ts" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 14 | "directory": "@tracerbench/spawn-chrome" 15 | }, 16 | "main": "dist/index.js", 17 | "types": "dist/index.d.ts", 18 | "scripts": { 19 | "build": "tsc -b", 20 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 21 | "fixlint": "eslint --ext .ts src --fix", 22 | "lint": "eslint --ext .ts src", 23 | "prepare": "yarn run build" 24 | }, 25 | "dependencies": { 26 | "@tracerbench/find-chrome": "^2.0.0", 27 | "@tracerbench/spawn": "^2.0.0", 28 | "tmp": "^0.2.1" 29 | }, 30 | "devDependencies": { 31 | "@types/tmp": "^0.2.0", 32 | "@typescript-eslint/eslint-plugin": "^5.59.1", 33 | "@typescript-eslint/parser": "^5.59.1", 34 | "eslint": "^8.39.0", 35 | "eslint-config-prettier": "^8.8.0", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-prettier": "^4.2.1", 38 | "eslint-plugin-simple-import-sort": "^10.0.0", 39 | "prettier": "^2.0.5", 40 | "typescript": "^5.0.4" 41 | }, 42 | "gitHead": "930bc11c8b01620e5095df9249f0647af68235b5" 43 | } 44 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/src/canonicalizeOptions.ts: -------------------------------------------------------------------------------- 1 | import type { ChromeSpawnOptions } from "../types"; 2 | 3 | const CANONICALIZE: { 4 | [K in keyof ChromeSpawnOptions]: Canonicalize; 5 | } & { 6 | [key: string]: Canonicalize; 7 | } = { 8 | additionalArguments: arrayOf("string"), 9 | chromeExecutable: primitive("string"), 10 | disableDefaultArguments: primitive("boolean"), 11 | headless: primitive("boolean"), 12 | stdio: enumOf("inherit", "ignore"), 13 | url: primitive("string"), 14 | userDataDir: primitive("string"), 15 | userDataRoot: primitive("string"), 16 | cwd: primitive("string"), 17 | extendEnv: primitive("boolean"), 18 | env: env(), 19 | }; 20 | 21 | type Canonicalize = (value: unknown, key: string) => T; 22 | 23 | interface PrimitiveMapping { 24 | boolean: boolean; 25 | string: string; 26 | } 27 | 28 | function primitive( 29 | type: T, 30 | ): Canonicalize { 31 | return (value, key) => { 32 | if (typeof value === type) { 33 | return value as PrimitiveMapping[T]; 34 | } 35 | return invalidOption(key, type); 36 | }; 37 | } 38 | 39 | function enumOf( 40 | ...tuple: T 41 | ): Canonicalize]> { 42 | return (value, key) => { 43 | if (typeof value === "string") { 44 | if (tuple.includes(value)) { 45 | return value as unknown as T[Exclude]; 46 | } 47 | } 48 | return invalidOption(key, tuple.map((v) => JSON.stringify(v)).join(" | ")); 49 | }; 50 | } 51 | 52 | function arrayOf( 53 | type: T, 54 | ): Canonicalize> { 55 | return (array, key) => { 56 | if (Array.isArray(array)) { 57 | for (const value of array) { 58 | if (typeof value !== type) { 59 | return invalidOption(key, `${type}[]`); 60 | } 61 | } 62 | return array as PrimitiveMapping[T][]; 63 | } 64 | return invalidOption(key, `${type}[]`); 65 | }; 66 | } 67 | 68 | function env(): Canonicalize<{ [name: string]: string | undefined }> { 69 | return (value, key) => { 70 | if (isObject(value)) { 71 | return value as { [name: string]: string | undefined }; 72 | } 73 | return invalidOption(key, "env"); 74 | }; 75 | } 76 | 77 | export default function canonicalizeOptions( 78 | options: unknown, 79 | ): ChromeSpawnOptions { 80 | const canonical: ChromeSpawnOptions & { 81 | [key: string]: ChromeSpawnOptions[keyof ChromeSpawnOptions]; 82 | } = { 83 | additionalArguments: undefined, 84 | chromeExecutable: undefined, 85 | disableDefaultArguments: false, 86 | headless: false, 87 | stdio: "ignore", 88 | url: undefined, 89 | userDataDir: undefined, 90 | userDataRoot: undefined, 91 | cwd: undefined, 92 | extendEnv: undefined, 93 | env: undefined, 94 | }; 95 | 96 | if (isObject(options)) { 97 | for (const key of Object.keys(canonical)) { 98 | const value = options[key]; 99 | if (value === undefined) { 100 | continue; 101 | } 102 | canonical[key] = CANONICALIZE[key](value, key); 103 | } 104 | } 105 | 106 | return canonical; 107 | } 108 | 109 | function isObject(options: unknown): options is { 110 | [key: string]: unknown; 111 | } { 112 | return typeof options === "object" && options !== null; 113 | } 114 | 115 | function invalidOption(key: string, type: string): never { 116 | throw new TypeError(`invalid option ${key} expected value to be ${type}`); 117 | } 118 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/src/createTmpDir.ts: -------------------------------------------------------------------------------- 1 | import * as tmp from "tmp"; 2 | 3 | tmp.setGracefulCleanup(); 4 | 5 | export default function createTmpDir(dir?: string): [string, () => void] { 6 | const tmpDir = tmp.dirSync({ 7 | dir, // base dir defaults to system temporary directory 8 | unsafeCleanup: true, // recursive like rm -r 9 | }); 10 | return [tmpDir.name, tmpDir.removeCallback]; 11 | } 12 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/src/defaultFlags.ts: -------------------------------------------------------------------------------- 1 | // reduce non determinism from background networking 2 | export const disableBackgroundNetworking = [ 3 | // Disable translation 4 | "--disable-features=TranslateUI", 5 | // Disable several subsystems which run network requests in the background. 6 | "--disable-background-networking", 7 | // Enables the recording of metrics reports but disables reporting. 8 | "--metrics-recording-only", 9 | // Disable default component extensions with background pages 10 | "--disable-component-extensions-with-background-pages", 11 | // Disables syncing browser data to a Google Account. 12 | "--disable-sync", 13 | "--disable-client-side-phishing-detection", 14 | "--disable-component-update", 15 | ]; 16 | 17 | export const disableTaskThrottling = [ 18 | "--disable-renderer-backgrounding", 19 | "--disable-backgrounding-occluded-windows", 20 | "--disable-background-timer-throttling", 21 | "--disable-ipc-flooding-protection", 22 | "--disable-hang-monitor", 23 | ]; 24 | 25 | export const disableFirstRun = [ 26 | // Skip First Run tasks, whether or not it's actually the First Run. 27 | "--no-first-run", 28 | // Disables installation of default apps on first run. 29 | "--disable-default-apps", 30 | // Disables the default browser check. 31 | "--no-default-browser-check", 32 | ]; 33 | 34 | export const automationFlags = [ 35 | // Enable indication that browser is controlled by automation. 36 | "--enable-automation", 37 | // Unresponsive page dialog 38 | "--disable-hang-monitor", 39 | // Suppresses all error dialogs when present. 40 | "--noerrdialogs", 41 | // Prevents permission prompts from appearing by denying instead of showing 42 | // prompts. 43 | "--deny-permission-prompts", 44 | "--autoplay-policy=no-user-gesture-required", 45 | "--disable-popup-blocking", 46 | "--disable-prompt-on-repost", 47 | "--disable-search-geolocation-disclosure", 48 | // linux password store 49 | "--password-store=basic", 50 | // mac password store 51 | "--use-mock-keychain", 52 | "--force-color-profile=srgb", 53 | ]; 54 | 55 | export const defaultFlags = disableFirstRun.concat( 56 | automationFlags, 57 | disableTaskThrottling, 58 | disableBackgroundNetworking, 59 | ); 60 | 61 | export const headlessFlags = [ 62 | "--headless", 63 | "--hide-scrollbars", 64 | "--mute-audio", 65 | ]; 66 | 67 | export default defaultFlags; 68 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/src/getArguments.ts: -------------------------------------------------------------------------------- 1 | import type { ArgumentOptions } from "../types"; 2 | import defaultFlags, { headlessFlags } from "./defaultFlags"; 3 | 4 | export default function getArguments( 5 | userDataDir: string, 6 | options: Partial, 7 | ): string[] { 8 | let args = [ 9 | "--remote-debugging-pipe", 10 | `--user-data-dir=${userDataDir}`, 11 | ] as string[]; 12 | if (options.disableDefaultArguments !== true) { 13 | args.push(...defaultFlags); 14 | } 15 | 16 | if ( 17 | options.additionalArguments !== undefined && 18 | Array.isArray(options.additionalArguments) 19 | ) { 20 | args.push(...options.additionalArguments); 21 | } 22 | 23 | if (options.headless === true) { 24 | args.push(...headlessFlags); 25 | } 26 | 27 | args = cleanupArgs(args); 28 | 29 | if (typeof options.url === "string" && options.url.length > 0) { 30 | args.push(options.url); 31 | } 32 | 33 | return args; 34 | } 35 | 36 | function cleanupArgs(args: string[]): string[] { 37 | const set = new Set(); 38 | const disabledFeatures = new Set(); 39 | const enabledFeatures = new Set(); 40 | for (const arg of args) { 41 | if (parseCommaDelimitedArg(enabledFeatures, "--enable-features=", arg)) { 42 | continue; 43 | } 44 | if (parseCommaDelimitedArg(disabledFeatures, "--disable-features=", arg)) { 45 | continue; 46 | } 47 | set.add(arg); 48 | } 49 | const cleaned = Array.from(set); 50 | if (enabledFeatures.size > 0) { 51 | cleaned.push( 52 | `--enable-features=${formatCommaDelimitedArg(enabledFeatures)}`, 53 | ); 54 | } 55 | if (disabledFeatures.size > 0) { 56 | cleaned.push( 57 | `--disable-features=${formatCommaDelimitedArg(disabledFeatures)}`, 58 | ); 59 | } 60 | return cleaned; 61 | } 62 | 63 | function parseCommaDelimitedArg( 64 | set: Set, 65 | prefix: string, 66 | arg: string, 67 | ): boolean { 68 | if (arg.startsWith(prefix)) { 69 | for (const item of arg.slice(prefix.length).split(",")) { 70 | set.add(item); 71 | } 72 | return true; 73 | } 74 | return false; 75 | } 76 | 77 | function formatCommaDelimitedArg(set: Set): string { 78 | return Array.from(set).join(","); 79 | } 80 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | AttachMessageTransport, 3 | Cancellation, 4 | DebugCallback, 5 | OnClose, 6 | OnMessage, 7 | Process, 8 | ProcessWithPipeMessageTransport, 9 | ProcessWithWebSocketUrl, 10 | RaceCancellation, 11 | SendMessage, 12 | SpawnOptions, 13 | Stdio, 14 | Task, 15 | Transport, 16 | TransportMapping, 17 | ArgumentOptions, 18 | ChromeSpawnOptions, 19 | Chrome, 20 | } from "../types"; 21 | export { default } from "./spawnChrome"; 22 | export { defaultFlags, headlessFlags } from "./defaultFlags"; 23 | export { default as getArguments } from "./getArguments"; 24 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/src/spawnChrome.ts: -------------------------------------------------------------------------------- 1 | import findChrome from "@tracerbench/find-chrome"; 2 | import spawn from "@tracerbench/spawn"; 3 | 4 | import type { Chrome, ChromeSpawnOptions, DebugCallback } from "../types"; 5 | import canonicalizeOptions from "./canonicalizeOptions"; 6 | import createTempDir from "./createTmpDir"; 7 | import getArguments from "./getArguments"; 8 | 9 | export default function spawnChrome( 10 | options?: Partial, 11 | debugCallback?: DebugCallback, 12 | ): Chrome { 13 | const canonicalized = canonicalizeOptions(options); 14 | 15 | let chromeExecutable = canonicalized.chromeExecutable; 16 | if (chromeExecutable === undefined) { 17 | chromeExecutable = findChrome(); 18 | } 19 | 20 | let userDataDir = canonicalized.userDataDir; 21 | let onExit: (() => void) | undefined; 22 | if (userDataDir === undefined) { 23 | const [tmpDir, removeTmpDir] = createTempDir(canonicalized.userDataRoot); 24 | userDataDir = tmpDir; 25 | onExit = () => { 26 | try { 27 | removeTmpDir(); 28 | } catch (e) { 29 | if (debugCallback !== undefined) { 30 | debugCallback( 31 | "Removing temp user data dir %o failed with %o", 32 | tmpDir, 33 | e, 34 | ); 35 | } 36 | } 37 | }; 38 | } 39 | 40 | const args = getArguments(userDataDir, canonicalized); 41 | 42 | const chromeProcess = Object.assign( 43 | spawn(chromeExecutable, args, canonicalized, "pipe", debugCallback), 44 | { 45 | userDataDir, 46 | }, 47 | ); 48 | 49 | if (onExit !== undefined) { 50 | chromeProcess.once("exit", onExit); 51 | } 52 | 53 | return chromeProcess; 54 | } 55 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["scripthost", "es2017"], 5 | "types": ["tmp", "node"], 6 | 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "newLine": "LF", 20 | 21 | "moduleResolution": "node", 22 | "module": "commonjs", 23 | "target": "ES2017" 24 | }, 25 | "include": ["src/*.ts"], 26 | "references": [ 27 | { 28 | "path": "../find-chrome" 29 | }, 30 | { 31 | "path": "../spawn" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /@tracerbench/spawn-chrome/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttachMessageTransport, 3 | Cancellation, 4 | DebugCallback, 5 | OnClose, 6 | OnMessage, 7 | Process, 8 | ProcessWithPipeMessageTransport, 9 | ProcessWithWebSocketUrl, 10 | RaceCancellation, 11 | SendMessage, 12 | SpawnOptions, 13 | Stdio, 14 | Task, 15 | Transport, 16 | TransportMapping, 17 | } from "@tracerbench/spawn"; 18 | 19 | export type { 20 | AttachMessageTransport, 21 | Cancellation, 22 | DebugCallback, 23 | OnClose, 24 | OnMessage, 25 | Process, 26 | ProcessWithPipeMessageTransport, 27 | ProcessWithWebSocketUrl, 28 | RaceCancellation, 29 | SendMessage, 30 | SpawnOptions, 31 | Stdio, 32 | Task, 33 | Transport, 34 | TransportMapping, 35 | }; 36 | 37 | export interface ArgumentOptions { 38 | /** 39 | * The url to open chrome at. 40 | */ 41 | url: string | undefined; 42 | 43 | /** 44 | * Disable the defaults flags. 45 | * 46 | * It still will add `--remote-debugging-pipe` and `--user-data-dir`. 47 | */ 48 | disableDefaultArguments: boolean; 49 | 50 | /** 51 | * Additional arguments to the defaults. 52 | */ 53 | additionalArguments: string[] | undefined; 54 | 55 | /** 56 | * Provide default headless arguments. 57 | */ 58 | headless: boolean; 59 | } 60 | 61 | export interface ChromeSpawnOptions extends ArgumentOptions, SpawnOptions { 62 | /** 63 | * Override finding the chrome executable. 64 | */ 65 | chromeExecutable: string | undefined; 66 | 67 | /** 68 | * Explicitly provide a user data directory. 69 | * 70 | * If there is already a chrome process running using this directory, 71 | * the spawn will fail. 72 | */ 73 | userDataDir: string | undefined; 74 | 75 | /** 76 | * The root directory for creating the user data tmp dir. 77 | */ 78 | userDataRoot: string | undefined; 79 | } 80 | 81 | export interface Chrome extends ProcessWithPipeMessageTransport { 82 | userDataDir: string; 83 | } 84 | -------------------------------------------------------------------------------- /@tracerbench/spawn/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | project: "./tsconfig.json", 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module", 7 | }, 8 | ignorePatterns: ["dist/", ".eslintrc.js"], 9 | extends: ["../../.eslintrc"], 10 | }; 11 | -------------------------------------------------------------------------------- /@tracerbench/spawn/README.md: -------------------------------------------------------------------------------- 1 | # @tracerbench/spawn 2 | 3 | Higher level API `execa` to spawn a process with either an `AttachMessageTransport` 4 | using "\0" delimited messages stdio pipes 3 and 4 or parses the stderr 5 | stream for a devtools websocket URL. 6 | -------------------------------------------------------------------------------- /@tracerbench/spawn/examples/pipe.js: -------------------------------------------------------------------------------- 1 | const spawn = require("@tracerbench/spawn").default; 2 | const findChrome = require("@tracerbench/find-chrome").default; 3 | const { inspect } = require("util"); 4 | 5 | const chrome = spawn( 6 | findChrome(), 7 | ["--remote-debugging-pipe", "--disable-extensions", "https://google.com"], 8 | "inherit", 9 | "pipe", 10 | ); 11 | 12 | const send = chrome.attach( 13 | message => { 14 | console.log(inspect(JSON.parse(message))); 15 | }, 16 | err => { 17 | if (err) { 18 | console.log(err.stack); 19 | } 20 | console.log("close"); 21 | }, 22 | ); 23 | 24 | let seq = 0; 25 | 26 | send( 27 | JSON.stringify({ 28 | id: ++seq, 29 | method: "Target.setDiscoverTargets", 30 | params: { 31 | discover: true, 32 | }, 33 | }), 34 | ); 35 | 36 | (async () => { 37 | await new Promise(resolve => setTimeout(resolve, 1000)); 38 | send( 39 | JSON.stringify({ 40 | id: ++seq, 41 | method: "Browser.close", 42 | }), 43 | ); 44 | await chrome.waitForExit(); 45 | })(); 46 | -------------------------------------------------------------------------------- /@tracerbench/spawn/examples/websocket.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { 3 | default: openWebSocket, 4 | } = require("@tracerbench/websocket-message-transport"); 5 | const { default: spawn } = require("@tracerbench/spawn"); 6 | const { disposablePromise } = require("race-cancellation"); 7 | const { EventEmitter } = require("events"); 8 | const debug = require("debug")("protocol"); 9 | 10 | main(`const obj = { 11 | hello: "world", 12 | }; 13 | debugger; 14 | console.log("end");`); 15 | 16 | /** 17 | * @param {import('@tracerbench/spawn/types').ProcessWithWebSocketUrl} child 18 | * @param {import('race-cancellation').RaceCancellation} raceCancellation 19 | */ 20 | async function debugScript(child, raceCancellation) { 21 | console.log("wait for debugger url"); 22 | const url = await child.url(); 23 | 24 | console.log("open websocket %s", url); 25 | 26 | const [attach, close] = await openWebSocket(url, raceCancellation); 27 | 28 | const [send, until] = connection(attach, raceCancellation); 29 | 30 | console.log("wait for break on start"); 31 | await Promise.all([ 32 | until("Debugger.paused"), 33 | send("Debugger.enable"), 34 | send("Runtime.enable"), 35 | send("Runtime.runIfWaitingForDebugger"), 36 | ]); 37 | 38 | console.log("resume and wait for debugger statement"); 39 | const [debuggerStatement] = await Promise.all([ 40 | until("Debugger.paused"), 41 | send("Debugger.resume"), 42 | ]); 43 | 44 | console.log("hit debugger statement"); 45 | 46 | console.log("eval obj variable"); 47 | let result = await send("Debugger.evaluateOnCallFrame", { 48 | callFrameId: debuggerStatement.callFrames[0].callFrameId, 49 | expression: "obj", 50 | returnByValue: true, 51 | }); 52 | console.log("result %O", result); 53 | 54 | console.log("resume until context destroyed"); 55 | await Promise.all([ 56 | until("Runtime.executionContextDestroyed"), 57 | send("Debugger.resume"), 58 | ]); 59 | 60 | console.log("close websocket"); 61 | close(); 62 | 63 | console.log("wait for exit"); 64 | await child.waitForExit(); 65 | 66 | console.log("node exited"); 67 | } 68 | 69 | async function main(script) { 70 | console.log("spawn node"); 71 | 72 | const child = spawn( 73 | process.execPath, 74 | ["--inspect-brk=0", "-e", script], 75 | "inherit", 76 | "websocket", 77 | ); 78 | try { 79 | await debugScript(child, child.raceExit); 80 | } finally { 81 | console.log("finally"); 82 | await child.dispose(); 83 | } 84 | } 85 | 86 | /** 87 | * @param {import('@tracerbench/message-transport').AttachMessageTransport} attach 88 | * @param {import('race-cancellation').RaceCancellation} raceCancellation 89 | */ 90 | function connection(attach, raceCancellation) { 91 | let seq = 0; 92 | let events = new EventEmitter(); 93 | let results = new Map(); 94 | 95 | const sendMessage = attach( 96 | data => handleMessage(data), 97 | error => { 98 | if (error) { 99 | events.emit("error", error); 100 | } 101 | debug("close"); 102 | }, 103 | ); 104 | 105 | return [send, until]; 106 | 107 | function handleMessage(data) { 108 | const message = JSON.parse(data); 109 | debug("RECV %O", message); 110 | if ("id" in message) { 111 | const resolve = results.get(message.id); 112 | if (resolve) resolve(message.result); 113 | } else events.emit(message.method, message.params); 114 | } 115 | 116 | async function send(method, params) { 117 | const id = ++seq; 118 | const message = { 119 | id, 120 | method, 121 | params, 122 | }; 123 | debug("SEND %O", message); 124 | try { 125 | sendMessage(JSON.stringify(message)); 126 | return await raceCancellation( 127 | () => 128 | new Promise(resolve => { 129 | results.set(id, resolve); 130 | }), 131 | ); 132 | } finally { 133 | results.delete(id); 134 | } 135 | } 136 | 137 | function until(event) { 138 | return disposablePromise(resolve => { 139 | events.on(event, resolve); 140 | return () => events.removeListener(event, resolve); 141 | }, raceCancellation); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /@tracerbench/spawn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tracerbench/spawn", 3 | "version": "2.0.0", 4 | "description": "High level spawn API for spawning process with a connection to the DevTools protocol.", 5 | "license": "BSD-2-Clause", 6 | "author": "Kris Selden ", 7 | "files": [ 8 | "dist", 9 | "src", 10 | "types.d.ts" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 15 | "directory": "@tracerbench/spawn" 16 | }, 17 | "main": "dist/index.js", 18 | "types": "dist/index.d.ts", 19 | "scripts": { 20 | "build": "tsc -b", 21 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 22 | "fixlint": "eslint --ext .ts src --fix", 23 | "lint": "eslint --ext .ts src", 24 | "prepare": "yarn run build" 25 | }, 26 | "dependencies": { 27 | "@tracerbench/message-transport": "^2.0.0", 28 | "debug": "^4.1.1", 29 | "execa": "^5.1.1", 30 | "race-cancellation": "^0.4.1" 31 | }, 32 | "devDependencies": { 33 | "@typescript-eslint/eslint-plugin": "^5.59.1", 34 | "@typescript-eslint/parser": "^5.59.1", 35 | "eslint": "^8.39.0", 36 | "eslint-config-prettier": "^8.8.0", 37 | "eslint-plugin-import": "^2.18.2", 38 | "eslint-plugin-prettier": "^4.2.1", 39 | "eslint-plugin-simple-import-sort": "^10.0.0", 40 | "prettier": "^2.0.5", 41 | "typescript": "^5.0.4" 42 | }, 43 | "gitHead": "930bc11c8b01620e5095df9249f0647af68235b5" 44 | } 45 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/execa.ts: -------------------------------------------------------------------------------- 1 | import execa = require("execa"); 2 | 3 | export default function wrappedExec( 4 | command: string, 5 | args: string[], 6 | opts: execa.Options, 7 | debugCallback: (formatter: string, ...args: unknown[]) => void, 8 | ): execa.ExecaChildProcess { 9 | const child = execa(command, args, opts); 10 | debugCallback( 11 | "execa(%o, %O, %O) => ChildProcess (pid: %o)", 12 | command, 13 | args, 14 | opts, 15 | child.pid, 16 | ); 17 | 18 | // even though the child promise is a promise of exit 19 | // it rejects on being signalled 20 | child.catch(() => { 21 | // ignore unhandled rejection from sending signal 22 | }); 23 | 24 | // 25 | return child; 26 | } 27 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./spawn"; 2 | export type { 3 | AttachMessageTransport, 4 | OnClose, 5 | OnMessage, 6 | SendMessage, 7 | Cancellation, 8 | RaceCancellation, 9 | Task, 10 | DebugCallback, 11 | Process, 12 | ProcessWithWebSocketUrl, 13 | ProcessWithPipeMessageTransport, 14 | TransportMapping, 15 | Transport, 16 | Stdio, 17 | SpawnOptions, 18 | } from "../types"; 19 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/newBufferSplitter.ts: -------------------------------------------------------------------------------- 1 | export type SplitterCallback = (split: Buffer) => void; 2 | 3 | export interface BufferSplitter { 4 | stop(): void; 5 | push(chunk: Buffer): void; 6 | flush(): void; 7 | } 8 | 9 | export default function newBufferSplitter( 10 | char: number, 11 | callback: SplitterCallback, 12 | ): BufferSplitter { 13 | const buffers: Buffer[] = []; 14 | let byteLength = 0; 15 | let stopped = false; 16 | 17 | return { 18 | flush, 19 | push, 20 | stop, 21 | }; 22 | 23 | function push(chunk: Buffer): void { 24 | if (stopped) { 25 | return; 26 | } 27 | 28 | let start = 0; 29 | let end = chunk.indexOf(char); 30 | 31 | while (end !== -1) { 32 | _push(chunk, start, end); 33 | 34 | _flush(); 35 | 36 | if (stopped) { 37 | return; 38 | } 39 | 40 | start = end + 1; 41 | end = chunk.indexOf(char, start); 42 | } 43 | 44 | // append remainder 45 | _push(chunk, start, chunk.length); 46 | } 47 | 48 | function flush(): void { 49 | if (stopped) { 50 | return; 51 | } 52 | 53 | _flush(); 54 | } 55 | 56 | function stop(): void { 57 | stopped = true; 58 | byteLength = 0; 59 | buffers.length = 0; 60 | } 61 | 62 | function _push(buffer: Buffer, start: number, end: number): void { 63 | const length = end - start; 64 | if (length > 0) { 65 | if (length !== buffer.byteLength) { 66 | buffer = buffer.slice(start, end); 67 | } 68 | buffers.push(buffer); 69 | byteLength += length; 70 | } 71 | } 72 | 73 | function _flush(): void { 74 | let split: Buffer | undefined; 75 | 76 | if (byteLength === 0) { 77 | return; 78 | } 79 | 80 | if (buffers.length === 1) { 81 | split = buffers[0]; 82 | } else if (buffers.length > 1) { 83 | split = Buffer.concat(buffers, byteLength); 84 | } 85 | 86 | buffers.length = 0; 87 | byteLength = 0; 88 | 89 | if (split !== undefined) { 90 | callback(split); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/newPipeMessageTransport.ts: -------------------------------------------------------------------------------- 1 | import type { AttachMessageTransport } from "../types"; 2 | import newBufferSplitter from "./newBufferSplitter"; 3 | import newTaskQueue from "./newTaskQueue"; 4 | 5 | const enum Char { 6 | NULL = 0, 7 | } 8 | 9 | export type Write = (data: Buffer) => void; 10 | export type EndWrite = () => void; 11 | export type OnRead = (chunk: Buffer) => void; 12 | export type OnReadEnd = () => void; 13 | export type OnClose = (err?: Error) => void; 14 | 15 | export type AttachProcess = ( 16 | onRead: OnRead, 17 | onReadEnd: OnReadEnd, 18 | onClose: OnClose, 19 | ) => [Write, EndWrite]; 20 | 21 | export default function newPipeMessageTransport( 22 | connect: AttachProcess, 23 | ): AttachMessageTransport { 24 | let attached = false; 25 | let closed = false; 26 | 27 | return (onMessage, onClose) => { 28 | if (attached) { 29 | throw new Error("already attached to transport"); 30 | } 31 | attached = true; 32 | 33 | const [write, endWrite] = connect(onRead, onReadEnd, enqueueClose); 34 | 35 | const enqueue = newTaskQueue(); 36 | const splitter = newBufferSplitter(Char.NULL, (split) => 37 | enqueueMessage(split.toString("utf8")), 38 | ); 39 | 40 | return sendMessage; 41 | 42 | function enqueueClose(error?: Error): void { 43 | if (closed) { 44 | return; 45 | } 46 | 47 | closed = true; 48 | 49 | if (error) { 50 | enqueue(() => onClose(error)); 51 | } else { 52 | enqueue(onClose); 53 | } 54 | 55 | endWrite(); 56 | } 57 | 58 | function enqueueMessage(message: string): void { 59 | enqueue(() => onMessage(message)); 60 | } 61 | 62 | function onRead(data: Buffer): void { 63 | splitter.push(data); 64 | } 65 | 66 | function onReadEnd(): void { 67 | splitter.flush(); 68 | } 69 | 70 | function sendMessage(message: string): void { 71 | write(Buffer.from(message + "\0", "utf8")); 72 | } 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/newProcess.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import type { ExecaChildProcess } from "execa"; 3 | import { 4 | cancellableRace, 5 | disposablePromise, 6 | throwIfCancelled, 7 | withRaceTimeout, 8 | } from "race-cancellation"; 9 | 10 | import type { 11 | Cancellation, 12 | DebugCallback, 13 | Process, 14 | RaceCancellation, 15 | } from "../types"; 16 | 17 | export default function newProcess( 18 | child: ExecaChildProcess, 19 | command: string, 20 | debugCallback: DebugCallback, 21 | ): Process { 22 | let hasExited = false; 23 | let lastError: Error | undefined; 24 | 25 | const emitter = new EventEmitter(); 26 | const [raceExit, cancel] = cancellableRace(); 27 | 28 | void child.on("error", (error) => { 29 | lastError = error; 30 | debugCallback("%o (pid: %o) 'error' event: %O", command, child.pid, error); 31 | onErrorOrExit(error); 32 | }); 33 | 34 | void child.on("exit", () => { 35 | debugCallback("%o (pid: %o) 'exit' event", command, child.pid); 36 | onErrorOrExit(); 37 | }); 38 | 39 | return { 40 | dispose, 41 | hasExited: () => hasExited, 42 | kill, 43 | off: emitter.removeListener.bind(emitter), 44 | on: emitter.on.bind(emitter), 45 | once: emitter.once.bind(emitter), 46 | raceExit, 47 | removeAllListeners: emitter.removeAllListeners.bind(emitter), 48 | removeListener: emitter.removeListener.bind(emitter), 49 | waitForExit, 50 | }; 51 | 52 | /* 53 | https://nodejs.org/api/child_process.html#child_process_event_exit 54 | 55 | The 'exit' event may or may not fire after an error has occurred. 56 | When listening to both the 'exit' and 'error' events, it is important to guard against accidentally invoking handler functions multiple times. 57 | */ 58 | function onErrorOrExit(error?: Error): void { 59 | if (hasExited) { 60 | return; 61 | } 62 | 63 | hasExited = true; 64 | 65 | if (error) { 66 | cancel(`process exited early: ${error.message}`); 67 | } else { 68 | cancel(`process exited early`); 69 | } 70 | 71 | emitter.emit("exit"); 72 | } 73 | 74 | async function exited( 75 | raceCancellation?: RaceCancellation, 76 | ): Promise { 77 | if (lastError) { 78 | throw lastError; 79 | } 80 | 81 | if (hasExited) { 82 | return; 83 | } 84 | 85 | return await disposablePromise((resolve, reject) => { 86 | void child.on("exit", resolve); 87 | void child.on("error", reject); 88 | return () => { 89 | void child.removeListener("exit", resolve); 90 | void child.removeListener("error", reject); 91 | }; 92 | }, raceCancellation); 93 | } 94 | 95 | async function waitForExit( 96 | timeout = 10000, 97 | raceCancellation?: RaceCancellation, 98 | ): Promise { 99 | if (hasExited) { 100 | return; 101 | } 102 | 103 | const result = await (timeout > 0 104 | ? withRaceTimeout(exited, timeout)(raceCancellation) 105 | : exited(raceCancellation)); 106 | return throwIfCancelled(result); 107 | } 108 | 109 | async function kill( 110 | timeout?: number, 111 | raceCancellation?: RaceCancellation, 112 | ): Promise { 113 | if (!child.killed && child.pid) { 114 | child.kill(); 115 | } 116 | 117 | await waitForExit(timeout, raceCancellation); 118 | } 119 | 120 | async function dispose(): Promise { 121 | try { 122 | await kill(); 123 | } catch (e) { 124 | // dispose is in finally and meant to be safe 125 | // we don't want to cover up error 126 | // just output for debugging 127 | debugCallback("%o (pid: %o) dispose error: %O", command, child.pid, e); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/newProcessWithPipeMessageTransport.ts: -------------------------------------------------------------------------------- 1 | import type { ProcessWithPipeMessageTransport, SpawnOptions } from "../types"; 2 | import execa from "./execa"; 3 | import createPipeMessageTransport from "./newPipeMessageTransport"; 4 | import newProcess from "./newProcess"; 5 | 6 | export default function newProcessWithPipeMessageTransport( 7 | command: string, 8 | args: string[], 9 | options: Partial, 10 | debugCallback: (formatter: unknown, ...args: unknown[]) => void, 11 | ): ProcessWithPipeMessageTransport { 12 | const { stdio = "ignore", cwd, env, extendEnv } = options; 13 | const child = execa( 14 | command, 15 | args, 16 | { 17 | // disable buffer, pipe or drain 18 | buffer: false, 19 | stdio: [stdio, stdio, stdio, "pipe", "pipe"], 20 | cwd, 21 | extendEnv, 22 | env, 23 | }, 24 | debugCallback, 25 | ); 26 | 27 | const process = newProcess(child, command, debugCallback); 28 | const [, , , writeStream, readStream] = child.stdio as [ 29 | NodeJS.WritableStream, 30 | NodeJS.ReadableStream, 31 | NodeJS.ReadableStream, 32 | NodeJS.WritableStream, 33 | NodeJS.ReadableStream, 34 | ]; 35 | 36 | const attach = createPipeMessageTransport((onRead, onReadEnd, onClose) => { 37 | void child.on("error", onClose); 38 | void child.on("exit", onClose); 39 | 40 | readStream.on("data", handleReadData); 41 | readStream.on("end", handleReadEnd); 42 | readStream.on("error", handleReadError); 43 | 44 | writeStream.on("close", handleWriteClose); 45 | writeStream.on("error", handleWriteError); 46 | 47 | return [(data) => writeStream.write(data), () => writeStream.end()]; 48 | 49 | function handleReadData(buffer: Buffer): void { 50 | debugEvent("read", "data", buffer.byteLength); 51 | onRead(buffer); 52 | } 53 | 54 | function handleReadEnd(): void { 55 | debugEvent("read", "end"); 56 | onReadEnd(); 57 | } 58 | 59 | function handleReadError(error: Error): void { 60 | debugEvent("read", "error", error); 61 | } 62 | 63 | function handleWriteError(error: Error | NodeJS.ErrnoException): void { 64 | debugEvent("write", "error", error); 65 | // writes while the other side is closing can cause EPIPE 66 | // just wait for close to actually happen and ignore it. 67 | if (error && "code" in error && error.code === "EPIPE") { 68 | return; 69 | } 70 | onClose(error); 71 | } 72 | 73 | function handleWriteClose(): void { 74 | debugEvent("write", "close"); 75 | onClose(); 76 | } 77 | 78 | function debugEvent( 79 | pipe: "read" | "write", 80 | event: string, 81 | arg?: unknown, 82 | ): void { 83 | if (arg === undefined) { 84 | debugCallback("%s pipe (pid: %o) %o event", pipe, child.pid, event); 85 | } else { 86 | debugCallback( 87 | "%s pipe (pid: %o) %o event: %O", 88 | pipe, 89 | child.pid, 90 | event, 91 | arg, 92 | ); 93 | } 94 | } 95 | }); 96 | 97 | return Object.assign(process, { attach }); 98 | } 99 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/newProcessWithWebSocketUrl.ts: -------------------------------------------------------------------------------- 1 | import type { RaceCancellation } from "race-cancellation"; 2 | import { combineRaceCancellation, throwIfCancelled } from "race-cancellation"; 3 | 4 | import type { ProcessWithWebSocketUrl, SpawnOptions, Stdio } from "../types"; 5 | import execa from "./execa"; 6 | import newProcess from "./newProcess"; 7 | import newWebSocketUrlParser from "./newWebSocketUrlParser"; 8 | 9 | export default function newProcessWithWebSocketUrl( 10 | command: string, 11 | args: string[], 12 | options: Partial, 13 | debugCallback: (formatter: unknown, ...args: unknown[]) => void, 14 | ): ProcessWithWebSocketUrl { 15 | const { stdio = "ignore", cwd, env, extendEnv } = options; 16 | const child = execa( 17 | command, 18 | args, 19 | { 20 | // disable buffer, pipe or drain 21 | buffer: false, 22 | stdio: [stdio, stdio, "pipe"], 23 | cwd, 24 | extendEnv, 25 | env, 26 | }, 27 | debugCallback, 28 | ); 29 | 30 | const process = newProcess(child, command, debugCallback); 31 | if (child.stderr === null) { 32 | throw new Error("missing stderr"); 33 | } 34 | return Object.assign(process, { 35 | url: createUrl(child.stderr, stdio, process.raceExit), 36 | }); 37 | } 38 | 39 | function createUrl( 40 | stderr: NodeJS.ReadableStream, 41 | stdio: Stdio, 42 | raceExit: RaceCancellation, 43 | ): (race?: RaceCancellation) => Promise { 44 | let promise: Promise | undefined; 45 | return url; 46 | 47 | async function url(raceCancellation?: RaceCancellation): Promise { 48 | return throwIfCancelled( 49 | await combineRaceCancellation( 50 | raceExit, 51 | raceCancellation, 52 | )(() => { 53 | if (promise === undefined) { 54 | promise = new Promise((resolve) => parseUrl(stderr, stdio, resolve)); 55 | } 56 | return promise; 57 | }), 58 | ); 59 | } 60 | } 61 | 62 | function parseUrl( 63 | stderr: NodeJS.ReadableStream, 64 | stdio: Stdio, 65 | callback: (url: string) => void, 66 | ): void { 67 | const parser = newWebSocketUrlParser(callback); 68 | stderr.pipe(parser); 69 | if (stdio === "inherit") { 70 | parser.pipe(process.stderr); 71 | } else if (stdio === "ignore") { 72 | parser.on("data", () => void 0); 73 | } else { 74 | throw new Error( 75 | `invalid stdio arg ${String(stdio)} expected ignore or inherit`, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/newTaskQueue.ts: -------------------------------------------------------------------------------- 1 | export type Task = () => void; 2 | 3 | /** 4 | * Ensures each message will have its own microtask queue. 5 | */ 6 | export default function newTaskQueue(): (task: Task) => void { 7 | const queue: Task[] = []; 8 | let scheduled = false; 9 | let read = 0; 10 | let write = 0; 11 | return enqueue; 12 | 13 | function schedule(): void { 14 | scheduled = true; 15 | setImmediate(next); 16 | } 17 | 18 | function next(): void { 19 | scheduled = false; 20 | dequeue(); 21 | } 22 | 23 | function enqueue(task: Task): void { 24 | queue[write++] = task; 25 | if (scheduled) { 26 | return; 27 | } 28 | schedule(); 29 | } 30 | 31 | function dequeue(): void { 32 | const task = queue[read]; 33 | // release memory 34 | queue[read++] = undefined as unknown as Task; 35 | if (read < write) { 36 | schedule(); 37 | } else { 38 | // empty reset back to zero 39 | read = write = 0; 40 | } 41 | task(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/newWebSocketUrlParser.ts: -------------------------------------------------------------------------------- 1 | import type { TransformCallback } from "stream"; 2 | import { Transform } from "stream"; 3 | 4 | import newBufferSplitter from "./newBufferSplitter"; 5 | 6 | const WS_URL_REGEX = 7 | /^(?:DevTools|Debugger) listening on (ws:\/\/[^:]+:\d+\/.+$)/; 8 | 9 | const enum Char { 10 | LF = 10, 11 | CR = 13, 12 | } 13 | 14 | const PASSTHROUGH: ParserState = { 15 | transform(chunk, _encoding, callback) { 16 | callback(undefined, chunk); 17 | }, 18 | flush(callback) { 19 | callback(); 20 | }, 21 | }; 22 | 23 | interface ParserState { 24 | transform(chunk: Buffer, encoding: string, callback: TransformCallback): void; 25 | flush(callback: TransformCallback): void; 26 | } 27 | 28 | export type UrlParsedCallback = (url: string) => void; 29 | 30 | export default function newWebSocketUrlParser( 31 | onUrlParsed: UrlParsedCallback, 32 | ): NodeJS.ReadWriteStream { 33 | let state = createParsingState((url) => { 34 | onUrlParsed(url); 35 | state = PASSTHROUGH; 36 | }); 37 | return new Transform({ 38 | transform(chunk: Buffer, encoding, callback) { 39 | state.transform(chunk, encoding, callback); 40 | }, 41 | flush(callback) { 42 | state.flush(callback); 43 | }, 44 | }); 45 | } 46 | 47 | function createParsingState(onUrlParsed: UrlParsedCallback): ParserState { 48 | const splitter = newBufferSplitter(Char.LF, (split) => { 49 | const url = findUrl(split); 50 | if (url !== undefined) { 51 | splitter.stop(); 52 | onUrlParsed(url); 53 | } 54 | }); 55 | 56 | return { 57 | transform(chunk, _encoding, callback) { 58 | splitter.push(chunk); 59 | callback(undefined, chunk); 60 | }, 61 | flush(callback) { 62 | splitter.flush(); 63 | callback(); 64 | }, 65 | }; 66 | } 67 | 68 | function findUrl(split: Buffer): string | undefined { 69 | let length = split.byteLength; 70 | 71 | // check CR LF 72 | if (length > 0 && split[length - 1] === Char.CR) { 73 | length--; 74 | } 75 | 76 | if (length === 0) { 77 | return; 78 | } 79 | 80 | const line = split.toString("utf8", 0, length); 81 | const match = WS_URL_REGEX.exec(line); 82 | if (match !== null) { 83 | const [, url] = match; 84 | return url; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /@tracerbench/spawn/src/spawn.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | 3 | import type { 4 | DebugCallback, 5 | ProcessWithPipeMessageTransport, 6 | ProcessWithWebSocketUrl, 7 | SpawnOptions, 8 | Stdio, 9 | Transport, 10 | } from "../types"; 11 | import newProcessWithPipeMessageTransport from "./newProcessWithPipeMessageTransport"; 12 | import newProcessWithWebSocketUrl from "./newProcessWithWebSocketUrl"; 13 | 14 | function spawn( 15 | executable: string, 16 | args: string[], 17 | options: Stdio | Partial | undefined, 18 | transport: "websocket", 19 | debugCallback?: DebugCallback, 20 | ): ProcessWithWebSocketUrl; 21 | function spawn( 22 | executable: string, 23 | args: string[], 24 | options?: Stdio | Partial, 25 | transport?: "pipe", 26 | debugCallback?: DebugCallback, 27 | ): ProcessWithPipeMessageTransport; 28 | function spawn( 29 | executable: string, 30 | args: string[], 31 | options: Stdio | Partial = { stdio: "ignore" }, 32 | transport: Transport = "pipe", 33 | debugCallback: DebugCallback = debug("@tracerbench/spawn"), 34 | ): ProcessWithPipeMessageTransport | ProcessWithWebSocketUrl { 35 | if (typeof options === "string") { 36 | options = { stdio: options }; 37 | } 38 | switch (transport) { 39 | case "pipe": 40 | return newProcessWithPipeMessageTransport( 41 | executable, 42 | args, 43 | options, 44 | debugCallback, 45 | ); 46 | case "websocket": 47 | return newProcessWithWebSocketUrl( 48 | executable, 49 | args, 50 | options, 51 | debugCallback, 52 | ); 53 | default: 54 | throw invalidTransport(transport); 55 | } 56 | } 57 | 58 | function invalidTransport(transport: never): Error { 59 | return new Error(`invalid transport argument "${String(transport)}"`); 60 | } 61 | 62 | export default spawn; 63 | -------------------------------------------------------------------------------- /@tracerbench/spawn/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["scripthost", "es2017"], 5 | "types": ["node", "execa"], 6 | 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "newLine": "LF", 20 | 21 | "moduleResolution": "node", 22 | "module": "commonjs", 23 | "target": "ES2017" 24 | }, 25 | "include": ["src/*.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /@tracerbench/spawn/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttachMessageTransport, 3 | OnClose, 4 | OnMessage, 5 | SendMessage, 6 | } from "@tracerbench/message-transport"; 7 | import type { Cancellation, RaceCancellation, Task } from "race-cancellation"; 8 | 9 | export type { 10 | AttachMessageTransport, 11 | OnClose, 12 | OnMessage, 13 | SendMessage, 14 | Cancellation, 15 | RaceCancellation, 16 | Task, 17 | }; 18 | 19 | export type DebugCallback = (formatter: unknown, ...args: unknown[]) => void; 20 | 21 | export interface Process { 22 | /** 23 | * Allows tasks to be raced against the exit of the process. 24 | */ 25 | raceExit: RaceCancellation; 26 | 27 | hasExited(): boolean; 28 | 29 | /** 30 | * Sends a SIGTERM with a timeout, if the process has not 31 | * exited by the timeout it sends a SIGKILL 32 | * @param options 33 | */ 34 | kill(timeout?: number, raceCancellation?: RaceCancellation): Promise; 35 | 36 | /** 37 | * Waits for exit of the process. 38 | * @param options 39 | */ 40 | waitForExit( 41 | timeout?: number, 42 | raceCancellation?: RaceCancellation, 43 | ): Promise; 44 | 45 | /** 46 | * Same as kill but an error will become an event since 47 | * this is intended to be awaited in a finally block. 48 | */ 49 | dispose(): Promise; 50 | 51 | on(event: "error", listener: (error: Error) => void): void; 52 | on(event: "exit", listener: () => void): void; 53 | once(event: "error", listener: (error: Error) => void): void; 54 | once(event: "exit", listener: () => void): void; 55 | off(event: "error", listener: (error: Error) => void): void; 56 | off(event: "exit", listener: () => void): void; 57 | removeListener(event: "error", listener: (error: Error) => void): void; 58 | removeListener(event: "exit", listener: () => void): void; 59 | removeAllListeners(event?: "error" | "exit"): void; 60 | } 61 | 62 | export interface ProcessWithWebSocketUrl extends Process { 63 | url: (raceCancellation?: RaceCancellation) => Promise; 64 | } 65 | 66 | export interface ProcessWithPipeMessageTransport extends Process { 67 | attach: AttachMessageTransport; 68 | } 69 | 70 | export type TransportMapping = { 71 | pipe: ProcessWithPipeMessageTransport; 72 | websocket: ProcessWithWebSocketUrl; 73 | }; 74 | 75 | export type Transport = keyof TransportMapping; 76 | 77 | export type Stdio = "ignore" | "inherit"; 78 | 79 | export interface SpawnOptions { 80 | stdio: Stdio; 81 | cwd: string | undefined; 82 | extendEnv: boolean | undefined; 83 | env: 84 | | { 85 | [name: string]: string | undefined; 86 | } 87 | | undefined; 88 | } 89 | -------------------------------------------------------------------------------- /@tracerbench/websocket-message-transport/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: "./tsconfig.json", 4 | tsconfigRootDir: __dirname, 5 | sourceType: "module", 6 | }, 7 | ignorePatterns: ["dist/", ".eslintrc.js"], 8 | extends: ["../../.eslintrc"], 9 | }; 10 | -------------------------------------------------------------------------------- /@tracerbench/websocket-message-transport/README.md: -------------------------------------------------------------------------------- 1 | # @tracerbench/websocket-message-transport 2 | 3 | Adapts the `ws` node module into a message transport. 4 | 5 | ```ts 6 | export default async function openWebSocket( 7 | url: string, 8 | raceCancellation?: RaceCancellation, 9 | ): Promise<[AttachMessageTransport, CloseWebSocket]> 10 | ``` 11 | -------------------------------------------------------------------------------- /@tracerbench/websocket-message-transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tracerbench/websocket-message-transport", 3 | "version": "2.0.0", 4 | "license": "BSD-2-Clause", 5 | "author": "Kris Selden ", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 13 | "directory": "@tracerbench/websocket-message-transport" 14 | }, 15 | "main": "dist/index.js", 16 | "types": "dist/index.d.ts", 17 | "scripts": { 18 | "build": "tsc -b", 19 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 20 | "fixlint": "eslint --ext .ts src --fix", 21 | "lint": "eslint --ext .ts src", 22 | "prepare": "yarn run build" 23 | }, 24 | "dependencies": { 25 | "@tracerbench/message-transport": "^2.0.0", 26 | "race-cancellation": "^0.4.1", 27 | "ws": "^8.13.0" 28 | }, 29 | "devDependencies": { 30 | "@types/ws": "^8.5.4", 31 | "@typescript-eslint/eslint-plugin": "^5.59.1", 32 | "@typescript-eslint/parser": "^5.59.1", 33 | "eslint": "^8.39.0", 34 | "eslint-config-prettier": "^8.8.0", 35 | "eslint-plugin-import": "^2.18.2", 36 | "eslint-plugin-prettier": "^4.2.1", 37 | "eslint-plugin-simple-import-sort": "^10.0.0", 38 | "prettier": "^2.0.5", 39 | "typescript": "^5.0.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /@tracerbench/websocket-message-transport/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttachMessageTransport, 3 | OnClose, 4 | OnMessage, 5 | SendMessage, 6 | } from "@tracerbench/message-transport"; 7 | import type { Cancellation, RaceCancellation, Task } from "race-cancellation"; 8 | import { disposablePromise } from "race-cancellation"; 9 | import NodeWebSocket = require("ws"); 10 | 11 | export type { 12 | AttachMessageTransport, 13 | OnClose, 14 | OnMessage, 15 | SendMessage, 16 | Cancellation, 17 | RaceCancellation, 18 | Task, 19 | }; 20 | export type CloseWebSocket = () => void; 21 | 22 | export default async function openWebSocket( 23 | url: string, 24 | raceCancellation?: RaceCancellation, 25 | ): Promise<[AttachMessageTransport, CloseWebSocket]> { 26 | const ws = new NodeWebSocket(url); 27 | let lastError: Error | undefined; 28 | 29 | ws.on("error", (err: Error) => { 30 | lastError = err; 31 | }); 32 | 33 | await disposablePromise((resolve, reject) => { 34 | ws.on("open", resolve); 35 | ws.on("close", onClose); 36 | function onClose(): void { 37 | let message = `Failed to open ${url}`; 38 | if (lastError !== undefined && lastError.stack) { 39 | message += `: ${lastError.stack}`; 40 | } 41 | reject(new Error(message)); 42 | } 43 | return () => { 44 | ws.removeListener("open", resolve); 45 | ws.removeListener("close", onClose); 46 | }; 47 | }, raceCancellation); 48 | 49 | return [ 50 | (onMessage, onClose) => { 51 | // we should be open still when attach is called 52 | // but double check here 53 | if (ws.readyState !== ws.OPEN) { 54 | setImmediate(handleClose); 55 | } 56 | 57 | let called = false; 58 | function handleClose(): void { 59 | if (called) { 60 | return; 61 | } 62 | called = true; 63 | ws.removeListener("message", onMessage); 64 | ws.removeListener("close", handleClose); 65 | onClose(lastError); 66 | } 67 | 68 | ws.on("message", onMessage); 69 | ws.on("close", handleClose); 70 | return (message) => ws.send(message); 71 | }, 72 | () => ws.close(), 73 | ]; 74 | } 75 | -------------------------------------------------------------------------------- /@tracerbench/websocket-message-transport/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["scripthost", "es2017"], 5 | "types": ["ws"], 6 | 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "newLine": "LF", 20 | 21 | "moduleResolution": "node", 22 | "module": "commonjs", 23 | "target": "ES2017" 24 | }, 25 | "files": ["src/index.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.0.0 (2020-06-28) 2 | 3 | #### :boom: Breaking Change 4 | * [#115](https://github.com/TracerBench/chrome-debugging-client/pull/115) feat: add more spawn options ([@krisselden](https://github.com/krisselden)) 5 | 6 | #### :rocket: Enhancement 7 | * [#115](https://github.com/TracerBench/chrome-debugging-client/pull/115) feat: add more spawn options ([@krisselden](https://github.com/krisselden)) 8 | 9 | #### Committers: 1 10 | - Kris Selden ([@krisselden](https://github.com/krisselden)) 11 | 12 | 13 | ## v1.3.1 (2020-06-26) 14 | 15 | #### :bug: Bug Fix 16 | * [#112](https://github.com/TracerBench/chrome-debugging-client/pull/112) Make temp user data dir cleanup infallible ([@krisselden](https://github.com/krisselden)) 17 | 18 | #### Committers: 2 19 | - Kris Selden ([@krisselden](https://github.com/krisselden)) 20 | - [@dependabot-preview[bot]](https://github.com/apps/dependabot-preview) 21 | 22 | 23 | ## v1.3.0 (2020-06-16) 24 | 25 | #### :rocket: Enhancement 26 | * [#83](https://github.com/TracerBench/chrome-debugging-client/pull/83) Improve performance of finding the Chrome executable for common cases. ([@krisselden](https://github.com/krisselden)) 27 | 28 | #### Committers: 2 29 | - Kris Selden ([@krisselden](https://github.com/krisselden)) 30 | - [@dependabot-preview[bot]](https://github.com/apps/dependabot-preview) 31 | 32 | 33 | ## 1.1.0 (2019-10-16) 34 | 35 | - v1.1.0 ([5952575](https://github.com/TracerBench/chrome-debugging-client/commit/5952575)) 36 | - chore: improve build performance ([03c8954](https://github.com/TracerBench/chrome-debugging-client/commit/03c8954)) 37 | - chore: update outdated deps ([f3fc2ba](https://github.com/TracerBench/chrome-debugging-client/commit/f3fc2ba)) 38 | - fix: chrome.close ([dc9e74c](https://github.com/TracerBench/chrome-debugging-client/commit/dc9e74c)) 39 | 40 | ## 1.0.3 (2019-10-15) 41 | 42 | - Update package.json ([3eb1143](https://github.com/TracerBench/chrome-debugging-client/commit/3eb1143)) 43 | - v1.0.3 ([707c390](https://github.com/TracerBench/chrome-debugging-client/commit/707c390)) 44 | - fix: bumping chrome-launcher for catalina fix ([8064f2e](https://github.com/TracerBench/chrome-debugging-client/commit/8064f2e)) 45 | - refactor(debug): Debug logging cleanup. ([ca7e8c9](https://github.com/TracerBench/chrome-debugging-client/commit/ca7e8c9)) 46 | - refactor(spawn): Improve debug output ([eb31b2a](https://github.com/TracerBench/chrome-debugging-client/commit/eb31b2a)) 47 | - docs(examples): Improve examples ([9687874](https://github.com/TracerBench/chrome-debugging-client/commit/9687874)) 48 | 49 | ## 1.0.2 (2019-08-12) 50 | 51 | - Bump lodash from 4.17.11 to 4.17.14 ([cdfb204](https://github.com/TracerBench/chrome-debugging-client/commit/cdfb204)) 52 | - Bump lodash.template from 4.4.0 to 4.5.0 ([f1b4a4e](https://github.com/TracerBench/chrome-debugging-client/commit/f1b4a4e)) 53 | - v1.0.2 ([a63772e](https://github.com/TracerBench/chrome-debugging-client/commit/a63772e)) 54 | - fix(spawn): Handle early exit better ([3a36691](https://github.com/TracerBench/chrome-debugging-client/commit/3a36691)) 55 | 56 | ## 1.0.1 (2019-07-10) 57 | 58 | - v1.0.1 ([e37b27b](https://github.com/TracerBench/chrome-debugging-client/commit/e37b27b)) 59 | - fix: fix dumb bug in graceful close ([bc3ec57](https://github.com/TracerBench/chrome-debugging-client/commit/bc3ec57)) 60 | - fix: version in test ([f2fdf4b](https://github.com/TracerBench/chrome-debugging-client/commit/f2fdf4b)) 61 | 62 | ## 1.0.0 (2019-07-02) 63 | 64 | - v1.0.0 ([5420991](https://github.com/TracerBench/chrome-debugging-client/commit/5420991)) 65 | - chore: remove version from test pacakge ([3b7b449](https://github.com/TracerBench/chrome-debugging-client/commit/3b7b449)) 66 | - chore: update deps ([ec1f996](https://github.com/TracerBench/chrome-debugging-client/commit/ec1f996)) 67 | - chore: update linting ([eec94e3](https://github.com/TracerBench/chrome-debugging-client/commit/eec94e3)) 68 | 69 | ## 1.0.0-beta.4 (2019-06-27) 70 | 71 | - v1.0.0-beta.3 ([f5c42f6](https://github.com/TracerBench/chrome-debugging-client/commit/f5c42f6)) 72 | - v1.0.0-beta.4 ([f2a70d7](https://github.com/TracerBench/chrome-debugging-client/commit/f2a70d7)) 73 | - test(spawn): inc debug for exec and args ([1d3ed6d](https://github.com/TracerBench/chrome-debugging-client/commit/1d3ed6d)) 74 | - fix(canonicalizeOptions): default headless to false ([8a7e283](https://github.com/TracerBench/chrome-debugging-client/commit/8a7e283)) 75 | 76 | ## 1.0.0-beta.2 (2019-06-21) 77 | 78 | - [fix]: type cleanup ([a9d5573](https://github.com/TracerBench/chrome-debugging-client/commit/a9d5573)) 79 | - Update README ([8986c03](https://github.com/TracerBench/chrome-debugging-client/commit/8986c03)) 80 | - v1.0.0-beta.2 ([3ea82b3](https://github.com/TracerBench/chrome-debugging-client/commit/3ea82b3)) 81 | - docs: had some unsaved READMEs ([edb329d](https://github.com/TracerBench/chrome-debugging-client/commit/edb329d)) 82 | 83 | ## 1.0.0-beta.1 (2019-06-05) 84 | 85 | - fix: fix test package to match workspace version ([c55620c](https://github.com/TracerBench/chrome-debugging-client/commit/c55620c)) 86 | - chore: bump package version ([40987bc](https://github.com/TracerBench/chrome-debugging-client/commit/40987bc)) 87 | - docs: add READMEs ([ae60db6](https://github.com/TracerBench/chrome-debugging-client/commit/ae60db6)) 88 | - BREAKING CHANGE!: New API ([b34aec5](https://github.com/TracerBench/chrome-debugging-client/commit/b34aec5)) 89 | - Update README.md ([f425195](https://github.com/TracerBench/chrome-debugging-client/commit/f425195)) 90 | 91 | ## 0.6.8 (2018-09-06) 92 | 93 | - bump version ([0d36c7a](https://github.com/TracerBench/chrome-debugging-client/commit/0d36c7a)) 94 | - Fix issue with disposing connections which was hidden ([3f3dcb9](https://github.com/TracerBench/chrome-debugging-client/commit/3f3dcb9)) 95 | 96 | ## 0.6.6 (2018-09-05) 97 | 98 | - breakup tests ([9e75fd7](https://github.com/TracerBench/chrome-debugging-client/commit/9e75fd7)) 99 | - bump version ([c8956ce](https://github.com/TracerBench/chrome-debugging-client/commit/c8956ce)) 100 | - Match chromedriver's 60 second timeout 50ms polling for ([3aeca0c](https://github.com/TracerBench/chrome-debugging-client/commit/3aeca0c)) 101 | - switch to tmp ([f239f43](https://github.com/TracerBench/chrome-debugging-client/commit/f239f43)) 102 | - update protocols ([7f3e748](https://github.com/TracerBench/chrome-debugging-client/commit/7f3e748)) 103 | - Update test runner to something more current. ([c898b38](https://github.com/TracerBench/chrome-debugging-client/commit/c898b38)) 104 | 105 | ## 0.6.5 (2018-08-23) 106 | 107 | - update deps and protocol ([e6d4fe4](https://github.com/TracerBench/chrome-debugging-client/commit/e6d4fe4)) 108 | 109 | ## 0.6.4 (2018-08-23) 110 | 111 | - housekeeping ([e2de465](https://github.com/TracerBench/chrome-debugging-client/commit/e2de465)) 112 | - release 0.6.4 ([89d65eb](https://github.com/TracerBench/chrome-debugging-client/commit/89d65eb)) 113 | 114 | ## 0.6.3 (2018-06-15) 115 | 116 | - 0.6.3 ([586180a](https://github.com/TracerBench/chrome-debugging-client/commit/586180a)) 117 | - Add npm badges ([9228100](https://github.com/TracerBench/chrome-debugging-client/commit/9228100)) 118 | - Add support for custom root path for user data dir ([a52e2aa](https://github.com/TracerBench/chrome-debugging-client/commit/a52e2aa)) 119 | 120 | ## 0.6.2 (2018-06-01) 121 | 122 | - [BUGFIX] disable buffering of stdio introduced with execa ([18b75ac](https://github.com/TracerBench/chrome-debugging-client/commit/18b75ac)) 123 | - v0.6.2 ([356aba7](https://github.com/TracerBench/chrome-debugging-client/commit/356aba7)) 124 | 125 | ## 0.6.1 (2018-05-31) 126 | 127 | - add once ([d8ec1eb](https://github.com/TracerBench/chrome-debugging-client/commit/d8ec1eb)) 128 | - Implement target session connection. ([f05fd3b](https://github.com/TracerBench/chrome-debugging-client/commit/f05fd3b)) 129 | - Update license ([9700640](https://github.com/TracerBench/chrome-debugging-client/commit/9700640)) 130 | - update readme ([b73aa58](https://github.com/TracerBench/chrome-debugging-client/commit/b73aa58)) 131 | - v0.6.1 ([a83a39f](https://github.com/TracerBench/chrome-debugging-client/commit/a83a39f)) 132 | 133 | ## 0.6.0 (2018-05-29) 134 | 135 | - Allow default arguments to be disabled. ([1754872](https://github.com/TracerBench/chrome-debugging-client/commit/1754872)) 136 | - v0.6.0 ([59fc542](https://github.com/TracerBench/chrome-debugging-client/commit/59fc542)) 137 | 138 | ## 0.5.0 (2018-05-25) 139 | 140 | - try to make test pass ([434bbb2](https://github.com/TracerBench/chrome-debugging-client/commit/434bbb2)) 141 | - Update protocol ([b5fdcc9](https://github.com/TracerBench/chrome-debugging-client/commit/b5fdcc9)) 142 | - Use chrome-launcher's chrome-finder to resolve. ([4b9bbfd](https://github.com/TracerBench/chrome-debugging-client/commit/4b9bbfd)) 143 | - v0.5.0 ([fef8547](https://github.com/TracerBench/chrome-debugging-client/commit/fef8547)) 144 | 145 | ## 0.4.7 (2018-05-16) 146 | 147 | - Expose more error information ([25719b4](https://github.com/TracerBench/chrome-debugging-client/commit/25719b4)) 148 | - Fix travis links ([d952238](https://github.com/TracerBench/chrome-debugging-client/commit/d952238)) 149 | - update protocol ([a083e9d](https://github.com/TracerBench/chrome-debugging-client/commit/a083e9d)) 150 | - v0.4.7 ([9961862](https://github.com/TracerBench/chrome-debugging-client/commit/9961862)) 151 | 152 | ## 0.4.6 (2018-03-17) 153 | 154 | - apply prettier ([ed99356](https://github.com/TracerBench/chrome-debugging-client/commit/ed99356)) 155 | - Ensure process exists, still cleanup chrome. ([a2e361e](https://github.com/TracerBench/chrome-debugging-client/commit/a2e361e)) 156 | - update deps ([c2fde2d](https://github.com/TracerBench/chrome-debugging-client/commit/c2fde2d)) 157 | - update protocols ([8d91587](https://github.com/TracerBench/chrome-debugging-client/commit/8d91587)) 158 | - v0.4.6 ([095ba6a](https://github.com/TracerBench/chrome-debugging-client/commit/095ba6a)) 159 | 160 | ## 0.4.5 (2017-12-11) 161 | 162 | - Update protocol ([0f683a9](https://github.com/TracerBench/chrome-debugging-client/commit/0f683a9)) 163 | - Update README.md ([427b14c](https://github.com/TracerBench/chrome-debugging-client/commit/427b14c)) 164 | - v0.4.5 ([bfefb4a](https://github.com/TracerBench/chrome-debugging-client/commit/bfefb4a)) 165 | 166 | ## 0.4.4 (2017-11-17) 167 | 168 | - Upgrade all the deps and regenerate the protocol. ([52550dd](https://github.com/TracerBench/chrome-debugging-client/commit/52550dd)) 169 | - v0.4.4 ([4737140](https://github.com/TracerBench/chrome-debugging-client/commit/4737140)) 170 | 171 | ## 0.4.3 (2017-10-14) 172 | 173 | - add documentation to address #7 ([fe917bf](https://github.com/TracerBench/chrome-debugging-client/commit/fe917bf)), closes [#7](https://github.com/TracerBench/chrome-debugging-client/issues/7) 174 | - Add prepare script ([5b46afc](https://github.com/TracerBench/chrome-debugging-client/commit/5b46afc)) 175 | - bump protocol ([53646c0](https://github.com/TracerBench/chrome-debugging-client/commit/53646c0)) 176 | - v0.4.3 ([1d2e56e](https://github.com/TracerBench/chrome-debugging-client/commit/1d2e56e)) 177 | 178 | ## 0.4.2 (2017-07-19) 179 | 180 | - 0.4.1 ([b26783d](https://github.com/TracerBench/chrome-debugging-client/commit/b26783d)) 181 | - add no-floating-promises lint rule ([e4adda3](https://github.com/TracerBench/chrome-debugging-client/commit/e4adda3)) 182 | - await session.dispose() in createSession ([bb01c74](https://github.com/TracerBench/chrome-debugging-client/commit/bb01c74)) 183 | - cleanup chrome travis setup ([bd8339f](https://github.com/TracerBench/chrome-debugging-client/commit/bd8339f)) 184 | - cleanup protocols ([1aa10f5](https://github.com/TracerBench/chrome-debugging-client/commit/1aa10f5)) 185 | - link to protocol viewer ([34c04db](https://github.com/TracerBench/chrome-debugging-client/commit/34c04db)) 186 | - Switch travis to use headless chrome for test. ([bd26aef](https://github.com/TracerBench/chrome-debugging-client/commit/bd26aef)) 187 | - update protocols ([4faeb02](https://github.com/TracerBench/chrome-debugging-client/commit/4faeb02)) 188 | - v0.4.2 ([d885a2b](https://github.com/TracerBench/chrome-debugging-client/commit/d885a2b)) 189 | 190 | ## 0.4.0 (2017-05-06) 191 | 192 | - Add script to generate protocols and include them. ([2272a75](https://github.com/TracerBench/chrome-debugging-client/commit/2272a75)) 193 | - cleanup, update typescript and linting. ([cb2e5d1](https://github.com/TracerBench/chrome-debugging-client/commit/cb2e5d1)) 194 | - more cleanup/reorg ([52edf31](https://github.com/TracerBench/chrome-debugging-client/commit/52edf31)) 195 | - rename interfaces ([f342eb2](https://github.com/TracerBench/chrome-debugging-client/commit/f342eb2)) 196 | - simplify ([bd42e23](https://github.com/TracerBench/chrome-debugging-client/commit/bd42e23)) 197 | - update README, add repo urls to package ([7987776](https://github.com/TracerBench/chrome-debugging-client/commit/7987776)) 198 | - use node LTS ([6660ca8](https://github.com/TracerBench/chrome-debugging-client/commit/6660ca8)) 199 | 200 | ## 0.3.0 (2017-04-10) 201 | 202 | - 0.3.0 ([874f5ea](https://github.com/TracerBench/chrome-debugging-client/commit/874f5ea)) 203 | - add disable-translate ([8ed9d0a](https://github.com/TracerBench/chrome-debugging-client/commit/8ed9d0a)) 204 | 205 | ## 0.2.5 (2017-03-08) 206 | 207 | - add domain interface generator ([cd939e4](https://github.com/TracerBench/chrome-debugging-client/commit/cd939e4)) 208 | - Adding arg to ignore certificate errors ([3299d85](https://github.com/TracerBench/chrome-debugging-client/commit/3299d85)) 209 | - bugfix ([71c6c94](https://github.com/TracerBench/chrome-debugging-client/commit/71c6c94)) 210 | - bump version ([eae2a49](https://github.com/TracerBench/chrome-debugging-client/commit/eae2a49)) 211 | - bump version ([35d90a5](https://github.com/TracerBench/chrome-debugging-client/commit/35d90a5)) 212 | - cleanup ([cf7043a](https://github.com/TracerBench/chrome-debugging-client/commit/cf7043a)) 213 | - Cleanup ([765bf09](https://github.com/TracerBench/chrome-debugging-client/commit/765bf09)) 214 | - cleanup and refactor ([3b49944](https://github.com/TracerBench/chrome-debugging-client/commit/3b49944)) 215 | - cleanup tests ([4c694df](https://github.com/TracerBench/chrome-debugging-client/commit/4c694df)) 216 | - disable caching ([62f65c0](https://github.com/TracerBench/chrome-debugging-client/commit/62f65c0)) 217 | - doh ([452e0ff](https://github.com/TracerBench/chrome-debugging-client/commit/452e0ff)) 218 | - export Tab ([30a1b24](https://github.com/TracerBench/chrome-debugging-client/commit/30a1b24)) 219 | - fix issue with tmpdir dispose. ([4a1237f](https://github.com/TracerBench/chrome-debugging-client/commit/4a1237f)) 220 | - fix license ([4b2e607](https://github.com/TracerBench/chrome-debugging-client/commit/4b2e607)) 221 | - fix typings on package.json ([51eb7e0](https://github.com/TracerBench/chrome-debugging-client/commit/51eb7e0)) 222 | - guard for fn ([19a84ad](https://github.com/TracerBench/chrome-debugging-client/commit/19a84ad)) 223 | - initial commit ([7bb75e2](https://github.com/TracerBench/chrome-debugging-client/commit/7bb75e2)) 224 | - misc package stuff ([0d65b78](https://github.com/TracerBench/chrome-debugging-client/commit/0d65b78)) 225 | - some minor cleanup ([97a645f](https://github.com/TracerBench/chrome-debugging-client/commit/97a645f)) 226 | - test codegen ([847201a](https://github.com/TracerBench/chrome-debugging-client/commit/847201a)) 227 | - try running on travis ([f1fd3e5](https://github.com/TracerBench/chrome-debugging-client/commit/f1fd3e5)) 228 | - update default args ([7e30fc1](https://github.com/TracerBench/chrome-debugging-client/commit/7e30fc1)) 229 | - update name and example ([56087e9](https://github.com/TracerBench/chrome-debugging-client/commit/56087e9)) 230 | - Update README.md ([939f785](https://github.com/TracerBench/chrome-debugging-client/commit/939f785)) 231 | - update typescript ([1339553](https://github.com/TracerBench/chrome-debugging-client/commit/1339553)) 232 | - use I prefix for header style interfaces instead of Impl suffix ([b753758](https://github.com/TracerBench/chrome-debugging-client/commit/b753758)) 233 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-CLAUSE LICENSE 2 | 3 | Copyright 2018 LinkedIn Corporation and Contributors. All Rights Reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-debugging-client 2 | 3 | [![Build Status](https://travis-ci.org/tracerbench/chrome-debugging-client.svg?branch=master)](https://travis-ci.org/tracerbench/chrome-debugging-client) 4 | [![npm](https://img.shields.io/npm/v/chrome-debugging-client.svg)](https://www.npmjs.com/package/chrome-debugging-client) 5 | [![install size](https://packagephobia.now.sh/badge?p=chrome-debugging-client)](https://packagephobia.now.sh/result?p=chrome-debugging-client) 6 | 7 | An async/await friendly Chrome debugging client with TypeScript support, 8 | designed with automation in mind. 9 | 10 | ## Table of Contents 11 | 12 | * [Features](#features) 13 | 14 | * [Examples](#examples) 15 | 16 | * [Print URL as PDF](#print-url-as-pdf) 17 | * [Node Debugging](#node-debugging) 18 | 19 | ## Features 20 | 21 | * Promise API for async/await (most debugger commands are meant to be sequential). 22 | * TypeScript support and uses "devtools-protocol" types, allowing you to pick a protocol version. 23 | * Launches Chrome with a new temp user data folder so Chrome launches an isolated instance. 24 | (regardless if you already have Chrome open). 25 | * Opens Chrome with a pipe message transport to the browser connection and supports 26 | attaching flattened session connections to targets. 27 | * Supports cancellation in a way that avoids unhandled rejections, and allows you to add combine 28 | additional cancellation concerns. 29 | * Supports seeing protocol debug messages with `DEBUG=chrome-debugging-client:*` 30 | * Use with race-cancellation library to add timeouts or other cancellation concerns to tasks 31 | using the connection. 32 | * The library was designed to be careful about not floating promises (promises are 33 | chained immediately after being created, combining concurrent promises with all 34 | or race), this avoids unhandled rejections. 35 | 36 | ## Examples 37 | 38 | ### Print URL as PDF 39 | 40 | ```js file=examples/printToPDF.js 41 | #!/usr/bin/env node 42 | const { writeFileSync } = require("fs"); 43 | const { spawnChrome } = require("chrome-debugging-client"); 44 | 45 | /** 46 | * Print a url to a PDF file. 47 | * @param url {string} 48 | * @param file {string} 49 | */ 50 | async function printToPDF(url, file) { 51 | const chrome = spawnChrome({ headless: true }); 52 | try { 53 | const browser = chrome.connection; 54 | 55 | // we create with a target of about:blank so that we can 56 | // setup Page events before navigating to url 57 | const { targetId } = await browser.send("Target.createTarget", { 58 | url: "about:blank", 59 | }); 60 | 61 | const page = await browser.attachToTarget(targetId); 62 | // enable events for Page domain 63 | await page.send("Page.enable"); 64 | 65 | // concurrently wait until load and navigate 66 | await Promise.all([ 67 | page.until("Page.loadEventFired"), 68 | page.send("Page.navigate", { url }), 69 | ]); 70 | 71 | const { data } = await page.send("Page.printToPDF"); 72 | 73 | writeFileSync(file, data, "base64"); 74 | 75 | // attempt graceful close 76 | await chrome.close(); 77 | } finally { 78 | // kill process if hasn't exited 79 | await chrome.dispose(); 80 | } 81 | 82 | console.log(`${url} written to ${file}`); 83 | } 84 | 85 | if (process.argv.length < 4) { 86 | console.log(`usage: printToPDF.js url file`); 87 | console.log( 88 | `example: printToPDF.js https://en.wikipedia.org/wiki/Binomial_coefficient Binomial_coefficient.pdf`, 89 | ); 90 | process.exit(1); 91 | } 92 | 93 | printToPDF(process.argv[2], process.argv[3]).catch((err) => { 94 | console.log("print failed %o", err); 95 | }); 96 | 97 | ``` 98 | 99 | ### Node Debugging 100 | 101 | ```js file=examples/nodeDebug.js 102 | #!/usr/bin/env node 103 | const { spawnWithWebSocket } = require("chrome-debugging-client"); 104 | 105 | async function main() { 106 | const script = `const obj = { 107 | hello: "world", 108 | }; 109 | console.log("end"); 110 | `; 111 | 112 | // start node requesting it break on start at debug port that 113 | // is available 114 | const node = await spawnWithWebSocket(process.execPath, [ 115 | // node will pick an available port and wait for debugger 116 | "--inspect-brk=0", 117 | "-e", 118 | script, 119 | ]); 120 | 121 | async function doDebugging() { 122 | const { connection } = node; 123 | 124 | // Setup console api handler 125 | connection.on("Runtime.consoleAPICalled", ({ type, args }) => { 126 | console.log(`console.${type}: ${JSON.stringify(args)}`); 127 | }); 128 | 129 | // We requested Node to break on start, so we runIfWaitingForDebugger 130 | // and wait for it to break at the start of our script. 131 | // These commands must be sent concurrently with 132 | // the pause event setup. 133 | const [ 134 | { 135 | callFrames: [ 136 | { 137 | location: { scriptId }, 138 | }, 139 | ], 140 | reason, 141 | }, 142 | ] = await Promise.all([ 143 | connection.until("Debugger.paused"), 144 | connection.send("Debugger.enable"), 145 | connection.send("Runtime.enable"), 146 | connection.send("Runtime.runIfWaitingForDebugger"), 147 | ]); 148 | // Right now we are paused at the start of the script 149 | console.log(`paused reason: ${reason}`); //= paused: Break on start 150 | console.log(`set breakpoint on line 3`); 151 | await connection.send("Debugger.setBreakpoint", { 152 | location: { 153 | lineNumber: 3, 154 | scriptId, 155 | }, 156 | }); 157 | 158 | console.log("resume and wait for next paused event"); 159 | const [breakpoint] = await Promise.all([ 160 | connection.until("Debugger.paused"), 161 | connection.send("Debugger.resume"), 162 | ]); 163 | const { 164 | callFrames: [{ location, callFrameId }], 165 | } = breakpoint; 166 | console.log(`paused at line ${location.lineNumber}`); 167 | 168 | console.log("evaluate `obj`"); 169 | const { result } = await connection.send("Debugger.evaluateOnCallFrame", { 170 | callFrameId, 171 | expression: "obj", 172 | returnByValue: true, 173 | }); 174 | console.log(JSON.stringify(result.value)); //= {"hello":"world"} 175 | 176 | console.log("resume and wait for execution context to be destroyed"); 177 | await Promise.all([ 178 | connection.until("Runtime.executionContextDestroyed"), 179 | connection.send("Debugger.resume"), 180 | ]); 181 | } 182 | 183 | try { 184 | await doDebugging(); 185 | 186 | // Node is still alive here and waiting for the debugger to disconnect 187 | console.log("close websocket"); 188 | node.close(); 189 | 190 | // Node should exit on its own after the websocket closes 191 | console.log("wait for exit"); 192 | await node.waitForExit(); 193 | 194 | console.log("node exited"); 195 | } finally { 196 | // kill process if still alive 197 | await node.dispose(); 198 | } 199 | } 200 | 201 | main().catch((err) => { 202 | console.log("print failed %o", err); 203 | }); 204 | 205 | ``` 206 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | 8 | ## Preparation 9 | 10 | Since the majority of the actual release process is automated, the primary 11 | remaining task prior to releasing is confirming that all pull requests that 12 | have been merged since the last release have been labeled with the appropriate 13 | `lerna-changelog` labels and the titles have been updated to ensure they 14 | represent something that would make sense to our users. Some great information 15 | on why this is important can be found at 16 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 17 | guiding principle here is that changelogs are for humans, not machines. 18 | 19 | When reviewing merged PR's the labels to be used are: 20 | 21 | * breaking - Used when the PR is considered a breaking change. 22 | * enhancement - Used when the PR adds a new feature or enhancement. 23 | * bug - Used when the PR fixes a bug included in a previous release. 24 | * documentation - Used when the PR adds or updates documentation. 25 | * internal - Used for internal changes that still require a mention in the 26 | changelog/release notes. 27 | 28 | 29 | ## Release 30 | 31 | Once the prep work is completed, the actual release is straight forward: 32 | 33 | * First ensure that you have `release-it` installed globally, generally done by 34 | using one of the following commands: 35 | 36 | ``` 37 | # using https://volta.sh 38 | volta install release-it 39 | 40 | # using Yarn 41 | yarn global add release-it 42 | 43 | # using npm 44 | npm install --global release-it 45 | ``` 46 | 47 | * Second, ensure that you have installed your projects dependencies: 48 | 49 | ``` 50 | yarn install 51 | ``` 52 | 53 | * And last (but not least 😁) do your release. It requires a 54 | [GitHub personal access token](https://github.com/settings/tokens) as 55 | `$GITHUB_AUTH` environment variable. Only "repo" access is needed; no "admin" 56 | or other scopes are required. 57 | 58 | ``` 59 | export GITHUB_AUTH="f941e0..." 60 | release-it 61 | ``` 62 | 63 | [release-it](https://github.com/release-it/release-it/) manages the actual 64 | release process. It will prompt you to to choose the version number after which 65 | you will have the chance to hand tweak the changelog to be used (for the 66 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 67 | pushing the tag and commits, etc. 68 | -------------------------------------------------------------------------------- /chrome-debugging-client.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "search.exclude": { 5 | "**/dist": true, 6 | "**/node_modules": true, 7 | "**/.git": true 8 | }, 9 | "files.watcherExclude": { 10 | "**/dist": true, 11 | "**/node_modules": true, 12 | "**/.git": true 13 | }, 14 | "eslint.validate": ["javascript", "typescript"], 15 | "editor.codeActionsOnSave": { 16 | "source.fixAll.eslint": true 17 | } 18 | }, 19 | "folders": [ 20 | { 21 | "path": "." 22 | } 23 | ], 24 | "launch": { 25 | "configurations": [ 26 | { 27 | "type": "node", 28 | "request": "launch", 29 | "name": "QUnit Tests", 30 | "program": "${workspaceFolder}/node_modules/qunit/bin/qunit", 31 | "args": ["test/*Test.js"], 32 | "internalConsoleOptions": "openOnSessionStart", 33 | "skipFiles": ["/**"], 34 | "sourceMaps": true 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /chrome-debugging-client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | project: "./tsconfig.json", 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module", 7 | }, 8 | ignorePatterns: ["dist/", ".eslintrc.js"], 9 | extends: ["../.eslintrc"], 10 | }; 11 | -------------------------------------------------------------------------------- /chrome-debugging-client/README.md: -------------------------------------------------------------------------------- 1 | # chrome-debugging-client 2 | 3 | [![Build Status](https://travis-ci.org/tracerbench/chrome-debugging-client.svg?branch=master)](https://travis-ci.org/tracerbench/chrome-debugging-client) 4 | [![npm](https://img.shields.io/npm/v/chrome-debugging-client.svg)](https://www.npmjs.com/package/chrome-debugging-client) 5 | [![install size](https://packagephobia.now.sh/badge?p=chrome-debugging-client)](https://packagephobia.now.sh/result?p=chrome-debugging-client) 6 | 7 | An async/await friendly Chrome debugging client with TypeScript support, 8 | designed with automation in mind. 9 | 10 | ## Table of Contents 11 | 12 | * [Features](#features) 13 | 14 | * [Examples](#examples) 15 | 16 | * [Print URL as PDF](#print-url-as-pdf) 17 | * [Node Debugging](#node-debugging) 18 | 19 | ## Features 20 | 21 | * Promise API for async/await (most debugger commands are meant to be sequential). 22 | * TypeScript support and uses "devtools-protocol" types, allowing you to pick a protocol version. 23 | * Launches Chrome with a new temp user data folder so Chrome launches an isolated instance. 24 | (regardless if you already have Chrome open). 25 | * Opens Chrome with a pipe message transport to the browser connection and supports 26 | attaching flattened session connections to targets. 27 | * Supports cancellation in a way that avoids unhandled rejections, and allows you to add combine 28 | additional cancellation concerns. 29 | * Supports seeing protocol debug messages with `DEBUG=chrome-debugging-client:*` 30 | * Use with race-cancellation library to add timeouts or other cancellation concerns to tasks 31 | using the connection. 32 | * The library was designed to be careful about not floating promises (promises are 33 | chained immediately after being created, combining concurrent promises with all 34 | or race), this avoids unhandled rejections. 35 | 36 | ## Examples 37 | 38 | ### Print URL as PDF 39 | 40 | ```js file=examples/printToPDF.js 41 | #!/usr/bin/env node 42 | const { writeFileSync } = require("fs"); 43 | const { spawnChrome } = require("chrome-debugging-client"); 44 | 45 | /** 46 | * Print a url to a PDF file. 47 | * @param url {string} 48 | * @param file {string} 49 | */ 50 | async function printToPDF(url, file) { 51 | const chrome = spawnChrome({ headless: true }); 52 | try { 53 | const browser = chrome.connection; 54 | 55 | // we create with a target of about:blank so that we can 56 | // setup Page events before navigating to url 57 | const { targetId } = await browser.send("Target.createTarget", { 58 | url: "about:blank", 59 | }); 60 | 61 | const page = await browser.attachToTarget(targetId); 62 | // enable events for Page domain 63 | await page.send("Page.enable"); 64 | 65 | // concurrently wait until load and navigate 66 | await Promise.all([ 67 | page.until("Page.loadEventFired"), 68 | page.send("Page.navigate", { url }), 69 | ]); 70 | 71 | const { data } = await page.send("Page.printToPDF"); 72 | 73 | writeFileSync(file, data, "base64"); 74 | 75 | // attempt graceful close 76 | await chrome.close(); 77 | } finally { 78 | // kill process if hasn't exited 79 | await chrome.dispose(); 80 | } 81 | 82 | console.log(`${url} written to ${file}`); 83 | } 84 | 85 | if (process.argv.length < 4) { 86 | console.log(`usage: printToPDF.js url file`); 87 | console.log( 88 | `example: printToPDF.js https://en.wikipedia.org/wiki/Binomial_coefficient Binomial_coefficient.pdf`, 89 | ); 90 | process.exit(1); 91 | } 92 | 93 | printToPDF(process.argv[2], process.argv[3]).catch((err) => { 94 | console.log("print failed %o", err); 95 | }); 96 | 97 | ``` 98 | 99 | ### Node Debugging 100 | 101 | ```js file=examples/nodeDebug.js 102 | #!/usr/bin/env node 103 | const { spawnWithWebSocket } = require("chrome-debugging-client"); 104 | 105 | async function main() { 106 | const script = `const obj = { 107 | hello: "world", 108 | }; 109 | console.log("end"); 110 | `; 111 | 112 | // start node requesting it break on start at debug port that 113 | // is available 114 | const node = await spawnWithWebSocket(process.execPath, [ 115 | // node will pick an available port and wait for debugger 116 | "--inspect-brk=0", 117 | "-e", 118 | script, 119 | ]); 120 | 121 | async function doDebugging() { 122 | const { connection } = node; 123 | 124 | // Setup console api handler 125 | connection.on("Runtime.consoleAPICalled", ({ type, args }) => { 126 | console.log(`console.${type}: ${JSON.stringify(args)}`); 127 | }); 128 | 129 | // We requested Node to break on start, so we runIfWaitingForDebugger 130 | // and wait for it to break at the start of our script. 131 | // These commands must be sent concurrently with 132 | // the pause event setup. 133 | const [ 134 | { 135 | callFrames: [ 136 | { 137 | location: { scriptId }, 138 | }, 139 | ], 140 | reason, 141 | }, 142 | ] = await Promise.all([ 143 | connection.until("Debugger.paused"), 144 | connection.send("Debugger.enable"), 145 | connection.send("Runtime.enable"), 146 | connection.send("Runtime.runIfWaitingForDebugger"), 147 | ]); 148 | // Right now we are paused at the start of the script 149 | console.log(`paused reason: ${reason}`); //= paused: Break on start 150 | console.log(`set breakpoint on line 3`); 151 | await connection.send("Debugger.setBreakpoint", { 152 | location: { 153 | lineNumber: 3, 154 | scriptId, 155 | }, 156 | }); 157 | 158 | console.log("resume and wait for next paused event"); 159 | const [breakpoint] = await Promise.all([ 160 | connection.until("Debugger.paused"), 161 | connection.send("Debugger.resume"), 162 | ]); 163 | const { 164 | callFrames: [{ location, callFrameId }], 165 | } = breakpoint; 166 | console.log(`paused at line ${location.lineNumber}`); 167 | 168 | console.log("evaluate `obj`"); 169 | const { result } = await connection.send("Debugger.evaluateOnCallFrame", { 170 | callFrameId, 171 | expression: "obj", 172 | returnByValue: true, 173 | }); 174 | console.log(JSON.stringify(result.value)); //= {"hello":"world"} 175 | 176 | console.log("resume and wait for execution context to be destroyed"); 177 | await Promise.all([ 178 | connection.until("Runtime.executionContextDestroyed"), 179 | connection.send("Debugger.resume"), 180 | ]); 181 | } 182 | 183 | try { 184 | await doDebugging(); 185 | 186 | // Node is still alive here and waiting for the debugger to disconnect 187 | console.log("close websocket"); 188 | node.close(); 189 | 190 | // Node should exit on its own after the websocket closes 191 | console.log("wait for exit"); 192 | await node.waitForExit(); 193 | 194 | console.log("node exited"); 195 | } finally { 196 | // kill process if still alive 197 | await node.dispose(); 198 | } 199 | } 200 | 201 | main().catch((err) => { 202 | console.log("print failed %o", err); 203 | }); 204 | 205 | ``` 206 | -------------------------------------------------------------------------------- /chrome-debugging-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-debugging-client", 3 | "version": "2.0.0", 4 | "description": "An async/await friendly Chrome debugging client with TypeScript support", 5 | "license": "BSD-2-Clause", 6 | "author": "Kris Selden ", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/TracerBench/chrome-debugging-client.git", 14 | "directory": "chrome-debugging-client" 15 | }, 16 | "main": "dist/index.js", 17 | "types": "dist/index.d.ts", 18 | "scripts": { 19 | "build": "tsc -b", 20 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 21 | "fixlint": "eslint --ext .ts src --fix", 22 | "lint": "eslint --ext .ts src", 23 | "prepare": "yarn run build" 24 | }, 25 | "dependencies": { 26 | "@tracerbench/find-chrome": "^2.0.0", 27 | "@tracerbench/message-transport": "^2.0.0", 28 | "@tracerbench/protocol-connection": "^2.0.0", 29 | "@tracerbench/spawn": "^2.0.0", 30 | "@tracerbench/spawn-chrome": "^2.0.0", 31 | "@tracerbench/websocket-message-transport": "^2.0.0", 32 | "debug": "^4.1.1", 33 | "race-cancellation": "^0.4.1" 34 | }, 35 | "devDependencies": { 36 | "@types/debug": "^4.1.5", 37 | "@types/node": "^18.16.0", 38 | "@typescript-eslint/eslint-plugin": "^5.59.1", 39 | "@typescript-eslint/parser": "^5.59.1", 40 | "eslint": "^8.39.0", 41 | "eslint-config-prettier": "^8.8.0", 42 | "eslint-plugin-import": "^2.18.2", 43 | "eslint-plugin-prettier": "^4.2.1", 44 | "eslint-plugin-simple-import-sort": "^10.0.0", 45 | "prettier": "^2.0.5", 46 | "typescript": "^5.0.4" 47 | }, 48 | "peerDependencies": { 49 | "devtools-protocol": "*" 50 | }, 51 | "gitHead": "930bc11c8b01620e5095df9249f0647af68235b5" 52 | } 53 | -------------------------------------------------------------------------------- /chrome-debugging-client/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttachMessageTransport, 3 | RootConnection, 4 | } from "@tracerbench/protocol-connection"; 5 | import _newProtocolConnection from "@tracerbench/protocol-connection"; 6 | import _spawn from "@tracerbench/spawn"; 7 | import type { 8 | Chrome, 9 | ChromeSpawnOptions, 10 | Process, 11 | ProcessWithPipeMessageTransport, 12 | Stdio, 13 | } from "@tracerbench/spawn-chrome"; 14 | import _spawnChrome from "@tracerbench/spawn-chrome"; 15 | import openWebSocket from "@tracerbench/websocket-message-transport"; 16 | import debug = require("debug"); 17 | import { EventEmitter } from "events"; 18 | import type { RaceCancellation } from "race-cancellation"; 19 | import { combineRaceCancellation, isCancellation } from "race-cancellation"; 20 | 21 | const debugSpawn = debug("chrome-debugging-client:spawn"); 22 | const debugTransport = debug("chrome-debugging-client:transport"); 23 | 24 | export function spawnChrome( 25 | options?: Partial, 26 | ): ChromeWithPipeConnection { 27 | return attachPipeTransport(_spawnChrome(options, debugSpawn)); 28 | } 29 | 30 | export function spawnWithPipe( 31 | executable: string, 32 | args: string[], 33 | options?: Partial, 34 | ): ProcessWithPipeConnection { 35 | return attachPipeTransport( 36 | _spawn(executable, args, options, "pipe", debugSpawn), 37 | ); 38 | } 39 | 40 | export async function spawnWithWebSocket( 41 | executable: string, 42 | args: string[], 43 | stdio?: Stdio, 44 | raceCancellation?: RaceCancellation, 45 | ): Promise { 46 | const process = _spawn(executable, args, stdio, "websocket", debugSpawn); 47 | const url = await process.url(raceCancellation); 48 | const [attach, close] = await openWebSocket( 49 | url, 50 | combineRaceCancellation(process.raceExit, raceCancellation), 51 | ); 52 | const connection = newProtocolConnection(attach, process.raceExit); 53 | return Object.assign(process, { connection, close }); 54 | } 55 | 56 | export function newProtocolConnection( 57 | attach: AttachMessageTransport, 58 | raceCancellation?: RaceCancellation, 59 | ): RootConnection { 60 | return _newProtocolConnection( 61 | attach, 62 | () => new EventEmitter(), 63 | debugTransport, 64 | raceCancellation, 65 | ); 66 | } 67 | 68 | export { default as openWebSocket } from "@tracerbench/websocket-message-transport"; 69 | 70 | export { default as findChrome } from "@tracerbench/find-chrome"; 71 | 72 | function attachPipeTransport

( 73 | process: P, 74 | ): P & { 75 | connection: RootConnection; 76 | close(timeout?: number, raceCancellation?: RaceCancellation): Promise; 77 | } { 78 | const connection = newProtocolConnection(process.attach, process.raceExit); 79 | return Object.assign(process, { close, connection }); 80 | 81 | async function close( 82 | timeout?: number, 83 | raceCancellation?: RaceCancellation, 84 | ): Promise { 85 | if (process.hasExited()) { 86 | return; 87 | } 88 | try { 89 | const waitForExit = process.waitForExit(timeout, raceCancellation); 90 | await Promise.race([waitForExit, sendBrowserClose()]); 91 | // double check in case send() won the race which is most of the time 92 | // sometimes chrome exits before send() gets a response 93 | await waitForExit; 94 | } catch (e) { 95 | // if we closed then we dont really care what the error is 96 | if (!process.hasExited()) { 97 | throw e; 98 | } 99 | } 100 | } 101 | 102 | async function sendBrowserClose(): Promise { 103 | try { 104 | await connection.send("Browser.close"); 105 | } catch (e) { 106 | // the browser sometimes closes the connection before sending 107 | // the response which will cancel the send 108 | if (!isCancellation(e)) { 109 | throw e; 110 | } 111 | } 112 | } 113 | } 114 | 115 | export interface ChromeWithPipeConnection extends Chrome { 116 | /** 117 | * Connection to devtools protocol https://chromedevtools.github.io/devtools-protocol/ 118 | */ 119 | connection: RootConnection; 120 | 121 | /** 122 | * Close browser. 123 | */ 124 | close(timeout?: number, raceCancellation?: RaceCancellation): Promise; 125 | } 126 | 127 | export interface ProcessWithPipeConnection extends Process { 128 | /** 129 | * Connection to devtools protocol https://chromedevtools.github.io/devtools-protocol/ 130 | */ 131 | connection: RootConnection; 132 | 133 | /** 134 | * Close browser. 135 | */ 136 | close(timeout?: number, raceCancellation?: RaceCancellation): Promise; 137 | } 138 | 139 | export interface ProcessWithWebSocketConnection extends Process { 140 | /** 141 | * Connection to devtools protocol https://chromedevtools.github.io/devtools-protocol/ 142 | */ 143 | connection: RootConnection; 144 | 145 | /** 146 | * Closes the web socket. 147 | */ 148 | close(): void; 149 | } 150 | 151 | export type { 152 | AttachJsonRpcTransport, 153 | AttachMessageTransport, 154 | AttachProtocolTransport, 155 | AttachSession, 156 | Cancellation, 157 | DebugCallback, 158 | DetachSession, 159 | ErrorResponse, 160 | Notification, 161 | OnClose, 162 | OnError, 163 | OnEvent, 164 | OnMessage, 165 | OnNotification, 166 | Protocol, 167 | ProtocolMapping, 168 | ProtocolError, 169 | ProtocolTransport, 170 | RaceCancellation, 171 | Request, 172 | Response, 173 | ResponseError, 174 | SendMessage, 175 | SendMethod, 176 | SendRequest, 177 | SuccessResponse, 178 | Task, 179 | ProtocolConnection, 180 | SessionConnection, 181 | RootConnection, 182 | ProtocolConnectionBase, 183 | EventListener, 184 | EventEmitter, 185 | EventPredicate, 186 | NewEventEmitter, 187 | TargetID, 188 | TargetInfo, 189 | SessionID, 190 | Method, 191 | Event, 192 | EventMapping, 193 | RequestMapping, 194 | ResponseMapping, 195 | VoidRequestMethod, 196 | MappedRequestMethod, 197 | MaybeMappedRequestMethod, 198 | VoidResponseMethod, 199 | MappedResponseMethod, 200 | VoidRequestVoidResponseMethod, 201 | VoidRequestMappedResponseMethod, 202 | VoidEvent, 203 | MappedEvent, 204 | SessionIdentifier, 205 | } from "@tracerbench/protocol-connection"; 206 | export type { 207 | Process, 208 | ProcessWithPipeMessageTransport, 209 | ProcessWithWebSocketUrl, 210 | SpawnOptions, 211 | Stdio, 212 | Transport, 213 | TransportMapping, 214 | ArgumentOptions, 215 | ChromeSpawnOptions, 216 | Chrome, 217 | } from "@tracerbench/spawn-chrome"; 218 | -------------------------------------------------------------------------------- /chrome-debugging-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["scripthost", "es2017"], 5 | "types": ["node"], 6 | 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | 10 | "declaration": true, 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "newLine": "LF", 20 | 21 | "moduleResolution": "node", 22 | "module": "commonjs", 23 | "target": "ES2017" 24 | }, 25 | "files": ["src/index.ts"], 26 | "references": [ 27 | { 28 | "path": "../@tracerbench/find-chrome" 29 | }, 30 | { 31 | "path": "../@tracerbench/protocol-connection" 32 | }, 33 | { 34 | "path": "../@tracerbench/spawn" 35 | }, 36 | { 37 | "path": "../@tracerbench/spawn-chrome" 38 | }, 39 | { 40 | "path": "../@tracerbench/websocket-message-transport" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /examples/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es2017: true, 6 | }, 7 | parserOptions: { 8 | project: "./tsconfig.json", 9 | tsconfigRootDir: __dirname, 10 | sourceType: "module", 11 | }, 12 | ignorePatterns: ["dist/", ".eslintrc.js"], 13 | extends: ["../.eslintrc"], 14 | rules: { 15 | "@typescript-eslint/no-var-requires": "off", 16 | "@typescript-eslint/explicit-function-return-type": "off", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /examples/nodeDebug.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { spawnWithWebSocket } = require("chrome-debugging-client"); 3 | 4 | async function main() { 5 | const script = `const obj = { 6 | hello: "world", 7 | }; 8 | console.log("end"); 9 | `; 10 | 11 | // start node requesting it break on start at debug port that 12 | // is available 13 | const node = await spawnWithWebSocket(process.execPath, [ 14 | // node will pick an available port and wait for debugger 15 | "--inspect-brk=0", 16 | "-e", 17 | script, 18 | ]); 19 | 20 | async function doDebugging() { 21 | const { connection } = node; 22 | 23 | // Setup console api handler 24 | connection.on("Runtime.consoleAPICalled", ({ type, args }) => { 25 | console.log(`console.${type}: ${JSON.stringify(args)}`); 26 | }); 27 | 28 | // We requested Node to break on start, so we runIfWaitingForDebugger 29 | // and wait for it to break at the start of our script. 30 | // These commands must be sent concurrently with 31 | // the pause event setup. 32 | const [ 33 | { 34 | callFrames: [ 35 | { 36 | location: { scriptId }, 37 | }, 38 | ], 39 | reason, 40 | }, 41 | ] = await Promise.all([ 42 | connection.until("Debugger.paused"), 43 | connection.send("Debugger.enable"), 44 | connection.send("Runtime.enable"), 45 | connection.send("Runtime.runIfWaitingForDebugger"), 46 | ]); 47 | // Right now we are paused at the start of the script 48 | console.log(`paused reason: ${reason}`); //= paused: Break on start 49 | console.log(`set breakpoint on line 3`); 50 | await connection.send("Debugger.setBreakpoint", { 51 | location: { 52 | lineNumber: 3, 53 | scriptId, 54 | }, 55 | }); 56 | 57 | console.log("resume and wait for next paused event"); 58 | const [breakpoint] = await Promise.all([ 59 | connection.until("Debugger.paused"), 60 | connection.send("Debugger.resume"), 61 | ]); 62 | const { 63 | callFrames: [{ location, callFrameId }], 64 | } = breakpoint; 65 | console.log(`paused at line ${location.lineNumber}`); 66 | 67 | console.log("evaluate `obj`"); 68 | const { result } = await connection.send("Debugger.evaluateOnCallFrame", { 69 | callFrameId, 70 | expression: "obj", 71 | returnByValue: true, 72 | }); 73 | console.log(JSON.stringify(result.value)); //= {"hello":"world"} 74 | 75 | console.log("resume and wait for execution context to be destroyed"); 76 | await Promise.all([ 77 | connection.until("Runtime.executionContextDestroyed"), 78 | connection.send("Debugger.resume"), 79 | ]); 80 | } 81 | 82 | try { 83 | await doDebugging(); 84 | 85 | // Node is still alive here and waiting for the debugger to disconnect 86 | console.log("close websocket"); 87 | node.close(); 88 | 89 | // Node should exit on its own after the websocket closes 90 | console.log("wait for exit"); 91 | await node.waitForExit(); 92 | 93 | console.log("node exited"); 94 | } finally { 95 | // kill process if still alive 96 | await node.dispose(); 97 | } 98 | } 99 | 100 | main().catch((err) => { 101 | console.log("print failed %o", err); 102 | }); 103 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-debugging-client-examples", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "checkjs": "tsc", 7 | "fixlint": "eslint . --fix", 8 | "lint": "eslint ." 9 | }, 10 | "dependencies": { 11 | "@types/node": "^18.16.0", 12 | "chrome-debugging-client": "^2.0.0", 13 | "devtools-protocol": "^0.0.1135028", 14 | "execa": "^5.1.1" 15 | }, 16 | "devDependencies": { 17 | "@typescript-eslint/eslint-plugin": "^5.59.1", 18 | "@typescript-eslint/parser": "^5.59.1", 19 | "eslint": "^8.39.0", 20 | "eslint-config-prettier": "^8.8.0", 21 | "eslint-plugin-import": "^2.18.2", 22 | "eslint-plugin-prettier": "^4.2.1", 23 | "eslint-plugin-simple-import-sort": "^10.0.0", 24 | "prettier": "^2.0.5", 25 | "typescript": "^5.0.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/printToPDF.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { writeFileSync } = require("fs"); 3 | const { spawnChrome } = require("chrome-debugging-client"); 4 | 5 | /** 6 | * Print a url to a PDF file. 7 | * @param url {string} 8 | * @param file {string} 9 | */ 10 | async function printToPDF(url, file) { 11 | const chrome = spawnChrome({ headless: true }); 12 | try { 13 | const browser = chrome.connection; 14 | 15 | // we create with a target of about:blank so that we can 16 | // setup Page events before navigating to url 17 | const { targetId } = await browser.send("Target.createTarget", { 18 | url: "about:blank", 19 | }); 20 | 21 | const page = await browser.attachToTarget(targetId); 22 | // enable events for Page domain 23 | await page.send("Page.enable"); 24 | 25 | // concurrently wait until load and navigate 26 | await Promise.all([ 27 | page.until("Page.loadEventFired"), 28 | page.send("Page.navigate", { url }), 29 | ]); 30 | 31 | const { data } = await page.send("Page.printToPDF"); 32 | 33 | writeFileSync(file, data, "base64"); 34 | 35 | // attempt graceful close 36 | await chrome.close(); 37 | } finally { 38 | // kill process if hasn't exited 39 | await chrome.dispose(); 40 | } 41 | 42 | console.log(`${url} written to ${file}`); 43 | } 44 | 45 | if (process.argv.length < 4) { 46 | console.log(`usage: printToPDF.js url file`); 47 | console.log( 48 | `example: printToPDF.js https://en.wikipedia.org/wiki/Binomial_coefficient Binomial_coefficient.pdf`, 49 | ); 50 | process.exit(1); 51 | } 52 | 53 | printToPDF(process.argv[2], process.argv[3]).catch((err) => { 54 | console.log("print failed %o", err); 55 | }); 56 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "strict": true, 6 | "noEmit": true, 7 | "moduleResolution": "node", 8 | "module": "commonjs", 9 | "target": "ES2017" 10 | }, 11 | "include": ["*.js"] 12 | } 13 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "useWorkspaces": true, 3 | "version": "1.2.0", 4 | "npmClient": "yarn" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-debugging-client-root", 3 | "private": true, 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/TracerBench/chrome-debugging-client" 7 | }, 8 | "workspaces": [ 9 | "@tracerbench/*", 10 | "chrome-debugging-client", 11 | "examples", 12 | "scripts", 13 | "test" 14 | ], 15 | "scripts": { 16 | "build": "tsc -b", 17 | "checkjs": "lerna run --stream checkjs", 18 | "clean": "lerna run clean", 19 | "fixlint": "lerna run --stream fixlint", 20 | "lint": "lerna run --stream lint", 21 | "prepare": "yarn build && lerna run prepare && yarn readme", 22 | "readme": "scripts/readme.js", 23 | "test": "qunit test/*Test.js" 24 | }, 25 | "dependencies": { 26 | "lerna": "^6.6.1" 27 | }, 28 | "devDependencies": { 29 | "release-it": "^15.10.1", 30 | "release-it-lerna-changelog": "^5.0.0", 31 | "release-it-yarn-workspaces": "^3.0.0" 32 | }, 33 | "publishConfig": { 34 | "registry": "https://registry.npmjs.org" 35 | }, 36 | "release-it": { 37 | "plugins": { 38 | "release-it-lerna-changelog": { 39 | "infile": "CHANGELOG.md", 40 | "launchEditor": true 41 | }, 42 | "release-it-yarn-workspaces": true 43 | }, 44 | "git": { 45 | "tagName": "v${version}" 46 | }, 47 | "github": { 48 | "release": true, 49 | "tokenRef": "GITHUB_AUTH" 50 | }, 51 | "npm": false 52 | }, 53 | "volta": { 54 | "node": "18.16.0", 55 | "yarn": "1.22.4" 56 | }, 57 | "version": "2.0.0" 58 | } 59 | -------------------------------------------------------------------------------- /scripts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es2017: true, 6 | }, 7 | parserOptions: { 8 | project: "./tsconfig.json", 9 | tsconfigRootDir: __dirname, 10 | sourceType: "module", 11 | }, 12 | ignorePatterns: ["dist/", ".eslintrc.js"], 13 | extends: ["../.eslintrc"], 14 | rules: { 15 | "@typescript-eslint/no-var-requires": "off", 16 | "@typescript-eslint/explicit-function-return-type": "off", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /scripts/import-code/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { split } = require("shell-split"); 4 | const visit = require("unist-util-visit"); 5 | 6 | /** @type {import("unified").Plugin} */ 7 | const importCode = () => replaceCodeTransform; 8 | 9 | module.exports = importCode; 10 | 11 | /** 12 | * @param {import("unist").Node} tree 13 | * @param {import("vfile").VFile} file 14 | */ 15 | function replaceCodeTransform(tree, file) { 16 | visit(tree, "code", (code) => processCodeNode(code, file)); 17 | return tree; 18 | } 19 | 20 | /** 21 | * @param {import("unist").Node} code 22 | * @param {import("vfile").VFile} file 23 | */ 24 | function processCodeNode(code, file) { 25 | const { meta } = code; 26 | if (typeof meta === "string") { 27 | const value = parseCodeMeta(meta, file); 28 | if (value) { 29 | code.value = value; 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * @param {string} meta 36 | * @param {import("vfile").VFile} file 37 | */ 38 | function parseCodeMeta(meta, file) { 39 | if (meta) { 40 | const parts = split(meta); 41 | for (const part of parts) { 42 | const [key, value] = part.split("=", 2); 43 | if (key === "file") { 44 | let codePath = value; 45 | const dirname = file.dirname; 46 | if (dirname) { 47 | codePath = path.resolve(dirname, codePath); 48 | } 49 | return fs.readFileSync(codePath, "utf8"); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-debugging-client-scripts", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "checkjs": "tsc", 7 | "fixlint": "eslint . --fix", 8 | "lint": "eslint ." 9 | }, 10 | "dependencies": { 11 | "@types/node": "^18.16.0", 12 | "remark": "^14.0.2", 13 | "remark-parse": "^10.0.1", 14 | "remark-stringify": "^10.0.2", 15 | "remark-toc": "^8.0.1", 16 | "shell-split": "^1.0.0", 17 | "unified": "^9.0.0", 18 | "unist-util-visit": "^2.0.0", 19 | "vfile": "^4.0.1" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^5.59.1", 23 | "@typescript-eslint/parser": "^5.59.1", 24 | "eslint": "^8.39.0", 25 | "eslint-config-prettier": "^8.8.0", 26 | "eslint-plugin-import": "^2.18.2", 27 | "eslint-plugin-prettier": "^4.2.1", 28 | "eslint-plugin-simple-import-sort": "^10.0.0", 29 | "prettier": "^2.0.5", 30 | "typescript": "^5.0.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scripts/readme.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const unified = require("unified"); 5 | const vfile = require("vfile"); 6 | 7 | const importCode = require("./import-code"); 8 | 9 | void main(); 10 | 11 | async function main() { 12 | const { default: parse } = await import("remark-parse"); 13 | const { default: stringify } = await import("remark-stringify"); 14 | const { default: toc } = await import("remark-toc"); 15 | const processor = unified() 16 | .use(parse) 17 | .use(toc) 18 | .use(importCode) 19 | .use(stringify); 20 | 21 | let readme = readReadme(path.resolve(__dirname, "../README.md")); 22 | 23 | readme = await processor.process(readme); 24 | 25 | writeReadme( 26 | path.resolve(__dirname, "../chrome-debugging-client/README.md"), 27 | readme, 28 | ); 29 | } 30 | 31 | /** 32 | * @param {string} path 33 | */ 34 | function readReadme(path) { 35 | const contents = fs.readFileSync(path, "utf8"); 36 | return vfile({ 37 | contents, 38 | path, 39 | }); 40 | } 41 | 42 | /** 43 | * @param {string} path 44 | * @param {import("vfile").VFile} file 45 | */ 46 | function writeReadme(path, file) { 47 | const contents = file.toString("utf8"); 48 | if (file.path) { 49 | // the above tranforms update the input 50 | // so we can write back out the input 51 | fs.writeFileSync(file.path, contents); 52 | } 53 | fs.writeFileSync(path, contents); 54 | } 55 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "checkJs": true, 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "module": "commonjs", 9 | "target": "ES2017" 10 | }, 11 | "files": ["types.d.ts", "readme.js"] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "remark-toc" { 2 | const toc: import("unified").Plugin; 3 | export = toc; 4 | } 5 | 6 | declare module "unist-util-visit" { 7 | function visit( 8 | tree: import("unist").Node, 9 | type: string, 10 | visitor: (node: import("unist").Node) => void, 11 | ): void; 12 | export = visit; 13 | } 14 | 15 | declare module "shell-split" { 16 | export function split(str: string): string[]; 17 | } 18 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | qunit: true, 6 | es2017: true, 7 | }, 8 | parserOptions: { 9 | project: "./tsconfig.json", 10 | tsconfigRootDir: __dirname, 11 | sourceType: "module", 12 | }, 13 | ignorePatterns: ["dist/", ".eslintrc.js"], 14 | extends: ["../.eslintrc"], 15 | rules: { 16 | "@typescript-eslint/no-var-requires": "off", 17 | "@typescript-eslint/explicit-function-return-type": "off", 18 | "@typescript-eslint/no-misused-promises": "off", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /test/examplesTest.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const path = require("path"); 3 | const tmp = require("tmp"); 4 | 5 | QUnit.module("examples", () => { 6 | QUnit.test("nodeDebug.js", async (assert) => { 7 | const expected = `paused reason: Break on start 8 | set breakpoint on line 3 9 | resume and wait for next paused event 10 | paused at line 3 11 | evaluate \`obj\` 12 | {"hello":"world"} 13 | resume and wait for execution context to be destroyed 14 | console.log: [{"type":"string","value":"end"}] 15 | close websocket 16 | wait for exit 17 | node exited`; 18 | const stdout = await runExample("nodeDebug.js"); 19 | assert.equal(stdout, expected); 20 | }); 21 | 22 | QUnit.test("printToPDF.js", async (assert) => { 23 | const dir = tmp.dirSync({ unsafeCleanup: true }); 24 | try { 25 | const outPath = path.resolve(dir.name, `blank.pdf`); 26 | const expected = `about:blank written to ${outPath}`; 27 | const stdout = await runExample("printToPDF.js", "about:blank", outPath); 28 | assert.equal(stdout, expected); 29 | } finally { 30 | dir.removeCallback(); 31 | } 32 | }); 33 | }); 34 | 35 | /** 36 | * @param {string} script 37 | * @param {...string} args 38 | * @return {Promise} 39 | */ 40 | async function runExample(script, ...args) { 41 | const { stdout } = await execa(process.execPath, [ 42 | path.resolve(__dirname, "../examples", script), 43 | ...args, 44 | ]); 45 | return stdout; 46 | } 47 | -------------------------------------------------------------------------------- /test/getArgumentsTest.js: -------------------------------------------------------------------------------- 1 | const { getArguments } = require("@tracerbench/spawn-chrome"); 2 | 3 | QUnit.module("getArguments", () => { 4 | QUnit.test("merges disable-features and enable-features", (assert) => { 5 | assert.deepEqual( 6 | getArguments("/user-data-dir", { 7 | disableDefaultArguments: true, 8 | }), 9 | ["--remote-debugging-pipe", "--user-data-dir=/user-data-dir"], 10 | ); 11 | }); 12 | 13 | QUnit.test("merges disable-features", (assert) => { 14 | assert.deepEqual( 15 | getArguments("/user-data-dir", { 16 | disableDefaultArguments: true, 17 | additionalArguments: [ 18 | "--disable-features=TranslationUI", 19 | "--disable-features=NetworkPrediction", 20 | ], 21 | }), 22 | [ 23 | "--remote-debugging-pipe", 24 | "--user-data-dir=/user-data-dir", 25 | "--disable-features=TranslationUI,NetworkPrediction", 26 | ], 27 | ); 28 | }); 29 | 30 | QUnit.test("merges enabled-features", (assert) => { 31 | assert.deepEqual( 32 | getArguments("/user-data-dir", { 33 | disableDefaultArguments: true, 34 | additionalArguments: [ 35 | "--enable-features=TranslationUI", 36 | "--enable-features=NetworkPrediction", 37 | ], 38 | }), 39 | [ 40 | "--remote-debugging-pipe", 41 | "--user-data-dir=/user-data-dir", 42 | "--enable-features=TranslationUI,NetworkPrediction", 43 | ], 44 | ); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-debugging-client-test", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "checkjs": "tsc", 7 | "fixlint": "eslint . --fix", 8 | "lint": "eslint .", 9 | "test": "qunit *Test.js" 10 | }, 11 | "dependencies": { 12 | "@tracerbench/spawn-chrome": "^2.0.0", 13 | "@types/debug": "^4.1.5", 14 | "@types/node": "^18.16.0", 15 | "@types/qunit": "^2.9.0", 16 | "@types/tmp": "^0.2.0", 17 | "chrome-debugging-client": "^2.0.0", 18 | "devtools-protocol": "^0.0.1135028", 19 | "execa": "^5.1.1", 20 | "qunit": "^2.9.1", 21 | "tmp": "^0.2.1" 22 | }, 23 | "devDependencies": { 24 | "@typescript-eslint/eslint-plugin": "^5.59.1", 25 | "@typescript-eslint/parser": "^5.59.1", 26 | "eslint": "^8.39.0", 27 | "eslint-config-prettier": "^8.8.0", 28 | "eslint-plugin-import": "^2.18.2", 29 | "eslint-plugin-prettier": "^4.2.1", 30 | "eslint-plugin-simple-import-sort": "^10.0.0", 31 | "prettier": "^2.0.5", 32 | "typescript": "^5.0.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/spawnChromeTest.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { spawnChrome } = require("chrome-debugging-client"); 3 | 4 | QUnit.module("spawnChrome", () => { 5 | QUnit.test( 6 | "connect to browser, create and attach to page target", 7 | async (assert) => { 8 | const chrome = spawnChrome({ 9 | headless: true, 10 | }); 11 | try { 12 | const browser = chrome.connection; 13 | 14 | browser.on("error", (err) => { 15 | assert.ok(false, `browser error ${err.stack ?? ""}`); 16 | }); 17 | 18 | const browserVersion = await browser.send("Browser.getVersion"); 19 | 20 | assert.ok(browserVersion.protocolVersion); 21 | assert.ok(browserVersion.product); 22 | assert.ok(browserVersion.userAgent); 23 | assert.ok(browserVersion.jsVersion); 24 | 25 | await browser.send("Security.setIgnoreCertificateErrors", { 26 | ignore: true, 27 | }); 28 | 29 | const { targetId } = await browser.send("Target.createTarget", { 30 | url: "about:blank", 31 | }); 32 | 33 | await browser.send("Target.activateTarget", { targetId }); 34 | 35 | const page = await browser.attachToTarget(targetId); 36 | 37 | assert.equal(page.targetId, targetId); 38 | assert.ok(page.sessionId); 39 | assert.ok(page.targetInfo.type, "page"); 40 | assert.ok(page.targetInfo.url, "about:blank"); 41 | 42 | page.on("error", (err) => { 43 | assert.ok(false, `target connection error ${err.stack ?? ""}`); 44 | }); 45 | 46 | let buffer = ""; 47 | await page.send("HeapProfiler.enable"); 48 | page.on("HeapProfiler.addHeapSnapshotChunk", (params) => { 49 | buffer += params.chunk; 50 | }); 51 | 52 | await page.send("HeapProfiler.takeHeapSnapshot", { 53 | reportProgress: false, 54 | }); 55 | 56 | assert.ok(buffer.length > 0, "received chunks"); 57 | 58 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 59 | const data = JSON.parse(buffer); 60 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 61 | assert.ok(data.snapshot.meta, "has snapshot"); 62 | 63 | await browser.send("Target.closeTarget", { targetId }); 64 | 65 | await chrome.close(); 66 | 67 | assert.ok(chrome.hasExited()); 68 | } finally { 69 | await chrome.dispose(); 70 | } 71 | }, 72 | ); 73 | 74 | QUnit.test("spawn chrome at bad path", async (assert) => { 75 | const chrome = spawnChrome({ 76 | chromeExecutable: "bad/path/to/chrome", 77 | headless: true, 78 | }); 79 | try { 80 | const browser = chrome.connection; 81 | 82 | try { 83 | await browser.send("Browser.getVersion"); 84 | assert.ok(false, "should not get here"); 85 | } catch (e) { 86 | assert.equal( 87 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 88 | e.message, 89 | "process exited early: spawn bad/path/to/chrome ENOENT", 90 | ); 91 | } 92 | 93 | assert.ok(chrome.hasExited()); 94 | } finally { 95 | await chrome.dispose(); 96 | } 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/spawnWithWebSocketTest.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { spawnWithWebSocket } = require("chrome-debugging-client"); 3 | 4 | QUnit.module("spawnWithWebSocket", () => { 5 | QUnit.test("run script with break on start", async (assert) => { 6 | // start node requesting it break on start at debug port that 7 | // is available 8 | const node = await spawnWithWebSocket(process.execPath, [ 9 | "--inspect-brk=0", 10 | "-e", 11 | `const obj = { 12 | hello: "world", 13 | }; 14 | debugger; 15 | console.log("end");`, 16 | ]); 17 | const { connection } = node; 18 | try { 19 | // we requested Node to break on start, so we runIfWaitingForDebugger 20 | // and wait for it to break at the start of our script 21 | await Promise.all([ 22 | connection.until("Debugger.paused"), 23 | connection.send("Debugger.enable"), 24 | connection.send("Runtime.enable"), 25 | connection.send("Runtime.runIfWaitingForDebugger"), 26 | ]); 27 | // right now we are paused at the start of the script 28 | 29 | // resume until debugger statement hit 30 | const [debuggerStatement] = await Promise.all([ 31 | connection.until("Debugger.paused"), 32 | connection.send("Debugger.resume"), 33 | ]); 34 | 35 | // get the call frame of the debugger statement 36 | const [callFrame] = debuggerStatement.callFrames; 37 | const { callFrameId } = callFrame; 38 | 39 | // eval obj at the debugger call frame 40 | const { result } = await connection.send("Debugger.evaluateOnCallFrame", { 41 | callFrameId, 42 | expression: "obj", 43 | returnByValue: true, 44 | }); 45 | 46 | assert.equal(result.type, "object"); 47 | assert.deepEqual(result.value, { hello: "world" }); 48 | 49 | // resume and wait for execution to be done 50 | await Promise.all([ 51 | connection.until("Runtime.executionContextDestroyed"), 52 | connection.send("Debugger.resume"), 53 | ]); 54 | 55 | // Node is still alive here and waiting for the debugger to disconnect 56 | // when we close the websocket after resuming 57 | // Node should exit on its own 58 | node.close(); 59 | 60 | await node.waitForExit(); 61 | } finally { 62 | await node.dispose(); 63 | } 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "strict": true, 6 | // giving an out directory helps build perf 7 | "outDir": "dist", 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "moduleResolution": "node", 12 | "module": "commonjs", 13 | "target": "ES2017", 14 | "types": ["node", "debug", "qunit"] 15 | }, 16 | "include": ["*Test.js"], 17 | "references": [ 18 | { 19 | "path": "../chrome-debugging-client" 20 | }, 21 | { 22 | "path": "../@tracerbench/spawn-chrome" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "chrome-debugging-client" 6 | } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------