Repository: firefox-devtools/vscode-firefox-debug Branch: master Commit: 8a387a10a3f7 Files: 144 Total size: 485.7 KB Directory structure: gitextract_cmpgi7eh/ ├── .gitignore ├── .mocharc.json ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dist/ │ ├── README.md │ ├── package.json │ └── terminator/ │ ├── main.js │ └── manifest.json ├── package.json ├── src/ │ ├── README.md │ ├── adapter/ │ │ ├── README.md │ │ ├── adapter/ │ │ │ ├── README.md │ │ │ ├── addonManager.ts │ │ │ ├── breakpoint.ts │ │ │ ├── breakpointsManager.ts │ │ │ ├── consoleAPICall.ts │ │ │ ├── dataBreakpointsManager.ts │ │ │ ├── descriptor.ts │ │ │ ├── environment.ts │ │ │ ├── eventBreakpointsManager.ts │ │ │ ├── frame.ts │ │ │ ├── getterValue.ts │ │ │ ├── objectGrip.ts │ │ │ ├── preview.ts │ │ │ ├── registry.ts │ │ │ ├── scope.ts │ │ │ ├── skipFilesManager.ts │ │ │ ├── source.ts │ │ │ ├── sourcesManager.ts │ │ │ ├── thread.ts │ │ │ ├── variable.ts │ │ │ └── variablesProvider.ts │ │ ├── configuration.ts │ │ ├── debugAdapterBase.ts │ │ ├── firefox/ │ │ │ ├── README.md │ │ │ ├── actorProxy/ │ │ │ │ ├── README.md │ │ │ │ ├── addons.ts │ │ │ │ ├── base.ts │ │ │ │ ├── breakpointList.ts │ │ │ │ ├── console.ts │ │ │ │ ├── descriptor.ts │ │ │ │ ├── device.ts │ │ │ │ ├── frame.ts │ │ │ │ ├── interface.ts │ │ │ │ ├── longString.ts │ │ │ │ ├── objectGrip.ts │ │ │ │ ├── preference.ts │ │ │ │ ├── root.ts │ │ │ │ ├── source.ts │ │ │ │ ├── target.ts │ │ │ │ ├── thread.ts │ │ │ │ ├── threadConfiguration.ts │ │ │ │ └── watcher.ts │ │ │ ├── connection.ts │ │ │ ├── launch.ts │ │ │ ├── protocol.d.ts │ │ │ ├── sourceMaps/ │ │ │ │ ├── README.md │ │ │ │ ├── info.ts │ │ │ │ ├── manager.ts │ │ │ │ ├── source.ts │ │ │ │ └── thread.ts │ │ │ └── transport.ts │ │ ├── firefoxDebugAdapter.ts │ │ ├── firefoxDebugSession.ts │ │ ├── location.ts │ │ └── util/ │ │ ├── delayedTask.ts │ │ ├── forkedLauncher.ts │ │ ├── fs.ts │ │ ├── log.ts │ │ ├── misc.ts │ │ ├── net.ts │ │ ├── pathMapper.ts │ │ └── pendingRequests.ts │ ├── common/ │ │ ├── configuration.ts │ │ ├── customEvents.ts │ │ ├── deferredMap.ts │ │ └── util.ts │ ├── extension/ │ │ ├── addPathMapping.ts │ │ ├── debugConfigurationProvider.ts │ │ ├── eventBreakpointsProvider.ts │ │ ├── loadedScripts/ │ │ │ ├── fileNode.ts │ │ │ ├── nonLeafNode.ts │ │ │ ├── provider.ts │ │ │ ├── rootNode.ts │ │ │ ├── sessionNode.ts │ │ │ └── treeNode.ts │ │ ├── main.ts │ │ ├── pathMappingWizard.ts │ │ └── popupAutohideManager.ts │ ├── test/ │ │ ├── setup.ts │ │ ├── sourceMapUtil.ts │ │ ├── testAccessorProperties.ts │ │ ├── testConfigurationParser.ts │ │ ├── testConsole.ts │ │ ├── testDataBreakpoints.ts │ │ ├── testDebugAddons.ts │ │ ├── testDebugWebWorkers.ts │ │ ├── testEvaluate.ts │ │ ├── testGulpSourceMaps.ts │ │ ├── testHitBreakpoints.ts │ │ ├── testInspectVariables.ts │ │ ├── testSetBreakpoints.ts │ │ ├── testSkipFiles.ts │ │ ├── testSourceActorCollection.ts │ │ ├── testStepThrough.ts │ │ ├── testTerminateAndCleanup.ts │ │ ├── testWebpackSourceMaps.ts │ │ └── util.ts │ └── typings/ │ ├── gulp-nop.d.ts │ └── map-sources.d.ts ├── testdata/ │ ├── web/ │ │ ├── debuggerStatement.js │ │ ├── dlscript.js │ │ ├── exception-sourcemap.js │ │ ├── exception-sourcemap.ts │ │ ├── exception.js │ │ ├── index.html │ │ ├── main.js │ │ ├── skip.js │ │ ├── sourceMaps/ │ │ │ ├── modules/ │ │ │ │ ├── f.js │ │ │ │ ├── g.js │ │ │ │ └── index.html │ │ │ └── scripts/ │ │ │ ├── f.js │ │ │ ├── g.js │ │ │ └── index.html │ │ ├── tsconfig.json │ │ └── worker.js │ ├── webExtension/ │ │ ├── addOn/ │ │ │ ├── backgroundscript.js │ │ │ ├── contentscript.js │ │ │ └── manifest.json │ │ └── index.html │ └── webExtension2/ │ ├── addOn/ │ │ ├── backgroundscript.js │ │ ├── contentscript.js │ │ └── manifest.json │ └── index.html ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ .nyc_output/ coverage/ dist/*.bundle.js dist/*.bundle.js.map dist/mappings.wasm dist/*LICENSE* ================================================ FILE: .mocharc.json ================================================ { "require": ["ts-node/register/transpile-only", "src/test/setup.ts"], "timeout": 20000, "slow": 6000, "exit": true, "spec": "src/test/test*.ts" } ================================================ FILE: .vscode/extensions.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // List of extensions which should be recommended for users of this workspace. "recommendations": [ "hbenl.vscode-mocha-test-adapter" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [ ] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "type": "pwa-node", "request": "launch", "name": "debug server", "program": "${workspaceFolder}/dist/adapter.bundle.js", "args": [ "--server=4711" ], "cwd": "${workspaceFolder}", "sourceMaps": true }, { "type": "pwa-extensionHost", "request": "launch", "name": "extension host", "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], "sourceMaps": true }, { "type": "firefox", "request": "launch", "name": "web test", "debugServer": 4711, "file": "${workspaceFolder}/testdata/web/index.html" }, { "type": "firefox", "request": "launch", "name": "webextension test", "debugServer": 4711, "addonPath": "${workspaceFolder}/testdata/webExtension/addOn", "file": "${workspaceFolder}/testdata/webExtension/index.html" } ], "compounds": [ { "name": "server & extension", "configurations": [ "debug server", "extension host" ] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.insertSpaces": false, "files.exclude": { ".git": true, "node_modules": true, ".nyc_output": true, "coverage": true }, "editor.rulers": [100], "typescript.tsdk": "./node_modules/typescript/lib", "editor.tabSize": 4 } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "type": "npm", "script": "watch", "label": "watch", "isBackground": true, "runOptions": { "runOn": "folderOpen" } }, { "type": "npm", "script": "typecheck-watch", "label": "typecheck-watch", "isBackground": true, "problemMatcher": "$tsc-watch", "runOptions": { "runOn": "folderOpen" } } ] } ================================================ FILE: .vscodeignore ================================================ src/** node_modules/** **/*.map package-lock.json tsconfig.json webpack.config.js .vscode/** .gitignore .nyc_output/** coverage/** testdata/** dist/package.json dist/README.md dist/LICENSE ================================================ FILE: CHANGELOG.md ================================================ ### Version 2.15.0 * fix various issues when debugging multiple tabs * use the URL in thread names ### Version 2.14.1 * fix truncated console messages * fix threads not showing up in the loaded scripts panel * only show content scripts when debugging a WebExtension * don't open a new tab in attach configurations * resume detached threads when they pause ### Version 2.14.0 * improve how console messages are rendered ### Version 2.13.0 * fix `clearConsoleOnReload` * fix output of styled console messages * fix showing the locations of early console messages * fix breakpoint issues with `reloadOnAttach` * add pathMappingIndex configuration property ### Version 2.12.1 * fix debugging of web extension content scripts in Firefox 135 ### Version 2.12.0 * add support for debugging without a launch configuration * add support for event listener breakpoints * fix handling of exited tabs and threads ### Version 2.11.0 * support restarting frames * support skipping debugger statements * fix breakpoints with hit counts on Windows ### Version 2.10.1 * fix path mapping on Windows ### Version 2.10.0 * migrate to new debug protocol methods * fix breakpoints not working after navigating ### Version 2.9.11 * handle console.time/timeEnd * fix the loaded scripts panel ### Version 2.9.10 * fix marketplace badge link in the README ### Version 2.9.9 * compatibility fix for Node 17 / VS Code 1.82 ### Version 2.9.8 * debug protocol update for Firefox 104 * fix the build on MacOS ### Version 2.9.7 * debug protocol update for Firefox 102 ### Version 2.9.6 * debug protocol update for Firefox 96 ### Version 2.9.5 * compatibility fix for VS Code 1.62.1 ### Version 2.9.4 * fix for breakpoints not being set in some situations * add the URL to a thread's name ### Version 2.9.3 * fix breakpoints only working after reloading the page * fix missing console messages * fix debugging WebWorkers * fix data breakpoints ### Version 2.9.2 * fix terminating Firefox at the end of the debug session * fix function scopes being with the name `[unknown]` * show an error message if the path mapping wizard couldn't update the launch configuration * set the webRoot configuration property to its default if necessary * support overriding the debugging port in the settings * add workaround for the snap version of VS Code ### Version 2.9.1 * add `enableCRAWorkaround` configuration property * fix the conditions for the `keepProfileChanges` configuration property on MacOS * fix the sorting of Arrays in the Variables view ### Version 2.9.0 * add `tabFilter` configuration property * open a new Firefox tab if no tab matches the `tabFilter` * allow `stable`, `developer` and `nightly` as values for firefoxExecutable * only suggest the Path Mapping Wizard if it can create a pathMapping * debug protocol fix for Firefox 78 ### Version 2.8.0 * debug protocol fix for Firefox 77 * add `tmpDir` configuration property * fix for skipping external URLs containing a query string * fixes for object previews ### Version 2.7.2 * debug protocol fixes for Firefox 76 ### Version 2.7.1 * debug protocol fix for Firefox 75 ### Version 2.7.0 * bugfix for breakpoints in Vue.js projects * add default pathMappings for Next.js projects * add support for wildcards in pathMappings * add `suggestPathMappingWizard` configuration property * various bugfixes ### Version 2.6.1 * fix for showing the launch configuration in a remote workspace * fix for re-enabling a watchpoint ### Version 2.6.0 * add the Path Mapping Wizard * extend the Loaded Scripts Explorer ### Version 2.5.5 * debug protocol fix for Firefox 72 ### Version 2.5.4 * fix version detection for Firefox 72 ### Version 2.5.3 * fix for create-react-app projects on Windows ### Version 2.5.2 * improve stepping performance * fix for debugging a WebExtension in a workspace subfolder on Windows * fix for debugging in a remote workspace on Windows ### Version 2.5.1 * fix removal of breakpoints from sourcemapped sources * improved default `pathMappings` for Angular CLI projects * sourcemapping accuracy improvements * fix initialization of exception breakpoints ### Version 2.5.0 * performance improvement for setting breakpoints in bundled sources * add support for the new column breakpoint UI introduced in VS Code 1.39 * remove support for Firefox <68 ### Version 2.4.0 * add support for `console.clear()` * add `clearConsoleOnReload` configuration property * try to load sourcemaps from disk first * fix WebExtension debugging in Firefox 71 ### Version 2.3.4 * fix a performance issue when reloading the page in the browser ### Version 2.3.3 * fix for hot module replacement: when a module was replaced, breakpoints in that module stopped working * fix for callstacks sometimes showing an error message "Couldn't find source adapter" ### Version 2.3.2 * fix support for data breakpoints with Firefox 70 ### Version 2.3.1 * update README.md ### Version 2.3.0 * disable the prompt to configure telemetry in temporary debug profiles * objects referenced in logpoints can now be inspected in the debug console * add support for the BigInt primitive type ### Version 2.2.0 * add support for VS Code remote development * add support for data breakpoints on object properties if Firefox supports watchpoints (an upcoming Firefox feature that will appear in Nightlies soon) ### Version 2.1.0 * add `timeout` launch configuration property ### Version 2.0.1 * build with babel and bundle with webpack - the resulting package is much smaller and loads faster Version 2.0.1 was only released on npm ### Version 2.0.0 * branding updated - this extension is now an official Firefox DevTool! ### Version 1.8.3 * fix for launching Firefox on MacOS and some Linux distros * increase the timeout for connecting to Firefox to 10 seconds and fix the handling of that timeout ### Version 1.8.2 * bugfix: globstars (`**`) in the skipFiles configuration didn't match path segments that contain `/./` * bugfix: show the generated source location of a stack frame if it can't be mapped to an original source location * bugfix: the debug adapter kept running after the end of the debug session until Firefox was closed ### Version 1.8.1 * bugfix: the temporary debug profile wasn't removed at the end of the debug session when using a recent Firefox version on Windows ### Version 1.8.0 * fix for Firefox 68 * remove support for legacy add-ons ### Version 1.7.11 * fix handling of case-insensitive paths on Windows when setting breakpoints * add workaround for broken sourcemaps generated by create-react-app ### Version 1.7.10 * bugfix: sometimes the exception view wasn't shown * further fixes for Firefox 67 ### Version 1.7.9 * further fixes for Firefox 67 ### Version 1.7.8 * fix for Firefox 66 ### Version 1.7.7 * fix for debug protocol changes in Firefox 67 ### Version 1.7.6 * fix for a debug protocol change in Firefox 66 ### Version 1.7.5 * fix for sourcemaps in WebExtensions * fix for `reAttach` on MacOS ### Version 1.7.4 * fix path mapping for configurations where `url` contains no path and no trailing slash (e.g. `"url": "http://localhost:8080"`) ### Version 1.7.3 * fix handling of relative sourceRoot in sourcemaps ### Version 1.7.2 * add log messages to monitor source-mapping performance ### Version 1.7.1 * WebExtension debugging: allow comments in manifest.json ### Version 1.7.0 * add the ability to override some launch configuration properties in the settings ### Version 1.6.0 * change the default for `sourceMaps` to `client` * improve the performance of client-side source-map handling * fix client-side source-map handling for source-maps with relative paths * fix detection of Firefox Developer Edition on MacOS ### Version 1.5.0 * WebExtension debugging: add commands and status bar button for toggling popup auto-hide ### Version 1.4.3 * improve accuracy of client-side sourcemap support ### Version 1.4.2 * compute the original locations of console and error events when `sourceMaps` is set to `client` ### Version 1.4.1 * also look for `firefox-developer-edition` when searching the Firefox executable on Linux ### Version 1.4.0 * add `liftAccessorsFromPrototypes` configuration property ### Version 1.3.0 * allow WebExtensions without an ID if they're installed via RDP * install WebExtensions via RDP by default ### Version 1.2.1 * fix `reloadOnChange` on Windows (thanks @Misiur) ### Version 1.2.0 * add support for breakpoints with hit counts * add support for log points ### Version 1.1.4 * add support for evaluating getter functions * fix path mapping of URLs that contain encoded characters ### Version 1.1.3 * path mapping bugfixes ### Version 1.1.2 * workaround for a timing issue with early beta versions of Firefox 60 * improve WebAssembly debugging support ### Version 1.1.1 * experimental support for WebAssembly debugging ### Version 1.1.0 * add support for creating `pathMappings` from the Loaded Scripts Explorer * bugfix for breakpoints being shown unverified (gray) even when they were successfully set * change default `pathMappings` for webpack to support Angular CLI projects ### Version 1.0.1 * fix debugging of WebExtensions that contain a `package.json` file * set the default `addonType` to `webExtension` in configuration snippets and documentation ### Version 1.0.0 * add default `pathMappings` for webpack * harmonize trailing slashes in user-specified `pathMappings` * Linux: search the Firefox executable in all directories in the user's `PATH` (thanks @agathver) * `addonType` now defaults to `webExtension` ### Version 0.17.0 * show object previews in the Variables and Watch sections of the Debug view * fix the Loaded Scripts Explorer when navigating in Firefox ### Version 0.16.1 * fix opening remote scripts from the Loaded Scripts Explorer * skip exceptions triggered from the debug console * add the ability to configure URLs that should not be mapped to local paths * remove deprecated VS Code APIs ### Version 0.16.0 * add Loaded Scripts Explorer * add support for Symbol-keyed properties (Firefox 56+) ### Version 0.15.4 * bugfix: `pathMappings` were ignored in `attach` configurations ### Version 0.15.3 * performance improvements ### Version 0.15.2 * handle absolute urls in source-maps, including a workaround for webpack weirdness ### Version 0.15.1 * on Windows the debug adapter sometimes didn't attach to WebExtensions that were installed as temporary add-ons - fixed ### Version 0.15.0 * add support for toggling the skip flag for single files while debugging * make `webRoot` optional if `pathMappings` are specified ### Version 0.14.1 * compatibility update for the upcoming VS Code 1.14 release ### Version 0.14.0 * fix WebExtension debugging in recent Firefox builds * add experimental `sourceMaps` configuration property ### Version 0.13.1 * add support for setting variable values in the debug side bar * add support for IntelliSense in the debug console ### Version 0.13.0 * add `reloadOnChange` configuration property ### Version 0.12.1 * fix temporary add-on installation on Windows ### Version 0.12.0 * add support for reloading add-ons * add `installAddonInProfile` configuration property ### Version 0.11.1 * bugfix: some function names were not shown in the call stack * bugfix: the tooltips of tabs for external source files didn't show the full url * bugfix: some accessor properties (e.g. window.window) were shown as undefined * bugfix for sporadical failures to attach to Firefox after launching it ### Version 0.11.0 * add `keepProfileChanges` configuration property * bugfix: the temporary profiles are now deleted reliably ### Version 0.10.0 * add `preferences` configuration property * add `showConsoleCallLocation` configuration property * support sending objects to the console (e.g. `console.log(document)`) * change the display of call stack, return values and exceptions to be more in line with other VS Code javascript debuggers ### Version 0.9.3 * fix slow initial startup of add-on debugging with the `reAttach` option ### Version 0.9.2 * support `reAttach` for add-on debugging ### Version 0.9.1 * fix `reAttach` on Windows ### Version 0.9.0 * Add `reAttach` and `reloadOnAttach` configuration properties ### Version 0.8.8 * bugfix: source files were not mapped to local files in VS Code 1.9 ### Version 0.8.7 * workaround for Firefox sending inaccurate source information in certain situations, which can break the `skipFiles` feature ### Version 0.8.6 * bugfix: some URLs were not handled correctly when processing sourcemapped sources ### Version 0.8.5 * send log messages from add-ons to the debug console ### Version 0.8.4 * bugfix: exceptions were not shown ### Version 0.8.3 * strip query strings from urls when converting them to local file paths ### Version 0.8.2 * fix skipFiles on Windows ### Version 0.8.1 * bugfix: sources could not be skipped during their first execution ### Version 0.8.0 * Add `skipFiles` configuration property * Add `pathMappings` configuration property * Add configuration snippets * Fix several bugs when evaluating watches and expressions entered in the debug console ### Version 0.7.7 * fix debugging of WebExtension content scripts in recent Firefox builds ### Version 0.7.6 * bugfix: breakpoints were sometimes not hit after a page reload ### Version 0.7.5 * bugfix: support javascript values of type Symbol * bugfix: evaluating expressions in the VS Code debug console sometimes stopped working ### Version 0.7.2 * Terminate the debug session when Firefox is closed ### Version 0.7.1 * Show the full url of sources that do not correspond to local files * bugfix for setting breakpoints in content scripts of `addonSdk` browser extensions ### Version 0.7.0 * Debugging Firefox add-ons * Launch mode now always creates a temporary profile: if a profile is specified in the launch configuration, it will be copied and modified to allow remote debugging * Launch mode now uses the developer edition of Firefox if it is found ### Version 0.6.5 * bugfix for sourcemaps with embedded source files ### Version 0.6.4 * Fix breakpoint handling when a Firefox tab is reloaded * Only send javascript-related warnings and errors from Firefox to the debug console ### Version 0.6.3 * Add configuration option for diagnostic logging * Make conversion between paths and urls more robust ### Version 0.6.2 * bugfix: stepping and resuming stopped working if a breakpoint was hit immediately after loading the page ### Version 0.6.1 * Fix debugging WebWorkers and multiple browser tabs in VSCode 1.2.0 ### Version 0.6.0 * Add support for evaluating javascript expressions in the debug console even if Firefox isn't paused * Add support for debugger statements ### Version 0.5.0 * Add support for call stack paging ### Version 0.4.0 * Add support for debugging WebWorkers * Add support for debugging multiple browser tabs * Fix exception breakpoints in VSCode 1.1.0 * Re-create the Firefox profile on every launch, unless a profile name or directory is configured ### Version 0.3.0 * Print messages from the Firefox console in the VS Code debug console * bugfix: resume the VS Code debugger when Firefox resumes, e.g. if the user reloads the page in Firefox while the debugger is paused ### Version 0.2.0 * Automatically create a Firefox profile for debugging ### Version 0.1.0 * Initial release ================================================ FILE: CODE_OF_CONDUCT.md ================================================ ## Mozilla Community Participation Guidelines This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report For more information on how to report violations of the CPG, please read our '[How to Report](https://www.mozilla.org/en-US/about/governance/policies/participation/reporting/)' page. ## Project Specific Etiquette ### Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Project maintainers who do not follow or enforce Mozilla's Participation Guidelines in good faith may face temporary or permanent repercussions. ================================================ FILE: CONTRIBUTING.md ================================================ # Hacking the VS Code Debug Adapter for Firefox ## Prerequisites First of all, you should familiarize yourself with the [debugging architecture of VS Code](https://code.visualstudio.com/api/extension-guides/debugger-extension#debugging-architecture-of-vs-code) and the [Firefox Remote Debugging Protocol](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md). ## Setup * fork and clone this repository and open it in VS Code * run `npm install` * run `npm run watch` or start the `watch` task in VS Code * optional but recommended: run `npm run typecheck-watch` or start the `typecheck-watch` task in VS Code Most source folders contain a `README` file giving an overview of the contained code. Start with the [top-level README](./src). ## Debugging This repository contains several launch configurations for debugging different parts of the code: * to debug the debug adapter itself (i.e. the code under `src/adapter/`), use the `debug server` configuration to start it as a stand-alone server in the node debugger. Then open a web application in a second VS Code window and add a launch configuration of type `firefox` to it. Add `"debugServer": 4711` to this launch configuration, so that VS Code will use the debug adapter that you just launched in the other VS Code window. Set a breakpoint in the `startSession()` of [`FirefoxDebugAdapter`](./src/adapter/firefoxDebugAdapter.ts) and start debugging the web application. * to debug the extension code (under `src/extension/`), use the `extension host` configuration. It will open a second VS Code window with the extension in it running in the node debugger. * the compound configuration `server & extension` will launch both of the configurations above * the `web test` and `webextension test` configurations can be used if you need to manually reproduce the mocha tests. You'll need to start the `debug server` configuration first. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-2017 Holger Benl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================


logo
VS Code Debugger for Firefox

Debug your JavaScript code running in Firefox from VS Code.

Marketplace badge

A VS Code extension to debug web applications and extensions running in the [Mozilla Firefox browser](https://www.mozilla.org/en-US/firefox/developer/?utm_medium=vscode_extension&utm_source=devtools). [📦 Install from VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=firefox-devtools.vscode-firefox-debug). ### Supported features * Pause [breakpoints](https://code.visualstudio.com/docs/editor/debugging#_breakpoints), including advanced [conditional](https://code.visualstudio.com/docs/editor/debugging#_conditional-breakpoints) and [inline](https://code.visualstudio.com/docs/editor/debugging#_inline-breakpoints) modes * Pause on object property changes with [Data breakpoints](https://code.visualstudio.com/docs/editor/debugging#_data-breakpoints) * Inject logging during debugging using [logpoints](https://code.visualstudio.com/docs/editor/debugging#_logpoints) * Debugging eval scripts, script tags, and scripts that are added dynamically and/or source mapped * *Variables* pane for inspecting and setting values * *Watch* pane for evaluating and watching expressions * *Console* for logging and REPL * Debugging Firefox extensions * Debugging Web Workers * Compatible with [remote development](https://code.visualstudio.com/docs/remote/remote-overview) ## Getting Started You can use this extension in **launch** or **attach** mode. In **launch** mode it will start an instance of Firefox navigated to the start page of your application and terminate it when you stop debugging. You can also set the `reAttach` option in your launch configuration to `true`, in this case Firefox won't be terminated at the end of your debugging session and the debugger will re-attach to it when you start the next debugging session - this is a lot faster than restarting Firefox every time. `reAttach` also works for WebExtension debugging: in this case, the WebExtension is (re-)installed as a [temporary add-on](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox). In **attach** mode the extension connects to a running instance of Firefox (which must be manually configured to allow remote debugging - see [below](#attach)). To configure these modes you must create a file `.vscode/launch.json` in the root directory of your project. You can do so manually or let VS Code create an example configuration for you by clicking the gear icon at the top of the Debug pane. Finally, if `.vscode/launch.json` already exists in your project, you can open it and add a configuration snippet to it using the *"Add Configuration"* button in the lower right corner of the editor. ### Launch Here's an example configuration for launching Firefox navigated to the local file `index.html` in the root directory of your project: ```json { "version": "0.2.0", "configurations": [ { "name": "Launch index.html", "type": "firefox", "request": "launch", "reAttach": true, "file": "${workspaceFolder}/index.html" } ] } ``` You may want (or need) to debug your application running on a Webserver (especially if it interacts with server-side components like Webservices). In this case replace the `file` property in your `launch` configuration with a `url` and a `webRoot` property. These properties are used to map urls to local files: ```json { "version": "0.2.0", "configurations": [ { "name": "Launch localhost", "type": "firefox", "request": "launch", "reAttach": true, "url": "http://localhost/index.html", "webRoot": "${workspaceFolder}" } ] } ``` The `url` property may point to a file or a directory, if it points to a directory it must end with a trailing `/` (e.g. `http://localhost/my-app/`). You may omit the `webRoot` property if you specify the `pathMappings` manually. For example, the above configuration would be equivalent to ```json { "version": "0.2.0", "configurations": [ { "name": "Launch localhost", "type": "firefox", "request": "launch", "reAttach": true, "url": "http://localhost/index.html", "pathMappings": [{ "url": "http://localhost", "path": "${workspaceFolder}" }] } ] } ``` Setting the `pathMappings` manually becomes necessary if the `url` points to a file or resource in a subdirectory of your project, e.g. `http://localhost/login/index.html`. ### Attach To use attach mode, you have to launch Firefox manually from a terminal with remote debugging enabled. Note that if you don't use Firefox Developer Edition, you must first configure Firefox to allow remote debugging. To do this, open the Developer Tools Settings and check the checkboxes labeled "Enable browser chrome and add-on debugging toolboxes" and "Enable remote debugging" (as described [here](https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Debugging_Firefox_Desktop#Enable_remote_debugging)). Alternatively you can set the following values in `about:config`: Preference Name | Value | Comment --------------------------------------|---------|--------- `devtools.debugger.remote-enabled` | `true` | Required `devtools.chrome.enabled` | `true` | Required `devtools.debugger.prompt-connection` | `false` | Recommended `devtools.debugger.force-local` | `false` | Set this only if you want to attach VS Code to Firefox running on a different machine (using the `host` property in the `attach` configuration) Then close Firefox and start it from a terminal like this: __Windows__ `"C:\Program Files\Mozilla Firefox\firefox.exe" -start-debugger-server` (This syntax is for a regular command prompt (cmd.exe), not PowerShell!) __OS X__ `/Applications/Firefox.app/Contents/MacOS/firefox -start-debugger-server` __Linux__ `firefox -start-debugger-server` Navigate to your web application and use this `launch.json` configuration to attach to Firefox: ```json { "version": "0.2.0", "configurations": [ { "name": "Launch index.html", "type": "firefox", "request": "attach" } ] } ``` If your application is running on a Webserver, you need to add the `url` and `webRoot` properties to the configuration (as in the second `launch` configuration example above). ### Skipping ("blackboxing") files You can tell the debugger to ignore certain files while debugging: When a file is ignored, the debugger won't break in that file and will skip it when you're stepping through your code. This is the same as "black boxing" scripts in the Firefox Developer Tools. There are two ways to enable this feature: * You can enable/disable this for single files while debugging by choosing "Toggle skipping this file" from the context menu of a frame in the call stack. * You can use the `skipFiles` configuration property, which takes an array of glob patterns specifying the files to be ignored. If the URL of a file can't be mapped to a local file path, the URL will be matched against these glob patterns, otherwise the local file path will be matched. Examples for glob patterns: * `"${workspaceFolder}/skipThis.js"` - will skip the file `skipThis.js` in the root folder of your project * `"**/skipThis.js"` - will skip files called `skipThis.js` in any folder * `"**/node_modules/**"` - will skip all files in `node_modules` folders anywhere in the project * `"http?(s):/**"` - will skip files that could not be mapped to local files * `"**/google.com/**"` - will skip files containing `/google.com/` in their url, in particular all files from the domain `google.com` (that could not be mapped to local files) ### Path mapping The debug adapter needs to map the URLs of javascript files (as seen by Firefox) to local file paths (as seen by VS Code). It creates a set of default path mappings from the configuration that work for most projects. However, depending on the setup of your project, they may not work for you, resulting in breakpoints being shown in gray (and Firefox not breaking on them) even after Firefox has loaded the corresponding file. In this case, you will have to define them manually using the `pathMappings` configuration property. The easiest way to do this is through the Path Mapping Wizard: when you try to set a breakpoint during a debug session in a file that couldn't be mapped to a URL, the debug adapter will offer to automatically create a path mapping for you. If you click "Yes" it will analyze the URLs loaded by Firefox and try to find a path mapping that maps this file and as many other workspace files as possible to URLs loaded by Firefox and it will add this mapping to your debug configuration. Note that this path mapping is just a guess, so you should check if it looks plausible to you. You can also call the Path Mapping Wizard from the command palette during a debug session. You can look at the Firefox URLs and how they are mapped to paths in the Loaded Scripts Explorer, which appears at the bottom of the debug side bar of VS Code during a debug session. By choosing "Map to local file" or "Map to local directory" from the context menu of a file or a directory, you can pick the corresponding local file or directory and a path mapping will automatically be added to your configuration. If you specify more than one mapping, the first mappings in the list will take precedence over subsequent ones and all of them will take precedence over the default mappings. The most common source of path mapping problems is webpack because the URLs that it generates depend on its configuration and different URL styles are in use. If your configuration contains a `webroot` property, the following mappings will be added by default in order to support most webpack setups: ``` { "url": "webpack:///~/", "path": "${webRoot}/node_modules/" } { "url": "webpack:///./~/", "path": "${webRoot}/node_modules/" } { "url": "webpack:///./", "path": "${webRoot}/" } { "url": "webpack:///src/", "path": "${webRoot}/src/" } { "url": "webpack:///node_modules/", "path": "${webRoot}/node_modules/" } { "url": "webpack:///webpack", "path": null } { "url": "webpack:///(webpack)", "path": null } { "url": "webpack:///pages/", "path": "${webRoot}/pages/" } { "url": "webpack://[name]_[chunkhash]/node_modules/", "path": "${webRoot}/node_modules/" } { "url": "webpack://[name]_[chunkhash]/", "path": null } { "url": "webpack:///", "path": "" } ``` When the `path` argument of a mapping is set to `null`, the corresponding URLs are prevented from being mapped to local files. In the webpack mappings shown above this is used to specify that URLs starting with `webpack:///webpack` or `webpack:///(webpack)` do not correspond to files in your workspace (because they are dynamically generated by webpack). It could also be used for URLs that dynamically generate their content on the server (e.g. PHP scripts) or if the content on the server is different from the local file content. For these URLs the debugger will show the content fetched from the server instead of the local file content. You can also use `*` as a wildcard in the `url` of a pathMapping. It will match any number of arbitrary characters except `/`. ### Debugging WebExtensions Here's an example configuration for WebExtension debugging: ```json { "version": "0.2.0", "configurations": [ { "name": "Launch WebExtension", "type": "firefox", "request": "launch", "reAttach": true, "addonPath": "${workspaceFolder}" } ] } ``` The `addonPath` property must be the absolute path to the directory containing `manifest.json`. You can reload your WebExtension using the command "Firefox: Reload add-on" (`extension.firefox.reloadAddon`) from the VS Code command palette. The WebExtension will also be reloaded when you restart the debugging session, unless you have set `reloadOnAttach` to `false`. You can also use the `reloadOnChange` property to let VS Code reload your WebExtension automatically whenever you change a file. You can enable/disable/toggle popup auto-hide using the commands "Firefox: Enable/Disable/Toggle popup auto-hide" (`extension.firefox.enablePopupAutohide` / `disablePopupAutohide` / `togglePopupAutohide`). ### Further optional configuration properties * `reAttach`: If you set this option to `true` in a `launch` configuration, Firefox won't be terminated at the end of your debugging session and the debugger will re-attach to it at the start of your next debugging session. * `reloadOnAttach`: This flag controls whether the web page(s) should be automatically reloaded after attaching to Firefox. The default is to reload in a `launch` configuration with the `reAttach` flag set to `true` and to not reload in an `attach` configuration. * `reloadOnChange`: Automatically reload the Firefox tabs or your WebExtension whenever files change. You can specify single files, directories or glob patterns to watch for file changes and additionally specify files to be ignored. Since watching files consumes system resources, make sure that you are not watching more files than necessary. The following example will watch all javascript files in your workspace except those under `node_modules`: ```json "reloadOnChange": { "watch": [ "${workspaceFolder}/**/*.js" ], "ignore": [ "${workspaceFolder}/node_modules/**" ] } ``` By default, the reloading will be "debounced": the debug adapter will wait until the last file change was 100 milliseconds ago before reloading. This is useful if your project uses a build system that generates multiple files - without debouncing, each file would trigger a separate reload. You can use `reloadOnChange.debounce` to change the debounce time span or to disable debouncing (by setting it to `0` or `false`). Instead of string arrays, you can also use a single string for `watch` and `ignore` and if you don't need to specify `ignore` or `debounce`, you can specify the `watch` value directly, e.g. ```json "reloadOnChange": "${workspaceFolder}/lib/*.js" ``` * `tabFilter`: Only attach to Firefox tabs with matching URLs. You can specify one or more URLs to include and/or URLs to exclude and the URLs can contain `*` wildcards. By default, a `tabFilter` is constructed from the `url` in your `launch` or `attach` configuration by replacing the last path segment with `*`. For example, if your configuration contains `"url": "http://localhost:3000/app/index.html"`, the default `tabFilter` will be `"http://localhost:3000/app/*"`. * `clearConsoleOnReload`: Clear the debug console in VS Code when the page is reloaded in Firefox. * `tmpDir`: The path of the directory to use for temporary files * `profileDir`, `profile`: You can specify a Firefox profile directory or the name of a profile created with the Firefox profile manager. The extension will create a copy of this profile in the system's temporary directory and modify the settings in this copy to allow remote debugging. You can also override these properties in your settings (see below). The default profile names used by Firefox are `default`, `dev-edition-default` and `default-nightly` for the stable, developer and nightly editions, respectively. * `keepProfileChanges`: Use the specified profile directly instead of creating a temporary copy. Since this profile will be permanently modified for debugging, you should only use this option with a dedicated debugging profile. You can also override this property in your settings (see below). * `port`: Firefox uses port 6000 for the debugger protocol by default. If you want to use a different port, you can set it with this property. You can also override this property in your settings (see below). * `timeout`: The timeout in seconds for the adapter to connect to Firefox after launching it. * `firefoxExecutable`: The absolute path to the Firefox executable or the name of a Firefox edition (`stable`, `developer` or `nightly`) to look for in its default installation path. If not specified, this extension will look for both stable and developer editions of Firefox; if both are available, it will use the developer edition. You can also override this property in your settings (see below). * `firefoxArgs`: An array of additional arguments used when launching Firefox (`launch` configuration only). You can also override this property in your settings (see below). * `host`: If you want to debug with Firefox running on a different machine, you can specify the device's address using this property (`attach` configuration only). * `log`: Configures diagnostic logging for this extension. This may be useful for troubleshooting (see below for examples). * `showConsoleCallLocation`: Set this option to `true` to append the source location of `console` calls to their output * `preferences`: Set additional Firefox preferences in the debugging profile * `popupAutohideButton`: Show a button in the status bar for toggling popup auto-hide (enabled by default when debugging a WebExtension) * `liftAccessorsFromPrototypes`: If there are accessor properties (getters and setters) defined on an object's prototype chain, you can "lift" them so they are displayed on the object itself. This is usually necessary in order to execute the getters, because otherwise they would be executed with `this` set to the object's prototype instead of the object itself. This property lets you set the number of prototype levels that should be scanned for accessor properties to lift. Note that this will slow the debugger down, so it's set to `0` by default. * `pathMappingIndex`: The name of the directory index file configured in the webserver, defaults to `index.html`. This will be appended to all URLs pointing to a directory (i.e. URLs ending with `/`) before trying to map them to file paths. * `suggestPathMappingWizard`: Suggest using the Path Mapping Wizard when you try to set a breakpoint in an unmapped source during a debug session. You may want to turn this off if some of the sources in your project are loaded on-demand (e.g. if you create multiple bundles with webpack and some of these bundles are only loaded as needed). * `enableCRAWorkaround`: Enable a workaround for facebook/create-react-app#6074: Adding/removing breakpoints doesn't work for sources that were changed after the dev-server was started ### Overriding configuration properties in your settings You can override some of the `launch.json` configuration properties in your user, workspace or folder settings. This can be useful to make machine-specific changes to your launch configuration without sharing them with other users. This setting | overrides this `launch.json` property -----------------------------|------------------------------------- `firefox.executable` | `firefoxExecutable` `firefox.args` | `firefoxArgs` `firefox.profileDir` | `profileDir` `firefox.profile` | `profile` `firefox.keepProfileChanges` | `keepProfileChanges` `firefox.port` | `port` ### Diagnostic logging The following example for the `log` property will write all log messages to the file `log.txt` in your workspace: ```json ... "log": { "fileName": "${workspaceFolder}/log.txt", "fileLevel": { "default": "Debug" } } ... ``` This example will write all messages about conversions from URLs to paths and all error messages to the VS Code console: ```json ... "log": { "consoleLevel": { "PathConversion": "Debug", "default": "Error" } } ... ``` ## Troubleshooting * Breakpoints that should get hit immediately after the javascript file is loaded may not work the first time: You will have to click "Reload" in Firefox for the debugger to stop at such a breakpoint. This is a weakness of the Firefox debug protocol: VS Code can't tell Firefox about breakpoints in a file before the execution of that file starts. * If your breakpoints remain unverified after launching the debugger (i.e. they appear gray instead of red), the conversion between file paths and urls may not work. The messages from the `PathConversion` logger may contain clues how to fix your configuration. Have a look at the "Diagnostic Logging" section for an example how to enable this logger. * If you think you've found a bug in this adapter please [file a bug report](https://github.com/firefox-devtools/vscode-firefox-debug/issues). It may be helpful if you create a log file (as described in the "Diagnostic Logging" section) and attach it to the bug report. ================================================ FILE: dist/README.md ================================================ # Debug Adapter for Firefox This package implements the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) for Firefox. It is part of the [VS Code Debugger for Firefox](https://marketplace.visualstudio.com/items?itemName=firefox-devtools.vscode-firefox-debug) but can be used by other IDEs as well to integrate with Firefox's debugger. ## Custom requests and events In addition to the standard Debug Adapter Protocol, the debug adapter implements some custom requests and events: ### Requests The following custom requests can be sent to the debug adapter: * `toggleSkippingFile` : toggle whether a particular file (identified by its URL) is skipped (aka blackboxed) during debugging * `reloadAddon` : reload the WebExtension that is being debugged * `setPopupAutohide` : set the popup auto-hide flag (config key `ui.popup.disable_autohide`, WebExtension debugging only) * `togglePopupAutohide` : toggle the popup auto-hide flag * `setActiveEventBreakpoints` : set event listener breakpoints ### Events The debug adapter sends these custom events: * `popupAutohide` : contains the initial state of the popup auto-hide flag (WebExtension debugging only) * `threadStarted` : sent when a thread-like actor (for a Tab, Worker or WebExtension) is started * `threadExited` : sent when a thread-like actor exited * `newSource` : sent when Firefox loaded a new source file * `removeSources` : sent when a thread-like actor discards its previously loaded sources, i.e. when a Tab is navigated to a new URL * `unknownSource` : sent when the user tries to set a breakpoint in a file that could not be mapped to a URL loaded by Firefox * `availableEvents` : contains the events for which event listener breakpoints can be set The event body types are defined [here](../src/common/customEvents.ts). The source events may be replaced by the [LoadedSourceEvent](https://microsoft.github.io/debug-adapter-protocol/specification#Events_LoadedSource) in the future. ================================================ FILE: dist/package.json ================================================ { "name": "firefox-debugadapter", "version": "2.15.0", "author": "Holger Benl ", "description": "Debug adapter for Firefox", "files": [ "adapter.bundle.js", "launcher.bundle.js", "mappings.wasm", "terminator/*", "README.md", "LICENSE" ], "main": "adapter.bundle.js", "repository": { "type": "git", "url": "git+https://github.com/firefox-devtools/vscode-firefox-debug.git" }, "license": "MIT", "bugs": { "url": "https://github.com/firefox-devtools/vscode-firefox-debug/issues" }, "homepage": "https://github.com/firefox-devtools/vscode-firefox-debug" } ================================================ FILE: dist/terminator/main.js ================================================ browser.windows.getAll().then((windows) => { for (const window of windows) { browser.windows.remove(window.id); } }); ================================================ FILE: dist/terminator/manifest.json ================================================ { "description": "A WebExtension that terminates Firefox by closing all its windows", "manifest_version": 2, "name": "Terminator", "version": "1.0", "homepage_url": "https://github.com/firefox-devtools/vscode-firefox-debug", "applications": { "gecko": { "id": "{5b498dbd-f841-423d-9e46-a735486a0eb7}" } }, "background": { "scripts": [ "main.js" ] } } ================================================ FILE: package.json ================================================ { "name": "vscode-firefox-debug", "displayName": "Debugger for Firefox", "version": "2.15.0", "author": "Holger Benl ", "publisher": "firefox-devtools", "description": "Debug your web application or browser extension in Firefox", "icon": "icon.png", "engines": { "vscode": "^1.80.0" }, "categories": [ "Debuggers" ], "scripts": { "reinstall": "rimraf node_modules package-lock.json && npm install", "clean": "rimraf dist/*.bundle.js dist/*.bundle.js.map dist/mappings.wasm coverage .nyc_output vscode-firefox-debug-*.vsix", "build": "webpack --mode=production", "watch": "webpack --watch --mode=development", "rebuild": "npm run clean && npm run build", "typecheck": "tsc", "typecheck-watch": "tsc -w", "test": "mocha", "cover": "nyc npm test && nyc report --reporter=lcov && nyc report --reporter=html", "package": "vsce package", "publish": "npm run rebuild && vsce publish", "package-npm": "cd dist && npm pack", "publish-npm": "npm run rebuild && cd dist && npm publish" }, "dependencies": { "@babel/polyfill": "^7.12.1", "@vscode/debugadapter": "^1.68.0", "chokidar": "^3.6.0", "core-js": "^3.39.0", "data-uri-to-buffer": "3.0.1", "debounce": "^2.2.0", "escape-string-regexp": "4.0.0", "file-uri-to-path": "^2.0.0", "file-url": "^4.0.0", "firefox-profile": "^4.7.0", "fs-extra": "^11.2.0", "is-absolute-url": "3.0.3", "minimatch": "^9.0.5", "source-map": "^0.7.4", "strip-json-comments": "3.1.1", "uuid": "^11.0.3", "vscode-uri": "^3.0.8" }, "devDependencies": { "@babel/cli": "^7.26.4", "@babel/core": "^7.26.0", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-env": "^7.26.0", "@babel/preset-typescript": "^7.26.0", "@gulp-sourcemaps/map-sources": "^1.0.0", "@types/debounce": "^1.2.4", "@types/fs-extra": "^11.0.4", "@types/gulp": "^4.0.17", "@types/gulp-concat": "^0.0.37", "@types/gulp-rename": "^2.0.6", "@types/gulp-sourcemaps": "^0.0.38", "@types/gulp-uglify": "^3.0.11", "@types/mocha": "^10.0.10", "@types/node": "^16.18.122", "@types/vscode": "~1.80.0", "@vscode/debugadapter-testsupport": "^1.68.0", "babel-loader": "^9.2.1", "copy-webpack-plugin": "^12.0.2", "dotenv": "^16.4.7", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", "gulp-nop": "0.0.3", "gulp-rename": "^2.0.0", "gulp-sourcemaps": "^3.0.0", "gulp-uglify": "^3.0.2", "mocha": "^11.0.1", "nyc": "^17.1.0", "original-fs": "^1.2.0", "rimraf": "^6.0.1", "terser-webpack-plugin": "^5.3.11", "ts-node": "^10.9.2", "typescript": "^5.7.2", "vsce": "^2.15.0", "webpack": "^5.97.1", "webpack-cli": "^5.1.4" }, "babel": { "presets": [ "@babel/typescript", [ "@babel/env", { "modules": false, "useBuiltIns": "usage", "corejs": 3 } ] ], "plugins": [ "@babel/proposal-class-properties", "@babel/proposal-object-rest-spread" ] }, "browserslist": [ "node 8" ], "nyc": { "include": [ "out/**/*.js" ], "exclude": [ "out/test/**/*.js" ] }, "repository": { "type": "git", "url": "https://github.com/firefox-devtools/vscode-firefox-debug.git" }, "keywords": [ "vscode", "firefox", "debug" ], "license": "MIT", "bugs": { "url": "https://github.com/firefox-devtools/vscode-firefox-debug/issues" }, "homepage": "https://github.com/firefox-devtools/vscode-firefox-debug", "extensionKind": [ "ui" ], "main": "./dist/extension.bundle.js", "activationEvents": [ "onDebug" ], "contributes": { "commands": [ { "command": "extension.firefox.reloadAddon", "title": "Firefox: Reload add-on" }, { "command": "extension.firefox.toggleSkippingFile", "title": "Toggle skipping this file" }, { "command": "extension.firefox.openScript", "title": "Open script" }, { "command": "extension.firefox.addPathMapping", "title": "Map to local directory" }, { "command": "extension.firefox.addFilePathMapping", "title": "Map to local file" }, { "command": "extension.firefox.addNullPathMapping", "title": "Don't map this directory" }, { "command": "extension.firefox.addNullFilePathMapping", "title": "Don't map this file" }, { "command": "extension.firefox.enablePopupAutohide", "title": "Firefox: Enable popup auto-hide" }, { "command": "extension.firefox.disablePopupAutohide", "title": "Firefox: Disable popup auto-hide" }, { "command": "extension.firefox.togglePopupAutohide", "title": "Firefox: Toggle popup auto-hide" }, { "command": "extension.firefox.pathMappingWizard", "title": "Firefox: Run the path mapping wizard" } ], "menus": { "debug/callstack/context": [ { "command": "extension.firefox.toggleSkippingFile", "when": "inDebugMode && debugType == 'firefox' && callStackItemType == 'stackFrame'" } ], "view/item/context": [ { "command": "extension.firefox.addPathMapping", "group": "addPathMapping@1", "when": "view == extension.firefox.loadedScripts && viewItem == directory" }, { "command": "extension.firefox.addFilePathMapping", "group": "addPathMapping@1", "when": "view == extension.firefox.loadedScripts && viewItem == file" }, { "command": "extension.firefox.addNullPathMapping", "group": "addPathMapping@2", "when": "view == extension.firefox.loadedScripts && viewItem == directory" }, { "command": "extension.firefox.addNullFilePathMapping", "group": "addPathMapping@2", "when": "view == extension.firefox.loadedScripts && viewItem == file" } ], "commandPalette": [ { "command": "extension.firefox.pathMappingWizard", "when": "editorIsOpen && inDebugMode && debugType == 'firefox'" }, { "command": "extension.firefox.toggleSkippingFile", "when": "false" }, { "command": "extension.firefox.openScript", "when": "false" }, { "command": "extension.firefox.addPathMapping", "when": "false" }, { "command": "extension.firefox.addFilePathMapping", "when": "false" }, { "command": "extension.firefox.addNullPathMapping", "when": "false" }, { "command": "extension.firefox.addNullFilePathMapping", "when": "false" } ] }, "configuration": { "title": "Firefox debug", "properties": { "firefox.executable": { "description": "Absolute path to the Firefox executable", "type": "string", "scope": "resource" }, "firefox.args": { "description": "Additional arguments passed to Firefox", "type": "array", "items": { "type": "string" }, "scope": "resource" }, "firefox.profileDir": { "description": "The path of the Firefox profile directory to use", "type": "string", "scope": "resource" }, "firefox.profile": { "description": "The name of the Firefox profile to use", "type": "string", "scope": "resource" }, "firefox.keepProfileChanges": { "description": "Use the specified profile directly instead of a temporary copy", "type": "boolean", "scope": "resource" }, "firefox.port": { "description": "The remote debugging port to use", "type": "number", "scope": "resource" } } }, "views": { "debug": [ { "id": "extension.firefox.loadedScripts", "name": "Loaded Scripts", "when": "inDebugMode && debugType == 'firefox'" }, { "id": "extension.firefox.eventBreakpoints", "name": "Event Listener Breakpoints", "when": "inDebugMode && debugType == 'firefox'" } ] }, "debuggers": [ { "type": "firefox", "label": "Firefox", "program": "./dist/adapter.bundle.js", "runtime": "node", "languages": [ "html" ], "initialConfigurations": [ { "name": "Launch index.html", "type": "firefox", "request": "launch", "reAttach": true, "file": "${workspaceFolder}/index.html" }, { "name": "Launch localhost", "type": "firefox", "request": "launch", "reAttach": true, "url": "http://localhost/index.html", "webRoot": "${workspaceFolder}" }, { "name": "Attach", "type": "firefox", "request": "attach" }, { "name": "Launch WebExtension", "type": "firefox", "request": "launch", "reAttach": true, "addonPath": "${workspaceFolder}" } ], "configurationSnippets": [ { "label": "Firefox: Launch (file)", "description": "Launch Firefox navigated to a local file in your project", "body": { "type": "firefox", "request": "launch", "reAttach": true, "name": "${1:Launch index.html}", "file": "^\"\\${workspaceFolder}/${2:index.html}\"" } }, { "label": "Firefox: Launch (server)", "description": "Launch Firefox navigated to your project running on a server", "body": { "type": "firefox", "request": "launch", "reAttach": true, "name": "${1:Launch localhost}", "url": "${2:http://localhost/index.html}", "webRoot": "^\"\\${workspaceFolder}${3:}\"" } }, { "label": "Firefox: Attach", "description": "Attach to a running Firefox process", "body": { "type": "firefox", "request": "attach", "name": "${1:Attach}" } }, { "label": "Firefox: WebExtension", "description": "Launch Firefox with your WebExtension project installed", "body": { "type": "firefox", "request": "launch", "reAttach": true, "name": "${1:Launch add-on}", "addonPath": "^\"\\${workspaceFolder}${2:}\"" } } ], "configurationAttributes": { "launch": { "required": [], "properties": { "file": { "type": "string", "description": "The file to open in the browser", "default": "${workspaceFolder}/index.html" }, "url": { "type": "string", "description": "The url to open in the browser" }, "webRoot": { "type": "string", "description": "If the 'url' property is specified, this property specifies the workspace absolute path corresponding to the path of the url", "default": "${workspaceFolder}" }, "firefoxExecutable": { "type": "string", "description": "Absolute path to the Firefox executable" }, "tmpDir": { "type": "string", "description": "The path of the directory to use for temporary files" }, "profileDir": { "type": "string", "description": "The path of the Firefox profile directory to use" }, "profile": { "type": "string", "description": "The name of the Firefox profile to use" }, "keepProfileChanges": { "type": "boolean", "description": "Use the specified profile directly instead of a temporary copy", "default": true }, "port": { "type": "number", "description": "The remote debugging port to use", "default": 6000 }, "timeout": { "type": "number", "description": "The timeout in seconds for the adapter to connect to Firefox after launching it", "default": 5 }, "firefoxArgs": { "type": "array", "description": "Additional arguments passed to Firefox", "items": { "type": "string" }, "default": [] }, "reAttach": { "type": "boolean", "description": "Don't terminate Firefox at the end of the debugging session and re-attach to it when starting the next session", "default": true }, "reloadOnAttach": { "type": "boolean", "description": "Reload all tabs after re-attaching to Firefox", "default": true }, "reloadOnChange": { "description": "Watch the specified files, directories or glob patterns and reload the tabs or add-on when they change", "type": [ "string", "array", "object" ], "items": { "type": "string" }, "properties": { "watch": { "description": "Files, directories or glob patterns to be watched for file changes", "type": [ "string", "array" ], "items": { "type": "string" }, "default": "${workspaceFolder}/**/*.js" }, "ignore": { "description": "Files, directories or glob patterns to be ignored", "type": [ "string", "array" ], "items": { "type": "string" }, "default": "**/node_modules/**" }, "debounce": { "description": "The time in milliseconds to wait after a file change before reloading, or false to start reloading immediately", "type": [ "number", "boolean" ] } }, "default": { "watch": "${workspaceFolder}/**/*.js", "ignore": "**/node_modules/**" } }, "clearConsoleOnReload": { "type": "boolean", "description": "Clear the debug console in VS Code when the page is reloaded in Firefox", "default": false }, "pathMappings": { "type": "array", "description": "Additional mappings from URLs (as seen by Firefox) to filesystem paths (as seen by VS Code)", "items": { "type": "object", "properties": { "url": { "type": "string", "description": "The URL as seen by Firefox" }, "path": { "type": [ "string", "null" ], "description": "The corresponding filesystem path as seen by VS Code" } } } }, "pathMappingIndex": { "type": "string", "description": "The name of the directory index file configured in the webserver", "default": "index.html" }, "skipFiles": { "type": "array", "description": "An array of glob patterns to skip when debugging", "items": { "type": "string" } }, "preferences": { "type": "object", "description": "Set additional Firefox preferences", "additionalProperties": { "type": [ "boolean", "integer", "string", "null" ] } }, "tabFilter": { "description": "Only attach to tabs whose URL matches this", "type": [ "string", "array", "object" ], "items": { "type": "string" }, "properties": { "include": { "description": "URLs to attach to", "type": [ "string", "array" ], "items": { "type": "string" }, "default": "*" }, "exclude": { "description": "URLs not to attach to", "type": [ "string", "array" ], "items": { "type": "string" }, "default": [] } }, "default": "*" }, "showConsoleCallLocation": { "type": "boolean", "description": "Show the location of console API calls", "default": true }, "addonPath": { "type": "string", "description": "The path of the directory containing the WebExtension", "default": "${workspaceFolder}" }, "popupAutohideButton": { "type": "boolean", "description": "Show a button in the status bar for toggling popup auto-hide (WebExtension debugging)", "default": false }, "liftAccessorsFromPrototypes": { "type": "number", "description": "The number of prototype levels that should be scanned for accessor properties", "default": 0 }, "suggestPathMappingWizard": { "type": "boolean", "description": "Suggest using the Path Mapping Wizard when the user tries to set a breakpoint in an unmapped source during a debug session", "default": true }, "enableCRAWorkaround": { "type": "boolean", "description": "Enable a workaround for breakpoints not working in projects created using create-react-app", "default": true }, "log": { "type": "object", "description": "Configuration for diagnostic logging of the debug adapter", "properties": { "fileName": { "type": "string", "description": "The name of the logfile", "default": "${workspaceFolder}/vscode-firefox-debug.log" }, "fileLevel": { "type": "object", "description": "The minimum loglevel(s) for messages written to the logfile", "properties": { "default": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ], "description": "The default loglevel" } }, "additionalProperties": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ] }, "default": { "default": "Debug" } }, "consoleLevel": { "type": "object", "description": "The minimum loglevel(s) for messages written to the console", "properties": { "default": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ], "description": "The default loglevel" } }, "additionalProperties": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ] }, "default": { "default": "Debug" } } }, "default": { "fileName": "${workspaceFolder}/vscode-firefox-debug.log", "fileLevel": { "default": "Debug" }, "consoleLevel": { "default": "Warn" } } } } }, "attach": { "required": [], "properties": { "url": { "type": "string", "description": "The url to open in the browser" }, "webRoot": { "type": "string", "description": "If the 'url' property is specified, this property specifies the workspace absolute path corresponding to the path of the url", "default": "${workspaceFolder}" }, "firefoxExecutable": { "type": "string", "description": "Absolute path to the Firefox executable" }, "profileDir": { "type": "string", "description": "The path of the Firefox profile directory to use" }, "port": { "type": "number", "description": "The remote debugging port to use", "default": 6000 }, "host": { "type": "string", "description": "The remote debugging host to use", "default": "localhost" }, "reloadOnAttach": { "type": "boolean", "description": "Reload all tabs after attaching to Firefox", "default": false }, "reloadOnChange": { "description": "Watch the specified files, directories or glob patterns and reload the tabs or add-on when they change", "type": [ "string", "array", "object" ], "items": { "type": "string" }, "properties": { "watch": { "description": "Files, directories or glob patterns to be watched for file changes", "type": [ "string", "array" ], "items": { "type": "string" }, "default": "${workspaceFolder}/**/*.js" }, "ignore": { "description": "Files, directories or glob patterns to be ignored", "type": [ "string", "array" ], "items": { "type": "string" }, "default": "**/node_modules/**" }, "debounce": { "description": "The time in milliseconds to wait after a file change before reloading, or false to start reloading immediately", "type": [ "number", "boolean" ] } }, "default": { "watch": "${workspaceFolder}/**/*.js", "ignore": "**/node_modules/**" } }, "clearConsoleOnReload": { "type": "boolean", "description": "Clear the debug console in VS Code when the page is reloaded in Firefox", "default": false }, "pathMappings": { "type": "array", "description": "Additional mappings from URLs (as seen by Firefox) to filesystem paths (as seen by VS Code)", "items": { "type": "object", "properties": { "url": { "type": "string", "description": "The URL as seen by Firefox" }, "path": { "type": [ "string", "null" ], "description": "The corresponding filesystem path as seen by VS Code" } } } }, "pathMappingIndex": { "type": "string", "description": "The name of the directory index file configured in the webserver", "default": "index.html" }, "skipFiles": { "type": "array", "description": "An array of glob patterns to skip when debugging", "items": { "type": "string" }, "default": [ "${workspaceFolder}/node_modules/**/*" ] }, "tabFilter": { "description": "Only attach to tabs whose URL matches this", "type": [ "string", "array", "object" ], "items": { "type": "string" }, "properties": { "include": { "description": "URLs to attach to", "type": [ "string", "array" ], "items": { "type": "string" }, "default": "*" }, "exclude": { "description": "URLs not to attach to", "type": [ "string", "array" ], "items": { "type": "string" }, "default": [] } }, "default": "*" }, "showConsoleCallLocation": { "type": "boolean", "description": "Show the location of console API calls", "default": true }, "addonPath": { "type": "string", "description": "The path of the directory containing the WebExtension", "default": "${workspaceFolder}" }, "popupAutohideButton": { "type": "boolean", "description": "Show a button in the status bar for toggling popup auto-hide (WebExtension debugging)", "default": false }, "liftAccessorsFromPrototypes": { "type": "number", "description": "The number of prototype levels that should be scanned for accessor properties", "default": 0 }, "suggestPathMappingWizard": { "type": "boolean", "description": "Suggest using the Path Mapping Wizard when the user tries to set a breakpoint in an unmapped source during a debug session", "default": true }, "enableCRAWorkaround": { "type": "boolean", "description": "Enable a workaround for breakpoints not working in projects created using create-react-app", "default": true }, "log": { "type": "object", "description": "Configuration for diagnostic logging of the debug adapter", "properties": { "fileName": { "type": "string", "description": "The name of the logfile", "default": "${workspaceFolder}/vscode-firefox-debug.log" }, "fileLevel": { "type": "object", "description": "The minimum loglevel(s) for messages written to the logfile", "properties": { "default": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ], "description": "The default loglevel" } }, "additionalProperties": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ] }, "default": { "default": "Debug" } }, "consoleLevel": { "type": "object", "description": "The minimum loglevel(s) for messages written to the console", "properties": { "default": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ], "description": "The default loglevel" } }, "additionalProperties": { "type": "string", "enum": [ "Debug", "Info", "Warn", "Error" ] }, "default": { "default": "Debug" } } }, "default": { "fileName": "${workspaceFolder}/vscode-firefox-debug.log", "fileLevel": { "default": "Debug" }, "consoleLevel": { "default": "Warn" } } } } } } } ] } } ================================================ FILE: src/README.md ================================================ This folder contains the sources for the Debug Adapter for Firefox extension. They are grouped into the following subfolders: * [`adapter`](./adapter) - the [debug adapter](https://code.visualstudio.com/api/extension-guides/debugger-extension#debugging-architecture-of-vs-code) itself, which is run in a separate process and talks to VS Code using the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) * [`extension`](./extension) - contains additional integration with VS Code (this code will be run in the VS Code extension host): * the Loaded Scripts Explorer, which is shown at the bottom of the debug view * a [DebugConfigurationProvider](https://code.visualstudio.com/api/extension-guides/debugger-extension#using-a-debugconfigurationprovider) that adds [machine-specific configuration](https://github.com/firefox-devtools/vscode-firefox-debug#overriding-configuration-properties-in-your-settings) (like the location of the Firefox executable, the name of the profile to use for debugging, etc.) from the user's VS Code settings to debug configurations * a button to toggle the “Disable Popup Auto-Hide” flag (for WebExtension debugging) from VS Code * [`test`](./test) - mocha tests for this extension * [`common`](./common) - a few interfaces and functions used throughout this extension * [`typings`](./typings) - type definition files for npm modules for which no type definitions exist on npm ================================================ FILE: src/adapter/README.md ================================================ This folder contains the sources for the Firefox [debug adapter](https://code.visualstudio.com/api/extension-guides/debugger-extension#debugging-architecture-of-vs-code). The entry point is the [`FirefoxDebugAdapter`](./firefoxDebugAdapter.ts) class, which receives the requests from VS Code and delegates most of the work to the [`FirefoxDebugSession`](./firefoxDebugSession.ts) class. The [`firefox`](./firefox) folder contains the code for launching and talking to Firefox. The [`coordinator`](./coordinator) folder contains classes that manage the states of ["threads"](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#interacting-with-thread-like-actors) in Firefox. The [`adapter`](./adapter) folder contains classes that translate between the debugging protocols of Firefox and VS Code. ================================================ FILE: src/adapter/adapter/README.md ================================================ This folder contains classes that translate between the [Firefox Remote Debugging Protocol](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md) and VS Code's [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/). Furthermore, there are classes for managing [breakpoints](./breakpointsManager.ts), [skipping](./skipFilesManager.ts) (or blackboxing) files in the debugger and [addon debugging](./addonManager.ts). ================================================ FILE: src/adapter/adapter/addonManager.ts ================================================ import { Log } from '../util/log'; import * as path from 'path'; import { ParsedAddonConfiguration } from '../configuration'; import { RootActorProxy } from '../firefox/actorProxy/root'; import { AddonsActorProxy } from '../firefox/actorProxy/addons'; import { PreferenceActorProxy } from '../firefox/actorProxy/preference'; import { FirefoxDebugSession } from '../firefoxDebugSession'; import { PopupAutohideEventBody } from '../../common/customEvents'; import { isWindowsPlatform } from '../../common/util'; import { DescriptorActorProxy } from '../firefox/actorProxy/descriptor'; const log = Log.create('AddonManager'); export const popupAutohidePreferenceKey = 'ui.popup.disable_autohide'; /** * When debugging a WebExtension, this class installs the WebExtension, attaches to it, reloads it * when desired and tells the [`PopupAutohideManager`](../../extension/popupAutohideManager.ts) the * initial state of the popup auto-hide flag by sending a custom event. */ export class AddonManager { private resolveAddonId!: (addonId: string) => void; public readonly addonId = new Promise(resolve => this.resolveAddonId = resolve); private readonly config: ParsedAddonConfiguration; private descriptorActor: DescriptorActorProxy | undefined = undefined; constructor( private readonly debugSession: FirefoxDebugSession ) { this.config = debugSession.config.addon!; } public async sessionStarted( rootActor: RootActorProxy, addonsActor: AddonsActorProxy, preferenceActor: PreferenceActorProxy ): Promise { const addonPath = isWindowsPlatform() ? path.normalize(this.config.path) : this.config.path; let result = await addonsActor.installAddon(addonPath); this.resolveAddonId(result.addon.id); await this.fetchDescriptor(rootActor); if (this.config.popupAutohideButton) { const popupAutohide = !(await preferenceActor.getBoolPref(popupAutohidePreferenceKey)); this.debugSession.sendCustomEvent('popupAutohide', { popupAutohide }); } } public async reloadAddon(): Promise { if (!this.descriptorActor) { throw 'Addon isn\'t attached'; } await this.descriptorActor.reload(); } private async fetchDescriptor(rootActor: RootActorProxy): Promise { const addons = await rootActor.fetchAddons(); addons.forEach(async addon => { if (addon.id === await this.addonId) { this.descriptorActor = new DescriptorActorProxy( addon.actor, 'webExtension', this.debugSession.firefoxDebugConnection ); if (!this.debugSession.processDescriptorMode) { const adapter = await this.debugSession.attachDescriptor(this.descriptorActor); await adapter.watcherActor.watchResources(['console-message', 'error-message', 'source', 'thread-state']); } } }); } } ================================================ FILE: src/adapter/adapter/breakpoint.ts ================================================ import { DebugProtocol } from '@vscode/debugprotocol'; import { MappedLocation } from '../location'; export class BreakpointInfo { /** * the actual location where the breakpoint was set (which may be different from the requested location) */ public actualLocation: MappedLocation | undefined; /** true if the breakpoint was successfully set */ public verified: boolean; /** how many times the breakpoint should be skipped initially */ public readonly hitLimit: number; public hitCount = 0; public constructor( public readonly id: number, public readonly requestedBreakpoint: DebugProtocol.SourceBreakpoint ) { this.verified = false; this.hitLimit = +(requestedBreakpoint.hitCondition || ''); } public isEquivalent(other: BreakpointInfo | DebugProtocol.SourceBreakpoint): boolean { const bp1 = this.requestedBreakpoint; const bp2 = (other instanceof BreakpointInfo) ? other.requestedBreakpoint : other; return (bp1.line === bp2.line) && (bp1.column === bp2.column) && (bp1.condition === bp2.condition) && (bp1.logMessage === bp2.logMessage); } } ================================================ FILE: src/adapter/adapter/breakpointsManager.ts ================================================ import { Log } from '../util/log'; import { BreakpointInfo } from './breakpoint'; import { DebugProtocol } from '@vscode/debugprotocol'; import { Event, Breakpoint, BreakpointEvent } from '@vscode/debugadapter'; import { FirefoxDebugSession } from '../firefoxDebugSession'; import { SourceMappingSourceActorProxy } from '../firefox/sourceMaps/source'; import { normalizePath } from '../util/fs'; let log = Log.create('BreakpointsManager'); /** * This class holds all breakpoints that have been set in VS Code and synchronizes them with all * sources in all threads in Firefox using [`SourceAdapter#updateBreakpoints()`](./source.ts). */ export class BreakpointsManager { private nextBreakpointId = 1; private readonly breakpointsBySourcePathOrUrl = new Map(); constructor( private readonly session: FirefoxDebugSession ) { session.breakpointLists.onRegistered(breakpointListActor => { [...this.breakpointsBySourcePathOrUrl.entries()].forEach(async ([sourcePath, breakpoints]) => { const sourceAdapter = await session.sources.getAdapterForPath(sourcePath); if (sourceAdapter.url) { breakpoints.forEach(async breakpointInfo => { const actualLocation = await sourceAdapter.findNextBreakableLocation( breakpointInfo.requestedBreakpoint.line, (breakpointInfo.requestedBreakpoint.column || 1) - 1 ); if (actualLocation) { breakpointInfo.actualLocation = actualLocation; let logValue: string | undefined; if (breakpointInfo.requestedBreakpoint.logMessage) { logValue = '...' + convertLogpointMessage(breakpointInfo.requestedBreakpoint.logMessage); } const location = actualLocation.generated ?? actualLocation; const url = actualLocation.generated ? sourceAdapter.generatedUrl : sourceAdapter.url; if (url) { breakpointListActor.setBreakpoint( url, location.line, location.column, breakpointInfo.requestedBreakpoint.condition, logValue ); } if (!breakpointInfo.verified) { this.verifyBreakpoint(breakpointInfo); } } }); } }); }); } /** * called by [`FirefoxDebugAdapter#setBreakpoints()`](../firefoxDebugAdapter.ts) whenever the * breakpoints have been changed by the user in VS Code */ public setBreakpoints( breakpoints: DebugProtocol.SourceBreakpoint[], sourcePathOrUrl: string ): BreakpointInfo[] { log.debug(`Setting ${breakpoints.length} breakpoints for ${sourcePathOrUrl}`); const normalizedPathOrUrl = normalizePath(sourcePathOrUrl); const oldBreakpointInfos = this.breakpointsBySourcePathOrUrl.get(normalizedPathOrUrl); const breakpointInfos = breakpoints.map( breakpoint => this.getOrCreateBreakpointInfo(breakpoint, oldBreakpointInfos) ); this.breakpointsBySourcePathOrUrl.set(normalizedPathOrUrl, breakpointInfos); breakpointInfos.forEach(async breakpointInfo => { if (!oldBreakpointInfos?.some(oldBreakpointInfo => oldBreakpointInfo === breakpointInfo)) { let sourceAdapter = this.session.sources.getExistingAdapterForPath(normalizedPathOrUrl); if (!sourceAdapter) { this.session.sendEvent(new Event('unknownSource', sourcePathOrUrl)); sourceAdapter = await this.session.sources.getAdapterForPath(normalizedPathOrUrl); } if (sourceAdapter.url) { const actualLocation = await sourceAdapter.findNextBreakableLocation( breakpointInfo.requestedBreakpoint.line, (breakpointInfo.requestedBreakpoint.column || 1) - 1 ); if (actualLocation) { breakpointInfo.actualLocation = actualLocation; let logValue: string | undefined; if (breakpointInfo.requestedBreakpoint.logMessage) { logValue = '...' + convertLogpointMessage(breakpointInfo.requestedBreakpoint.logMessage); } const location = actualLocation.generated ?? actualLocation; const url = actualLocation.generated ? sourceAdapter.generatedUrl : sourceAdapter.url; if (url) { for (const [, breakpointListActor] of this.session.breakpointLists) { breakpointListActor.setBreakpoint( url, location.line, location.column, breakpointInfo.requestedBreakpoint.condition, logValue ); } } if (!breakpointInfo.verified) { this.verifyBreakpoint(breakpointInfo); } } } } }); if (oldBreakpointInfos) { oldBreakpointInfos.forEach(async oldBreakpointInfo => { if (!breakpointInfos.some(breakpointInfo => breakpointInfo.requestedBreakpoint.line === oldBreakpointInfo.requestedBreakpoint.line && breakpointInfo.requestedBreakpoint.column === oldBreakpointInfo.requestedBreakpoint.column )) { const sourceAdapter = await this.session.sources.getAdapterForPath(normalizedPathOrUrl); if (sourceAdapter.url) { const actualLocation = await sourceAdapter.findNextBreakableLocation( oldBreakpointInfo.requestedBreakpoint.line, (oldBreakpointInfo.requestedBreakpoint.column || 1) - 1 ); if (actualLocation) { const location = actualLocation.generated ?? actualLocation; const url = actualLocation.generated ? sourceAdapter.generatedUrl : sourceAdapter.url; if (url) { for (const [, breakpointListActor] of this.session.breakpointLists) { breakpointListActor.removeBreakpoint( url, location.line, location.column ); } } } } } }); } return breakpointInfos; } public getBreakpoints(sourcePathOrUrl: string) { return this.breakpointsBySourcePathOrUrl.get(normalizePath(sourcePathOrUrl)); } private verifyBreakpoint(breakpointInfo: BreakpointInfo): void { if (!breakpointInfo.actualLocation) return; let breakpoint: DebugProtocol.Breakpoint = new Breakpoint( true, breakpointInfo.actualLocation.line, breakpointInfo.actualLocation.column + 1); breakpoint.id = breakpointInfo.id; this.session.sendEvent(new BreakpointEvent('changed', breakpoint)); breakpointInfo.verified = true; } private getOrCreateBreakpointInfo( requestedBreakpoint: DebugProtocol.SourceBreakpoint, oldBreakpointInfos: BreakpointInfo[] | undefined ): BreakpointInfo { if (oldBreakpointInfos) { const oldBreakpointInfo = oldBreakpointInfos.find( breakpointInfo => breakpointInfo.isEquivalent(requestedBreakpoint) ); if (oldBreakpointInfo) { return oldBreakpointInfo; } } return new BreakpointInfo(this.nextBreakpointId++, requestedBreakpoint); } } /** * convert the message of a logpoint (which can contain javascript expressions in curly braces) * to a javascript expression that evaluates to an array of values to be displayed in the debug console * (doesn't support escaping or nested curly braces) */ export function convertLogpointMessage(msg: string): string { // split `msg` into string literals and javascript expressions const items: string[] = []; let currentPos = 0; while (true) { const leftBrace = msg.indexOf('{', currentPos); if (leftBrace < 0) { items.push(JSON.stringify(msg.substring(currentPos))); break; } else { let rightBrace = msg.indexOf('}', leftBrace + 1); if (rightBrace < 0) rightBrace = msg.length; items.push(JSON.stringify(msg.substring(currentPos, leftBrace))); items.push(msg.substring(leftBrace + 1, rightBrace)); currentPos = rightBrace + 1; } } // the appended `reduce()` call will convert all non-object values to strings and concatenate consecutive strings return `[${items.join(',')}].reduce((a,c)=>{if(typeof c==='object'&&c){a.push(c,'')}else{a.push(a.pop()+c)}return a},[''])`; } ================================================ FILE: src/adapter/adapter/consoleAPICall.ts ================================================ import { VariablesProvider } from './variablesProvider'; import { VariableAdapter } from './variable'; import { ThreadAdapter } from './thread'; /** * Adapter class for representing a `consoleAPICall` event from Firefox. */ export class ConsoleAPICallAdapter implements VariablesProvider { public readonly variablesProviderId: number; public readonly referenceExpression = undefined; public readonly referenceFrame = undefined; private readonly argsAdapter: VariableAdapter; public constructor( args: VariableAdapter[], preview: string, public readonly threadAdapter: ThreadAdapter ) { this.variablesProviderId = threadAdapter.debugSession.variablesProviders.register(this); this.argsAdapter = VariableAdapter.fromArgumentList(args, preview, threadAdapter); } public getVariables(): Promise { return Promise.resolve(this.argsAdapter ? [this.argsAdapter] : []); } } export class ArgumentListAdapter implements VariablesProvider { public readonly variablesProviderId: number; public readonly referenceExpression = undefined; public readonly referenceFrame = undefined; public constructor( private readonly args: VariableAdapter[], public readonly threadAdapter: ThreadAdapter ) { this.variablesProviderId = threadAdapter.debugSession.variablesProviders.register(this); } public getVariables(): Promise { return Promise.resolve(this.args); } } ================================================ FILE: src/adapter/adapter/dataBreakpointsManager.ts ================================================ import { Log } from '../util/log'; import { VariablesProvider } from './variablesProvider'; import { Registry } from './registry'; import { DebugProtocol } from '@vscode/debugprotocol'; import { ObjectGripAdapter } from './objectGrip'; const log = Log.create('DataBreakpointsManager'); export class DataBreakpointsManager { private dataBreakpoints = new Set(); constructor( private readonly variablesProviders: Registry ) {} public static encodeDataId(variablesProviderId: number, property: string): string { return `${variablesProviderId}.${property}`; } public static decodeDataId(dataId: string): { variablesProviderId: number, property: string } { const separatorIndex = dataId.indexOf('.'); return { variablesProviderId: +dataId.substring(0, separatorIndex), property: dataId.substring(separatorIndex + 1) }; } public async setDataBreakpoints(newDataBreakpoints: DebugProtocol.DataBreakpoint[]): Promise { log.debug(`Setting ${newDataBreakpoints.length} data breakpoints`); const oldDataBreakpoints = new Set(this.dataBreakpoints); for (const dataBreakpoint of newDataBreakpoints) { if (!oldDataBreakpoints.has(dataBreakpoint.dataId)) { const type = (dataBreakpoint.accessType === 'read') ? 'get' : 'set'; await this.addDataBreakpoint(dataBreakpoint.dataId, type); } else { oldDataBreakpoints.delete(dataBreakpoint.dataId); } } for (const dataBreakpoint of oldDataBreakpoints) { await this.removeDataBreakpoint(dataBreakpoint); } this.dataBreakpoints = new Set(newDataBreakpoints.map(dataBreakpoint => dataBreakpoint.dataId)); } private async addDataBreakpoint(dataId: string, type: 'get' | 'set'): Promise { const { variablesProviderId, property } = DataBreakpointsManager.decodeDataId(dataId); const variablesProvider = this.variablesProviders.find(variablesProviderId); if (variablesProvider instanceof ObjectGripAdapter) { log.debug(`Adding data breakpoint for property ${property} of object #${variablesProviderId}`); await variablesProvider.actor.addWatchpoint(property, dataId, type); } else { log.warn(`Couldn't find object #${variablesProviderId}`); } } private async removeDataBreakpoint(dataId: string): Promise { const { variablesProviderId, property } = DataBreakpointsManager.decodeDataId(dataId); const variablesProvider = this.variablesProviders.find(variablesProviderId); if (variablesProvider instanceof ObjectGripAdapter) { log.debug(`Removing data breakpoint for property ${property} of object #${variablesProviderId}`); await variablesProvider.actor.removeWatchpoint(property); } else { log.warn(`Couldn't find object #${variablesProviderId}`); } } } ================================================ FILE: src/adapter/adapter/descriptor.ts ================================================ import { Registry } from './registry'; import { DescriptorActorProxy } from '../firefox/actorProxy/descriptor'; import { BreakpointListActorProxy } from '../firefox/actorProxy/breakpointList'; import { WatcherActorProxy } from '../firefox/actorProxy/watcher'; import { ThreadConfigurationActorProxy } from '../firefox/actorProxy/threadConfiguration'; import { ThreadAdapter } from './thread'; export class DescriptorAdapter { public readonly id: number; private readonly configuratorId: number; private readonly breakpointListId: number; public readonly threads = new Set(); public constructor( private readonly descriptorRegistry: Registry, private readonly configurators: Registry, private readonly breakpointLists: Registry, public readonly descriptorActor: DescriptorActorProxy, public readonly watcherActor: WatcherActorProxy, private readonly configurator: ThreadConfigurationActorProxy, private readonly breakpointList: BreakpointListActorProxy ) { this.id = descriptorRegistry.register(this); this.configuratorId = configurators.register(configurator); this.breakpointListId = breakpointLists.register(breakpointList); } public dispose() { for (const thread of this.threads) { thread.dispose(); } this.descriptorRegistry.unregister(this.id); this.configurators.unregister(this.configuratorId); this.breakpointLists.unregister(this.breakpointListId); this.descriptorActor.dispose(); this.configurator.dispose(); this.breakpointList.dispose(); this.watcherActor.dispose(); } } ================================================ FILE: src/adapter/adapter/environment.ts ================================================ import { Log } from '../util/log'; import { ScopeAdapter, ObjectScopeAdapter, LocalVariablesScopeAdapter, FunctionScopeAdapter } from './scope'; import { FrameAdapter } from './frame'; let log = Log.create('EnvironmentAdapter'); /** * Abstract adapter base class for a lexical environment. * Used to create [`ScopeAdapter`](./scope.ts)s which then create `Scope` objects for VS Code. */ export abstract class EnvironmentAdapter { protected environment: T; protected parent?: EnvironmentAdapter; public constructor(environment: T) { this.environment = environment; if (environment.parent !== undefined) { this.parent = EnvironmentAdapter.from(environment.parent); } } /** factory function for creating an EnvironmentAdapter of the appropriate type */ public static from(environment: FirefoxDebugProtocol.Environment): EnvironmentAdapter { switch (environment.type) { case 'object': return new ObjectEnvironmentAdapter(environment); case 'function': return new FunctionEnvironmentAdapter(environment); case 'with': return new WithEnvironmentAdapter(environment); case 'block': return new BlockEnvironmentAdapter(environment); default: throw new Error(`Unknown environment type ${environment.type}`); } } public getScopeAdapters(frameAdapter: FrameAdapter): ScopeAdapter[] { let scopes = this.getAllScopeAdapters(frameAdapter); return scopes; } protected getAllScopeAdapters(frameAdapter: FrameAdapter): ScopeAdapter[] { let scopes: ScopeAdapter[]; if (this.parent !== undefined) { scopes = this.parent.getAllScopeAdapters(frameAdapter); } else { scopes = []; } let ownScope = this.getOwnScopeAdapter(frameAdapter); scopes.unshift(ownScope); return scopes; } protected abstract getOwnScopeAdapter(frameAdapter: FrameAdapter): ScopeAdapter; } export class ObjectEnvironmentAdapter extends EnvironmentAdapter { public constructor(environment: FirefoxDebugProtocol.ObjectEnvironment) { super(environment); } protected getOwnScopeAdapter(frameAdapter: FrameAdapter): ScopeAdapter { let grip = this.environment.object; if ((typeof grip === 'boolean') || (typeof grip === 'number') || (typeof grip === 'string')) { throw new Error(`Object environment with unexpected grip of type ${typeof grip}`); } else if (grip.type !== 'object') { throw new Error(`Object environment with unexpected grip of type ${grip.type}`); } else { let objectGrip = grip; let name = `Object: ${objectGrip.class}`; return new ObjectScopeAdapter(name, objectGrip, frameAdapter); } } } export class FunctionEnvironmentAdapter extends EnvironmentAdapter { public constructor(environment: FirefoxDebugProtocol.FunctionEnvironment) { super(environment); } protected getOwnScopeAdapter(frameAdapter: FrameAdapter): ScopeAdapter { let funcName = this.environment.function.displayName; let scopeName: string; if (funcName) { scopeName = `Local: ${funcName}`; } else { log.error(`Unexpected function in function environment: ${JSON.stringify(this.environment.function)}`); scopeName = '[unknown]'; } return new FunctionScopeAdapter(scopeName, this.environment.bindings, frameAdapter); } } export class WithEnvironmentAdapter extends EnvironmentAdapter { public constructor(environment: FirefoxDebugProtocol.WithEnvironment) { super(environment); } protected getOwnScopeAdapter(frameAdapter: FrameAdapter): ScopeAdapter { let grip = this.environment.object; if ((typeof grip === 'boolean') || (typeof grip === 'number') || (typeof grip === 'string')) { throw new Error(`"with" environment with unexpected grip of type ${typeof grip}`); } else if (grip.type !== 'object') { throw new Error(`"with" environment with unexpected grip of type ${grip.type}`); } else { let objectGrip = grip; let name = `With: ${objectGrip.class}`; return new ObjectScopeAdapter(name, objectGrip, frameAdapter); } } } export class BlockEnvironmentAdapter extends EnvironmentAdapter { public constructor(environment: FirefoxDebugProtocol.BlockEnvironment) { super(environment); } protected getOwnScopeAdapter(frameAdapter: FrameAdapter): ScopeAdapter { return new LocalVariablesScopeAdapter('Block', this.environment.bindings.variables, frameAdapter); } } ================================================ FILE: src/adapter/adapter/eventBreakpointsManager.ts ================================================ import { AvailableEventCategory } from '../../common/customEvents'; import { FirefoxDebugSession } from '../firefoxDebugSession'; import { compareStrings } from '../util/misc'; export class EventBreakpointsManager { public readonly availableEvents: AvailableEventCategory[] = []; constructor( private readonly session: FirefoxDebugSession ) { session.threads.onRegistered(async threadAdapter => { const newAvailableEvents = await threadAdapter.actor.getAvailableEventBreakpoints(); let categoryWasAdded = false; for (const newCategory of newAvailableEvents) { const category = this.availableEvents.find(category => category.name === newCategory.name); if (!category) { this.availableEvents.push({ name: newCategory.name, events: newCategory.events.map(newEvent => ({ name: newEvent.name, id: newEvent.id, })), }); categoryWasAdded = true; continue; } let eventWasAdded = false; for (const newEvent of newCategory.events) { if (!category.events.find(event => event.id === newEvent.id)) { category.events.push({ name: newEvent.name, id: newEvent.id, }); eventWasAdded = true; } } if (eventWasAdded) { category.events.sort((e1, e2) => compareStrings(e1.name, e2.name)) } } if (categoryWasAdded) { this.availableEvents.sort((c1, c2) => compareStrings(c1.name, c2.name)); } this.session.sendCustomEvent('availableEvents', this.availableEvents); }); } public async setActiveEventBreakpoints(ids: string[]) { await Promise.all(this.session.breakpointLists.map(breakpointList => breakpointList.setActiveEventBreakpoints(ids) )); } } ================================================ FILE: src/adapter/adapter/frame.ts ================================================ import { Log } from '../util/log'; import { ThreadAdapter } from './thread'; import { EnvironmentAdapter } from './environment'; import { ScopeAdapter } from './scope'; import { StackFrame } from '@vscode/debugadapter'; import { Registry } from './registry'; import { FrameActorProxy } from '../firefox/actorProxy/frame'; let log = Log.create('FrameAdapter'); /** * Adapter class for a stackframe. */ export class FrameAdapter { public readonly id: number; private _scopeAdapters?: ScopeAdapter[]; public constructor( private readonly frameRegistry: Registry, public readonly frame: FirefoxDebugProtocol.Frame, public readonly threadAdapter: ThreadAdapter ) { this.id = frameRegistry.register(this); } public async getStackframe(): Promise { let sourceActorName = this.frame.where.actor; let sourceAdapter = await this.threadAdapter.debugSession.sources.getAdapterForActor(sourceActorName); let name: string; switch (this.frame.type) { case 'call': const callFrame = this.frame as FirefoxDebugProtocol.CallFrame; name = callFrame.displayName || '[anonymous function]'; break; case 'global': name = '[Global]'; break; case 'eval': case 'clientEvaluate': name = '[eval]'; break; case 'wasmcall': name = '[wasm]'; break; default: name = `[${this.frame.type}]`; log.error(`Unexpected frame type ${this.frame.type}`); break; } return new StackFrame(this.id, name, sourceAdapter.source, this.frame.where.line, (this.frame.where.column || 0) + 1); } public async getScopeAdapters(): Promise { if (!this._scopeAdapters) { const frameActor = new FrameActorProxy(this.frame.actor, this.threadAdapter.debugSession.firefoxDebugConnection); const environment = await frameActor.getEnvironment(); frameActor.dispose(); if (environment.type) { const environmentAdapter = EnvironmentAdapter.from(environment); this._scopeAdapters = environmentAdapter.getScopeAdapters(this); if (this.frame.this !== undefined) { this._scopeAdapters[0].addThis(this.frame.this); } } else { this._scopeAdapters = []; } } return this._scopeAdapters; } public dispose(): void { this.frameRegistry.unregister(this.id); } } ================================================ FILE: src/adapter/adapter/getterValue.ts ================================================ import { VariablesProvider } from './variablesProvider'; import { ThreadAdapter } from './thread'; import { FrameAdapter } from './frame'; import { VariableAdapter } from './variable'; /** * Adapter class for an accessor property with a getter (i.e. a property defined using * [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) * with an accessor descriptor containing a `get` function or using the new ES6 syntax for defining * accessors). * The value of such a property can only be determined by executing the getter function, but that may * have side-effects. Therefore it is not executed and the corresponding [`VariableAdapter`](./variable.ts) * will show a text like "Getter & Setter - expand to execute Getter" to the user. When the user * clicks on this text, the getter will be executed by the `getVariables()` method and the value * will be displayed. * * Note that if the accessor property is not defined on the object itself but on one of its * prototypes, the user would have to navigate to the prototype to find it and if he then executed * the getter, it would be executed with `this` set to the prototype, which is usually not the * desired behavior. Therefore it is possible to "lift" accessor properties to an object from its * prototype chain using the `liftAccessorsFromPrototypes` configuration property. */ export class GetterValueAdapter implements VariablesProvider { public readonly variablesProviderId: number; public get threadAdapter(): ThreadAdapter { return this.variableAdapter.threadAdapter; } /** a javascript expression that will execute the getter */ public get referenceExpression(): string | undefined { return this.variableAdapter.referenceExpression; } /** the stackframe to use when executing the `referenceExpression` */ public get referenceFrame(): FrameAdapter | undefined { return this.variableAdapter.referenceFrame; } public constructor(private readonly variableAdapter: VariableAdapter) { this.variablesProviderId = this.threadAdapter.debugSession.variablesProviders.register(this); } /** execute the getter and return a VariableAdapter for the value returned by the getter */ public async getVariables(): Promise { if (this.referenceExpression && this.referenceFrame) { const grip = await this.threadAdapter.evaluateRaw( this.referenceExpression, true, this.referenceFrame.frame.actor ); const variableAdapter = VariableAdapter.fromGrip( 'Value from Getter', this.referenceExpression, this.referenceFrame, grip, false, this.threadAdapter, true ); return [ variableAdapter ]; } else { return []; } } } ================================================ FILE: src/adapter/adapter/objectGrip.ts ================================================ import { VariablesProvider } from './variablesProvider'; import { VariableAdapter } from './variable'; import { FrameAdapter } from './frame'; import { ThreadAdapter } from './thread'; import { ObjectGripActorProxy } from '../firefox/actorProxy/objectGrip'; /** * Adapter class for a javascript object. */ export class ObjectGripAdapter implements VariablesProvider { public readonly variablesProviderId: number; public readonly actor: ObjectGripActorProxy; public get threadAdapter(): ThreadAdapter { return this.variableAdapter.threadAdapter; } /** a javascript expression for accessing the object represented by this adapter */ public get referenceExpression(): string | undefined { return this.variableAdapter.referenceExpression; } /** the stackframe to use when executing the `referenceExpression` */ public get referenceFrame(): FrameAdapter | undefined { return this.variableAdapter.referenceFrame; } public constructor( private readonly variableAdapter: VariableAdapter, objectGrip: FirefoxDebugProtocol.ObjectGrip, public threadLifetime: boolean, private readonly isPrototype: boolean ) { this.actor = this.threadAdapter.debugSession.getOrCreateObjectGripActorProxy(objectGrip); this.actor.increaseRefCount(); this.variablesProviderId = this.threadAdapter.debugSession.variablesProviders.register(this); this.threadAdapter.registerObjectGripAdapter(this); } /** * get the referenced object's properties and its prototype as an array of Variables. * This method can only be called when the thread is paused. */ public async getVariables(): Promise { let prototypeAndProperties = await this.actor.fetchPrototypeAndProperties(); let variables: VariableAdapter[] = []; let symbolVariables: VariableAdapter[] = []; let safeGetterValues = prototypeAndProperties.safeGetterValues || {}; let symbolProperties = prototypeAndProperties.ownSymbols || []; for (let varname in prototypeAndProperties.ownProperties) { if (!safeGetterValues[varname]) { variables.push(VariableAdapter.fromPropertyDescriptor( varname, this.referenceExpression, this.referenceFrame, prototypeAndProperties.ownProperties[varname], this.threadLifetime, this.threadAdapter)); } } for (let varname in safeGetterValues) { variables.push(VariableAdapter.fromSafeGetterValueDescriptor( varname, this.referenceExpression, this.referenceFrame, safeGetterValues[varname], this.threadLifetime, this.threadAdapter)); } for (let symbolProperty of symbolProperties) { symbolVariables.push(VariableAdapter.fromPropertyDescriptor( symbolProperty.name, undefined, undefined, symbolProperty.descriptor, this.threadLifetime, this.threadAdapter)); } let prototypeVariable: VariableAdapter | undefined = undefined; let accessorsFromPrototypes: VariableAdapter[] = []; if (prototypeAndProperties.prototype.type !== 'null') { prototypeVariable = VariableAdapter.fromGrip( '__proto__', this.referenceExpression, this.referenceFrame, prototypeAndProperties.prototype, this.threadLifetime, this.threadAdapter ); if (!this.isPrototype) { const prototypeLevels = this.threadAdapter.debugSession.config.liftAccessorsFromPrototypes; if (prototypeLevels > 0) { accessorsFromPrototypes = await this.fetchAccessorsFromPrototypes(prototypeVariable, prototypeLevels); } } } /** Array-Objects are already sorted, sorting them again as strings messes up the order */ let isArray = (prototypeAndProperties.prototype.type == 'object' && prototypeAndProperties.prototype.class == 'Array'); if (!isArray) { VariableAdapter.sortVariables(variables); } VariableAdapter.sortVariables(symbolVariables); VariableAdapter.sortVariables(accessorsFromPrototypes); variables.push(...symbolVariables); variables.push(...accessorsFromPrototypes); if (prototypeVariable) { variables.push(prototypeVariable); } return variables; } /** * used to "lift" accessor properties from the prototype chain to an object if the * `liftAccessorsFromPrototypes` configuration property is set. * Have a look at the [`GetterValueAdapter`](./getterValue.ts) for more info. */ private async fetchAccessorsFromPrototypes( prototypeVariable: VariableAdapter, levels: number ): Promise { let objectGripAdapter: ObjectGripAdapter | undefined = prototypeVariable.variablesProvider; let variables: VariableAdapter[] = []; let level = 0; while ((level < levels) && objectGripAdapter) { let prototypeAndProperties = await objectGripAdapter.actor.fetchPrototypeAndProperties(); for (const varname in prototypeAndProperties.ownProperties) { const propertyDescriptor = prototypeAndProperties.ownProperties[varname]; if ((varname !== '__proto__') && (propertyDescriptor).get) { variables.push(VariableAdapter.fromPropertyDescriptor( varname, this.referenceExpression, this.referenceFrame, propertyDescriptor, this.threadLifetime, this.threadAdapter )); } } prototypeVariable = VariableAdapter.fromGrip( '__proto__', this.referenceExpression, this.referenceFrame, prototypeAndProperties.prototype, this.threadLifetime, this.threadAdapter ); objectGripAdapter = prototypeVariable.variablesProvider; level++; } return variables; } public dispose(): void { this.actor.decreaseRefCount(); this.threadAdapter.debugSession.variablesProviders.unregister(this.variablesProviderId); } } ================================================ FILE: src/adapter/adapter/preview.ts ================================================ import { Log } from '../util/log'; const log = Log.create('Preview'); const maxProperties = 5; const maxArrayItems = 5; const maxStringChars = 20; const maxAttributes = 5; const maxParameters = 5; /** generates the preview string for an object shown in the debugger's Variables view */ export function renderPreview(objectGrip: FirefoxDebugProtocol.ObjectGrip): string { try { if ((objectGrip.class === 'Function') || (objectGrip as FirefoxDebugProtocol.FunctionGrip).parameterNames) { if (objectGrip.class !== 'Function') { log.warn(`Looks like a FunctionGrip but has a different class: ${JSON.stringify(objectGrip)}`); } return renderFunctionGrip(objectGrip); } const preview = objectGrip.preview; if (!preview) { return objectGrip.class; } if (preview.kind === 'Object') { return renderObjectPreview(preview, objectGrip.class); } else if (preview.kind === 'ArrayLike') { return renderArrayLikePreview(preview, objectGrip.class); } else if ((objectGrip.class === 'Date') && (preview.kind === undefined)) { const date = new Date(preview.timestamp); return date.toString(); } else if (preview.kind === 'ObjectWithURL') { return `${objectGrip.class} ${preview.url}`; } else if ((preview.kind === 'DOMNode') && (preview.nodeType === 1)) { return renderDOMElementPreview(preview); } else if (preview.kind === 'Error') { return `${objectGrip.class}: ${preview.message}`; } else { return objectGrip.class; } } catch (e) { log.error(`renderPreview failed for ${JSON.stringify(objectGrip)}: ${e}`); return ''; } } function renderObjectPreview(preview: FirefoxDebugProtocol.ObjectPreview, className: string): string { const renderedProperties: string[] = []; let i = 0; for (const property in preview.ownProperties) { var valueGrip = preview.ownProperties[property].value; if (!valueGrip) { continue; } const renderedValue = renderGrip(valueGrip); renderedProperties.push(`${property}: ${renderedValue}`); if (++i >= maxProperties) { renderedProperties.push('\u2026'); break; } } if (i < maxProperties && preview.ownSymbols) { for (const symbolProperty of preview.ownSymbols) { const renderedValue = renderGrip(symbolProperty.descriptor.value); renderedProperties.push(`Symbol(${symbolProperty.name}): ${renderedValue}`); if (++i >= maxProperties) { renderedProperties.push('\u2026'); break; } } } const renderedObject = `{${renderedProperties.join(', ')}}`; if (className === 'Object') { return renderedObject; } else { return `${className} ${renderedObject}`; } } function renderDOMElementPreview(preview: FirefoxDebugProtocol.DOMNodePreview): string { if (!preview.attributes) { return `<${preview.nodeName}>`; } const renderedAttributes: string[] = []; let i = 0; for (const attribute in preview.attributes) { const renderedValue = renderGrip(preview.attributes[attribute]); renderedAttributes.push(`${attribute}=${renderedValue}`); if (++i >= maxAttributes) { renderedAttributes.push('\u2026'); break; } } if (renderedAttributes.length === 0) { return `<${preview.nodeName}>`; } else { return `<${preview.nodeName} ${renderedAttributes.join(' ')}>`; } } function renderArrayLikePreview(preview: FirefoxDebugProtocol.ArrayLikePreview, className: string): string { let result = `${className}(${preview.length})`; if (preview.items && preview.items.length > 0) { const renderCount = Math.min(preview.items.length, maxArrayItems); const itemsToRender = preview.items.slice(0, renderCount); const renderedItems = itemsToRender.map(item => renderGripOrNull(item)); if (renderCount < preview.items.length) { renderedItems.push('\u2026'); } result += ` [${renderedItems.join(', ')}]`; } return result; } function renderFunctionGrip(functionGrip: FirefoxDebugProtocol.FunctionGrip): string { let parameters = ''; if (functionGrip.parameterNames && functionGrip.parameterNames.every(parameterName => typeof parameterName === 'string')) { let parameterNames = functionGrip.parameterNames; if (parameterNames.length > maxParameters) { parameterNames = parameterNames.slice(0, maxParameters); parameterNames.push('\u2026'); } parameters = parameterNames.join(', '); } const functionName = functionGrip.displayName || functionGrip.name || 'function'; return `${functionName}(${parameters}) {\u2026}`; } function renderGripOrNull(gripOrNull: FirefoxDebugProtocol.Grip | null): string { if (gripOrNull === null) { return "_"; } else { return renderGrip(gripOrNull); } } export function renderGrip(grip: FirefoxDebugProtocol.Grip): string { if ((typeof grip === 'boolean') || (typeof grip === 'number')) { return grip.toString(); } else if (typeof grip === 'string') { if (grip.length > maxStringChars) { return `"${grip.substr(0, maxStringChars)}\u2026"`; } else { return `"${grip}"`; } } else { switch (grip.type) { case 'null': case 'undefined': case 'Infinity': case '-Infinity': case 'NaN': case '-0': return grip.type; case 'BigInt': return `${(grip).text}n`; case 'longString': const initial = (grip).initial; if (initial.length > maxStringChars) { return `${initial.substr(0, maxStringChars)}\u2026`; } else { return initial; } case 'symbol': let symbolName = (grip).name; return `Symbol(${symbolName})`; case 'object': let objectGrip = grip; return renderPreview(objectGrip); default: log.warn(`Unexpected object grip of type ${grip.type}: ${JSON.stringify(grip)}`); return ''; } } } ================================================ FILE: src/adapter/adapter/registry.ts ================================================ import { EventEmitter } from 'events'; /** * A generic collection of objects identified by a numerical ID. * The ID is generated and returned when an object is added to the collection using `register()`. */ export class Registry extends EventEmitter implements Iterable<[number, T]> { private objectsById = new Map(); private nextId = 1; /** * add an object to the registry and return the ID generated for it */ public register(obj: T): number { let id = this.nextId++; this.objectsById.set(id, obj); this.emit('registered', obj); return id; } public unregister(id: number): boolean { return this.objectsById.delete(id); } public has(id: number): boolean { return this.objectsById.has(id); } public find(id: number): T | undefined { return this.objectsById.get(id); } public get count() { return this.objectsById.size; } public [Symbol.iterator](): Iterator<[number, T]> { return this.objectsById[Symbol.iterator](); } public map(f: (obj: T) => S): S[] { let result: S[] = []; for (let [, obj] of this.objectsById) { result.push(f(obj)); } return result; } public filter(f: (obj: T) => boolean): T[] { let result: T[] = []; for (let [, obj] of this.objectsById) { if (f(obj)) { result.push(obj); } } return result; } public onRegistered(cb: (obj: T) => void) { this.on('registered', cb); } } ================================================ FILE: src/adapter/adapter/scope.ts ================================================ import { ThreadAdapter } from './thread'; import { FrameAdapter } from './frame'; import { VariableAdapter } from './variable'; import { Scope } from '@vscode/debugadapter'; import { VariablesProvider } from './variablesProvider'; /** * Abstract adapter base class for a javascript scope. */ export abstract class ScopeAdapter implements VariablesProvider { public readonly variablesProviderId: number; public readonly referenceExpression = ''; public get threadAdapter(): ThreadAdapter { return this.referenceFrame.threadAdapter; } public thisVariable?: VariableAdapter; public returnVariable?: VariableAdapter; protected constructor( public readonly name: string, public readonly referenceFrame: FrameAdapter ) { this.threadAdapter.registerScopeAdapter(this); this.variablesProviderId = this.threadAdapter.debugSession.variablesProviders.register(this); } public static fromGrip(name: string, grip: FirefoxDebugProtocol.Grip, referenceFrame: FrameAdapter): ScopeAdapter { if ((typeof grip === 'object') && (grip.type === 'object')) { return new ObjectScopeAdapter(name, grip, referenceFrame); } else { return new SingleValueScopeAdapter(name, grip, referenceFrame); } } public addThis(thisValue: FirefoxDebugProtocol.Grip) { this.thisVariable = VariableAdapter.fromGrip( 'this', this.referenceExpression, this.referenceFrame, thisValue, false, this.threadAdapter); } public addReturnValue(returnValue: FirefoxDebugProtocol.Grip) { this.returnVariable = VariableAdapter.fromGrip( 'Return value', undefined, this.referenceFrame, returnValue, false, this.threadAdapter); } public getScope(): Scope { return new Scope(this.name, this.variablesProviderId); } public async getVariables(): Promise { // we make a (shallow) copy of the variables array because we're going to modify it let variables = [ ...await this.getVariablesInt() ]; if (this.thisVariable) { variables.unshift(this.thisVariable); } if (this.returnVariable) { variables.unshift(this.returnVariable); } return variables; } protected abstract getVariablesInt(): Promise; public dispose(): void { this.threadAdapter.debugSession.variablesProviders.unregister(this.variablesProviderId); } } export class SingleValueScopeAdapter extends ScopeAdapter { private variableAdapter: VariableAdapter; public constructor(name: string, grip: FirefoxDebugProtocol.Grip, referenceFrame: FrameAdapter) { super(name, referenceFrame); this.variableAdapter = VariableAdapter.fromGrip( '', this.referenceExpression, this.referenceFrame, grip, false, this.threadAdapter); } protected getVariablesInt(): Promise { return Promise.resolve([this.variableAdapter]); } } export class ObjectScopeAdapter extends ScopeAdapter { private variableAdapter: VariableAdapter; public constructor(name: string, object: FirefoxDebugProtocol.ObjectGrip, referenceFrame: FrameAdapter) { super(name, referenceFrame); this.variableAdapter = VariableAdapter.fromGrip( '', this.referenceExpression, this.referenceFrame, object, false, this.threadAdapter); } protected getVariablesInt(): Promise { return this.variableAdapter.variablesProvider!.getVariables(); } } export class LocalVariablesScopeAdapter extends ScopeAdapter { public variables: VariableAdapter[] = []; public constructor(name: string, variableDescriptors: FirefoxDebugProtocol.PropertyDescriptors, referenceFrame: FrameAdapter) { super(name, referenceFrame); for (let varname in variableDescriptors) { this.variables.push(VariableAdapter.fromPropertyDescriptor( varname, this.referenceExpression, this.referenceFrame, variableDescriptors[varname], false, this.threadAdapter)); } VariableAdapter.sortVariables(this.variables); } protected getVariablesInt(): Promise { return Promise.resolve(this.variables); } } export class FunctionScopeAdapter extends ScopeAdapter { public variables: VariableAdapter[] = []; public constructor(name: string, bindings: FirefoxDebugProtocol.FunctionBindings, referenceFrame: FrameAdapter) { super(name, referenceFrame); bindings.arguments.forEach((arg) => { for (let varname in arg) { this.variables.push(VariableAdapter.fromPropertyDescriptor( varname, this.referenceExpression, this.referenceFrame, arg[varname], false, this.threadAdapter)); } }); for (let varname in bindings.variables) { this.variables.push(VariableAdapter.fromPropertyDescriptor( varname, this.referenceExpression, this.referenceFrame, bindings.variables[varname], false, this.threadAdapter)); } VariableAdapter.sortVariables(this.variables); } protected getVariablesInt(): Promise { return Promise.resolve(this.variables); } } ================================================ FILE: src/adapter/adapter/skipFilesManager.ts ================================================ import isAbsoluteUrl from 'is-absolute-url'; import { Log } from '../util/log'; import { isWindowsPlatform as detectWindowsPlatform } from '../../common/util'; import { SourcesManager } from './sourcesManager'; import { Registry } from './registry'; import { ThreadAdapter } from './thread'; let log = Log.create('SkipFilesManager'); /** * This class determines which files should be skipped (aka blackboxed). Files to be skipped are * configured using the `skipFiles` configuration property or by using the context menu on a * stackframe in VS Code. */ export class SkipFilesManager { private readonly isWindowsPlatform = detectWindowsPlatform(); /** * Files that were configured to (not) be skipped by using the context menu on a * stackframe in VS Code. This overrides the `skipFiles` configuration property. */ private readonly dynamicFiles = new Map(); public constructor( private readonly configuredFilesToSkip: RegExp[], private readonly sources: SourcesManager, private readonly threads: Registry ) {} public shouldSkip(pathOrUrl: string): boolean { if (this.dynamicFiles.has(pathOrUrl)) { let result = this.dynamicFiles.get(pathOrUrl)!; if (log.isDebugEnabled()) { log.debug(`skipFile is set dynamically to ${result} for ${pathOrUrl}`); } return result; } let testee = pathOrUrl.replace('/./', '/'); if (this.isWindowsPlatform && !isAbsoluteUrl(pathOrUrl)) { testee = testee.replace(/\\/g, '/'); } for (let regExp of this.configuredFilesToSkip) { if (regExp.test(testee)) { if (log.isDebugEnabled()) { log.debug(`skipFile is set per configuration to true for ${pathOrUrl}`); } return true; } } if (log.isDebugEnabled()) { log.debug(`skipFile is not set for ${pathOrUrl}`); } return false; } public async toggleSkipping(pathOrUrl: string): Promise { const skipFile = !this.shouldSkip(pathOrUrl); this.dynamicFiles.set(pathOrUrl, skipFile); log.info(`Setting skipFile to ${skipFile} for ${pathOrUrl}`); let promises: Promise[] = []; const sourceAdapters = this.sources.findSourceAdaptersForPathOrUrl(pathOrUrl); for (const sourceAdapter of sourceAdapters) { promises.push(sourceAdapter.setBlackBoxed(skipFile)); } for (const [, thread] of this.threads) { thread.triggerStackframeRefresh(); } await Promise.all(promises); } } ================================================ FILE: src/adapter/adapter/source.ts ================================================ import { Log } from '../util/log'; import { MappedLocation } from '../location'; import { DebugProtocol } from '@vscode/debugprotocol'; import { Source } from '@vscode/debugadapter'; import { ISourceActorProxy } from '../firefox/actorProxy/source'; import { SourceMappingSourceActorProxy } from '../firefox/sourceMaps/source'; import { Registry } from './registry'; const log = Log.create('SourceAdapter'); /** * Adapter class for a javascript source. */ export class SourceAdapter { public readonly url: string | undefined; public readonly generatedUrl: string | undefined; public readonly introductionType: 'scriptElement' | 'eval' | 'Function' | 'debugger eval' | 'wasm' | undefined; public readonly id: number; public readonly source: Source; public readonly actors: SourceActorCollection; private blackboxed = false; public get isBlackBoxed() { return this.blackboxed; } public constructor( actor: ISourceActorProxy, /** the path or url as seen by VS Code */ public readonly path: string | undefined, sourceRegistry: Registry ) { this.url = actor.url ?? undefined; if (actor instanceof SourceMappingSourceActorProxy) { this.generatedUrl = actor.underlyingActor.url ?? undefined; } this.introductionType = actor.source.introductionType ?? undefined; this.id = sourceRegistry.register(this); let sourceName = ''; if (actor.url) { sourceName = actor.url.split('/').pop()!.split('#')[0]; } else { sourceName = `${actor.source.introductionType || 'Script'} ${this.id}`; } if (path !== undefined) { this.source = new Source(sourceName, path); } else { this.source = new Source(sourceName, actor.url || undefined, this.id); } this.actors = new SourceActorCollection(actor); } public async setBlackBoxed(blackboxed: boolean) { if (this.blackboxed === blackboxed) { return; } this.blackboxed = blackboxed; (this.source).presentationHint = blackboxed ? 'deemphasize' : 'normal'; await this.actors.runWithAllActors(actor => actor.setBlackbox(blackboxed)); } public getBreakableLines(): Promise { return this.actors.runWithSomeActor(actor => actor.getBreakableLines()); } public getBreakableLocations(line: number): Promise { return this.actors.runWithSomeActor(actor => actor.getBreakableLocations(line)); } public fetchSource(): Promise { return this.actors.runWithSomeActor(actor => actor.fetchSource()); } public async findNextBreakableLocation( requestedLine: number, requestedColumn: number ): Promise { let breakableLocations = await this.getBreakableLocations(requestedLine); for (const location of breakableLocations) { if (location.column >= requestedColumn) { return location; } } const breakableLines = await this.getBreakableLines(); for (const line of breakableLines) { if (line > requestedLine) { breakableLocations = await this.getBreakableLocations(line); if (breakableLocations.length > 0) { return breakableLocations[0]; } } } return undefined; } } export class SourceActorCollection { private readonly actors: ISourceActorProxy[]; private someActor: Promise; private resolveSomeActor: ((actor: ISourceActorProxy) => void) | undefined; public constructor(actor: ISourceActorProxy) { this.actors = [actor]; this.someActor = Promise.resolve(actor); } public add(actor: ISourceActorProxy) { this.actors.push(actor); if (this.resolveSomeActor) { this.resolveSomeActor(actor); this.resolveSomeActor = undefined; } } public remove(actor: ISourceActorProxy) { const index = this.actors.indexOf(actor); if (index >= 0) { this.actors.splice(index, 1); if (this.actors.length > 0) { this.someActor = Promise.resolve(this.actors[0]); } else { this.someActor = new Promise(resolve => this.resolveSomeActor = resolve); } } } public async runWithSomeActor(fn: (actor: ISourceActorProxy) => Promise): Promise { while (true) { const actor = await this.someActor; try { return await fn(actor); } catch (err: any) { if (err.error === 'noSuchActor') { this.remove(actor); continue; } throw err; } } } public async runWithAllActors(fn: (actor: ISourceActorProxy) => Promise): Promise { await Promise.all(this.actors.map(actor => fn(actor))); } } ================================================ FILE: src/adapter/adapter/sourcesManager.ts ================================================ import { Log } from '../util/log'; import { ISourceActorProxy } from "../firefox/actorProxy/source"; import { DeferredMap } from "../../common/deferredMap"; import { pathsAreEqual } from "../util/misc"; import { PathMapper } from "../util/pathMapper"; import { Registry } from "./registry"; import { SourceAdapter } from './source'; import { normalizePath } from '../util/fs'; const log = Log.create('SourcesManager'); export class SourcesManager { private readonly adapters = new Registry(); private readonly adaptersByPath = new DeferredMap(); private readonly adaptersByActor = new DeferredMap(); private readonly adaptersByUrl = new DeferredMap(); constructor(private readonly pathMapper: PathMapper) {} public addActor(actor: ISourceActorProxy) { log.debug(`Adding source ${actor.name}`); let adapter: SourceAdapter | undefined = actor.url ? this.adaptersByUrl.getExisting(actor.url) : undefined; const path = this.pathMapper.convertFirefoxSourceToPath(actor.source); const normalizedPath = path ? normalizePath(path) : undefined; if (adapter) { adapter.actors.add(actor); } else { if (normalizedPath) { adapter = this.adaptersByPath.getExisting(normalizedPath); } if (adapter) { adapter.actors.add(actor); } else { adapter = new SourceAdapter(actor, path, this.adapters); } } if (normalizedPath) { this.adaptersByPath.set(normalizedPath, adapter); } this.adaptersByActor.set(actor.name, adapter); if (actor.url) { this.adaptersByUrl.set(actor.url, adapter); } return adapter; } public removeActor(actor: ISourceActorProxy) { log.info(`Removing source ${actor.name}`); const adapter = this.adaptersByActor.getExisting(actor.name); if (!adapter) return; this.adaptersByActor.delete(actor.name); adapter.actors.remove(actor); } public getAdapterForID(id: number) { return this.adapters.find(id); } public getAdapterForPath(path: string) { return this.adaptersByPath.get(path); } public getExistingAdapterForPath(path: string) { return this.adaptersByPath.getExisting(path); } public getAdapterForActor(actor: string) { return this.adaptersByActor.get(actor); } public getAdapterForUrl(url: string) { return this.adaptersByUrl.get(url); } public findSourceAdaptersForPathOrUrl(pathOrUrl: string): SourceAdapter[] { if (!pathOrUrl) return []; return this.adapters.filter((sourceAdapter) => pathsAreEqual(pathOrUrl, sourceAdapter.path) || (sourceAdapter.url === pathOrUrl) ); } public findSourceAdaptersForUrlWithoutQuery(url: string): SourceAdapter[] { return this.adapters.filter((sourceAdapter) => { let sourceUrl = sourceAdapter.url; if (!sourceUrl) return false; let queryStringIndex = sourceUrl.indexOf('?'); if (queryStringIndex >= 0) { sourceUrl = sourceUrl.substr(0, queryStringIndex); } return url === sourceUrl; }); } } ================================================ FILE: src/adapter/adapter/thread.ts ================================================ import { EventEmitter } from 'events'; import { IThreadActorProxy } from '../firefox/actorProxy/thread'; import { ConsoleActorProxy } from '../firefox/actorProxy/console'; import { FrameAdapter } from './frame'; import { ScopeAdapter } from './scope'; import { ObjectGripAdapter } from './objectGrip'; import { VariablesProvider } from './variablesProvider'; import { VariableAdapter } from './variable'; import { Variable } from '@vscode/debugadapter'; import { Log } from '../util/log'; import { FirefoxDebugSession } from '../firefoxDebugSession'; import { TargetActorProxy } from '../firefox/actorProxy/target'; import { ISourceActorProxy } from '../firefox/actorProxy/source'; let log = Log.create('ThreadAdapter'); export type TargetType = 'tab' | 'iframe' | 'worker' | 'backgroundScript' | 'contentScript'; /** * Adapter class for a thread */ export class ThreadAdapter extends EventEmitter { public id: number; public get actorName() { return this.actor.name; } public get url(): string | undefined { return this.targetActor.target.url; } /** * All `SourceAdapter`s for this thread. They will be disposed when this `ThreadAdapter` is disposed. */ public readonly sourceActors = new Set(); /** * When the thread is paused, this is set to a Promise that resolves to the `FrameAdapter`s for * the stacktrace for the current thread pause. At the end of the thread pause, these are disposed. */ private framesPromise: Promise | undefined = undefined; /** * All `ScopeAdapter`s that have been created for the current thread pause. They will be disposed * at the end of the thread pause. */ private scopes: ScopeAdapter[] = []; /** * All `ObjectGripAdapter`s that should be disposed at the end of the current thread pause */ private pauseLifetimeObjects: ObjectGripAdapter[] = []; /** * All `ObjectGripAdapter`s that should be disposed when this `ThreadAdapter` is disposed */ private threadLifetimeObjects: ObjectGripAdapter[] = []; public threadPausedReason?: FirefoxDebugProtocol.ThreadPausedReason; public constructor( public readonly type: TargetType, public readonly name: string, public readonly actor: IThreadActorProxy, public readonly targetActor: TargetActorProxy, private readonly consoleActor: ConsoleActorProxy, public readonly debugSession: FirefoxDebugSession ) { super(); this.id = debugSession.threads.register(this); } public registerScopeAdapter(scopeAdapter: ScopeAdapter) { this.scopes.push(scopeAdapter); } public registerObjectGripAdapter(objectGripAdapter: ObjectGripAdapter) { if (objectGripAdapter.threadLifetime) { this.threadLifetimeObjects.push(objectGripAdapter); } else { this.pauseLifetimeObjects.push(objectGripAdapter); } } /** * extend the given adapter's lifetime to threadLifetime (if it isn't already) */ public threadLifetime(objectGripAdapter: ObjectGripAdapter): void { if (!objectGripAdapter.threadLifetime) { const index = this.pauseLifetimeObjects.indexOf(objectGripAdapter); if (index >= 0) { this.pauseLifetimeObjects.splice(index, 1); } this.threadLifetimeObjects.push(objectGripAdapter); objectGripAdapter.threadLifetime = true; } } public interrupt(): Promise { return this.actor.interrupt(); } public resume(): Promise { return this.actor.resume(); } public stepOver(): Promise { return this.actor.resume('next'); } public stepIn(): Promise { return this.actor.resume('step'); } public stepOut(): Promise { return this.actor.resume('finish'); } public restartFrame(frameActor: string): Promise { return this.actor.resume('restart', frameActor); } public fetchAllStackFrames(): Promise { if (!this.framesPromise) { this.framesPromise = (async () => { let frames = await this.actor.fetchStackFrames(); let frameAdapters = frames.map((frame) => new FrameAdapter(this.debugSession.frames, frame, this)); let threadPausedReason = this.threadPausedReason; if ((threadPausedReason !== undefined) && (frameAdapters.length > 0)) { const scopeAdapters = await frameAdapters[0].getScopeAdapters(); if (threadPausedReason.frameFinished !== undefined) { if (threadPausedReason.frameFinished.return !== undefined) { scopeAdapters[0].addReturnValue( threadPausedReason.frameFinished.return); } else if (threadPausedReason.frameFinished.throw !== undefined) { scopeAdapters.unshift(ScopeAdapter.fromGrip( 'Exception', threadPausedReason.frameFinished.throw, frameAdapters[0])); } } else if (threadPausedReason.exception !== undefined) { scopeAdapters.unshift(ScopeAdapter.fromGrip( 'Exception', threadPausedReason.exception, frameAdapters[0])); } } return frameAdapters; })(); } return this.framesPromise; } public async fetchStackFrames(start: number, count: number): Promise<[FrameAdapter[], number]> { let frameAdapters = await this.fetchAllStackFrames(); let requestedFrames = (count > 0) ? frameAdapters.slice(start, start + count) : frameAdapters.slice(start); return [requestedFrames, frameAdapters.length]; } /** this will cause VS Code to reload the current stackframes from this adapter */ public triggerStackframeRefresh(): void { this.debugSession.sendStoppedEvent(this, this.threadPausedReason); } public async fetchVariables(variablesProvider: VariablesProvider): Promise { let variableAdapters = await variablesProvider.getVariables(); return variableAdapters.map((variableAdapter) => variableAdapter.getVariable()); } public evaluateRaw(expr: string, skipBreakpoints: boolean, frameActorName?: string): Promise { return this.consoleActor.evaluate(expr, skipBreakpoints, frameActorName); } public async evaluate(expr: string, skipBreakpoints: boolean, frameActorName?: string): Promise { let grip = await this.consoleActor.evaluate(expr, skipBreakpoints, frameActorName); let variableAdapter = this.variableFromGrip(grip, true); return variableAdapter.getVariable(); } public async autoComplete(text: string, column: number, frameActorName?: string): Promise { return await this.consoleActor.autoComplete(text, column, frameActorName); } private variableFromGrip(grip: FirefoxDebugProtocol.Grip | undefined, threadLifetime: boolean): VariableAdapter { if (grip !== undefined) { return VariableAdapter.fromGrip('', undefined, undefined, grip, threadLifetime, this); } else { return new VariableAdapter('', undefined, undefined, 'undefined', this); } } /** * Called by the `ThreadCoordinator` before resuming the thread */ public async disposePauseLifetimeAdapters(): Promise { if (this.framesPromise) { let frames = await this.framesPromise; frames.forEach((frameAdapter) => { frameAdapter.dispose(); }); this.framesPromise = undefined; } this.scopes.forEach((scopeAdapter) => { scopeAdapter.dispose(); }); this.scopes = []; this.pauseLifetimeObjects.forEach((objectGripAdapter) => { objectGripAdapter.dispose(); }); this.pauseLifetimeObjects = []; } public async dispose(): Promise { await this.disposePauseLifetimeAdapters(); this.threadLifetimeObjects.forEach((objectGripAdapter) => { objectGripAdapter.dispose(); }); for (const sourceActor of this.sourceActors) { this.debugSession.sources.removeActor(sourceActor); sourceActor.dispose(); } this.actor.dispose(); this.targetActor.dispose(); this.consoleActor.dispose(); this.debugSession.threads.unregister(this.id); } } ================================================ FILE: src/adapter/adapter/variable.ts ================================================ import { Log } from '../util/log'; import { ThreadAdapter } from './thread'; import { ObjectGripAdapter } from './objectGrip'; import { FrameAdapter } from './frame'; import { Variable } from '@vscode/debugadapter'; import { DebugProtocol } from '@vscode/debugprotocol'; import { accessorExpression, compareStrings } from '../util/misc'; import { renderPreview } from './preview'; import { VariablesProvider } from './variablesProvider'; import { GetterValueAdapter } from './getterValue'; import { ArgumentListAdapter } from './consoleAPICall'; let log = Log.create('VariableAdapter'); /** * Adapter class for anything that will be sent to VS Code as a Variable. * At the very least a Variable needs a name and a string representation of its value. * If the VariableAdapter represents anything that can have child variables, it also needs a * [`VariablesProvider`](./variablesProvider.ts) that will be used to fetch the child variables when * requested by VS Code. */ export class VariableAdapter { private _variablesProvider: VariablesProvider | undefined; public get variablesProvider(): VariablesProvider | undefined { return this._variablesProvider; } public constructor( public readonly varname: string, public readonly referenceExpression: string | undefined, public readonly referenceFrame: FrameAdapter | undefined, public readonly displayValue: string, public readonly threadAdapter: ThreadAdapter ) {} public getVariable(): Variable { let variable = new Variable(this.varname, this.displayValue, this.variablesProvider ? this.variablesProvider.variablesProviderId : undefined); (variable).evaluateName = this.referenceExpression; return variable; } /** * factory function for creating a VariableAdapter from an * [object grip](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#objects) */ public static fromGrip( varname: string, parentReferenceExpression: string | undefined, referenceFrame: FrameAdapter | undefined, grip: FirefoxDebugProtocol.Grip, threadLifetime: boolean, threadAdapter: ThreadAdapter, useParentReferenceExpression?: boolean ): VariableAdapter { let referenceExpression = useParentReferenceExpression ? parentReferenceExpression : accessorExpression(parentReferenceExpression, varname); if ((typeof grip === 'boolean') || (typeof grip === 'number')) { return new VariableAdapter(varname, referenceExpression, referenceFrame, grip.toString(), threadAdapter); } else if (typeof grip === 'string') { return new VariableAdapter(varname, referenceExpression, referenceFrame, `"${grip}"`, threadAdapter); } else { switch (grip.type) { case 'null': case 'undefined': case 'Infinity': case '-Infinity': case 'NaN': case '-0': return new VariableAdapter( varname, referenceExpression, referenceFrame, grip.type, threadAdapter); case 'BigInt': return new VariableAdapter( varname, referenceExpression, referenceFrame, `${(grip).text}n`, threadAdapter); case 'longString': return new VariableAdapter( varname, referenceExpression, referenceFrame, (grip).initial, threadAdapter); case 'symbol': let symbolName = (grip).name; return new VariableAdapter( varname, referenceExpression, referenceFrame, `Symbol(${symbolName})`, threadAdapter); case 'object': let objectGrip = grip; let displayValue = renderPreview(objectGrip); let variableAdapter = new VariableAdapter( varname, referenceExpression, referenceFrame, displayValue, threadAdapter); variableAdapter._variablesProvider = new ObjectGripAdapter( variableAdapter, objectGrip, threadLifetime, (varname === '__proto__')); return variableAdapter; default: log.warn(`Unexpected object grip of type ${grip.type}: ${JSON.stringify(grip)}`); return new VariableAdapter( varname, referenceExpression, referenceFrame, grip.type, threadAdapter); } } } /** * factory function for creating a VariableAdapter from a * [property descriptor](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#property-descriptors) */ public static fromPropertyDescriptor( varname: string, parentReferenceExpression: string | undefined, referenceFrame: FrameAdapter | undefined, propertyDescriptor: FirefoxDebugProtocol.PropertyDescriptor, threadLifetime: boolean, threadAdapter: ThreadAdapter ): VariableAdapter { if ((propertyDescriptor).value !== undefined) { return VariableAdapter.fromGrip( varname, parentReferenceExpression, referenceFrame, (propertyDescriptor).value, threadLifetime, threadAdapter); } else { let referenceExpression = accessorExpression(parentReferenceExpression, varname); let accessorPropertyDescriptor = propertyDescriptor; let hasGetter = VariableAdapter.isFunctionGrip(accessorPropertyDescriptor.get); let hasSetter = VariableAdapter.isFunctionGrip(accessorPropertyDescriptor.set); let displayValue: string; if (hasGetter) { displayValue = 'Getter'; if (hasSetter) { displayValue += ' & Setter'; } displayValue += ' - expand to execute Getter'; } else if (hasSetter) { displayValue = 'Setter'; } else { log.error(`${referenceExpression} is neither a data property nor does it have a getter or setter`); displayValue = 'Error'; } let variableAdapter = new VariableAdapter( varname, referenceExpression, referenceFrame, displayValue, threadAdapter); if (hasGetter) { variableAdapter._variablesProvider = new GetterValueAdapter(variableAdapter); } return variableAdapter; } } /** * factory function for creating a VariableAdapter from a * [safe getter value descriptor](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#property-descriptors) */ public static fromSafeGetterValueDescriptor( varname: string, parentReferenceExpression: string | undefined, referenceFrame: FrameAdapter | undefined, safeGetterValueDescriptor: FirefoxDebugProtocol.SafeGetterValueDescriptor, threadLifetime: boolean, threadAdapter: ThreadAdapter ): VariableAdapter { return VariableAdapter.fromGrip( varname, parentReferenceExpression, referenceFrame, safeGetterValueDescriptor.getterValue, threadLifetime, threadAdapter); } public static fromArgumentList( args: VariableAdapter[], preview: string, threadAdapter: ThreadAdapter ): VariableAdapter { const variableAdapter = new VariableAdapter('arguments', undefined, undefined, preview, threadAdapter); variableAdapter._variablesProvider = new ArgumentListAdapter(args, threadAdapter); return variableAdapter; } public static sortVariables(variables: VariableAdapter[]): void { variables.sort((var1, var2) => compareStrings(var1.varname, var2.varname)); } private static isFunctionGrip(grip: FirefoxDebugProtocol.Grip) { return ( (typeof grip === 'object') && (grip.type === 'object') && ((grip).class === 'Function') ); } } ================================================ FILE: src/adapter/adapter/variablesProvider.ts ================================================ import { ThreadAdapter } from './thread'; import { VariableAdapter } from './variable'; import { FrameAdapter } from './frame'; /** * This interface must be implemented by any adapter that can provide child variables. */ export interface VariablesProvider { readonly variablesProviderId: number; readonly threadAdapter: ThreadAdapter; readonly referenceFrame: FrameAdapter | undefined; readonly referenceExpression: string | undefined; getVariables(): Promise; } ================================================ FILE: src/adapter/configuration.ts ================================================ import * as os from 'os'; import * as path from 'path'; import * as uuid from 'uuid'; import isAbsoluteUrl from 'is-absolute-url'; import RegExpEscape from 'escape-string-regexp'; import { Log } from './util/log'; import { findAddonId, normalizePath } from './util/misc'; import { isExecutable } from './util/fs'; import { Minimatch } from 'minimatch'; import FirefoxProfile from 'firefox-profile'; import { isWindowsPlatform } from '../common/util'; import { LaunchConfiguration, AttachConfiguration, CommonConfiguration, ReloadConfiguration, DetailedReloadConfiguration, TabFilterConfiguration } from '../common/configuration'; import { urlDirname } from './util/net'; let log = Log.create('ParseConfiguration'); export interface NormalizedReloadConfiguration { watch: string[]; ignore: string[]; debounce: number; } export interface ParsedTabFilterConfiguration { include: RegExp[]; exclude: RegExp[]; } export interface ParsedConfiguration { attach?: ParsedAttachConfiguration; launch?: ParsedLaunchConfiguration; addon?: ParsedAddonConfiguration; pathMappings: PathMappings; pathMappingIndex: string; filesToSkip: RegExp[]; reloadOnChange?: NormalizedReloadConfiguration, tabFilter: ParsedTabFilterConfiguration, clearConsoleOnReload: boolean, showConsoleCallLocation: boolean; liftAccessorsFromPrototypes: number; suggestPathMappingWizard: boolean; terminate: boolean; enableCRAWorkaround: boolean; } export interface ParsedAttachConfiguration { host: string; port: number; url?: string; firefoxExecutable?: string; profileDir?: string; reloadTabs: boolean; } export interface FirefoxPreferences { [key: string]: boolean | number | string; } type PathMapping = { url: string | RegExp, path: string | null }; export type PathMappings = PathMapping[]; export interface ParsedLaunchConfiguration { firefoxExecutable: string; firefoxArgs: string[]; profileDir: string; srcProfileDir?: string; preferences: FirefoxPreferences; tmpDirs: string[]; port: number; timeout: number; detached: boolean; } export interface ParsedAddonConfiguration { path: string; id: string | undefined; popupAutohideButton: boolean; } /** * Reads the configuration that was provided by VS Code, checks that it's consistent, * adds default values and returns it in a form that is easier to work with */ export async function parseConfiguration( config: LaunchConfiguration | AttachConfiguration ): Promise { let attach: ParsedAttachConfiguration | undefined = undefined; let launch: ParsedLaunchConfiguration | undefined = undefined; let addon: ParsedAddonConfiguration | undefined = undefined; let port = config.port || 6000; let timeout = 5; let pathMappings: PathMappings = []; let url: string | undefined = undefined; if (config.request === 'launch') { let tmpDirs: string[] = []; if (config.reAttach) { attach = { host: 'localhost', port, reloadTabs: (config.reloadOnAttach !== false) }; } let firefoxExecutable = await findFirefoxExecutable(config.firefoxExecutable); let firefoxArgs: string[] = [ '-start-debugger-server', String(port), '-no-remote' ]; if (config.firefoxArgs) { firefoxArgs.push(...config.firefoxArgs); } let { profileDir, srcProfileDir } = await parseProfileConfiguration(config, tmpDirs); firefoxArgs.push('-profile', profileDir); let preferences = createFirefoxPreferences(config.preferences); if (config.file) { if (!path.isAbsolute(config.file)) { throw 'The "file" property in the launch configuration has to be an absolute path'; } let fileUrl = config.file; if (isWindowsPlatform()) { fileUrl = 'file:///' + fileUrl.replace(/\\/g, '/'); } else { fileUrl = 'file://' + fileUrl; } firefoxArgs.push(fileUrl); url = fileUrl; } else if (config.url) { firefoxArgs.push(config.url); url = config.url; } else if (config.addonPath) { firefoxArgs.push('about:blank'); } else { throw 'You need to set either "file" or "url" in the launch configuration'; } if (typeof config.timeout === 'number') { timeout = config.timeout; } let detached = true; if (os.platform() === 'darwin') { if (!config.reAttach) { detached = false; if (config.keepProfileChanges) { // on MacOS we need to terminate the browser using `process.kill()`, which may // be interpreted by Firefox as a crash, so we add this option to prevent // starting into safe mode preferences['toolkit.startup.max_resumed_crashes'] = -1; } } } launch = { firefoxExecutable, firefoxArgs, profileDir, srcProfileDir, preferences, tmpDirs, port, timeout, detached }; } else { // config.request === 'attach' const firefoxExecutable = config.firefoxExecutable ? await findFirefoxExecutable(config.firefoxExecutable) : undefined; url = config.url; attach = { host: config.host || 'localhost', port, url, firefoxExecutable, profileDir: config.profileDir, reloadTabs: !!config.reloadOnAttach }; } if (config.pathMappings) { pathMappings.push(...config.pathMappings.map(harmonizeTrailingSlashes).map(handleWildcards)); } if (config.addonPath) { addon = await parseAddonConfiguration(config, pathMappings); } const webRoot = parseWebRootConfiguration(config, pathMappings); if (webRoot) { pathMappings.push({ url: 'webpack:///~/', path: webRoot + '/node_modules/' }); pathMappings.push({ url: 'webpack:///./~/', path: webRoot + '/node_modules/' }); pathMappings.push({ url: 'webpack:///./', path: webRoot + '/' }); pathMappings.push({ url: 'webpack:///src/', path: webRoot + '/src/' }); pathMappings.push({ url: 'webpack:///node_modules/', path: webRoot + '/node_modules/' }); pathMappings.push({ url: 'webpack:///webpack', path: null }); pathMappings.push({ url: 'webpack:///(webpack)', path: null }); pathMappings.push({ url: 'webpack:///pages/', path: webRoot + '/pages/' }); pathMappings.push({ url: 'webpack://[name]_[chunkhash]/node_modules/', path: webRoot + '/node_modules/' }); pathMappings.push({ url: 'webpack://[name]_[chunkhash]/', path: null }); } pathMappings.push({ url: (isWindowsPlatform() ? 'webpack:///' : 'webpack://'), path: '' }); pathMappings.push({ url: (isWindowsPlatform() ? 'file:///' : 'file://'), path: ''}); const pathMappingIndex = config.pathMappingIndex ?? 'index.html'; let filesToSkip = parseSkipFilesConfiguration(config); let reloadOnChange = parseReloadConfiguration(config.reloadOnChange); const tabFilter = parseTabFilterConfiguration(config.tabFilter, url); const clearConsoleOnReload = !!config.clearConsoleOnReload; let showConsoleCallLocation = config.showConsoleCallLocation || false; let liftAccessorsFromPrototypes = config.liftAccessorsFromPrototypes || 0; let suggestPathMappingWizard = config.suggestPathMappingWizard; if (suggestPathMappingWizard === undefined) { suggestPathMappingWizard = true; } const terminate = (config.request === 'launch') && !config.reAttach; const enableCRAWorkaround = !!config.enableCRAWorkaround; return { attach, launch, addon, pathMappings, pathMappingIndex, filesToSkip, reloadOnChange, tabFilter, clearConsoleOnReload, showConsoleCallLocation, liftAccessorsFromPrototypes, suggestPathMappingWizard, terminate, enableCRAWorkaround }; } function harmonizeTrailingSlashes(pathMapping: PathMapping): PathMapping { if ((typeof pathMapping.url === 'string') && (typeof pathMapping.path === 'string') && (pathMapping.path.length > 0)) { if (pathMapping.url.endsWith('/')) { if (pathMapping.path.endsWith('/')) { return pathMapping; } else { return { url: pathMapping.url, path: pathMapping.path + '/' }; } } else { if (pathMapping.path.endsWith('/')) { return { url: pathMapping.url + '/', path: pathMapping.path }; } else { return pathMapping; } } } else { return pathMapping; } } function handleWildcards(pathMapping: PathMapping): PathMapping { if ((typeof pathMapping.url === 'string') && (pathMapping.url.indexOf('*') >= 0)) { const regexp = '^' + pathMapping.url.split('*').map(RegExpEscape).join('[^/]*') + '(.*)$'; return { url: new RegExp(regexp), path: pathMapping.path }; } else { return pathMapping; } } async function findFirefoxExecutable(configuredPath?: string): Promise { let candidates: string[]; if (configuredPath) { if ([ 'stable', 'developer', 'nightly' ].indexOf(configuredPath) >= 0) { candidates = getExecutableCandidates(configuredPath as any); } else if (await isExecutable(configuredPath)) { return configuredPath; } else { throw 'Couldn\'t find the Firefox executable. Please correct the path given in your launch configuration.'; } } else { candidates = getExecutableCandidates(); } for (let i = 0; i < candidates.length; i++) { if (await isExecutable(candidates[i])) { return candidates[i]; } } throw 'Couldn\'t find the Firefox executable. Please specify the path by setting "firefoxExecutable" in your launch configuration.'; } export function getExecutableCandidates(edition?: 'stable' | 'developer' | 'nightly'): string[] { if (edition === undefined) { return [ ...getExecutableCandidates('developer'), ...getExecutableCandidates('stable') ]; } const platform = os.platform(); if ([ 'linux', 'freebsd', 'sunos' ].indexOf(platform) >= 0) { const paths = process.env.PATH!.split(':'); switch (edition) { case 'stable': return [ ...paths.map(dir => path.join(dir, 'firefox')) ]; case 'developer': return [ ...paths.map(dir => path.join(dir, 'firefox-developer-edition')), ...paths.map(dir => path.join(dir, 'firefox-developer')), ]; case 'nightly': return [ ...paths.map(dir => path.join(dir, 'firefox-nightly')), ]; } } switch (edition) { case 'stable': if (platform === 'darwin') { return [ '/Applications/Firefox.app/Contents/MacOS/firefox' ]; } else if (platform === 'win32') { return [ 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', 'C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe' ]; } break; case 'developer': if (platform === 'darwin') { return [ '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', '/Applications/FirefoxDeveloperEdition.app/Contents/MacOS/firefox' ]; } else if (platform === 'win32') { return [ 'C:\\Program Files\\Firefox Developer Edition\\firefox.exe', 'C:\\Program Files (x86)\\Firefox Developer Edition\\firefox.exe' ]; } break; case 'nightly': if (platform === 'darwin') { return [ '/Applications/Firefox Nightly.app/Contents/MacOS/firefox' ] } else if (platform === 'win32') { return [ 'C:\\Program Files\\Firefox Nightly\\firefox.exe', 'C:\\Program Files (x86)\\Firefox Nightly\\firefox.exe' ]; } break; } return []; } async function parseProfileConfiguration(config: LaunchConfiguration, tmpDirs: string[]) : Promise<{ profileDir: string, srcProfileDir?: string }> { let profileDir: string; let srcProfileDir: string | undefined; if (config.profileDir) { if (config.profile) { throw 'You can set either "profile" or "profileDir", but not both'; } srcProfileDir = config.profileDir; } else if (config.profile) { srcProfileDir = await findFirefoxProfileDir(config.profile); } if (config.keepProfileChanges) { if (srcProfileDir) { profileDir = srcProfileDir; srcProfileDir = undefined; } else { throw 'To enable "keepProfileChanges" you need to set either "profile" or "profileDir"'; } } else { const tmpDir = config.tmpDir || os.tmpdir(); profileDir = path.join(tmpDir, `vscode-firefox-debug-profile-${uuid.v4()}`); tmpDirs.push(profileDir); } return { profileDir, srcProfileDir }; } function findFirefoxProfileDir(profileName: string): Promise { return new Promise((resolve, reject) => { let finder = new FirefoxProfile.Finder(); finder.getPath(profileName, (err, path) => { if (err) { reject(err); } else { resolve(path); } }); }); } function createFirefoxPreferences( additionalPreferences?: { [key: string]: boolean | number | string | null } ): FirefoxPreferences { let preferences: FirefoxPreferences = {}; // Remote debugging settings preferences['devtools.chrome.enabled'] = true; preferences['devtools.debugger.prompt-connection'] = false; preferences['devtools.debugger.remote-enabled'] = true; preferences['extensions.autoDisableScopes'] = 10; preferences['xpinstall.signatures.required'] = false; preferences['extensions.sdk.console.logLevel'] = 'all'; // Skip check for default browser on startup preferences['browser.shell.checkDefaultBrowser'] = false; // Hide the telemetry infobar preferences['datareporting.policy.dataSubmissionPolicyBypassNotification'] = true; // Do not redirect user when a milestone upgrade of Firefox is detected preferences['browser.startup.homepage_override.mstone'] = 'ignore'; // Disable the UI tour preferences['browser.uitour.enabled'] = false; // Do not warn on quitting Firefox preferences['browser.warnOnQuit'] = false; if (additionalPreferences !== undefined) { for (let key in additionalPreferences) { let value = additionalPreferences[key]; if (value !== null) { preferences[key] = value; } else { delete preferences[key]; } } } return preferences; } function parseWebRootConfiguration(config: CommonConfiguration, pathMappings: PathMappings): string | undefined { if (config.url) { if (!config.webRoot) { if ((config.request === 'launch') && !config.pathMappings) { throw `If you set "url" you also have to set "webRoot" or "pathMappings" in the ${config.request} configuration`; } return undefined; } else if (!path.isAbsolute(config.webRoot) && !isAbsoluteUrl(config.webRoot)) { throw `The "webRoot" property in the ${config.request} configuration has to be an absolute path`; } let webRootUrl = config.url; if (webRootUrl.lastIndexOf('/') > 7) { webRootUrl = webRootUrl.substr(0, webRootUrl.lastIndexOf('/')); } let webRoot = isAbsoluteUrl(config.webRoot) ? config.webRoot : normalizePath(config.webRoot); pathMappings.forEach((pathMapping) => { const to = pathMapping.path; if ((typeof to === 'string') && (to.substr(0, 10) === '${webRoot}')) { pathMapping.path = webRoot + to.substr(10); } }); pathMappings.push({ url: webRootUrl, path: webRoot }); return webRoot; } else if (config.webRoot) { throw `If you set "webRoot" you also have to set "url" in the ${config.request} configuration`; } return undefined; } function parseSkipFilesConfiguration(config: CommonConfiguration): RegExp[] { let filesToSkip: RegExp[] = []; if (config.skipFiles) { config.skipFiles.forEach((glob) => { let minimatch = new Minimatch(glob); let regExp = minimatch.makeRe(); if (regExp) { filesToSkip.push(regExp); } else { log.warn(`Invalid glob pattern "${glob}" specified in "skipFiles"`); } }) } return filesToSkip; } function parseReloadConfiguration( reloadConfig: ReloadConfiguration | undefined ): NormalizedReloadConfiguration | undefined { if (reloadConfig === undefined) { return undefined; } const defaultDebounce = 100; if (typeof reloadConfig === 'string') { return { watch: [ normalizePath(reloadConfig) ], ignore: [], debounce: defaultDebounce }; } else if (Array.isArray(reloadConfig)) { return { watch: reloadConfig.map(path => normalizePath(path)), ignore: [], debounce: defaultDebounce }; } else { let _config = reloadConfig; let watch: string[]; if (typeof _config.watch === 'string') { watch = [ _config.watch ]; } else { watch = _config.watch; } watch = watch.map((path) => normalizePath(path)); let ignore: string[]; if (_config.ignore === undefined) { ignore = []; } else if (typeof _config.ignore === 'string') { ignore = [ _config.ignore ]; } else { ignore = _config.ignore; } ignore = ignore.map((path) => normalizePath(path)); let debounce: number; if (typeof _config.debounce === 'number') { debounce = _config.debounce; } else { debounce = (_config.debounce !== false) ? defaultDebounce : 0; } return { watch, ignore, debounce }; } } function parseTabFilterConfiguration( tabFilterConfig?: TabFilterConfiguration, url?: string ): ParsedTabFilterConfiguration { if (tabFilterConfig === undefined) { if (url) { return { include: [ new RegExp(RegExpEscape(urlDirname(url)) + '.*') ], exclude: [] }; } else { return { include: [ /.*/ ], exclude: [] }; } } if ((typeof tabFilterConfig === 'string') || Array.isArray(tabFilterConfig)) { return { include: parseTabFilter(tabFilterConfig), exclude: [] } } else { return { include: (tabFilterConfig.include !== undefined) ? parseTabFilter(tabFilterConfig.include) : [ /.*/ ], exclude: (tabFilterConfig.exclude !== undefined) ? parseTabFilter(tabFilterConfig.exclude) : [] } } } function parseTabFilter(tabFilter: string | string[]): RegExp[] { if (typeof tabFilter === 'string') { const parts = tabFilter.split('*').map(part => RegExpEscape(part)); const regExp = new RegExp(`^${parts.join('.*')}$`); return [ regExp ]; } else { return tabFilter.map(f => parseTabFilter(f)[0]); } } async function parseAddonConfiguration( config: LaunchConfiguration | AttachConfiguration, pathMappings: PathMappings ): Promise { let addonPath = config.addonPath!; const popupAutohideButton = (config.popupAutohideButton !== false); let addonId = await findAddonId(addonPath); let sanitizedAddonPath = addonPath; if (sanitizedAddonPath[sanitizedAddonPath.length - 1] === '/') { sanitizedAddonPath = sanitizedAddonPath.substr(0, sanitizedAddonPath.length - 1); } pathMappings.push({ url: new RegExp('^moz-extension://[0-9a-f-]*(/.*)$'), path: sanitizedAddonPath }); if (addonId) { // this pathMapping may no longer be necessary, I haven't seen this kind of URL recently... let rewrittenAddonId = addonId.replace('{', '%7B').replace('}', '%7D'); pathMappings.push({ url: new RegExp(`^jar:file:.*/extensions/${rewrittenAddonId}.xpi!(/.*)$`), path: sanitizedAddonPath }); } return { path: addonPath, id: addonId, popupAutohideButton } } ================================================ FILE: src/adapter/debugAdapterBase.ts ================================================ import process from 'process'; import util from 'util'; import { DebugProtocol } from '@vscode/debugprotocol'; import { DebugSession } from '@vscode/debugadapter'; import { Log } from './util/log'; const log = Log.create('main'); process.on('unhandledRejection', (e: any) => log.error(`Unhandled rejection: ${util.inspect(e)}\n${e.stack}`)); /** * This class extends the base class provided by VS Code for debug adapters, * offering a Promise-based API instead of the callback-based API used by VS Code */ export abstract class DebugAdapterBase extends DebugSession { public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { super(debuggerLinesStartAt1, isServer); } protected abstract initialize(args: DebugProtocol.InitializeRequestArguments): DebugProtocol.Capabilities | undefined; protected abstract launch(args: DebugProtocol.LaunchRequestArguments): Promise; protected abstract attach(args: DebugProtocol.AttachRequestArguments): Promise; protected abstract disconnect(args: DebugProtocol.DisconnectArguments): Promise; protected abstract breakpointLocations(args: DebugProtocol.BreakpointLocationsArguments): Promise<{ breakpoints: DebugProtocol.BreakpointLocation[] }>; protected abstract setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): { breakpoints: DebugProtocol.Breakpoint[] }; protected abstract setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): void; protected abstract pause(args: DebugProtocol.PauseArguments): Promise; protected abstract next(args: DebugProtocol.NextArguments): Promise; protected abstract stepIn(args: DebugProtocol.StepInArguments): Promise; protected abstract stepOut(args: DebugProtocol.StepOutArguments): Promise; protected abstract continue(args: DebugProtocol.ContinueArguments): Promise<{ allThreadsContinued?: boolean }>; protected abstract getSource(args: DebugProtocol.SourceArguments): Promise<{ content: string, mimeType?: string }>; protected abstract getThreads(): { threads: DebugProtocol.Thread[] }; protected abstract getStackTrace(args: DebugProtocol.StackTraceArguments): Promise<{ stackFrames: DebugProtocol.StackFrame[], totalFrames?: number }>; protected abstract getScopes(args: DebugProtocol.ScopesArguments): Promise<{ scopes: DebugProtocol.Scope[] }>; protected abstract getVariables(args: DebugProtocol.VariablesArguments): Promise<{ variables: DebugProtocol.Variable[] }>; protected abstract setVariable(args: DebugProtocol.SetVariableArguments): Promise<{ value: string, variablesReference?: number }>; protected abstract evaluate(args: DebugProtocol.EvaluateArguments): Promise<{ result: string, type?: string, variablesReference: number, namedVariables?: number, indexedVariables?: number }>; protected abstract getCompletions(args: DebugProtocol.CompletionsArguments): Promise<{ targets: DebugProtocol.CompletionItem[] }>; protected abstract dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null, description: string, accessTypes?: DebugProtocol.DataBreakpointAccessType[], canPersist?: boolean }>; protected abstract setDataBreakpoints(args: DebugProtocol.SetDataBreakpointsArguments): Promise<{ breakpoints: DebugProtocol.Breakpoint[] }>; protected abstract restartFrame(args: DebugProtocol.RestartFrameArguments): Promise; protected abstract reloadAddon(): Promise; protected abstract toggleSkippingFile(url: string): Promise; protected abstract setPopupAutohide(popupAutohide: boolean): Promise; protected abstract togglePopupAutohide(): Promise; protected abstract setActiveEventBreakpoints(args: string[]): Promise; protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { this.handleRequest(response, () => this.initialize(args)); } protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { this.handleRequestAsync(response, () => this.disconnect(args)); } protected launchRequest(response: DebugProtocol.LaunchResponse, args: DebugProtocol.LaunchRequestArguments): void { this.handleRequestAsync(response, () => this.launch(args)); } protected attachRequest(response: DebugProtocol.AttachResponse, args: DebugProtocol.AttachRequestArguments): void { this.handleRequestAsync(response, () => this.attach(args)); } protected breakpointLocationsRequest(response: DebugProtocol.BreakpointLocationsResponse, args: DebugProtocol.BreakpointLocationsArguments, request?: DebugProtocol.Request): void { this.handleRequestAsync(response, () => this.breakpointLocations(args)); } protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { this.handleRequest(response, () => this.setBreakpoints(args)); } protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments): void { this.handleRequest(response, () => this.setExceptionBreakpoints(args)); } protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments): void { this.handleRequestAsync(response, () => this.pause(args)); } protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { this.handleRequestAsync(response, () => this.next(args)); } protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void { this.handleRequestAsync(response, () => this.stepIn(args)); } protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void { this.handleRequestAsync(response, () => this.stepOut(args)); } protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { this.handleRequestAsync(response, () => this.continue(args)); } protected sourceRequest(response: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments): void { this.handleRequestAsync(response, () => this.getSource(args)); } protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { this.handleRequest(response, () => this.getThreads()); } protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { this.handleRequestAsync(response, () => this.getStackTrace(args)); } protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { this.handleRequestAsync(response, () => this.getScopes(args)); } protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { this.handleRequestAsync(response, () => this.getVariables(args)); } protected setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): void { this.handleRequestAsync(response, () => this.setVariable(args)); } protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { this.handleRequestAsync(response, () => this.evaluate(args)); } protected completionsRequest(response: DebugProtocol.CompletionsResponse, args: DebugProtocol.CompletionsArguments): void { this.handleRequestAsync(response, () => this.getCompletions(args)); } protected dataBreakpointInfoRequest(response: DebugProtocol.DataBreakpointInfoResponse, args: DebugProtocol.DataBreakpointInfoArguments): void { this.handleRequestAsync(response, () => this.dataBreakpointInfo(args)); } protected setDataBreakpointsRequest(response: DebugProtocol.SetDataBreakpointsResponse, args: DebugProtocol.SetDataBreakpointsArguments): void { this.handleRequestAsync(response, () => this.setDataBreakpoints(args)); } protected restartFrameRequest(response: DebugProtocol.RestartFrameResponse, args: DebugProtocol.RestartFrameArguments): void { this.handleRequestAsync(response, () => this.restartFrame(args)); } protected customRequest(command: string, response: DebugProtocol.Response, args: any): void { this.handleRequestAsync(response, async () => { switch(command) { case 'reloadAddon': return await this.reloadAddon(); case 'toggleSkippingFile': return await this.toggleSkippingFile(args); case 'setPopupAutohide': return await this.setPopupAutohide(args === 'true'); case 'togglePopupAutohide': return await this.togglePopupAutohide(); case 'setActiveEventBreakpoints': return await this.setActiveEventBreakpoints(args); } }); } private handleRequest(response: TResponse, executeRequest: () => TResponseBody): void { try { response.body = executeRequest(); } catch (err) { response.success = false; response.message = this.errorString(err); } this.sendResponse(response); } private async handleRequestAsync(response: TResponse, executeRequest: () => Promise): Promise { try { response.body = await executeRequest(); } catch (err) { response.success = false; response.message = this.errorString(err); } this.sendResponse(response); } private errorString(err: any) { if ((typeof err === 'object') && (err !== null) && (typeof err.message === 'string')) { return err.message; } else { return String(err); } } } ================================================ FILE: src/adapter/firefox/README.md ================================================ This folder contains the code for launching and talking to Firefox. The [`launchFirefox()`](./launch.ts) function launches Firefox and establishes the TCP connection for the debugger protocol. The connection's socket is passed to the [`DebugConnection`](./connection.ts) class which implements the [Firefox Remote Debugging Protocol](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md) using the [`DebugProtocolTransport`](./transport.ts) class for the [transport layer](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#stream-transport) of the protocol. It passes the responses and events it receives to [proxy classes](./actorProxy) for the Firefox [actors](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#actors). The [`sourceMaps`](./sourceMaps) folder contains the debug adapter's source-map support. ================================================ FILE: src/adapter/firefox/actorProxy/README.md ================================================ This folder contains proxy classes for the Firefox [actors](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#actors). They implement the [`ActorProxy`](./interface.ts) interface and provide a Promise-based API for sending requests to and an EventEmitter-based API for receiving events from these actors. ================================================ FILE: src/adapter/firefox/actorProxy/addons.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; let log = Log.create('AddonsActorProxy'); /** * Proxy class for an addons actor * ([spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/addon/addons.js)) */ export class AddonsActorProxy extends BaseActorProxy { constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public installAddon(addonPath: string): Promise { return this.sendCachedRequest( `installTemporaryAddon:${addonPath}`, { type: 'installTemporaryAddon', addonPath } ); } } ================================================ FILE: src/adapter/firefox/actorProxy/base.ts ================================================ import { EventEmitter } from 'events'; import { DebugConnection } from '../connection'; import { ActorProxy } from './interface'; import { PendingRequests } from '../../util/pendingRequests'; import { Log } from '../../util/log'; export abstract class BaseActorProxy extends EventEmitter implements ActorProxy { private readonly pendingRequests = new PendingRequests(); private readonly cachedRequestPromises = new Map>(); constructor( public readonly name: string, protected readonly connection: DebugConnection, private readonly log: Log ) { super(); this.connection.register(this); } sendRequest, S>(request: T): Promise { return new Promise((resolve, reject) => { this.pendingRequests.enqueue({ resolve, reject }); this.connection.sendRequest({ ...request, to: this.name }); }); } sendCachedRequest, R, S>(key: string, request: T, convert?: (r: R) => S): Promise { if (!this.cachedRequestPromises.has(key)) { this.cachedRequestPromises.set(key, (async () => { const response: R = await this.sendRequest(request); return convert ? convert(response) : response; })()); } return this.cachedRequestPromises.get(key)!; } sendRequestWithoutResponse>(request: T): void { this.connection.sendRequest({ ...request, to: this.name }); } async getRequestTypes(): Promise { return (await this.sendRequest( { type: 'requestTypes' }) ).requestTypes; } isEvent(message: FirefoxDebugProtocol.Response): boolean { return !!message.type; } handleEvent(event: FirefoxDebugProtocol.Event): void { this.log.warn(`Unknown message: ${JSON.stringify(event)}`); } receiveMessage(message: FirefoxDebugProtocol.Response): void { if (message.error) { this.pendingRequests.rejectOne(message); } else if (this.isEvent(message)) { this.handleEvent(message as FirefoxDebugProtocol.Event); } else { this.pendingRequests.resolveOne(message); } } dispose(): void { this.connection.unregister(this); } } ================================================ FILE: src/adapter/firefox/actorProxy/breakpointList.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; const log = Log.create('BreakpointListActorProxy'); export class BreakpointListActorProxy extends BaseActorProxy { constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public async setBreakpoint(sourceUrl: string, line: number, column: number, condition?: string, logValue?: string) { await this.sendRequest({ type: 'setBreakpoint', location: { sourceUrl, line, column }, options: { condition, logValue } }); } public async removeBreakpoint(sourceUrl: string, line: number, column: number) { await this.sendRequest({ type: 'removeBreakpoint', location: { sourceUrl, line, column } }); } public async setActiveEventBreakpoints(ids: string[]) { await this.sendRequest({ type: 'setActiveEventBreakpoints', ids }); } } ================================================ FILE: src/adapter/firefox/actorProxy/console.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { exceptionGripToString } from '../../util/misc'; import { BaseActorProxy } from './base'; import { DeferredMap } from '../../../common/deferredMap'; let log = Log.create('ConsoleActorProxy'); /** * Proxy class for a console actor */ export class ConsoleActorProxy extends BaseActorProxy { private evaluationResults = new DeferredMap(); constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public async evaluate(expr: string, disableBreaks: boolean, frameActorName?: string): Promise { const resultIDResponse: FirefoxDebugProtocol.ResultIDResponse = await this.sendRequest({ type: 'evaluateJSAsync', text: expr, frameActor: frameActorName, disableBreaks }); const result = await this.evaluationResults.get(resultIDResponse.resultID); this.evaluationResults.delete(resultIDResponse.resultID); return result; } public async autoComplete(text: string, column: number, frameActor?: string) { const response: FirefoxDebugProtocol.AutoCompleteResponse = await this.sendRequest({ type: 'autocomplete', text, cursor: column, frameActor }); return response.matches; } handleEvent(event: FirefoxDebugProtocol.EvaluationResultResponse): void { if (event.type === 'evaluationResult') { this.evaluationResults.set(event.resultID, event.exceptionMessage ? exceptionGripToString(event.exception) : event.result); } else { log.warn(`Unknown message: ${JSON.stringify(event)}`); } } } ================================================ FILE: src/adapter/firefox/actorProxy/descriptor.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; import { WatcherActorProxy } from './watcher'; let log = Log.create('DescriptorActorProxy'); export type DescriptorType = 'tab' | 'webExtension' | 'process'; /** * Proxy class for a TabDescriptor or WebExtensionDescriptor actor */ export class DescriptorActorProxy extends BaseActorProxy { constructor( name: string, public readonly type: DescriptorType, connection: DebugConnection ) { super(name, connection, log); } public async getWatcher(): Promise { return await this.sendCachedRequest( 'getWatcher', { type: 'getWatcher', enableWindowGlobalThreadActors: this.type === 'process' ? true : undefined, isServerTargetSwitchingEnabled: this.type !== 'process' ? true : undefined, isPopupDebuggingEnabled: this.type === 'tab' ? false : undefined, }, (response: FirefoxDebugProtocol.GetWatcherResponse) => new WatcherActorProxy(response.actor, !!response.traits.content_script, this.connection) ); } public async reload() { await this.sendRequest({ type: 'reloadDescriptor' }); } public onDestroyed(cb: () => void) { this.on('destroyed', cb); } handleEvent(event: FirefoxDebugProtocol.DescriptorDestroyedEvent): void { if (event.type === 'descriptor-destroyed') { log.debug(`Descriptor ${this.name} destroyed`); this.emit('destroyed'); } else { log.warn(`Unknown message: ${JSON.stringify(event)}`); } } } ================================================ FILE: src/adapter/firefox/actorProxy/device.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; let log = Log.create('DeviceActorProxy'); /** * Proxy class for the device actor */ export class DeviceActorProxy extends BaseActorProxy { constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public getDescription(): Promise { return this.sendCachedRequest( 'getDescription', { type: 'getDescription' }, (response: FirefoxDebugProtocol.DeviceDescriptionResponse) => response.value ); } } ================================================ FILE: src/adapter/firefox/actorProxy/frame.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; let log = Log.create('FrameActorProxy'); /** * Proxy class for a frame actor * ([docs](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#listing-stack-frames), * [spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/frame.js)) */ export class FrameActorProxy extends BaseActorProxy { constructor(name: string, connection: DebugConnection) { super(name, connection, log); } isEvent(message: FirefoxDebugProtocol.Response): boolean { return false; } public getEnvironment(): Promise { return this.sendCachedRequest('getEnvironment', { type: 'getEnvironment' }); } } ================================================ FILE: src/adapter/firefox/actorProxy/interface.ts ================================================ /** * An ActorProxy is a client-side reference to an actor on the server side of the * Mozilla Debugging Protocol as defined in * https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md */ export interface ActorProxy { /** the name that is used for the actor in Firefox debug protocol messages */ readonly name: string; /** called by the [DebugConnection](../connection.ts) class to deliver this actor's messages */ receiveMessage(response: FirefoxDebugProtocol.Response): void; } ================================================ FILE: src/adapter/firefox/actorProxy/longString.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; let log = Log.create('LongStringGripActorProxy'); /** * Proxy class for a long string grip actor * ([docs](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#long-strings), * [spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/string.js)) */ export class LongStringGripActorProxy extends BaseActorProxy { constructor(private grip: FirefoxDebugProtocol.LongStringGrip, connection: DebugConnection) { super(grip.actor, connection, log); } public fetchContent(): Promise { return this.sendCachedRequest( 'content', { type: 'substring', start: 0, end: this.grip.length }, (response: { substring: string }) => response.substring ); } } ================================================ FILE: src/adapter/firefox/actorProxy/objectGrip.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; let log = Log.create('ObjectGripActorProxy'); /** * Proxy class for an object grip actor * ([docs](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#objects), * [spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/object.js)) */ export class ObjectGripActorProxy extends BaseActorProxy { private _refCount = 0; constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public get refCount() { return this._refCount; } public increaseRefCount() { this._refCount++; } public decreaseRefCount() { this._refCount--; if (this._refCount === 0) { this.connection.unregister(this); } } public fetchPrototypeAndProperties(): Promise { return this.sendCachedRequest( 'prototypeAndProperties', { type: 'prototypeAndProperties' } ); } public addWatchpoint(property: string, label: string, watchpointType: 'get' | 'set'): void { this.sendRequestWithoutResponse({ type: 'addWatchpoint', property, label, watchpointType }); } public removeWatchpoint(property: string): void { this.sendRequestWithoutResponse({ type: 'removeWatchpoint', property }); } public threadLifetime(): Promise { return this.sendCachedRequest('threadGrip', { type: 'threadGrip' }, () => undefined); } } ================================================ FILE: src/adapter/firefox/actorProxy/preference.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; let log = Log.create('PreferenceActorProxy'); /** * Proxy class for a preference actor * ([spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/preference.js)) */ export class PreferenceActorProxy extends BaseActorProxy { constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public async getBoolPref(pref: string): Promise { let prefString = await this.getPref(pref, 'Bool'); return (prefString === 'true'); } public getCharPref(pref: string): Promise { return this.getPref(pref, 'Char'); } public async getIntPref(pref: string): Promise { let prefString = await this.getPref(pref, 'Bool'); return parseInt(prefString, 10); } public setBoolPref(pref: string, val: boolean): Promise { return this.setPref(pref, val, 'Bool'); } public setCharPref(pref: string, val: string): Promise { return this.setPref(pref, val, 'Char'); } public setIntPref(pref: string, val: number): Promise { return this.setPref(pref, val, 'Int'); } private async getPref( pref: string, type: 'Bool' | 'Char' | 'Int' ): Promise { const response: { value: any } = await this.sendRequest({ type: `get${type}Pref`, value: pref }); return response.value.toString(); } private setPref( pref: string, val: boolean | string | number, type: 'Bool' | 'Char' | 'Int' ): Promise { return this.sendRequest({ type: `set${type}Pref`, name: pref, value: val }); } } ================================================ FILE: src/adapter/firefox/actorProxy/root.ts ================================================ import { Log } from '../../util/log'; import { DescriptorActorProxy } from './descriptor'; import { PreferenceActorProxy } from './preference'; import { AddonsActorProxy } from './addons'; import { DeviceActorProxy } from './device'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; import { delay } from '../../../common/util'; let log = Log.create('RootActorProxy'); export interface FetchRootResult { preference: PreferenceActorProxy, addons: AddonsActorProxy | undefined, device: DeviceActorProxy } /** * Proxy class for a root actor * ([docs](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#the-root-actor), * [spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/root.js)) */ export class RootActorProxy extends BaseActorProxy { private tabs = new Map(); constructor(connection: DebugConnection) { super('root', connection, log); } isEvent(message: FirefoxDebugProtocol.Response): boolean { return !!(message.type || message.applicationType); } public async fetchRoot(): Promise { const rootResponse: FirefoxDebugProtocol.RootResponse = await this.sendCachedRequest( 'getRoot', { type: 'getRoot' } ); const preferenceActor = this.connection.getOrCreate(rootResponse.preferenceActor, () => new PreferenceActorProxy(rootResponse.preferenceActor, this.connection)); let addonsActor: AddonsActorProxy | undefined; const addonsActorName = rootResponse.addonsActor; if (addonsActorName) { addonsActor = this.connection.getOrCreate(addonsActorName, () => new AddonsActorProxy(addonsActorName, this.connection)); } const deviceActor = this.connection.getOrCreate(rootResponse.deviceActor, () => new DeviceActorProxy(rootResponse.deviceActor, this.connection)); return { preference: preferenceActor, addons: addonsActor, device: deviceActor }; } public async getProcess(id: number): Promise { return this.sendCachedRequest( `getProcess(${id})`, { type: 'getProcess', id }, (response: FirefoxDebugProtocol.GetProcessResponse) => new DescriptorActorProxy(response.processDescriptor.actor, 'process', this.connection) ) } public async fetchTabs(): Promise> { let tabsResponse: FirefoxDebugProtocol.TabsResponse = await this.sendRequest({ type: 'listTabs' }); while (tabsResponse.tabs.length === 0) { await delay(100); tabsResponse = await this.sendRequest({ type: 'listTabs' }); } log.debug(`Received ${tabsResponse.tabs.length} tabs`); // convert the Tab array into a map of TabDescriptorActorProxies, re-using already // existing proxies and emitting tabOpened events for new ones const currentTabs = new Map(); for (const tab of tabsResponse.tabs) { let tabDescriptorActor: DescriptorActorProxy; if (this.tabs.has(tab.actor)) { tabDescriptorActor = this.tabs.get(tab.actor)!; } else { log.debug(`Tab ${tab.actor} opened`); tabDescriptorActor = new DescriptorActorProxy(tab.actor, 'tab', this.connection); this.emit('tabOpened', tabDescriptorActor); } currentTabs.set(tab.actor, tabDescriptorActor); } // emit tabClosed events for tabs that have disappeared this.tabs.forEach((actorsForTab, tabActorName) => { if (!currentTabs.has(tabActorName)) { log.debug(`Tab ${tabActorName} closed`); this.emit('tabClosed', actorsForTab); } }); this.tabs = currentTabs; return currentTabs; } public async fetchAddons(): Promise { const addonsResponse: FirefoxDebugProtocol.AddonsResponse = await this.sendRequest({ type: 'listAddons' }); return addonsResponse.addons; } handleEvent(event: FirefoxDebugProtocol.Event): void { if (event.applicationType) { this.emit('init', event); } else if (['tabListChanged', 'addonListChanged'].includes(event.type)) { this.emit(event.type); } else if (event.type !== 'forwardingCancelled') { log.warn(`Unknown message: ${JSON.stringify(event)}`); } } public onInit(cb: (response: FirefoxDebugProtocol.InitialResponse) => void) { this.on('init', cb); } public onTabOpened(cb: (actorsForTab: DescriptorActorProxy) => void) { this.on('tabOpened', cb); } public onTabClosed(cb: (actorsForTab: DescriptorActorProxy) => void) { this.on('tabClosed', cb); } public onTabListChanged(cb: () => void) { this.on('tabListChanged', cb); } public onAddonListChanged(cb: () => void) { this.on('addonListChanged', cb); } } ================================================ FILE: src/adapter/firefox/actorProxy/source.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { MappedLocation, Range } from '../../location'; import { BaseActorProxy } from './base'; let log = Log.create('SourceActorProxy'); export interface ISourceActorProxy { name: string; source: FirefoxDebugProtocol.Source; url: string | null; getBreakableLines(): Promise; getBreakableLocations(line: number): Promise; fetchSource(): Promise; setBlackbox(blackbox: boolean): Promise; dispose(): void; } /** * Proxy class for a source actor * ([docs](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#loading-script-sources), * [spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/source.js)) */ export class SourceActorProxy extends BaseActorProxy implements ISourceActorProxy { constructor( public readonly source: FirefoxDebugProtocol.Source, connection: DebugConnection ) { super(source.actor, connection, log); } public get url() { return this.source.url; } public getBreakableLines(): Promise { log.debug(`Fetching breakableLines of ${this.url}`); return this.sendCachedRequest( 'getBreakableLines', { type: 'getBreakableLines' }, (response: FirefoxDebugProtocol.GetBreakableLinesResponse) => response.lines ); } public async getBreakableLocations(line: number): Promise { log.debug(`Fetching breakpointPositions of line ${line} in ${this.url}`); const positions = await this.getBreakpointPositionsForRange({ start: { line, column: 0 }, end: { line, column: Number.MAX_SAFE_INTEGER } }); if (positions[line]) { return (positions[line].map(column => ({ line, column }))); } else { return []; } } public getBreakpointPositionsForRange(range: Range): Promise { log.debug(`Fetching breakpoint positions of ${this.url} for range: ${JSON.stringify(range)}`); return this.sendCachedRequest( `getBreakpointPositionsCompressed_${range.start.line}:${range.start.column}-${range.end.line}:${range.end.line}`, { type: 'getBreakpointPositionsCompressed', query: { start: { line: range.start.line, column: range.start.column }, end: { line: range.end.line, column: range.end.column }, }, }, (response: FirefoxDebugProtocol.GetBreakpointPositionsCompressedResponse) => response.positions ); } public fetchSource(): Promise { log.debug(`Fetching source of ${this.url}`); return this.sendCachedRequest( 'source', { type: 'source' }, (response: FirefoxDebugProtocol.SourceResponse) => response.source ); } public async setBlackbox(blackbox: boolean): Promise { log.debug(`Setting blackboxing of ${this.url} to ${blackbox}`); await this.sendRequest({ type: blackbox ? 'blackbox' : 'unblackbox' }); } } ================================================ FILE: src/adapter/firefox/actorProxy/target.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; import { ISourceActorProxy, SourceActorProxy } from './source'; const log = Log.create('TargetActorProxy'); export class TargetActorProxy extends BaseActorProxy { public destroyed = false; constructor( public readonly target: FirefoxDebugProtocol.TargetAvailableEvent['target'], connection: DebugConnection ) { super(target.actor, connection, log); } public reload() { return this.sendRequest({ type: 'reload' }); } public onConsoleMessages(cb: (consoleMessages: FirefoxDebugProtocol.ConsoleMessage[]) => void) { this.on('console-message', cb); } public onErrorMessages(cb: (errorMessages: { pageError: FirefoxDebugProtocol.PageError }[]) => void) { this.on('error-message', cb); } public onSources(cb: (sources: ISourceActorProxy[]) => void) { this.on('source', async underlyingSources => { let allMappedSources: ISourceActorProxy[] = []; for (let underlyingSource of underlyingSources) { let info = await this.connection.sourceMaps.getOrCreateSourceMappingInfo(underlyingSource.source); allMappedSources.push(...info.sources); if (!info.sources.some(actor => actor === underlyingSource)) { allMappedSources.push(underlyingSource); } } if (!this.destroyed) { cb(allMappedSources); } }); } public onThreadState(cb: (threadState: FirefoxDebugProtocol.ThreadState) => void) { this.on('thread-state', cb); } handleEvent(event: FirefoxDebugProtocol.ResourcesAvailableEvent | FirefoxDebugProtocol.ResourceAvailableForm | FirefoxDebugProtocol.FrameUpdateEvent): void { if (event.type === 'resources-available-array') { for (const resource of event.array) { if (resource[0] === 'source') { this.emit('source', resource[1].map(source => new SourceActorProxy(source, this.connection))); } else if (resource[0] === 'thread-state') { for (let state of resource[1]) { this.emit('thread-state', state); } } else { this.emit(resource[0], resource[1]); } } } else if (event.type === 'resource-available-form') { for (const resource of event.resources) { switch (resource.resourceType) { case 'source': { this.emit('source', [new SourceActorProxy(resource, this.connection)]); break; } case 'thread-state': { this.emit('thread-state', resource); break; } case 'console-message': { this.emit('console-message', [resource.message]); break; } case 'error-message': { this.emit('error-message', [resource]); break; } } } } else if (event.type === 'frameUpdate') { log.debug("frameUpdate from TargetActor: " + JSON.stringify(event)); } else { log.warn(`Unknown message: ${JSON.stringify(event)}`); } } } ================================================ FILE: src/adapter/firefox/actorProxy/thread.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; let log = Log.create('ThreadActorProxy'); export interface IThreadActorProxy { name: string; resume(resumeLimitType?: 'next' | 'step' | 'finish' | 'restart', frameActorID?: string): Promise; interrupt(immediately?: boolean): Promise; fetchStackFrames(start?: number, count?: number): Promise; getAvailableEventBreakpoints() : Promise; dispose(): void; } /** * A ThreadActorProxy is a proxy for a "thread-like actor" (a Tab, Worker or Addon) in Firefox * ([docs](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#interacting-with-thread-like-actors), * [spec](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/specs/thread.js)) */ export class ThreadActorProxy extends BaseActorProxy implements IThreadActorProxy { constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public async resume(resumeLimitType?: 'next' | 'step' | 'finish' | 'restart', frameActorID?: string): Promise { const resumeLimit = resumeLimitType ? { type: resumeLimitType } : undefined; this.sendRequest({ type: 'resume', resumeLimit, frameActorID }); } public async interrupt(immediately?: boolean): Promise { await this.sendRequest({ type: 'interrupt', when: immediately ? '' : 'onNext' }); } public async fetchStackFrames(start = 0, count = 1000): Promise { const response: { frames: FirefoxDebugProtocol.Frame[] } = await this.sendRequest({ type: 'frames', start, count }); return response.frames; } public async getAvailableEventBreakpoints() : Promise { return this.sendCachedRequest( 'getAvailableEventBreakpoints', { type: 'getAvailableEventBreakpoints' }, (response: FirefoxDebugProtocol.GetAvailableEventBreakpointsResponse) => response.value ); } handleEvent(event: FirefoxDebugProtocol.Event): void { if (!['paused', 'resumed'].includes(event.type)) { log.warn(`Unknown message: ${JSON.stringify(event)}`); } } } ================================================ FILE: src/adapter/firefox/actorProxy/threadConfiguration.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; const log = Log.create('ThreadConfigurationActorProxy'); export class ThreadConfigurationActorProxy extends BaseActorProxy { constructor(name: string, connection: DebugConnection) { super(name, connection, log); } public async updateConfiguration(configuration: Partial) { await this.sendRequest({ type: 'updateConfiguration', configuration }); } } ================================================ FILE: src/adapter/firefox/actorProxy/watcher.ts ================================================ import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { BaseActorProxy } from './base'; import { BreakpointListActorProxy } from './breakpointList'; import { ConsoleActorProxy } from './console'; import { TargetActorProxy } from './target'; import { IThreadActorProxy, ThreadActorProxy } from './thread'; import { SourceMappingThreadActorProxy } from '../sourceMaps/thread'; import { ThreadConfigurationActorProxy } from './threadConfiguration'; export type ResourceType = 'console-message' | 'error-message' | 'source' | 'thread-state'; export type TargetType = 'frame' | 'worker' | 'content_script'; const log = Log.create('WatcherActorProxy'); export class WatcherActorProxy extends BaseActorProxy { constructor( name: string, public readonly supportsContentScriptTargets: boolean, connection: DebugConnection ) { super(name, connection, log); } public async getBreakpointList() { return await this.sendCachedRequest( 'getBreakpointListActor', { type: 'getBreakpointListActor' }, (response: FirefoxDebugProtocol.GetBreakpointListResponse) => new BreakpointListActorProxy(response.breakpointList.actor, this.connection) ); } public async getThreadConfiguration() { return await this.sendCachedRequest( 'getThreadConfigurationActor', { type: 'getThreadConfigurationActor' }, (response: FirefoxDebugProtocol.GetThreadConfigurationResponse) => new ThreadConfigurationActorProxy(response.configuration.actor, this.connection) ); } public async watchResources(resourceTypes: ResourceType[]) { await this.sendRequest({ type: 'watchResources', resourceTypes }); } public async watchTargets(targetType: TargetType) { await this.sendRequest({ type: 'watchTargets', targetType }); } public onTargetAvailable(cb: (target: [TargetActorProxy, IThreadActorProxy, ConsoleActorProxy]) => void) { this.on('targetAvailable', cb); } public onTargetDestroyed(cb: (targetActorName: string) => void) { this.on('targetDestroyed', cb); } handleEvent(event: FirefoxDebugProtocol.TargetAvailableEvent | FirefoxDebugProtocol.TargetDestroyedEvent): void { if (event.type === 'target-available-form') { const targetActorProxy = new TargetActorProxy(event.target, this.connection); const threadActorProxy = new ThreadActorProxy(event.target.threadActor, this.connection); const sourcemappingThreadActorProxy = new SourceMappingThreadActorProxy(threadActorProxy, this.connection); const consoleActorProxy = new ConsoleActorProxy(event.target.consoleActor, this.connection); this.emit('targetAvailable', [targetActorProxy, sourcemappingThreadActorProxy, consoleActorProxy]); } else if (event.type === 'target-destroyed-form') { this.emit('targetDestroyed', event.target.actor); } else { log.warn(`Unknown message: ${JSON.stringify(event)}`); } } } ================================================ FILE: src/adapter/firefox/connection.ts ================================================ import { Log } from '../util/log'; import { Socket } from 'net'; import { DebugProtocolTransport } from './transport'; import { ActorProxy } from './actorProxy/interface'; import { RootActorProxy } from './actorProxy/root'; import { PathMapper } from '../util/pathMapper'; import { SourceMapsManager } from './sourceMaps/manager'; import { SourcesManager } from '../adapter/sourcesManager'; let log = Log.create('DebugConnection'); /** * Connects to a target supporting the Firefox Debugging Protocol and sends and receives messages */ export class DebugConnection { private transport: DebugProtocolTransport; private actors: Map; public readonly sourceMaps: SourceMapsManager; public readonly rootActor: RootActorProxy; constructor(pathMapper: PathMapper, sources: SourcesManager, socket: Socket) { this.actors = new Map(); this.sourceMaps = new SourceMapsManager(pathMapper, sources, this); this.rootActor = new RootActorProxy(this); this.transport = new DebugProtocolTransport(socket); this.transport.on('message', (message: FirefoxDebugProtocol.Response) => { if (this.actors.has(message.from)) { if (log.isDebugEnabled()) { log.debug(`Received response/event ${JSON.stringify(message)}`); } this.actors.get(message.from)!.receiveMessage(message); } else { log.error('Unknown actor: ' + JSON.stringify(message)); } }); } public sendRequest(request: T) { if (log.isDebugEnabled()) { log.debug(`Sending request ${JSON.stringify(request)}`); } this.transport.sendMessage(request); } public register(actor: ActorProxy): void { this.actors.set(actor.name, actor); } public unregister(actor: ActorProxy): void { this.actors.delete(actor.name); } public has(actorName: string): boolean { return this.actors.has(actorName); } public getOrCreate(actorName: string, createActor: () => T): T { if (this.actors.has(actorName)) { return this.actors.get(actorName); } else { return createActor(); } } public disconnect(): Promise { return this.transport.disconnect(); } } ================================================ FILE: src/adapter/firefox/launch.ts ================================================ import * as path from 'path'; import * as fs from 'fs-extra'; import { spawn, fork, ChildProcess } from 'child_process'; import FirefoxProfile from 'firefox-profile'; import { ParsedLaunchConfiguration, ParsedAttachConfiguration, getExecutableCandidates } from '../configuration'; import { isExecutable } from '../util/fs'; /** * Launches Firefox after preparing the debug profile. * If Firefox is launched "detached" (the default unless we are on MacOS and the `reAttach` flag * in the launch configuration is set to `false`), it creates one or even two intermediate * child processes for launching Firefox: * * one of them will wait for the Firefox process to exit and then remove any temporary directories * created by this debug adapter * * the other one is used to work around a bug in the node version that is distributed with VS Code * (and that runs this debug adapter), which fails to properly detach from child processes. * See [this issue](https://github.com/microsoft/vscode/issues/22022) for an explanation of the * bug and how to work around it. * * The intermediate child processes execute the [forkedLauncher](../util/forkedLauncher.ts) script. */ export async function launchFirefox(launch: ParsedLaunchConfiguration): Promise { await prepareDebugProfile(launch); // workaround for an issue with the snap version of VS Code // (see e.g. https://github.com/microsoft/vscode/issues/85344) const env = { ...process.env }; if (env.SNAP) { delete env['GDK_PIXBUF_MODULE_FILE']; delete env['GDK_PIXBUF_MODULEDIR']; } let childProc: ChildProcess | undefined = undefined; if (launch.detached) { let forkedLauncherPath = path.join(__dirname, './launcher.bundle.js'); let forkArgs: string[]; switch (launch.tmpDirs.length) { case 0: forkArgs = [ 'spawnDetached', launch.firefoxExecutable, ...launch.firefoxArgs ]; break; case 1: forkArgs = [ 'forkDetached', forkedLauncherPath, 'spawnAndRemove', launch.tmpDirs[0], launch.firefoxExecutable, ...launch.firefoxArgs ]; break; default: forkArgs = [ 'forkDetached', forkedLauncherPath, 'spawnAndRemove2', launch.tmpDirs[0], launch.tmpDirs[1], launch.firefoxExecutable, ...launch.firefoxArgs ]; break; } fork(forkedLauncherPath, forkArgs, { env, execArgv: [] }); } else { childProc = spawn(launch.firefoxExecutable, launch.firefoxArgs, { env, detached: true }); childProc.stdout?.on('data', () => undefined); childProc.stderr?.on('data', () => undefined); childProc.unref(); } return childProc; } export async function openNewTab( config: ParsedAttachConfiguration, description: FirefoxDebugProtocol.DeviceDescription ): Promise { if (!config.url) return true; let firefoxExecutable = config.firefoxExecutable; if (!firefoxExecutable) { let firefoxEdition: 'stable' | 'developer' | 'nightly' | undefined; if (description.channel === 'release') { firefoxEdition = 'stable'; } else if (description.channel === 'aurora') { firefoxEdition = 'developer'; } else if (description.channel === 'nightly') { firefoxEdition = 'nightly'; } if (firefoxEdition) { const candidates = getExecutableCandidates(firefoxEdition); for (let i = 0; i < candidates.length; i++) { if (await isExecutable(candidates[i])) { firefoxExecutable = candidates[i]; break; } } } if (!firefoxExecutable) return false; } const firefoxArgs = config.profileDir ? [ '--profile', config.profileDir ] : [ '-P', description.profile ]; firefoxArgs.push(config.url); spawn(firefoxExecutable, firefoxArgs); return true; } async function prepareDebugProfile(config: ParsedLaunchConfiguration): Promise { var profile = await createDebugProfile(config); for (let key in config.preferences) { profile.setPreference(key, config.preferences[key]); } profile.updatePreferences(); return profile; } function createDebugProfile(config: ParsedLaunchConfiguration): Promise { return new Promise(async (resolve, reject) => { if (config.srcProfileDir) { FirefoxProfile.copy({ profileDirectory: config.srcProfileDir, destinationDirectory: config.profileDir }, (err, profile) => { if (err || !profile) { reject(err); } else { profile.shouldDeleteOnExit(false); resolve(profile); } }); } else { await fs.ensureDir(config.profileDir); let profile = new FirefoxProfile({ destinationDirectory: config.profileDir }); profile.shouldDeleteOnExit(false); resolve(profile); } }); } ================================================ FILE: src/adapter/firefox/protocol.d.ts ================================================ declare namespace FirefoxDebugProtocol { interface Request { to: string; type: string; } interface Response { [key: string]: any; from: string; } interface Event extends Response { type: string; } interface TypedResponse extends Response { type: string; } interface ErrorResponse extends Response { error: string; message: string; } interface RequestTypesResponse extends Response { requestTypes: string[]; } interface InitialResponse extends Response { applicationType: string; traits: { breakpointWhileRunning?: boolean, nativeLogpoints?: boolean, watchpoints?: boolean, webExtensionAddonConnect?: boolean, noPauseOnThreadActorAttach?: boolean supportsEnableWindowGlobalThreadActors?: boolean }; } interface RootResponse extends Response { preferenceActor: string; addonsActor?: string; deviceActor: string; } interface GetProcessResponse { processDescriptor: ProcessDescriptor; } interface ProcessDescriptor { actor: string; id: number; isParent: boolean; isWindowlessParent: boolean; traits: { watcher: boolean; supportsReloadDescriptor: boolean; } } interface TabsResponse extends Response { tabs: (Tab | TabDescriptor)[]; } interface Tab { actor: string; title: string; url: string; consoleActor: string; threadActor?: string; } interface TabDescriptor { actor: string; } interface TabDescriptorTargetResponse extends Response { frame: Tab; } interface AddonsResponse extends Response { addons: Addon[]; } interface Addon { actor: string; id: string; name: string; isWebExtension?: boolean; url: string; consoleActor?: string; iconURL?: string; debuggable?: boolean; temporarilyInstalled?: boolean; traits?: { highlightable: boolean; networkMonitor: boolean; } } interface InstallAddonResponse extends Response { addon: { id: string, actor: boolean }; } interface GetTargetResponse extends Response { form: { actor: string; url: string; consoleActor: string; threadActor: string; } } interface DeviceDescriptionResponse extends Response { value: DeviceDescription; } interface DeviceDescription { profile: string; channel: string; } interface TabAttachedResponse extends TypedResponse { threadActor: string; } interface TabWillNavigateResponse extends TypedResponse { state: string; url: string; } interface TabDidNavigateResponse extends TypedResponse { state: string; url: string; title: string; } interface FramesDestroyedResponse extends TypedResponse { destroyAll: true; } interface FrameUpdateResponse extends TypedResponse { frames: { id: string; parentID?: string; url: string; title: string; }[]; } interface WorkersResponse extends Response { workers: Worker[]; } interface Worker { actor: string; url: string; type: number; } interface WorkerAttachedResponse extends TypedResponse { url: string; } interface WorkerConnectedResponse extends TypedResponse { threadActor: string; consoleActor: string; } interface LegacyGetCachedMessagesResponse extends Response { messages: LegacyCachedMessage[]; } type LegacyCachedMessage = (ConsoleAPICallResponseBody & { _type: 'ConsoleAPI' }) | (PageErrorResponseBody & { _type: 'PageError' }); interface GetCachedMessagesResponse extends Response { messages: CachedMessage[]; } type CachedMessage = { type: 'consoleAPICall', message: ConsoleAPICallResponseBody } | { type: 'pageError', pageError: PageErrorResponseBody }; interface PageErrorResponse extends TypedResponse { pageError: PageErrorResponseBody; } interface PageErrorResponseBody { errorMessage: string; sourceName: string; lineText: string; lineNumber: number; columnNumber: number; category: string; timeStamp: number; info: boolean; warning: boolean; error: boolean; exception: boolean; strict: boolean; private: boolean; stacktrace: { filename: string; functionname: string; line: number; column: number; }[] | null; } interface ConsoleAPICallResponse extends TypedResponse { message: ConsoleAPICallResponseBody; } interface ConsoleAPICallResponseBody { arguments: Grip[]; filename: string; functionName: string; groupName: string; lineNumber: number; columnNumber: number; category: string; timeStamp: number; level: string; workerType: string; private: boolean; styles: any[]; //? counter: any; //? timer: any; //? } interface ConsoleMessage { arguments: Grip[]; filename: string; level: string; lineNumber: number; columnNumber: number; timeStamp: number; timer: { name: string; duration: number; error?: string; }; styles?: any[]; // sourceId, innerWindowID } interface LogMessageResponse extends TypedResponse { message: string; timeStamp: number; } interface ResultIDResponse extends Response { resultID: string; } interface EvaluationResultResponse extends TypedResponse { input: string; resultID: string; result: Grip; exception?: Grip | null; exceptionMessage?: string; exceptionDocURL?: string; timestamp: number; helperResult: any; //? } interface AutoCompleteResponse extends Response { matches: string[]; matchProp: string; } interface ThreadPausedResponse extends TypedResponse { actor: string; frame: Frame; poppedFrames: Frame[]; why: ThreadPausedReason; } interface ThreadPausedReason { type: 'attached' | 'interrupted' | 'resumeLimit' | 'debuggerStatement' | 'breakpoint' | 'watchpoint' | 'getWatchpoint' | 'setWatchpoint' | 'clientEvaluated' | 'pauseOnDOMEvents' | 'alreadyPaused' | 'exception'; frameFinished?: CompletionValue; // if type is 'resumeLimit' or 'clientEvaluated' exception?: Grip; // if type is 'exception' actors?: string[]; // if type is 'breakpoint' or 'watchpoint' } interface GetBreakableLinesResponse extends Response { lines: number[]; } interface GetBreakpointPositionsCompressedResponse extends Response { positions: BreakpointPositions; } interface BreakpointPositions { [line: string]: number[]; } interface SourceResponse extends Response { source: Grip; } interface SetBreakpointResponse extends Response { actor: string; isPending: boolean; actualLocation?: SourceLocation; } interface PrototypeAndPropertiesResponse extends TypedResponse { prototype: ObjectGrip | { type: 'null' }; ownProperties: PropertyDescriptors; safeGetterValues?: SafeGetterValueDescriptors; ownSymbols?: NamedPropertyDescriptor[]; } interface GetWatcherResponse extends Response { actor: string; traits: { content_script?: boolean; }; } interface GetBreakpointListResponse extends Response { breakpointList: { actor: string; }; } interface GetThreadConfigurationResponse extends Response { configuration: { actor: string; }; } interface ThreadConfiguration { shouldPauseOnDebuggerStatement: boolean; pauseOnExceptions: boolean; ignoreCaughtExceptions: boolean; shouldIncludeSavedFrames: boolean; shouldIncludeAsyncLiveFrames: boolean; skipBreakpoints: boolean; logEventBreakpoints: boolean; observeAsmJS: boolean; pauseOverlay: boolean; } interface GetAvailableEventBreakpointsResponse extends Response { value: AvailableEventCategory[]; } interface AvailableEventCategory { name: string; events: AvailableEvent[]; } interface AvailableEvent { id: string; name: string; type: 'simple' | 'event' | 'script'; eventType?: string; notificationType?: string; targetTypes?: ('global' | 'node' | 'websocket' | 'worker' | 'xhr')[]; } interface TargetAvailableEvent extends Event { type: 'target-available-form'; target: { url?: string; actor: string; consoleActor: string; threadActor: string; targetType?: 'process' | 'frame' | 'worker' | 'shared_worker' | 'service_worker' | 'content_script'; isTopLevelTarget?: boolean; isFallbackExtensionDocument?: boolean; // set for all frame targets innerWindowId?: number; // set for iframe targets parentInnerWindowId?: number; // set for all frame targets topInnerWindowId?: number; // set for worker targets relatedDocumentInnerWindowId?: number; addonId?: string; }; } interface TargetDestroyedEvent extends Event { type: 'target-destroyed-form'; target: { actor: string; }; } interface DescriptorDestroyedEvent extends Event { type: 'descriptor-destroyed'; } interface ThreadState { state: 'paused' | 'resumed'; frame?: Frame; why?: ThreadPausedReason; } interface CompletionValue { return?: Grip; throw?: Grip; terminated?: boolean; } interface Frame { type: 'global' | 'call' | 'eval' | 'clientEvaluate' | 'wasmcall'; actor: string; depth: number; this?: Grip; where: SourceLocation; environment?: Environment; } interface GlobalFrame extends Frame { source: Source; } interface CallFrame extends Frame { displayName?: string; arguments: Grip[]; } interface EvalFrame extends Frame { } interface ClientEvalFrame extends Frame { } interface SourceLocation { actor: string; line?: number; column?: number; } interface Source { actor: string; url: string | null; introductionType?: 'scriptElement' | 'eval' | 'Function' | 'debugger eval' | 'wasm' | null; introductionUrl: string | null; isBlackBoxed: boolean; isPrettyPrinted: boolean; isSourceMapped: boolean; generatedUrl: string | null; sourceMapURL: string | null; addonID?: string; addonPath?: string; } interface SourceResource extends Source { resourceType: 'source'; } interface ConsoleMessageResource { resourceType: 'console-message'; message: ConsoleMessage; } interface ErrorMessageResource { resourceType: 'error-message'; pageError: PageError; } interface ThreadStateResource extends ThreadState { resourceType: 'thread-state'; } interface ResourceAvailableForm extends Event { type: 'resource-available-form'; resources: (SourceResource | ConsoleMessageResource | ErrorMessageResource | ThreadStateResource)[]; } type Sources = ['source', Source[]]; type ConsoleMessages = ['console-message', ConsoleMessage[]]; type ErrorMessages = ['error-message', PageError[]]; type ThreadStates = ['thread-state', ThreadState[]]; type Resources = (Sources | ConsoleMessages | ErrorMessages | ThreadStates)[]; interface ResourcesAvailableEvent extends Event { type: 'resources-available-array'; array: Resources; } interface FrameUpdateEvent extends Event { type: 'frameUpdate'; } interface PageError { errorMessage: string; sourceName: string; lineText: string; lineNumber: number; columnNumber: number; category: string; timeStamp: number; info: boolean; warning: boolean; error: boolean; exception: boolean; strict: boolean; private: boolean; stacktrace: { filename: string; functionname: string; line: number; column: number; }[] | null; } interface Environment { type?: 'object' | 'function' | 'with' | 'block'; actor?: string; parent?: Environment; } interface ObjectEnvironment extends Environment { object: Grip; } interface FunctionEnvironment extends Environment { function: { displayName: string; }; bindings: FunctionBindings; } interface WithEnvironment extends Environment { object: Grip; } interface BlockEnvironment extends Environment { bindings: Bindings; } interface Bindings { variables: PropertyDescriptors; } interface FunctionBindings extends Bindings { arguments: PropertyDescriptors[]; } interface PropertyDescriptor { enumerable: boolean; configurable: boolean; } interface DataPropertyDescriptor extends PropertyDescriptor { value: Grip; writable: boolean; } interface AccessorPropertyDescriptor extends PropertyDescriptor { get: Grip; set: Grip; } interface SafeGetterValueDescriptor { getterValue: Grip; getterPrototypeLevel: number; enumerable: boolean; writable: boolean; } interface PropertyDescriptors { [name: string]: PropertyDescriptor; } interface SafeGetterValueDescriptors { [name: string]: SafeGetterValueDescriptor; } interface NamedPropertyDescriptor { name: string; descriptor: PropertyDescriptor; } interface NamedDataPropertyDescriptor { name: string; descriptor: DataPropertyDescriptor; } type Grip = boolean | number | string | ComplexGrip; interface ComplexGrip { type: 'null' | 'undefined' | 'Infinity' | '-Infinity' | 'NaN' | '-0' | 'BigInt' | 'longString' | 'symbol' | 'object'; } interface ObjectGrip extends ComplexGrip { type: 'object'; class: string; actor: string; preview?: Preview; } type Preview = ObjectPreview | DatePreview | ObjectWithURLPreview | DOMNodePreview | DOMEventPreview | ArrayLikePreview | ErrorPreview; interface ObjectPreview { kind: 'Object'; ownProperties: { [name: string]: PropertyPreview }; ownPropertiesLength: number; ownSymbols?: NamedDataPropertyDescriptor[]; ownSymbolsLength?: number; safeGetterValues?: { [name: string]: SafeGetterValuePreview }; } interface PropertyPreview { configurable: boolean; enumerable: boolean; writable?: boolean; value?: Grip; get?: FunctionGrip; set?: FunctionGrip; } interface DatePreview { kind: undefined; timestamp: number; } interface ObjectWithURLPreview { kind: 'ObjectWithURL'; url: string; } interface DOMNodePreview { kind: 'DOMNode'; nodeType: number; nodeName: string; isConnected: boolean; location?: string; attributes?: { [name: string]: string }; attributesLength?: number; } interface DOMEventPreview { kind: 'DOMEvent'; type: string; properties: Object; target?: ObjectGrip; } interface ArrayLikePreview { kind: 'ArrayLike'; length: number; items?: (Grip | null)[]; } interface SafeGetterValuePreview { getterValue: Grip; getterPrototypeLevel: number; enumerable: boolean; writable: boolean; } interface ErrorPreview { kind: 'Error'; name: string; message: string; fileName: string; lineNumber: number; columnNumber: number; stack: string; } interface FunctionGrip extends ObjectGrip { name?: string; displayName?: string; userDisplayName?: string; parameterNames?: string[]; location?: { url: string; line?: number; column?: number; }; } interface LongStringGrip extends ComplexGrip { type: 'longString'; initial: string; length: number; actor: string; } interface SymbolGrip extends ComplexGrip { type: 'symbol'; name: string; } interface BigIntGrip extends ComplexGrip { type: 'BigInt'; text: string; } } ================================================ FILE: src/adapter/firefox/sourceMaps/README.md ================================================ This folder contains the debug adapter's source-map support. It contains the [`SourceMappingSourceActorProxy`](./source.ts), which implements the same interface as the [`SourceActorProxy`](../actorProxy/source.ts). When debugging source-mapped sources, there will be a `SourceActorProxy` for each generated source and, on top of that, a `SourceMappingSourceActorProxy` for each original source. Unlike other `ActorProxy` implementations, a `SourceMappingSourceActorProxy` does not correspond (and talk) directly to a Firefox actor. Instead, it forwards all requests, responses and events to/from the `SourceActorProxy` for the corresponding generated source, translating between original and generated locations on-the-fly. Likewise, there is the [`SourceMappingThreadActorProxy`](./thread.ts) working on top of a regular [`ThreadActorProxy`](../actorProxy/thread.ts), fetching and processing source-maps, creating the `SourceMappingSourceActorProxy` for each original source and translating between original and generated locations in the messages forwarded to/from the corresponding `ThreadActorProxy`. ================================================ FILE: src/adapter/firefox/sourceMaps/info.ts ================================================ import * as url from 'url'; import { Log } from '../../util/log'; import { isWindowsPlatform as detectWindowsPlatform } from '../../../common/util'; import { ISourceActorProxy, SourceActorProxy } from '../actorProxy/source'; import { SourceMapConsumer, BasicSourceMapConsumer, MappingItem } from 'source-map'; import { UrlLocation, LocationWithColumn } from '../../location'; let GREATEST_LOWER_BOUND = SourceMapConsumer.GREATEST_LOWER_BOUND; let LEAST_UPPER_BOUND = SourceMapConsumer.LEAST_UPPER_BOUND; const isWindowsPlatform = detectWindowsPlatform(); const windowsAbsolutePathRegEx = /^[a-zA-Z]:[\/\\]/; declare module "source-map" { interface MappingItem { lastGeneratedColumn?: number | null; } } const log = Log.create('SourceMappingInfo'); export class SourceMappingInfo { private columnSpansComputed = false; public get hasSourceMap(): boolean { return !!this.sourceMapConsumer; } public constructor( public readonly sources: ISourceActorProxy[], public readonly underlyingSource: SourceActorProxy, public readonly sourceMapUri?: string, private readonly sourceMapConsumer?: BasicSourceMapConsumer, private readonly sourceRoot?: string ) {} public computeColumnSpans(): void { if (this.sourceMapConsumer && !this.columnSpansComputed) { this.sourceMapConsumer.computeColumnSpans(); this.columnSpansComputed = true; } } public originalLocationFor(generatedLocation: LocationWithColumn): UrlLocation | undefined { if (!this.sourceMapConsumer) { return { ...generatedLocation, url: this.sources[0].url || undefined }; } let consumerArgs = { line: generatedLocation.line, column: generatedLocation.column || 0, bias: GREATEST_LOWER_BOUND }; if (this.underlyingSource.source.introductionType === 'wasm') { consumerArgs.column = consumerArgs.line; consumerArgs.line = 1; } let originalLocation = this.sourceMapConsumer.originalPositionFor(consumerArgs); if (originalLocation.source === null) { consumerArgs.bias = LEAST_UPPER_BOUND; originalLocation = this.sourceMapConsumer.originalPositionFor(consumerArgs); } if (originalLocation.source === null) { log.warn(`Got original location ${JSON.stringify(originalLocation)} for generated location ${JSON.stringify(generatedLocation)}`); return undefined; } originalLocation.source = this.resolveSource(originalLocation.source); if ((this.underlyingSource.source.introductionType === 'wasm') && originalLocation.line) { originalLocation.line--; } if (originalLocation.line !== null) { return { url: originalLocation.source, line: originalLocation.line, column: (originalLocation.column !== null) ? originalLocation.column : undefined }; } else { return undefined; } } public eachMapping(callback: (mapping: MappingItem) => void): void { if (this.sourceMapConsumer) { this.sourceMapConsumer.eachMapping(mappingItem => { const lastGeneratedColumn = (mappingItem.lastGeneratedColumn !== undefined) ? (mappingItem.lastGeneratedColumn || mappingItem.generatedColumn) : undefined; callback({ ...mappingItem, originalColumn: mappingItem.originalColumn, generatedColumn: mappingItem.generatedColumn, lastGeneratedColumn }); }, undefined, SourceMapConsumer.GENERATED_ORDER); } } public sourceContentFor(source: string): string | undefined { if (this.sourceMapConsumer) { return this.sourceMapConsumer.sourceContentFor(source) || undefined; } return undefined; } public syncBlackboxFlag(): void { if ((this.sources.length === 1) && (this.sources[0] === this.underlyingSource)) { return; } let blackboxUnderlyingSource = this.sources.every((source) => source.source.isBlackBoxed); if (this.underlyingSource.source.isBlackBoxed !== blackboxUnderlyingSource) { this.underlyingSource.setBlackbox(blackboxUnderlyingSource); } } public disposeSource(source: ISourceActorProxy): void { let sourceIndex = this.sources.indexOf(source); if (sourceIndex >= 0) { this.sources.splice(sourceIndex, 1); if (this.sources.length === 0) { this.underlyingSource.dispose(); } } } public resolveSource(sourceUrl: string): string { // some tools (e.g. create-react-app) use absolute _paths_ instead of _urls_ here, // we work around this bug by converting anything that looks like an absolute path // into a url if (isWindowsPlatform) { if (windowsAbsolutePathRegEx.test(sourceUrl)) { sourceUrl = encodeURI('file:///' + sourceUrl.replace(/\\/g, '/')); } } else { if (sourceUrl.startsWith('/')) { sourceUrl = encodeURI('file://' + sourceUrl); } } if (this.sourceRoot) { sourceUrl = url.resolve(this.sourceRoot, sourceUrl); } return sourceUrl; } public findUnresolvedSource(resolvedSource: string): string | undefined { if (!this.sourceMapConsumer) return undefined; for (const source of this.sourceMapConsumer.sources) { if ((source === resolvedSource) || (this.resolveSource(source) === resolvedSource)) { return source; } } return undefined; } } ================================================ FILE: src/adapter/firefox/sourceMaps/manager.ts ================================================ import * as url from 'url'; import * as fs from 'fs-extra'; import isAbsoluteUrl from 'is-absolute-url'; import { SourceMapConsumer, RawSourceMap } from 'source-map'; import { Log } from '../../util/log'; import { getUri, urlDirname } from '../../util/net'; import { PathMapper } from '../../util/pathMapper'; import { PendingRequest } from '../../util/pendingRequests'; import { DebugConnection } from '../connection'; import { SourceActorProxy } from '../actorProxy/source'; import { SourceMappingSourceActorProxy } from './source'; import { SourceMappingInfo } from './info'; import { UrlLocation } from '../../location'; import { SourcesManager } from '../../adapter/sourcesManager'; let log = Log.create('SourceMapsManager'); export class SourceMapsManager { private sourceMappingInfos = new Map>(); private pendingSources = new Map>(); public constructor( private readonly pathMapper: PathMapper, private readonly sources: SourcesManager, private readonly connection: DebugConnection ) {} public getOrCreateSourceMappingInfo(source: FirefoxDebugProtocol.Source): Promise { if (this.sourceMappingInfos.has(source.actor)) { if (this.pendingSources.has(source.actor)) { const pending = this.pendingSources.get(source.actor)!; this.pendingSources.delete(source.actor); (async () => { try { const sourceMappingInfos = await this.createSourceMappingInfo(source); pending.resolve(sourceMappingInfos); } catch(e) { pending.reject(e); } })(); } return this.sourceMappingInfos.get(source.actor)!; } else { let sourceMappingInfoPromise = this.createSourceMappingInfo(source); this.sourceMappingInfos.set(source.actor, sourceMappingInfoPromise); return sourceMappingInfoPromise; } } public getSourceMappingInfo(actor: string): Promise { if (this.sourceMappingInfos.has(actor)) { return this.sourceMappingInfos.get(actor)!; } else { const promise = new Promise((resolve, reject) => { this.pendingSources.set(actor, { resolve, reject }); }); this.sourceMappingInfos.set(actor, promise); return promise; } } public async findOriginalLocation( generatedUrl: string, line: number, column?: number ): Promise { if (generatedUrl === 'debugger eval code') { return undefined; } // sometimes Firefox sends a location shortly before sending information about // the corresponding source, so we try to wait for it const adapter = await Promise.race([ this.sources.getAdapterForUrl(generatedUrl), new Promise(resolve => setTimeout(resolve, 200)) ]); if (!adapter) { return undefined; } for (const infoPromise of this.sourceMappingInfos.values()) { const info = await infoPromise; if (generatedUrl === info.underlyingSource.url) { const originalLocation = info.originalLocationFor({ line, column: column || 0 }); if (originalLocation && originalLocation.url && originalLocation.line) { return { url: originalLocation.url, line: originalLocation.line, column: originalLocation.column || 0 }; } } } return undefined; } public async applySourceMapToFrame(frame: FirefoxDebugProtocol.Frame): Promise { const sourceMappingInfo = await this.getSourceMappingInfo(frame.where.actor); const source = sourceMappingInfo.underlyingSource.source; if (source && sourceMappingInfo && sourceMappingInfo.hasSourceMap && frame.where.line) { let originalLocation = sourceMappingInfo.originalLocationFor({ line: frame.where.line, column: frame.where.column || 0 }); if (originalLocation && originalLocation.url) { frame.where = { actor: `${source.actor}!${originalLocation.url}`, line: originalLocation.line || undefined, column: originalLocation.column || undefined } } } } private async createSourceMappingInfo(source: FirefoxDebugProtocol.Source): Promise { if (log.isDebugEnabled()) { log.debug(`Trying to sourcemap ${JSON.stringify(source)}`); } let sourceActor = this.connection.getOrCreate( source.actor, () => new SourceActorProxy(source, this.connection)); let sourceMapUrl = source.sourceMapURL; if (!sourceMapUrl) { return new SourceMappingInfo([sourceActor], sourceActor); } if (!isAbsoluteUrl(sourceMapUrl)) { if (source.url) { sourceMapUrl = url.resolve(urlDirname(source.url), sourceMapUrl); } else { log.warn(`Can't create absolute sourcemap URL from ${sourceMapUrl} - giving up`); return new SourceMappingInfo([sourceActor], sourceActor); } } let rawSourceMap: RawSourceMap | undefined = undefined; try { const sourceMapPath = this.pathMapper.convertFirefoxUrlToPath(sourceMapUrl); if (sourceMapPath && !isAbsoluteUrl(sourceMapPath)) { try { // TODO support remote development - this only works for local files const sourceMapString = await fs.readFile(sourceMapPath, 'utf8'); log.debug('Loaded sourcemap from disk'); rawSourceMap = JSON.parse(sourceMapString); log.debug('Parsed sourcemap'); } catch(e) { log.debug(`Failed reading sourcemap from ${sourceMapPath} - trying to fetch it from ${sourceMapUrl}`); } } if (!rawSourceMap) { const sourceMapString = await getUri(sourceMapUrl); log.debug('Received sourcemap'); rawSourceMap = JSON.parse(sourceMapString); log.debug('Parsed sourcemap'); } } catch(e) { log.warn(`Failed fetching sourcemap from ${sourceMapUrl} - giving up`); return new SourceMappingInfo([sourceActor], sourceActor); } let sourceMapConsumer = await new SourceMapConsumer(rawSourceMap!); let sourceMappingSourceActors: SourceMappingSourceActorProxy[] = []; let sourceRoot = rawSourceMap!.sourceRoot; if (!sourceRoot && source.url) { sourceRoot = urlDirname(source.url); } else if ((sourceRoot !== undefined) && !isAbsoluteUrl(sourceRoot)) { sourceRoot = url.resolve(sourceMapUrl, sourceRoot); } log.debug('Created SourceMapConsumer'); let sourceMappingInfo = new SourceMappingInfo( sourceMappingSourceActors, sourceActor, sourceMapUrl, sourceMapConsumer, sourceRoot); for (let origSource of sourceMapConsumer.sources) { origSource = sourceMappingInfo.resolveSource(origSource); let sourceMappingSource = this.createOriginalSource(source, origSource, sourceMapUrl); let sourceMappingSourceActor = new SourceMappingSourceActorProxy( sourceMappingSource, sourceMappingInfo); sourceMappingSourceActors.push(sourceMappingSourceActor); } return sourceMappingInfo; } private createOriginalSource( generatedSource: FirefoxDebugProtocol.Source, originalSourceUrl: string | null, sourceMapUrl: string ): FirefoxDebugProtocol.Source { return { actor: `${generatedSource.actor}!${originalSourceUrl}`, url: originalSourceUrl, introductionUrl: generatedSource.introductionUrl, introductionType: generatedSource.introductionType, generatedUrl: generatedSource.url, isBlackBoxed: false, isPrettyPrinted: false, isSourceMapped: true, sourceMapURL: sourceMapUrl } } } ================================================ FILE: src/adapter/firefox/sourceMaps/source.ts ================================================ import { Log } from '../../util/log'; import { ISourceActorProxy, SourceActorProxy } from '../actorProxy/source'; import { SourceMappingInfo } from './info'; import { getUri } from '../../util/net'; import { MappedLocation, Range } from '../../location'; const log = Log.create('SourceMappingSourceActorProxy'); interface Breakables { lines: number[]; locations: Map; } export class SourceMappingSourceActorProxy implements ISourceActorProxy { public get name(): string { return this.source.actor; } public get url(): string { return this.source.url!; } public get underlyingActor(): SourceActorProxy { return this.sourceMappingInfo.underlyingSource; } private allBreakablesPromise?: Promise; public constructor( public readonly source: FirefoxDebugProtocol.Source, private readonly sourceMappingInfo: SourceMappingInfo ) {} public async getBreakableLines(): Promise { if (!this.allBreakablesPromise) { this.allBreakablesPromise = this.getAllBreakables(); } const allBreakables = await this.allBreakablesPromise; return allBreakables.lines; } public async getBreakableLocations(line: number): Promise { if (!this.allBreakablesPromise) { this.allBreakablesPromise = this.getAllBreakables(); } const allBreakableLocations = await this.allBreakablesPromise; return allBreakableLocations.locations.get(line) || []; } private async getAllBreakables(): Promise { this.sourceMappingInfo.computeColumnSpans(); if (log.isDebugEnabled()) log.debug(`Calculating ranges for ${this.url} within its generated source`); const unresolvedSource = this.sourceMappingInfo.findUnresolvedSource(this.source.url!); const generatedRanges: Range[] = []; let currentRange: Range | undefined = undefined; this.sourceMappingInfo.eachMapping(mapping => { if (mapping.source === unresolvedSource) { if (!currentRange) { currentRange = { start: { line: mapping.generatedLine, column: mapping.generatedColumn }, end: { line: mapping.generatedLine, column: mapping.lastGeneratedColumn || 0 } } } else { const lastGeneratedColumn = (mapping as any).lastGeneratedColumn || mapping.generatedColumn; currentRange.end = { line: mapping.generatedLine, column: lastGeneratedColumn } } } else { if (currentRange) { generatedRanges.push(currentRange); currentRange = undefined; } } }); if (currentRange) { generatedRanges.push(currentRange); } const mappedBreakableLocations = new Map(); const originalBreakableColumns = new Map>(); for (const range of generatedRanges) { if (log.isDebugEnabled()) log.debug(`Fetching generated breakpoint locations for ${this.url}, ${range.start.line}:${range.start.column} - ${range.end.line}:${range.end.column}`); const generatedBreakableLocations = await this.sourceMappingInfo.underlyingSource.getBreakpointPositionsForRange(range); if (log.isDebugEnabled()) log.debug(`Computing original breakpoint locations for ${Object.keys(generatedBreakableLocations).length} generated lines`); for (const generatedLineString in generatedBreakableLocations) { for (const generatedColumn of generatedBreakableLocations[generatedLineString]) { const generatedLine = +generatedLineString; const originalLocation = this.sourceMappingInfo.originalLocationFor({ line: generatedLine, column: generatedColumn }); if ((originalLocation === undefined) || (originalLocation.line === null) || (originalLocation.url !== this.url)) { continue; } if (!mappedBreakableLocations.has(originalLocation.line)) { mappedBreakableLocations.set(originalLocation.line, []); originalBreakableColumns.set(originalLocation.line, new Set()); } const originalColumn = originalLocation.column || 0; if (!originalBreakableColumns.get(originalLocation.line)!.has(originalColumn)) { mappedBreakableLocations.get(originalLocation.line)!.push({ line: originalLocation.line, column: originalColumn, generated: { line: generatedLine, column: generatedColumn } }); originalBreakableColumns.get(originalLocation.line)!.add(originalColumn); } } } } const breakableLines: number[] = []; for (const line of mappedBreakableLocations.keys()) { if (mappedBreakableLocations.get(line)!.length > 0) { breakableLines.push(+line); } } breakableLines.sort(); return { lines: breakableLines, locations: mappedBreakableLocations }; } public async fetchSource(): Promise { if (log.isDebugEnabled()) log.debug(`Fetching source for ${this.url}`); let embeddedSource = this.sourceMappingInfo.sourceContentFor(this.url); if (embeddedSource) { if (log.isDebugEnabled()) log.debug(`Got embedded source for ${this.url}`); return embeddedSource; } else { const source = await getUri(this.url); if (log.isDebugEnabled()) log.debug(`Got non-embedded source for ${this.url}`); return source; } } public async setBlackbox(blackbox: boolean): Promise { this.source.isBlackBoxed = blackbox; this.sourceMappingInfo.syncBlackboxFlag(); } public dispose(): void { this.sourceMappingInfo.disposeSource(this); } } ================================================ FILE: src/adapter/firefox/sourceMaps/thread.ts ================================================ import { EventEmitter } from 'events'; import { Log } from '../../util/log'; import { DebugConnection } from '../connection'; import { IThreadActorProxy } from '../actorProxy/thread'; let log = Log.create('SourceMappingThreadActorProxy'); export class SourceMappingThreadActorProxy extends EventEmitter implements IThreadActorProxy { public constructor( private readonly underlyingActorProxy: IThreadActorProxy, private readonly connection: DebugConnection ) { super(); } public get name(): string { return this.underlyingActorProxy.name; } public async fetchStackFrames( start?: number, count?: number ): Promise { let stackFrames = await this.underlyingActorProxy.fetchStackFrames(start, count); await Promise.all(stackFrames.map((frame) => this.connection.sourceMaps.applySourceMapToFrame(frame))); return stackFrames; } public resume(resumeLimitType?: 'next' | 'step' | 'finish' | 'restart', frameActorID?: string): Promise { return this.underlyingActorProxy.resume(resumeLimitType, frameActorID); } public interrupt(immediately: boolean = true): Promise { return this.underlyingActorProxy.interrupt(immediately); } public async getAvailableEventBreakpoints() : Promise { return this.underlyingActorProxy.getAvailableEventBreakpoints(); } public dispose(): void { this.underlyingActorProxy.dispose(); } } ================================================ FILE: src/adapter/firefox/transport.ts ================================================ import { Socket } from 'net'; import { Log } from '../util/log'; import { EventEmitter } from 'events'; let log = Log.create('DebugProtocolTransport'); /** * Implements the Remote Debugging Protocol Stream Transport as defined in * https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#stream-transport * Currently bulk data packets are unsupported and error handling is nonexistent */ export class DebugProtocolTransport extends EventEmitter { private static initialBufferLength = 11; // must be large enough to receive a complete header private buffer: Buffer; private bufferedLength: number; private receivingHeader: boolean; constructor( private socket: Socket ) { super(); this.buffer = Buffer.alloc(DebugProtocolTransport.initialBufferLength); this.bufferedLength = 0; this.receivingHeader = true; let firstChunkReceived = false; this.socket.on('data', (chunk: Buffer) => { if (!firstChunkReceived) { firstChunkReceived = true; log.debug(`First chunk received from Firefox: ${chunk.toString('hex')}`); } let processedLength = 0; while (processedLength < chunk.length) { // copy the maximum number of bytes possible into this.buffer let copyLength = Math.min(chunk.length - processedLength, this.buffer.length - this.bufferedLength); chunk.copy(this.buffer, this.bufferedLength, processedLength, processedLength + copyLength); processedLength += copyLength; this.bufferedLength += copyLength; if (this.receivingHeader) { // did we receive a complete header yet? for (var i = 0; i < this.bufferedLength; i++) { if (this.buffer[i] === 58) { // header is complete: parse it let bodyLength = +this.buffer.toString('ascii', 0, i); if (bodyLength > 1000000) { log.debug(`Going to receive message with ${bodyLength} bytes in body (initial chunk contained ${chunk.length} bytes)`); } // create a buffer for the message body let bodyBuffer = Buffer.alloc(bodyLength); // copy the start of the body from this.buffer this.buffer.copy(bodyBuffer, 0, i + 1); // replace this.buffer with bodyBuffer this.buffer = bodyBuffer; this.bufferedLength = this.bufferedLength - (i + 1); this.receivingHeader = false; break; } } } else { // did we receive the complete body yet? if (this.bufferedLength === this.buffer.length) { if (this.bufferedLength > 1000000) { log.info(`Received ${this.bufferedLength} bytes`); } // body is complete: parse and emit it let msgString = this.buffer.toString('utf8'); this.emit('message', JSON.parse(msgString)); // get ready to receive the next header this.buffer = Buffer.alloc(DebugProtocolTransport.initialBufferLength); this.bufferedLength = 0; this.receivingHeader = true; } } } }); } public sendMessage(msg: any): void { let msgBuf = Buffer.from(JSON.stringify(msg), 'utf8'); this.socket.write(msgBuf.length + ':', 'ascii'); this.socket.write(msgBuf); } public disconnect(): Promise { return new Promise((resolve, reject) => { this.socket.on('close', () => resolve()); this.socket.end(); }); } } ================================================ FILE: src/adapter/firefoxDebugAdapter.ts ================================================ import { URI } from 'vscode-uri'; import { DebugProtocol } from '@vscode/debugprotocol'; import { DebugSession, StoppedEvent, OutputEvent, Thread, Variable, Breakpoint } from '@vscode/debugadapter'; import { Log } from './util/log'; import { accessorExpression } from './util/misc'; import { DebugAdapterBase } from './debugAdapterBase'; import { ThreadAdapter } from './adapter/thread'; import { SourceAdapter } from './adapter/source'; import { LaunchConfiguration, AttachConfiguration } from '../common/configuration'; import { parseConfiguration } from './configuration'; import { FirefoxDebugSession, ThreadConfiguration } from './firefoxDebugSession'; import { popupAutohidePreferenceKey } from './adapter/addonManager'; import { ObjectGripAdapter } from './adapter/objectGrip'; import { DataBreakpointsManager } from './adapter/dataBreakpointsManager'; import { normalizePath } from './util/fs'; let log = Log.create('FirefoxDebugAdapter'); export class FirefoxDebugAdapter extends DebugAdapterBase { private session!: FirefoxDebugSession; public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { super(debuggerLinesStartAt1, isServer); if (!isServer) { Log.consoleLog = (msg: string) => { this.sendEvent(new OutputEvent(msg + '\n')); } } } protected initialize(args: DebugProtocol.InitializeRequestArguments): DebugProtocol.Capabilities { return { supportsConfigurationDoneRequest: false, supportsEvaluateForHovers: false, supportsFunctionBreakpoints: false, supportsConditionalBreakpoints: true, supportsSetVariable: true, supportsCompletionsRequest: true, supportsDelayedStackTraceLoading: true, supportsHitConditionalBreakpoints: true, supportsLogPoints: true, supportsDataBreakpoints: true, supportsBreakpointLocationsRequest: true, supportsRestartFrame: true, supportsANSIStyling: true, exceptionBreakpointFilters: [ { filter: 'all', label: 'All Exceptions', default: false }, { filter: 'uncaught', label: 'Uncaught Exceptions', default: true }, { filter: 'debugger', label: 'Debugger Statements', default: true } ] }; } protected async launch(args: LaunchConfiguration): Promise { await this.startSession(args); } protected async attach(args: AttachConfiguration): Promise { await this.startSession(args); } private async startSession(config: LaunchConfiguration | AttachConfiguration): Promise { if (config.log) { Log.setConfig(config.log); } let parsedConfig = await parseConfiguration(config); this.session = new FirefoxDebugSession(parsedConfig, (ev) => this.sendEvent(ev)); await this.session.start(); } protected async breakpointLocations( args: DebugProtocol.BreakpointLocationsArguments ): Promise<{ breakpoints: DebugProtocol.BreakpointLocation[]; }> { if (!args.source.path) return { breakpoints: [] }; const sourceAdapter = await this.session.sources.getAdapterForPath(normalizePath(args.source.path)); const positions = await sourceAdapter.getBreakableLocations(args.line); const breakpoints: DebugProtocol.BreakpointLocation[] = []; for (const position of positions) { breakpoints.push({ line: position.line, column: position.column + 1 }); } return { breakpoints }; } protected setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): { breakpoints: DebugProtocol.Breakpoint[] } { const requestedBreakpoints = args.breakpoints; if (requestedBreakpoints === undefined) { log.error('setBreakpoints request without any breakpoints'); return { breakpoints: [] }; } // a path for local sources or a url (as seen by either VS Code or Firefox) for remote sources const sourcePathOrUrl = args.source.path; if (sourcePathOrUrl === undefined) { throw 'Couldn\'t set breakpoint: unknown source path'; } const breakpointInfos = this.session.breakpointsManager.setBreakpoints(requestedBreakpoints, sourcePathOrUrl); const breakpoints = breakpointInfos.map(breakpointInfo => { const breakpoint: DebugProtocol.Breakpoint = new Breakpoint( breakpointInfo.verified, breakpointInfo.requestedBreakpoint.line, breakpointInfo.requestedBreakpoint.column ); breakpoint.id = breakpointInfo.id; return breakpoint; }); return { breakpoints }; } protected setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): void { log.debug(`Setting exception filters: ${JSON.stringify(args.filters)}`); const threadConfiguration: ThreadConfiguration = { pauseOnExceptions: args.filters.includes('all') || args.filters.includes('uncaught'), ignoreCaughtExceptions: !args.filters.includes('all'), shouldPauseOnDebuggerStatement: args.filters.includes('debugger'), }; this.session.setThreadConfiguration(threadConfiguration); } protected async pause(args: DebugProtocol.PauseArguments): Promise { let threadAdapter = this.getThreadAdapter(args.threadId); this.session.setActiveThread(threadAdapter); await threadAdapter.interrupt(); let stoppedEvent = new StoppedEvent('interrupt', threadAdapter.id); (stoppedEvent).body.allThreadsStopped = false; this.sendEvent(stoppedEvent); } protected async next(args: DebugProtocol.NextArguments): Promise { let threadAdapter = this.getThreadAdapter(args.threadId); this.session.setActiveThread(threadAdapter); await threadAdapter.stepOver(); } protected async stepIn(args: DebugProtocol.StepInArguments): Promise { let threadAdapter = this.getThreadAdapter(args.threadId); this.session.setActiveThread(threadAdapter); await threadAdapter.stepIn(); } protected async stepOut(args: DebugProtocol.StepOutArguments): Promise { let threadAdapter = this.getThreadAdapter(args.threadId); this.session.setActiveThread(threadAdapter); await threadAdapter.stepOut(); } protected async continue(args: DebugProtocol.ContinueArguments): Promise<{ allThreadsContinued?: boolean }> { let threadAdapter = this.getThreadAdapter(args.threadId); this.session.setActiveThread(threadAdapter); await threadAdapter.resume(); return { allThreadsContinued: false }; } protected async getSource(args: DebugProtocol.SourceArguments): Promise<{ content: string, mimeType?: string }> { let sourceAdapter: SourceAdapter | undefined; if (args.sourceReference !== undefined) { sourceAdapter = this.session.sources.getAdapterForID(args.sourceReference); } else if (args.source?.path) { sourceAdapter = this.session.sources.findSourceAdaptersForPathOrUrl(args.source.path)[0]; if (!sourceAdapter && args.source.path.indexOf('?') < 0) { // workaround for VSCode issue #32845: the url may have contained a query string that got lost, // in this case we look for a Source whose url is the same if the query string is removed sourceAdapter = this.session.sources.findSourceAdaptersForUrlWithoutQuery(args.source.path)[0]; } } if (!sourceAdapter) { throw new Error('Failed sourceRequest: the requested source can\'t be found'); } let sourceGrip = await sourceAdapter.fetchSource(); if (typeof sourceGrip === 'string') { return { content: sourceGrip, mimeType: 'text/javascript' }; } else { let longStringGrip = sourceGrip; let longStringActor = this.session.getOrCreateLongStringGripActorProxy(longStringGrip); let content = await longStringActor.fetchContent(); return { content, mimeType: 'text/javascript' }; } } protected getThreads(): { threads: DebugProtocol.Thread[] } { log.debug(`${this.session.threads.count} threads`); let threads = this.session.threads.map( (threadAdapter) => new Thread(threadAdapter.id, threadAdapter.name)); return { threads }; } protected async getStackTrace(args: DebugProtocol.StackTraceArguments): Promise<{ stackFrames: DebugProtocol.StackFrame[], totalFrames?: number }> { let threadAdapter = this.getThreadAdapter(args.threadId); this.session.setActiveThread(threadAdapter); let [frameAdapters, totalFrames] = await threadAdapter.fetchStackFrames(args.startFrame || 0, args.levels || 0); let stackFrames = await Promise.all( frameAdapters.map((frameAdapter) => frameAdapter.getStackframe()) ); return { stackFrames, totalFrames }; } protected async getScopes(args: DebugProtocol.ScopesArguments): Promise<{ scopes: DebugProtocol.Scope[] }> { let frameAdapter = this.session.frames.find(args.frameId); if (!frameAdapter) { throw new Error('Failed scopesRequest: the requested frame can\'t be found'); } this.session.setActiveThread(frameAdapter.threadAdapter); const scopeAdapters = await frameAdapter.getScopeAdapters(); const scopes = scopeAdapters.map((scopeAdapter) => scopeAdapter.getScope()); return { scopes }; } protected async getVariables(args: DebugProtocol.VariablesArguments): Promise<{ variables: DebugProtocol.Variable[] }> { let variablesProvider = this.session.variablesProviders.find(args.variablesReference); if (!variablesProvider) { throw new Error('Failed variablesRequest: the requested object reference can\'t be found'); } this.session.setActiveThread(variablesProvider.threadAdapter); try { let variables = await variablesProvider.threadAdapter.fetchVariables(variablesProvider); return { variables }; } catch(err) { let msg: string; if (err === 'No such actor') { msg = 'Value can\'t be inspected - this is probably due to Firefox bug #1249962'; } else { msg = String(err); } return { variables: [ new Variable('Error from debugger', msg) ]}; } } protected async setVariable(args: DebugProtocol.SetVariableArguments): Promise<{ value: string, variablesReference?: number }> { let variablesProvider = this.session.variablesProviders.find(args.variablesReference); if (variablesProvider === undefined) { throw new Error('Failed setVariableRequest: the requested context can\'t be found') } if (variablesProvider.referenceFrame === undefined) { throw new Error('Failed setVariableRequest: the requested context has no associated stack frame'); } let referenceExpression = accessorExpression(variablesProvider.referenceExpression, args.name); let setterExpression = `${referenceExpression} = ${args.value}`; let frameActorName = variablesProvider.referenceFrame.frame.actor; let result = await variablesProvider.threadAdapter.evaluate(setterExpression, false, frameActorName); return { value: result.value, variablesReference: result.variablesReference }; } protected async evaluate(args: DebugProtocol.EvaluateArguments): Promise<{ result: string, type?: string, variablesReference: number, namedVariables?: number, indexedVariables?: number }> { let variable: Variable | undefined = undefined; if (args.context === 'watch') { if (args.frameId !== undefined) { let frameAdapter = this.session.frames.find(args.frameId); if (frameAdapter !== undefined) { this.session.setActiveThread(frameAdapter.threadAdapter); let threadAdapter = frameAdapter.threadAdapter; let frameActorName = frameAdapter.frame.actor; variable = await threadAdapter.evaluate(args.expression, true, frameActorName); } else { log.warn(`Couldn\'t find specified frame for evaluating ${args.expression}`); throw 'not available'; } } else { let threadAdapter = this.session.getActiveThread(); if (threadAdapter !== undefined) { variable = await threadAdapter.evaluate(args.expression, true); } else { log.info(`Couldn't find a thread for evaluating watch ${args.expression}`); throw 'not available'; } } } else { let threadAdapter = this.session.getActiveThread(); if (threadAdapter !== undefined) { let frameActorName: string | undefined = undefined; if (args.frameId !== undefined) { let frameAdapter = this.session.frames.find(args.frameId); if (frameAdapter !== undefined) { frameActorName = frameAdapter.frame.actor; } } variable = await threadAdapter.evaluate(args.expression, false, frameActorName); } else { log.info(`Couldn't find a thread for evaluating ${args.expression}`); throw 'not available'; } } return { result: variable.value, variablesReference: variable.variablesReference }; } protected async getCompletions(args: DebugProtocol.CompletionsArguments): Promise<{ targets: DebugProtocol.CompletionItem[] }> { let matches: string[]; if (args.frameId !== undefined) { let frameAdapter = this.session.frames.find(args.frameId); if (frameAdapter === undefined) { log.warn(`Couldn\'t find specified frame for auto-completing ${args.text}`); throw 'not available'; } this.session.setActiveThread(frameAdapter.threadAdapter); let threadAdapter = frameAdapter.threadAdapter; let frameActorName = frameAdapter.frame.actor; matches = await threadAdapter.autoComplete(args.text, args.column - 1, frameActorName); } else { let threadAdapter = this.session.getActiveThread(); if (threadAdapter === undefined) { log.warn(`Couldn't find a thread for auto-completing ${args.text}`); throw 'not available'; } matches = await threadAdapter.autoComplete(args.text, args.column - 1); } return { targets: matches.map((match) => { label: match }) }; } protected async dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null, description: string, accessTypes?: DebugProtocol.DataBreakpointAccessType[], canPersist?: boolean }> { if (!this.session.dataBreakpointsManager) { return { dataId: null, description: "Your version of Firefox doesn't support watchpoints / data breakpoints" }; } if (args.variablesReference !== undefined) { const provider = this.session.variablesProviders.find(args.variablesReference); if (provider instanceof ObjectGripAdapter) { try { await provider.actor.threadLifetime(); provider.threadAdapter.threadLifetime(provider); return { dataId: DataBreakpointsManager.encodeDataId(args.variablesReference, args.name), description: args.name, accessTypes: [ 'read', 'write' ] }; } catch {} } } return { dataId: null, description: 'Data breakpoints are only supported on object properties' }; } protected async setDataBreakpoints(args: DebugProtocol.SetDataBreakpointsArguments): Promise<{ breakpoints: DebugProtocol.Breakpoint[] }> { if (!this.session.dataBreakpointsManager) { if (args.breakpoints.length === 0) { return { breakpoints: [] }; } else { throw "Your version of Firefox doesn't support watchpoints / data breakpoints"; } } await this.session.dataBreakpointsManager.setDataBreakpoints(args.breakpoints); return { breakpoints: new Array(args.breakpoints.length).fill({ verified: true }) } } protected async restartFrame(args: DebugProtocol.RestartFrameArguments): Promise { const frameAdapter = this.session.frames.find(args.frameId); if (!frameAdapter) { throw new Error('Failed restartFrameRequest: the requested frame can\'t be found'); } this.session.setActiveThread(frameAdapter.threadAdapter); await frameAdapter.threadAdapter.restartFrame(frameAdapter.frame.actor); } protected async reloadAddon(): Promise { if (!this.session.addonManager) { throw 'This command is only available when debugging an addon' } await this.session.addonManager.reloadAddon(); } protected async toggleSkippingFile(url: string): Promise { if (url.startsWith('file://')) { const path = URI.parse(url).fsPath; await this.session.skipFilesManager.toggleSkipping(path); } else { await this.session.skipFilesManager.toggleSkipping(url); } } protected async setPopupAutohide(enabled: boolean): Promise { await this.session.preferenceActor.setBoolPref(popupAutohidePreferenceKey, !enabled); } protected async togglePopupAutohide(): Promise { const currentValue = await this.session.preferenceActor.getBoolPref(popupAutohidePreferenceKey); const newValue = !currentValue; await this.session.preferenceActor.setBoolPref(popupAutohidePreferenceKey, newValue); return !newValue; } protected setActiveEventBreakpoints(args: string[] | undefined): Promise { return this.session.eventBreakpointsManager.setActiveEventBreakpoints(args ?? []); } protected async disconnect(args: DebugProtocol.DisconnectArguments): Promise { await this.session.stop(); } private getThreadAdapter(threadId: number): ThreadAdapter { let threadAdapter = this.session.threads.find(threadId); if (!threadAdapter) { throw new Error(`Unknown threadId ${threadId}`); } return threadAdapter; } } DebugSession.run(FirefoxDebugAdapter); ================================================ FILE: src/adapter/firefoxDebugSession.ts ================================================ import * as path from 'path'; import * as fs from 'fs-extra'; import { Socket } from 'net'; import { ChildProcess } from 'child_process'; import * as chokidar from 'chokidar'; import debounce from 'debounce'; import isAbsoluteUrl from 'is-absolute-url'; import { DebugProtocol } from '@vscode/debugprotocol'; import { InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, ThreadEvent, ContinuedEvent, Event } from '@vscode/debugadapter'; import { Log } from './util/log'; import { AddonManager } from './adapter/addonManager'; import { launchFirefox, openNewTab } from './firefox/launch'; import { DebugConnection } from './firefox/connection'; import { ObjectGripActorProxy } from './firefox/actorProxy/objectGrip'; import { LongStringGripActorProxy } from './firefox/actorProxy/longString'; import { AddonsActorProxy } from './firefox/actorProxy/addons'; import { IThreadActorProxy } from './firefox/actorProxy/thread'; import { ConsoleActorProxy } from './firefox/actorProxy/console'; import { ISourceActorProxy } from './firefox/actorProxy/source'; import { FrameAdapter } from './adapter/frame'; import { SourceAdapter } from './adapter/source'; import { VariablesProvider } from './adapter/variablesProvider'; import { VariableAdapter } from './adapter/variable'; import { Registry } from './adapter/registry'; import { TargetType, ThreadAdapter } from './adapter/thread'; import { ConsoleAPICallAdapter } from './adapter/consoleAPICall'; import { BreakpointsManager } from './adapter/breakpointsManager'; import { DataBreakpointsManager } from './adapter/dataBreakpointsManager'; import { SkipFilesManager } from './adapter/skipFilesManager'; import { ParsedConfiguration } from './configuration'; import { PathMapper } from './util/pathMapper'; import { isWindowsPlatform as detectWindowsPlatform, delay } from '../common/util'; import { connect, waitForSocket } from './util/net'; import { NewSourceEventBody, ThreadStartedEventBody, ThreadExitedEventBody } from '../common/customEvents'; import { PreferenceActorProxy } from './firefox/actorProxy/preference'; import { DeviceActorProxy } from './firefox/actorProxy/device'; import { TargetActorProxy } from './firefox/actorProxy/target'; import { BreakpointListActorProxy } from './firefox/actorProxy/breakpointList'; import { SourceMapsManager } from './firefox/sourceMaps/manager'; import { SourcesManager } from './adapter/sourcesManager'; import { ThreadConfigurationActorProxy } from './firefox/actorProxy/threadConfiguration'; import { DescriptorAdapter } from './adapter/descriptor'; import { DescriptorActorProxy } from './firefox/actorProxy/descriptor'; import { EventBreakpointsManager } from './adapter/eventBreakpointsManager'; import { renderGrip } from './adapter/preview'; import { shortenUrl } from './util/misc'; let log = Log.create('FirefoxDebugSession'); let consoleActorLog = Log.create('ConsoleActor'); export type ThreadConfiguration = Pick< FirefoxDebugProtocol.ThreadConfiguration, 'pauseOnExceptions' | 'ignoreCaughtExceptions' | 'shouldPauseOnDebuggerStatement' >; export class FirefoxDebugSession { public readonly isWindowsPlatform = detectWindowsPlatform(); public processDescriptorMode!: boolean; public readonly pathMapper: PathMapper; public readonly sources: SourcesManager; public sourceMaps!: SourceMapsManager; public readonly breakpointsManager: BreakpointsManager; public readonly dataBreakpointsManager: DataBreakpointsManager; public readonly eventBreakpointsManager: EventBreakpointsManager; public readonly skipFilesManager: SkipFilesManager; public readonly addonManager?: AddonManager; private reloadWatcher?: chokidar.FSWatcher; private firefoxProc?: ChildProcess; private firefoxClosedPromise?: Promise; public firefoxDebugConnection!: DebugConnection; private firefoxDebugSocketClosed = false; private firefoxDebugSocketClosedPromise?: Promise; public preferenceActor!: PreferenceActorProxy; public addonsActor?: AddonsActorProxy; public deviceActor!: DeviceActorProxy; public readonly descriptors = new Registry(); public readonly threads = new Registry(); public readonly frames = new Registry(); public readonly variablesProviders = new Registry(); public readonly breakpointLists = new Registry(); public readonly threadConfigurators = new Registry(); private readonly threadsByTargetActorName = new Map(); public threadConfiguration: ThreadConfiguration = { pauseOnExceptions: true, ignoreCaughtExceptions: true, shouldPauseOnDebuggerStatement: true, }; private reloadTabs = false; /** * The ID of the last thread that the user interacted with. This thread will be used when the * user wants to evaluate an expression in VS Code's debug console. */ private lastActiveThreadId: number = 0; public constructor( public readonly config: ParsedConfiguration, public readonly sendEvent: (ev: DebugProtocol.Event) => void ) { this.pathMapper = new PathMapper(this.config.pathMappings, this.config.pathMappingIndex, this.config.addon); this.sources = new SourcesManager(this.pathMapper); this.breakpointsManager = new BreakpointsManager(this); this.dataBreakpointsManager = new DataBreakpointsManager(this.variablesProviders); this.eventBreakpointsManager = new EventBreakpointsManager(this); this.skipFilesManager = new SkipFilesManager(this.config.filesToSkip, this.sources, this.threads); if (this.config.addon) { this.addonManager = new AddonManager(this); } } /** * Connect to Firefox and start the debug session. Returns a Promise that is resolved when the * initial response from Firefox was processed. */ public start(): Promise { return new Promise(async (resolve, reject) => { let socket: Socket; try { log.debug("Connecting to Firefox"); socket = await this.connectToFirefox(); log.debug("Connected"); } catch(err: any) { if (!err?.message && this.config.attach) { reject(new Error(`Couldn't connect to Firefox - please ensure it is running and listening on port ${this.config.attach.port} for debugger connections`)); } else { reject(err); } return; } this.firefoxDebugSocketClosedPromise = new Promise(resolve => { socket.once('close', () => { log.info('Connection to Firefox closed - terminating debug session'); this.firefoxDebugSocketClosed = true; resolve(); this.sendEvent(new TerminatedEvent()); }); }); this.firefoxDebugConnection = new DebugConnection(this.pathMapper, this.sources, socket); this.sourceMaps = this.firefoxDebugConnection.sourceMaps; let rootActor = this.firefoxDebugConnection.rootActor; if (!this.processDescriptorMode) { // attach to all tabs, register the corresponding threads and inform VSCode about them rootActor.onTabOpened(async (tabDescriptorActor) => { if (this.reloadTabs) { await tabDescriptorActor.reload(); await delay(200); } const adapter = await this.attachDescriptor(tabDescriptorActor); await adapter.watcherActor.watchResources(['console-message', 'error-message', 'source', 'thread-state']); }); rootActor.onTabListChanged(() => { rootActor.fetchTabs(); }); } rootActor.onInit(async (initialResponse) => { if (initialResponse.traits.webExtensionAddonConnect && !initialResponse.traits.nativeLogpoints) { reject('Your version of Firefox is not supported anymore - please upgrade to Firefox 68 or later'); return; } this.processDescriptorMode = !!initialResponse.traits.supportsEnableWindowGlobalThreadActors; const actors = await rootActor.fetchRoot(); this.preferenceActor = actors.preference; this.addonsActor = actors.addons; this.deviceActor = actors.device; let adapter: DescriptorAdapter | undefined; if (this.processDescriptorMode) { const parentProcess = await rootActor.getProcess(0); adapter = await this.attachDescriptor(parentProcess); } else { rootActor.fetchTabs().then(() => this.reloadTabs = false); } if (this.addonManager) { if (actors.addons) { await this.addonManager.sessionStarted(rootActor, actors.addons, actors.preference); } else { reject('No AddonsActor received from Firefox'); } } if (this.processDescriptorMode) { await adapter?.watcherActor.watchResources(['console-message', 'error-message', 'source', 'thread-state']); } resolve(); }); if (this.config.reloadOnChange) { this.reloadWatcher = chokidar.watch(this.config.reloadOnChange.watch, { ignored: this.config.reloadOnChange.ignore, ignoreInitial: true }); let reload: () => void; if (this.config.addon) { reload = () => { if (this.addonManager) { log.debug('Reloading add-on'); this.addonManager.reloadAddon(); } } } else { reload = () => { log.debug('Reloading tabs'); for (let [, thread] of this.threads) { if (thread.type === 'tab') { thread.targetActor.reload(); } } } } if (this.config.reloadOnChange.debounce > 0) { reload = debounce(reload, this.config.reloadOnChange.debounce); } this.reloadWatcher.on('add', reload); this.reloadWatcher.on('change', reload); this.reloadWatcher.on('unlink', reload); } // now we are ready to accept breakpoints -> fire the initialized event to give UI a chance to set breakpoints this.sendEvent(new InitializedEvent()); }); } /** * Terminate the debug session */ public async stop(): Promise { await this.disconnectFirefoxAndCleanup(); } public setThreadConfiguration(threadConfiguration: ThreadConfiguration) { this.threadConfiguration = threadConfiguration; for (let [, threadConfigurator] of this.threadConfigurators) { threadConfigurator.updateConfiguration(this.threadConfiguration); } } public setActiveThread(threadAdapter: ThreadAdapter): void { this.lastActiveThreadId = threadAdapter.id; } public getActiveThread(): ThreadAdapter | undefined { let threadAdapter = this.threads.find(this.lastActiveThreadId); if (threadAdapter !== undefined) { return threadAdapter; } // last active thread not found -> we return the first thread we get from the registry for (let [, threadAdapter] of this.threads) { this.setActiveThread(threadAdapter); return threadAdapter; } return undefined; } public getOrCreateObjectGripActorProxy(objectGrip: FirefoxDebugProtocol.ObjectGrip): ObjectGripActorProxy { return this.firefoxDebugConnection.getOrCreate(objectGrip.actor, () => new ObjectGripActorProxy(objectGrip.actor, this.firefoxDebugConnection)); } public getOrCreateLongStringGripActorProxy(longStringGrip: FirefoxDebugProtocol.LongStringGrip): LongStringGripActorProxy { return this.firefoxDebugConnection.getOrCreate(longStringGrip.actor, () => new LongStringGripActorProxy(longStringGrip, this.firefoxDebugConnection)); } private async connectToFirefox(): Promise { let socket: Socket | undefined = undefined; if (this.config.attach) { try { socket = await connect(this.config.attach.port, this.config.attach.host); this.reloadTabs = this.config.attach.reloadTabs; } catch(err) { if (!this.config.launch) { throw err; } } } if (socket === undefined) { const firefoxProc = await launchFirefox(this.config.launch!); if (firefoxProc && !this.config.launch!.detached) { // set everything up so that Firefox can be terminated at the end of this debug session this.firefoxProc = firefoxProc; // firefoxProc may be a short-lived startup process - we remove the reference to it // when it exits so that we don't try to kill it with a SIGTERM signal (which may // end up killing an unrelated process) at the end of this debug session this.firefoxProc.once('exit', () => { this.firefoxProc = undefined; }); // the `close` event from firefoxProc seems to be the only reliable notification // that Firefox is exiting this.firefoxClosedPromise = new Promise(resolve => { this.firefoxProc!.once('close', resolve); }); } socket = await waitForSocket(this.config.launch!.port, this.config.launch!.timeout); } return socket; } private async disconnectFirefoxAndCleanup(): Promise { if (this.reloadWatcher !== undefined) { this.reloadWatcher.close(); this.reloadWatcher = undefined; } if (!this.config.terminate) { await this.firefoxDebugConnection.disconnect(); return; } if (this.firefoxProc) { log.debug('Trying to kill Firefox using a SIGTERM signal'); this.firefoxProc.kill('SIGTERM'); await Promise.race([ this.firefoxClosedPromise, delay(1000) ]); } else if (!this.firefoxDebugSocketClosed && this.addonsActor) { log.debug('Trying to close Firefox using the Terminator WebExtension'); const terminatorPath = path.join(__dirname, 'terminator'); await this.addonsActor.installAddon(terminatorPath); await Promise.race([ this.firefoxDebugSocketClosedPromise, delay(1000) ]); } if (!this.firefoxDebugSocketClosed) { log.warn("Couldn't terminate Firefox"); await this.firefoxDebugConnection.disconnect(); return; } if (this.config.launch && (this.config.launch.tmpDirs.length > 0)) { // after closing all connections to this debug adapter Firefox will still be using // the temporary profile directory for a short while before exiting await delay(500); log.debug("Removing " + this.config.launch.tmpDirs.join(" , ")); try { await Promise.all(this.config.launch.tmpDirs.map( (tmpDir) => fs.remove(tmpDir))); } catch (err) { log.warn(`Failed to remove temporary directory: ${err}`); } } } public async attachDescriptor(descriptorActor: DescriptorActorProxy) { const watcherActor = await descriptorActor.getWatcher(); const [configurator, breakpointList] = await Promise.all([ watcherActor.getThreadConfiguration(), watcherActor.getBreakpointList() ]); const adapter = new DescriptorAdapter( this.descriptors, this.threadConfigurators, this.breakpointLists, descriptorActor, watcherActor, configurator, breakpointList ); descriptorActor.onDestroyed(() => { for (const threadAdapter of adapter.threads) { this.sendThreadExitedEvent(threadAdapter); this.threadsByTargetActorName.delete(threadAdapter.targetActor.name); } adapter.dispose(); }); watcherActor.onTargetAvailable(async ([targetActor, threadActor, consoleActor]) => { let skip = false; if (descriptorActor.type === 'webExtension' && targetActor.target.isFallbackExtensionDocument) { skip = true; } if (targetActor.target.addonId && (!this.addonManager || targetActor.target.addonId !== await this.addonManager.addonId)) { skip = true; } const url = targetActor.target.url; if ( descriptorActor.type === 'process' && !targetActor.target.addonId && url && (!this.config.tabFilter.include.some(tabFilter => tabFilter.test(url)) || this.config.tabFilter.exclude.some(tabFilter => tabFilter.test(url))) ) { skip = true; } if (skip) { log.warn('Not attaching to this thread'); targetActor.onThreadState(event => { if (event.state === 'paused') { log.info("Detached thread paused, resuming"); threadActor.resume(); } }); return; } let type: TargetType; let name: string; if ( (descriptorActor.type === 'tab' && targetActor.name.includes("contentScriptTarget")) || (descriptorActor.type === 'process' && targetActor.target.targetType === 'content_script') ) { type = 'contentScript'; name = 'Content scripts'; } else if ( descriptorActor.type === 'webExtension' || (descriptorActor.type === 'process' && targetActor.target.addonId) ) { type = 'backgroundScript'; name = 'Background scripts'; } else { const { parentInnerWindowId, relatedDocumentInnerWindowId, url } = targetActor.target; if (relatedDocumentInnerWindowId) { type = 'worker'; name = `Worker ${shortenUrl(url ?? '')}`; } else if (parentInnerWindowId) { type = 'iframe'; name = `IFrame ${shortenUrl(url ?? '')}`; } else { type = 'tab'; name = `Tab ${shortenUrl(url ?? '')}`; } } const threadAdapter = await this.attachThread(type, name, targetActor, threadActor, consoleActor); adapter.threads.add(threadAdapter); }); watcherActor.onTargetDestroyed(targetActorName => { const threadAdapter = this.threadsByTargetActorName.get(targetActorName); if (!threadAdapter) { log.debug(`Unknown target actor ${targetActorName} (already destroyed?)`); return; } if (threadAdapter.type === 'tab' && this.config.clearConsoleOnReload) { this.sendEvent(new OutputEvent('\x1b[2J')); } threadAdapter.targetActor.destroyed = true; this.sendThreadExitedEvent(threadAdapter); this.threadsByTargetActorName.delete(targetActorName); adapter.threads.delete(threadAdapter); threadAdapter.dispose(); }); await Promise.all([ watcherActor.watchTargets('frame'), watcherActor.watchTargets('worker'), this.config.addon && watcherActor.supportsContentScriptTargets ? watcherActor.watchTargets('content_script') : Promise.resolve(), configurator.updateConfiguration(this.threadConfiguration) ]); return adapter; } private async attachThread( type: TargetType, name: string, targetActor: TargetActorProxy, threadActor: IThreadActorProxy, consoleActor: ConsoleActorProxy, ) { const threadAdapter = new ThreadAdapter(type, name, threadActor, targetActor, consoleActor, this); log.info(`Attaching ${name}`); this.threadsByTargetActorName.set(targetActor.name, threadAdapter); this.sendThreadStartedEvent(threadAdapter); targetActor.onConsoleMessages(async messages => { for (const message of messages) { await this.sendConsoleMessage(message, threadAdapter); } }); targetActor.onErrorMessages(async messages => { for (const { pageError } of messages) { consoleActorLog.debug(`Page Error: ${JSON.stringify(pageError)}`); if (pageError.category === 'content javascript') { let category = pageError.exception ? 'stderr' : 'stdout'; let outputEvent = new OutputEvent(pageError.errorMessage + '\n', category); await this.addLocation(outputEvent, pageError.sourceName, pageError.lineNumber, pageError.columnNumber); this.sendEvent(outputEvent); } } }); targetActor.onSources(sources => { for (const source of sources) { this.attachSource(source, threadAdapter); } }); targetActor.onThreadState(async event => { if (event.state === 'paused') { await this.sourceMaps.applySourceMapToFrame(event.frame!); const sourceLocation = event.frame!.where; try { const sourceAdapter = await this.sources.getAdapterForActor(sourceLocation.actor); if (sourceAdapter.isBlackBoxed) { // skipping (or blackboxing) source files is usually done by Firefox itself, // but when the debugger hits an exception in a source that was just loaded and // should be skipped, we may not have been able to tell Firefox that we want // to skip this file, so we have to do it here threadAdapter.resume(); return; } if ((event.why?.type === 'breakpoint') && event.why.actors && (event.why.actors.length > 0) && sourceAdapter.path ) { const breakpointInfo = this.breakpointsManager.getBreakpoints(sourceAdapter.path)?.find(bpInfo => bpInfo.actualLocation && bpInfo.actualLocation.line === sourceLocation.line && bpInfo.actualLocation.column === sourceLocation.column ); if (breakpointInfo?.hitLimit) { // Firefox doesn't have breakpoints with hit counts, so we have to // implement this here breakpointInfo.hitCount++; if (breakpointInfo.hitCount < breakpointInfo.hitLimit) { threadAdapter.resume(); return; } } } } catch(err) { log.warn(String(err)); } if (event.why?.type === 'exception') { let frames = await threadAdapter.fetchAllStackFrames(); let startFrame = (frames.length > 0) ? frames[frames.length - 1] : undefined; if (startFrame) { try { const sourceAdapter = await this.sources.getAdapterForActor(startFrame.frame.where.actor); if (sourceAdapter.introductionType === 'debugger eval') { // skip exceptions triggered by debugger eval code threadAdapter.resume(); return; } } catch(err) { log.warn(String(err)); } } } threadAdapter.threadPausedReason = event.why; // pre-fetch the stackframes, we're going to need them later threadAdapter.fetchAllStackFrames(); log.info(`Thread ${threadActor.name} paused , reason: ${event.why?.type}`); this.sendStoppedEvent(threadAdapter, event.why); } if (event.state === 'resumed') { log.info(`Thread ${threadActor.name} resumed`); // TODO we really want to do this synchronously, // otherwise we may process the next pause before this has finished await threadAdapter.disposePauseLifetimeAdapters(); this.sendEvent(new ContinuedEvent(threadAdapter.id)); } }); return threadAdapter; } private attachSource(sourceActor: ISourceActorProxy, threadAdapter: ThreadAdapter): void { const sourceAdapter = this.sources.addActor(sourceActor); // check if this source should be skipped const source = sourceActor.source; let skipThisSource: boolean | undefined = undefined; if (sourceAdapter.path !== undefined) { skipThisSource = this.skipFilesManager.shouldSkip(sourceAdapter.path); } else if (source.generatedUrl && (!source.url || !isAbsoluteUrl(source.url))) { skipThisSource = this.skipFilesManager.shouldSkip(this.pathMapper.removeQueryString(source.generatedUrl)); } else if (source.url) { skipThisSource = this.skipFilesManager.shouldSkip(this.pathMapper.removeQueryString(source.url)); } if (skipThisSource !== undefined) { if (skipThisSource !== sourceAdapter.isBlackBoxed) { sourceAdapter.setBlackBoxed(skipThisSource); } else if (skipThisSource) { sourceActor.setBlackbox(skipThisSource); } } threadAdapter.sourceActors.add(sourceActor); this.sendNewSourceEvent(threadAdapter, sourceAdapter); } private async sendConsoleMessage(message: FirefoxDebugProtocol.ConsoleMessage, threadAdapter: ThreadAdapter) { consoleActorLog.debug(`Console API: ${JSON.stringify(message)}`); if (message.level === 'clear') { this.sendEvent(new OutputEvent('\x1b[2J')); return; } if (message.level === 'time' && !message.timer?.error) { // Match what is done in Firefox console and don't show anything when the timer starts return; } let category = (message.level === 'error') ? 'stderr' : (message.level === 'warn') ? 'console' : 'stdout'; let outputEvent: DebugProtocol.OutputEvent; if (message.level === 'time' && message.timer?.error === "timerAlreadyExists") { outputEvent = new OutputEvent(`Timer “${message.timer.name}” already exists`, 'console'); } else if ( (message.level === 'timeLog' || message.level === 'timeEnd') && message.timer?.error === "timerDoesntExist" ) { outputEvent = new OutputEvent(`Timer “${message.timer.name}” doesn't exist`, 'console'); } else { const args: VariableAdapter[] = []; const previews = message.arguments.map((grip, index) => { if (message.timer && index === 0) { // The first argument is the timer name const renderedTimer = `${message.timer.name}: ${message.timer.duration}ms`; return message.level === 'timeEnd' ? `${renderedTimer} - timer ended` : renderedTimer; } if (typeof grip === 'object' && grip.type === 'object') { args.push(VariableAdapter.fromGrip(`arg${index}`, undefined, undefined, grip, true, threadAdapter)); } return typeof grip === 'string' ? grip : renderGrip(grip); }); let msg = previews.join(' '); if (this.config.showConsoleCallLocation) { const filename = this.pathMapper.convertFirefoxUrlToPath(message.filename); msg += ` (${filename}:${message.lineNumber}:${message.columnNumber})`; } outputEvent = new OutputEvent(`${msg}\n`, category); if (args.length > 0) { const argsAdapter = new ConsoleAPICallAdapter(args, msg, threadAdapter); outputEvent.body.variablesReference = argsAdapter.variablesProviderId; } } await this.addLocation(outputEvent, message.filename, message.lineNumber, message.columnNumber); this.sendEvent(outputEvent); } private async addLocation( outputEvent: DebugProtocol.OutputEvent, url: string, line: number, column: number ) { const originalLocation = await this.sourceMaps.findOriginalLocation(url, line, column); if (originalLocation?.url) { const sourceAdapter = await this.sources.getAdapterForUrl(originalLocation.url); if (sourceAdapter) { outputEvent.body.source = sourceAdapter.source; outputEvent.body.line = originalLocation.line; outputEvent.body.column = originalLocation.column; } } } public sendStoppedEvent( threadAdapter: ThreadAdapter, reason?: FirefoxDebugProtocol.ThreadPausedReason ): void { let pauseType = reason ? reason.type : 'interrupt'; let stoppedEvent: DebugProtocol.StoppedEvent = new StoppedEvent(pauseType, threadAdapter.id); stoppedEvent.body.allThreadsStopped = false; if (reason && reason.exception) { if (typeof reason.exception === 'string') { stoppedEvent.body.text = reason.exception; } else if ((typeof reason.exception === 'object') && (reason.exception.type === 'object')) { let exceptionGrip = reason.exception; if (exceptionGrip.preview && (exceptionGrip.preview.kind === 'Error')) { stoppedEvent.body.text = `${exceptionGrip.class}: ${exceptionGrip.preview.message}`; } else { stoppedEvent.body.text = exceptionGrip.class; } } } this.sendEvent(stoppedEvent); } /** tell VS Code and the [Loaded Scripts Explorer](../extension/loadedScripts) about a new thread */ private sendThreadStartedEvent(threadAdapter: ThreadAdapter): void { this.sendEvent(new ThreadEvent('started', threadAdapter.id)); this.sendEvent(new Event('threadStarted', { name: threadAdapter.name, id: threadAdapter.id })); } /** tell VS Code and the [Loaded Scripts Explorer](../extension/loadedScripts) to remove a thread */ private sendThreadExitedEvent(threadAdapter: ThreadAdapter): void { this.sendEvent(new ThreadEvent('exited', threadAdapter.id)); this.sendEvent(new Event('threadExited', { id: threadAdapter.id })); } /** tell the [Loaded Scripts Explorer](../extension/loadedScripts) about a new source */ private sendNewSourceEvent(threadAdapter: ThreadAdapter, sourceAdapter: SourceAdapter): void { const sourceUrl = sourceAdapter.url; if (sourceUrl && !sourceUrl.startsWith('javascript:')) { this.sendEvent(new Event('newSource', { threadId: threadAdapter.id, sourceId: sourceAdapter.id, url: sourceUrl, path: sourceAdapter.path })); } } public sendCustomEvent(event: string, eventBody: any): void { this.sendEvent(new Event(event, eventBody)); } } ================================================ FILE: src/adapter/location.ts ================================================ export interface Location { line: number; column?: number; } export interface UrlLocation extends Location { url?: string; } export interface LocationWithColumn extends Location { column: number; } export interface MappedLocation extends LocationWithColumn { generated?: LocationWithColumn; } export interface Range { start: LocationWithColumn; end: LocationWithColumn; } ================================================ FILE: src/adapter/util/delayedTask.ts ================================================ import { Log } from './log'; let log = Log.create('DelayedTask'); export class DelayedTask { private state: 'waiting' | 'running' | 'finished'; private resolve!: (result: T) => void; private reject!: (reason?: any) => void; public readonly promise: Promise; public constructor( private task: () => Promise, ) { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); this.state = 'waiting'; } public async execute(): Promise { if (this.state !== 'waiting') { log.error(`Tried to execute DelayedTask, but it is ${this.state}`); return; } let result: T; try { this.state = 'running'; result = await this.task(); this.resolve(result); } catch (err) { this.reject(err); throw err; } this.state = 'finished'; } public cancel(reason?: any): void { if (this.state !== 'waiting') { log.error(`Tried to cancel DelayedTask, but it is ${this.state}`); return; } this.reject(reason); this.state = 'finished'; } } ================================================ FILE: src/adapter/util/forkedLauncher.ts ================================================ import { spawn, fork } from 'child_process'; import * as fs from 'fs-extra'; /** * This script is used by the [launchFirefox()](../firefox/launch.ts) function when `reAttach` is * set to true in the launch configuration. */ let args = process.argv.splice(2); let cmd = args.shift(); if (cmd === 'spawnDetached') { let exe = args.shift(); let childProc = spawn(exe!, args, { detached: true, stdio: 'ignore' }); childProc.unref(); } else if (cmd === 'forkDetached') { let script = args.shift(); let childProc = fork(script!, args, { detached: true, stdio: 'ignore' }); childProc.unref(); } else if (cmd === 'spawnAndRemove') { let pathToRemove = args.shift(); let exe = args.shift(); let childProc = spawn(exe!, args); childProc.stdout.on('data', () => undefined); childProc.stderr.on('data', () => undefined); childProc.once('close', () => setTimeout(() => fs.remove(pathToRemove!), 500)); } else if (cmd === 'spawnAndRemove2') { let pathToRemove = args.shift(); let pathToRemove2 = args.shift(); let exe = args.shift(); let childProc = spawn(exe!, args); childProc.stdout.on('data', () => undefined); childProc.stderr.on('data', () => undefined); childProc.once('close', () => setTimeout(() => fs.remove(pathToRemove!, () => fs.remove(pathToRemove2!)), 500)); } ================================================ FILE: src/adapter/util/fs.ts ================================================ import * as fs from 'fs-extra'; import { isWindowsPlatform as detectWindowsPlatform } from '../../common/util'; import { Log } from './log'; let log = Log.create('fs'); export async function isExecutable(path: string): Promise { try { await fs.access(path, fs.constants.X_OK); return true; } catch (e) { return false; } } const isWindowsPlatform = detectWindowsPlatform(); const windowsAbsolutePathRegEx = /^[a-zA-Z]:\\/; export function normalizePath(sourcePathOrUrl: string): string { if (isWindowsPlatform && windowsAbsolutePathRegEx.test(sourcePathOrUrl)) { return sourcePathOrUrl.toLowerCase(); } else { return sourcePathOrUrl; } } ================================================ FILE: src/adapter/util/log.ts ================================================ import * as fs from 'fs-extra'; import { LogConfiguration, LogLevel } from '../../common/configuration'; enum NumericLogLevel { Debug, Info, Warn, Error } /** * The logger used throughout the debug adapter. * It is configured using the `log` property in the launch or attach configuration. */ export class Log { private static startTime = Date.now(); private static config: LogConfiguration = {}; private static logs = new Map(); private static fileDescriptor?: number; public static async setConfig(newConfig: LogConfiguration) { if (Log.fileDescriptor !== undefined) { await fs.close(Log.fileDescriptor); Log.fileDescriptor = undefined; } Log.config = newConfig; if (Log.config.fileName) { try { Log.fileDescriptor = await fs.open(Log.config.fileName, 'w'); } catch(e) {} } Log.logs.forEach((log) => log.configure()); } public static consoleLog: (msg: string) => void = console.log; public static create(name: string): Log { return new Log(name); } private fileLevel?: NumericLogLevel; private consoleLevel?: NumericLogLevel; private minLevel?: NumericLogLevel; constructor(private name: string) { this.configure(); Log.logs.set(name, this); } public debug(msg: string): void { this.log(msg, NumericLogLevel.Debug, 'DEBUG'); } public info(msg: string): void { this.log(msg, NumericLogLevel.Info, 'INFO '); } public warn(msg: string): void { this.log(msg, NumericLogLevel.Warn, 'WARN '); } public error(msg: string): void { this.log(msg, NumericLogLevel.Error, 'ERROR'); } public isDebugEnabled(): boolean { return (this.minLevel !== undefined) && (this.minLevel <= NumericLogLevel.Debug); } public isInfoEnabled(): boolean { return (this.minLevel !== undefined) && (this.minLevel <= NumericLogLevel.Info); } public isWarnEnabled(): boolean { return (this.minLevel !== undefined) && (this.minLevel <= NumericLogLevel.Warn); } public isErrorEnabled(): boolean { return (this.minLevel !== undefined) && (this.minLevel <= NumericLogLevel.Error); } private configure() { this.fileLevel = undefined; if (Log.config.fileName && Log.config.fileLevel) { this.fileLevel = this.convertLogLevel(Log.config.fileLevel[this.name]); if (this.fileLevel === undefined) { this.fileLevel = this.convertLogLevel(Log.config.fileLevel['default']); } } if (Log.config.consoleLevel) { this.consoleLevel = this.convertLogLevel(Log.config.consoleLevel[this.name]); if (this.consoleLevel === undefined) { this.consoleLevel = this.convertLogLevel(Log.config.consoleLevel['default']); } } this.minLevel = this.fileLevel; if ((this.consoleLevel !== undefined) && ((this.minLevel === undefined) || (this.consoleLevel < this.minLevel))) { this.minLevel = this.consoleLevel; } } private convertLogLevel(logLevel: LogLevel): NumericLogLevel | undefined { if (!logLevel) { return undefined; } switch (logLevel) { case 'Debug': return NumericLogLevel.Debug; case 'Info': return NumericLogLevel.Info; case 'Warn': return NumericLogLevel.Warn; case 'Error': return NumericLogLevel.Error; } } private log(msg: string, level: NumericLogLevel, displayLevel: string) { if ((this.minLevel !== undefined) && (level >= this.minLevel)) { let elapsedTime = (Date.now() - Log.startTime) / 1000; let elapsedTimeString = elapsedTime.toFixed(3); while (elapsedTimeString.length < 7) { elapsedTimeString = '0' + elapsedTimeString; } let logMsg = displayLevel + '|' + elapsedTimeString + '|' + this.name + ': ' + msg; if ((Log.fileDescriptor !== undefined) && (this.fileLevel !== undefined) && (level >= this.fileLevel)) { fs.write(Log.fileDescriptor, logMsg + '\n', (err, written, str) => {}); } if ((this.consoleLevel !== undefined) && (level >= this.consoleLevel)) { Log.consoleLog(logMsg); } } } } ================================================ FILE: src/adapter/util/misc.ts ================================================ import * as path from 'path'; import * as fs from 'fs-extra'; import stripJsonComments from 'strip-json-comments'; import { isWindowsPlatform } from '../../common/util'; import isAbsoluteUrl from 'is-absolute-url'; /** * compare file paths or urls, taking into account whether filenames are case sensitive on the current platform */ export function pathsAreEqual(path1: string, path2: string | undefined) { if (path2 === undefined) return false; if (isWindowsPlatform() && !isAbsoluteUrl(path1)) { return path1.toUpperCase() === path2.toUpperCase(); } else { return path1 === path2; } } /** * replace `\` with `/` on windows and remove trailing slashes */ export function normalizePath(rawPath: string) { let normalized = path.normalize(rawPath); if (isWindowsPlatform()) { normalized = normalized.replace(/\\/g, '/'); } if (normalized[normalized.length - 1] === '/') { normalized = normalized.substr(0, normalized.length - 1); } return normalized; } export function shortenUrl(url: string) { if (url.startsWith('file://')) { const i = url.lastIndexOf('/'); return i >= 0 ? url.substring(i + 1) : url; } const i = url.indexOf('://'); return i >= 0 ? url.substring(i + 3) : url; } /** * extract an error message from an exception * [grip](https://github.com/mozilla/gecko-dev/blob/master/devtools/docs/backend/protocol.md#grips) */ export function exceptionGripToString(grip: FirefoxDebugProtocol.Grip | null | undefined) { if ((typeof grip === 'object') && (grip !== null) && (grip.type === 'object')) { let preview = (grip).preview; if (preview && (preview.kind === 'Error')) { if (preview.name === 'ReferenceError') { return 'not available'; } let str = (preview.name !== undefined) ? (preview.name + ': ') : ''; str += (preview.message !== undefined) ? preview.message : ''; if (str !== '') { return str; } } } else if (typeof grip === 'string') { return grip; } return 'unknown error'; } const identifierExpression = /^[a-zA-Z_$][a-zA-Z_$]*$/; /** * create a javascript expression for accessing a property of an object */ export function accessorExpression(objectExpression: string | undefined, propertyName: string): string | undefined { if (objectExpression === undefined) { return undefined; } else if (objectExpression === '') { return propertyName; } else if (identifierExpression.test(propertyName)) { return `${objectExpression}.${propertyName}`; } else { const escapedPropertyName = propertyName.replace('\\', '\\\\').replace('\'', '\\\''); return `${objectExpression}['${escapedPropertyName}']`; } } /** * extract the addon id from a WebExtension's `manifest.json` */ export async function findAddonId(addonPath: string): Promise { try { const rawManifest = await fs.readFile(path.join(addonPath, 'manifest.json'), { encoding: 'utf8' }); const manifest = JSON.parse(stripJsonComments(rawManifest)); const id = ((manifest.applications || {}).gecko || {}).id; return id; } catch (err) { throw `Couldn't parse manifest.json: ${err}`; } } export function compareStrings(s1: string, s2: string): number { if (s1 < s2) { return -1; } else if (s1 === s2) { return 0; } else { return 1; } } ================================================ FILE: src/adapter/util/net.ts ================================================ import * as url from 'url'; import * as fs from 'fs-extra'; import * as net from 'net'; import * as http from 'http'; import * as https from 'https'; import { setDefaultResultOrder } from 'dns'; import fileUriToPath from 'file-uri-to-path'; import dataUriToBuffer from 'data-uri-to-buffer'; import { Log } from './log'; import { delay } from '../../common/util'; let log = Log.create('net'); /** * connect to a TCP port */ export function connect(port: number, host?: string): Promise { setDefaultResultOrder("ipv4first"); return new Promise((resolve, reject) => { let socket = net.connect(port, host || 'localhost'); socket.on('connect', () => resolve(socket)); socket.on('error', reject); }); } /** * Try to connect to a TCP port and keep retrying for the number of seconds given by `timeout` * if the connection is rejected initially. * Used to connect to Firefox after launching it. */ export async function waitForSocket(port: number, timeout: number): Promise { const maxIterations = timeout * 5; let lastError: any; for (var i = 0; i < maxIterations; i++) { try { return await connect(port); } catch(err) { lastError = err; await delay(200); } } throw lastError; } export function urlBasename(url: string): string { let lastSepIndex = url.lastIndexOf('/'); if (lastSepIndex < 0) { return url; } else { return url.substring(lastSepIndex + 1); } } export function urlDirname(url: string): string { let lastSepIndex = url.lastIndexOf('/'); if (lastSepIndex < 8) { return url; } else { return url.substring(0, lastSepIndex + 1); } } /** * fetch the document from a URI, with support for the http(s), file and data schemes */ export async function getUri(uri: string): Promise { if (uri.startsWith('data:')) { return dataUriToBuffer(uri).toString(); } if (uri.startsWith('file:')) { return await fs.readFile(fileUriToPath(uri), 'utf8'); } if (!uri.startsWith('http:') && !uri.startsWith('https:')) { throw new Error(`Fetching ${uri} not supported`); } return await new Promise((resolve, reject) => { const parsedUrl = url.parse(uri); const get = (parsedUrl.protocol === 'https:') ? https.get : http.get; const options = Object.assign({ rejectUnauthorized: false }, parsedUrl) as https.RequestOptions; get(options, response => { let responseData = ''; response.on('data', chunk => responseData += chunk); response.on('end', () => { if (response.statusCode === 200) { resolve(responseData); } else { log.error(`HTTP GET failed with: ${response.statusCode} ${response.statusMessage}`); reject(new Error(responseData.trim())); } }); }).on('error', e => { log.error(`HTTP GET failed: ${e}`); reject(e); }); }); } ================================================ FILE: src/adapter/util/pathMapper.ts ================================================ import * as path from 'path'; import * as url from 'url'; import isAbsoluteUrl from 'is-absolute-url'; import { Log } from './log'; import { PathMappings, ParsedAddonConfiguration } from '../configuration'; import { isWindowsPlatform as detectWindowsPlatform } from '../../common/util'; import { urlDirname } from './net'; let log = Log.create('PathConversion'); let isWindowsPlatform = detectWindowsPlatform(); const windowsAbsolutePathRegEx = /^[a-zA-Z]:[\/\\]/; /** * This class is used to map the URLs as seen by Firefox to local paths as seen by VS Code. * It is configured using the `pathMappings` property in the launch or attach configuration. */ export class PathMapper { constructor( private readonly pathMappings: PathMappings, private readonly pathMappingIndex: string, private readonly addonConfig?: ParsedAddonConfiguration ) {} public convertFirefoxSourceToPath(source: FirefoxDebugProtocol.Source): string | undefined { if (!source) return undefined; if (source.addonID && this.addonConfig && (source.addonID === this.addonConfig.id)) { let sourcePath = this.removeQueryString(path.join(this.addonConfig.path, source.addonPath!)); log.debug(`Addon script path: ${sourcePath}`); return sourcePath; } else if (source.isSourceMapped && source.generatedUrl && source.url && !isAbsoluteUrl(source.url)) { let originalPathOrUrl = source.url; if (path.isAbsolute(originalPathOrUrl)) { log.debug(`Sourcemapped absolute path: ${originalPathOrUrl}`); if (isWindowsPlatform) { originalPathOrUrl = path.normalize(originalPathOrUrl); } return originalPathOrUrl; } else { let generatedUrl = source.generatedUrl; if ((source.introductionType === 'wasm') && generatedUrl.startsWith('wasm:')) { generatedUrl = generatedUrl.substr(5); } let sourcePath: string | undefined; if (originalPathOrUrl.startsWith('../')) { let generatedPath = this.convertFirefoxUrlToPath(generatedUrl); if (!generatedPath) return undefined; sourcePath = path.join(path.dirname(generatedPath), originalPathOrUrl); } else { let sourceUrl = url.resolve(urlDirname(generatedUrl), originalPathOrUrl); sourcePath = this.convertFirefoxUrlToPath(sourceUrl); if (!sourcePath) return undefined; } sourcePath = this.removeQueryString(sourcePath); log.debug(`Sourcemapped path: ${sourcePath}`); return sourcePath; } } else if (source.url) { return this.convertFirefoxUrlToPath(source.url); } else { return undefined; } } public convertFirefoxUrlToPath(url: string): string | undefined { if (url.endsWith('/')) { url += this.pathMappingIndex; } for (var i = 0; i < this.pathMappings.length; i++) { let { url: from, path: to } = this.pathMappings[i]; if (typeof from === 'string') { if (url.substr(0, from.length) === from) { if (to === null) { log.debug(`Url ${url} not converted to path`); return undefined; } let thePath = this.removeQueryString(to + decodeURIComponent(url.substr(from.length))); if (isWindowsPlatform && windowsAbsolutePathRegEx.test(thePath)) { thePath = path.normalize(thePath); } log.debug(`Converted url ${url} to path ${thePath}`); return thePath; } } else { let match = from.exec(url); if (match) { if (to === null) { log.debug(`Url ${url} not converted to path`); return undefined; } let thePath = this.removeQueryString(to + decodeURIComponent(match[1])); if (isWindowsPlatform && windowsAbsolutePathRegEx.test(thePath)) { thePath = path.normalize(thePath); } log.debug(`Converted url ${url} to path ${thePath}`); return thePath; } } } log.info(`Can't convert url ${url} to path`); return undefined; } removeQueryString(path: string): string { let queryStringIndex = path.indexOf('?'); if (queryStringIndex >= 0) { return path.substr(0, queryStringIndex); } else { return path; } } } ================================================ FILE: src/adapter/util/pendingRequests.ts ================================================ import { Log } from './log'; let log = Log.create('PendingRequests'); export interface PendingRequest { resolve: (t: T) => void; reject: (err: any) => void; } export class PendingRequests { private pendingRequests: PendingRequest[] = []; public enqueue(req: PendingRequest) { this.pendingRequests.push(req); } public resolveOne(t: T) { if (this.pendingRequests.length > 0) { let request = this.pendingRequests.shift()!; request.resolve(t); } else { log.error(`Received response without corresponding request: ${JSON.stringify(t)}`); } } public rejectOne(err: any) { if (this.pendingRequests.length > 0) { let request = this.pendingRequests.shift()!; request.reject(err); } else { log.error(`Received error response without corresponding request: ${JSON.stringify(err)}`); } } public isEmpty(): boolean { return (this.pendingRequests.length === 0); } public resolveAll(t: T) { this.pendingRequests.forEach((req) => req.resolve(t)); this.pendingRequests = []; } public rejectAll(err: any) { this.pendingRequests.forEach((req) => req.reject(err)); this.pendingRequests = []; } } ================================================ FILE: src/common/configuration.ts ================================================ import { DebugProtocol } from '@vscode/debugprotocol'; /** * A launch configuration, as provided by VS Code */ export interface LaunchConfiguration extends CommonConfiguration, DebugProtocol.LaunchRequestArguments { request: 'launch'; file?: string; tmpDir?: string; profile?: string; keepProfileChanges?: boolean; preferences?: { [key: string]: boolean | number | string | null }; port?: number; firefoxArgs?: string[]; timeout?: number; reAttach?: boolean; } /** * An attach configuration, as provided by VS Code */ export interface AttachConfiguration extends CommonConfiguration, DebugProtocol.AttachRequestArguments { request: 'attach'; port?: number; host?: string; } /** * Common properties of launch and attach configurations */ export interface CommonConfiguration { request: 'launch' | 'attach'; url?: string; webRoot?: string; firefoxExecutable?: string; profileDir?: string; reloadOnAttach?: boolean; reloadOnChange?: ReloadConfiguration; tabFilter?: TabFilterConfiguration; clearConsoleOnReload?: boolean; pathMappings?: { url: string, path: string | null }[]; pathMappingIndex?: string; skipFiles?: string[]; showConsoleCallLocation?: boolean; log?: LogConfiguration; addonPath?: string; popupAutohideButton?: boolean; liftAccessorsFromPrototypes?: number; suggestPathMappingWizard?: boolean; enableCRAWorkaround?: boolean; } export type ReloadConfiguration = string | string[] | DetailedReloadConfiguration; export interface DetailedReloadConfiguration { watch: string | string[]; ignore?: string | string[]; debounce?: number | boolean; } export type TabFilterConfiguration = string | string[] | DetailedTabFilterConfiguration; export interface DetailedTabFilterConfiguration { include?: string | string[]; exclude?: string | string[]; } export declare type LogLevel = 'Debug' | 'Info' | 'Warn' | 'Error'; export interface LogConfiguration { fileName?: string; fileLevel?: { [logName: string]: LogLevel }; consoleLevel?: { [logName: string]: LogLevel }; } ================================================ FILE: src/common/customEvents.ts ================================================ export interface ThreadStartedEventBody { name: string; id: number; } export interface ThreadExitedEventBody { id: number; } export interface NewSourceEventBody { threadId: number; sourceId: number; /** as seen by Firefox */ url: string | undefined; /** path or url as seen by VS Code */ path: string | undefined; } export interface RemoveSourcesEventBody { threadId: number; } export interface PopupAutohideEventBody { popupAutohide: boolean; } export interface AvailableEventCategory { name: string; events: AvailableEvent[]; } export interface AvailableEvent { id: string; name: string; } export type AvailableEventsEventBody = AvailableEventCategory[]; ================================================ FILE: src/common/deferredMap.ts ================================================ export class DeferredMap { private readonly pending = new Map void>(); private readonly existing = new Map(); private readonly promises = new Map>(); public get(key: S): Promise { if (this.promises.has(key)) { return this.promises.get(key)!; } const promise = new Promise(resolve => { this.pending.set(key, resolve); }); this.promises.set(key, promise); return promise; } public getExisting(key: S): T | undefined { return this.existing.get(key); } public getAllExisting(): T[] { return [...this.existing.values()]; } public set(key: S, value: T): void { if (this.pending.has(key)) { this.pending.get(key)!(value); this.pending.delete(key); } if (!this.promises.has(key)) { this.promises.set(key, Promise.resolve(value)); } this.existing.set(key, value); } public delete(key: S) { this.pending.delete(key); this.existing.delete(key); this.promises.delete(key); } } ================================================ FILE: src/common/util.ts ================================================ import * as os from 'os'; export function delay(timeout: number): Promise { return new Promise((resolve) => { setTimeout(resolve, timeout); }); } export function isWindowsPlatform(): boolean { return (os.platform() === 'win32'); } ================================================ FILE: src/extension/addPathMapping.ts ================================================ import * as path from 'path'; import * as vscode from 'vscode'; import { TreeNode } from './loadedScripts/treeNode'; interface LaunchConfig { type: string; name: string; } interface LaunchConfigReference { workspaceFolder: vscode.WorkspaceFolder; launchConfigFile: vscode.WorkspaceConfiguration; index: number; } export async function addPathMapping(treeNode: TreeNode): Promise { const launchConfigReference = _findLaunchConfig(); if (!launchConfigReference) return; const openDialogResult = await vscode.window.showOpenDialog({ canSelectFiles: (treeNode.treeItem.contextValue === 'file'), canSelectFolders: (treeNode.treeItem.contextValue === 'directory'), canSelectMany: false, defaultUri: launchConfigReference.workspaceFolder.uri, openLabel: 'Map to this ' + treeNode.treeItem.contextValue }); if (!openDialogResult || (openDialogResult.length === 0)) { return; } let path = (openDialogResult[0].scheme === 'file') ? openDialogResult[0].fsPath : openDialogResult[0].toString(); if (treeNode.treeItem.contextValue === 'directory') { path += '/'; } const success = await addPathMappingToLaunchConfig(launchConfigReference, treeNode.getFullPath(), path); if (success) { await showLaunchConfig(launchConfigReference.workspaceFolder); vscode.window.showWarningMessage('Configuration was modified - please restart your debug session for the changes to take effect'); } } export async function addNullPathMapping(treeNode: TreeNode): Promise { const launchConfigReference = _findLaunchConfig(); if (!launchConfigReference) return; const success = await addPathMappingToLaunchConfig(launchConfigReference, treeNode.getFullPath(), null); if (success) { await showLaunchConfig(launchConfigReference.workspaceFolder); vscode.window.showWarningMessage('Configuration was modified - please restart your debug session for the changes to take effect'); } } function _findLaunchConfig(): LaunchConfigReference | undefined { const debugSession = vscode.debug.activeDebugSession; if (!debugSession) { vscode.window.showErrorMessage('No active debug session'); return undefined; } const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders) { vscode.window.showErrorMessage('No open folder'); return undefined; } const launchConfigReference = findLaunchConfig(workspaceFolders, debugSession); if (!launchConfigReference) { vscode.window.showErrorMessage(`Couldn't find configuration for active debug session '${debugSession.name}'`); } return launchConfigReference; } export function findLaunchConfig( workspaceFolders: readonly vscode.WorkspaceFolder[], activeDebugSession: vscode.DebugSession ): LaunchConfigReference | undefined { for (const workspaceFolder of workspaceFolders) { const launchConfigFile = vscode.workspace.getConfiguration('launch', workspaceFolder.uri); const launchConfigs: LaunchConfig[] | undefined = launchConfigFile.get('configurations'); if (launchConfigs) { for (let index = 0; index < launchConfigs.length; index++) { if ((launchConfigs[index].type === activeDebugSession.type) && (launchConfigs[index].name === activeDebugSession.name)) { return { workspaceFolder, launchConfigFile, index }; } } } } return undefined; } export async function addPathMappingToLaunchConfig( launchConfigReference: LaunchConfigReference, url: string, path: string | null ): Promise { const configurations = launchConfigReference.launchConfigFile.get('configurations'); const configuration = configurations[launchConfigReference.index]; if (!configuration.pathMappings) { configuration.pathMappings = []; } const workspacePath = launchConfigReference.workspaceFolder.uri.fsPath; if (path && path.startsWith(workspacePath)) { path = '${workspaceFolder}' + path.substr(workspacePath.length); } const pathMappings: any[] = configuration.pathMappings; pathMappings.unshift({ url, path }); try { await launchConfigReference.launchConfigFile.update('configurations', configurations, vscode.ConfigurationTarget.WorkspaceFolder); return true; } catch(e: any) { vscode.window.showErrorMessage(e.message); return false; } } export async function showLaunchConfig(workspaceFolder: vscode.WorkspaceFolder): Promise { const uri = workspaceFolder.uri.with({ path: path.posix.join(workspaceFolder.uri.path, '.vscode/launch.json') }); const document = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(document); } ================================================ FILE: src/extension/debugConfigurationProvider.ts ================================================ import path from 'path'; import * as vscode from 'vscode'; import { LaunchConfiguration, AttachConfiguration } from '../common/configuration'; import { vscodeUriToPath } from './pathMappingWizard'; const folderVar = '${workspaceFolder}'; export class DebugConfigurationProvider implements vscode.DebugConfigurationProvider { /** * this method is called by VS Code before a debug session is started and makes modifications * to the debug configuration: * - some values can be overridden by corresponding VS Code settings * - if the configuration contains `url` but neither `webRoot` nor `pathMappings`, * `webRoot` is set to the workspace folder * - when running in a remote workspace, we resolve `${workspaceFolder}` ourselves because * VS Code resolves it to a local path in the remote workspace but we need the remote URI instead * - when running in a remote workspace, we check that configuration values that need to point * to local files don't contain `${workspaceFolder}` */ resolveDebugConfiguration( folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration & (LaunchConfiguration | AttachConfiguration) ): vscode.DebugConfiguration { if (!debugConfiguration.type) { // The user wants to debug without a launch configuration - we create one for // opening the currently active HTML file in Firefox const document = vscode.window.activeTextEditor?.document; if (!document || document.languageId !== 'html') { throw new Error("Please open an HTML file for debugging"); } if (document.uri.scheme !== 'file') { throw new Error("Debugging without a launch configuration is not supported in remote workspaces"); } const file = document.uri.fsPath; return { name: `Debug ${path.basename(file)} with Firefox`, type: 'firefox', request: 'launch', file }; } debugConfiguration = { ...debugConfiguration }; this.overrideFromSettings(folder, debugConfiguration); if (debugConfiguration.url && !debugConfiguration.webRoot && !debugConfiguration.pathMappings && folder) { debugConfiguration.webRoot = vscodeUriToPath(folder.uri); } if (folder && (folder.uri.scheme === 'vscode-remote')) { this.resolveWorkspaceFolder(folder, debugConfiguration); this.checkLocal(debugConfiguration); } return debugConfiguration; } private overrideFromSettings( folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration & (LaunchConfiguration | AttachConfiguration) ): void { const settings = vscode.workspace.getConfiguration('firefox', folder ? folder.uri : null); const executable = this.getSetting(settings, 'executable'); if (executable) { debugConfiguration.firefoxExecutable = executable; } const args = this.getSetting(settings, 'args'); if (args) { debugConfiguration.firefoxArgs = args; } const profileDir = this.getSetting(settings, 'profileDir'); if (profileDir) { debugConfiguration.profileDir = profileDir; } const profile = this.getSetting(settings, 'profile'); if (profile) { debugConfiguration.profile = profile; } const keepProfileChanges = this.getSetting(settings, 'keepProfileChanges'); if (keepProfileChanges !== undefined) { debugConfiguration.keepProfileChanges = keepProfileChanges; } const port = this.getSetting(settings, 'port'); if (port !== undefined) { debugConfiguration.port = port; } } /** * read a value from the user's VS Code settings. If the user hasn't set a value, this * method returns `undefined` (instead of the default value for the given key). */ private getSetting(settings: vscode.WorkspaceConfiguration, key: string): T | undefined { const values = settings.inspect(key); if (!values) return undefined; if (values.workspaceFolderValue !== undefined) return values.workspaceFolderValue; if (values.workspaceValue !== undefined) return values.workspaceValue; if (values.globalValue !== undefined) return values.globalValue; return undefined; } private resolveWorkspaceFolder( folder: vscode.WorkspaceFolder, debugConfiguration: vscode.DebugConfiguration & (LaunchConfiguration | AttachConfiguration) ): void { const uri = folder.uri.toString(); if (debugConfiguration.webRoot) { debugConfiguration.webRoot = debugConfiguration.webRoot.replace(folderVar, uri); } if (debugConfiguration.pathMappings) { const resolvedPathMappings: { url: string, path: string | null }[] = []; for (const pathMapping of debugConfiguration.pathMappings) { if (pathMapping.path) { resolvedPathMappings.push({ url: pathMapping.url, path: pathMapping.path.replace(folderVar, uri) }); } else { resolvedPathMappings.push(pathMapping); } } debugConfiguration.pathMappings = resolvedPathMappings; } } private checkLocal( debugConfiguration: vscode.DebugConfiguration & (LaunchConfiguration | AttachConfiguration) ): void { function check(errorMsg: string) { return function(str: string): void { if (str.indexOf(folderVar) >= 0) { throw new Error(errorMsg); } } } if (debugConfiguration.reloadOnChange) { const checkReload = check("The debug adapter can't watch files in a remote workspace for changes"); if (typeof debugConfiguration.reloadOnChange === 'string') { checkReload(debugConfiguration.reloadOnChange); } else if (Array.isArray(debugConfiguration.reloadOnChange)) { debugConfiguration.reloadOnChange.forEach(checkReload); } else { if (typeof debugConfiguration.reloadOnChange.watch === 'string') { checkReload(debugConfiguration.reloadOnChange.watch); } else { debugConfiguration.reloadOnChange.watch.forEach(checkReload); } if (debugConfiguration.reloadOnChange.ignore) { if (typeof debugConfiguration.reloadOnChange.ignore === 'string') { checkReload(debugConfiguration.reloadOnChange.ignore); } else { debugConfiguration.reloadOnChange.ignore.forEach(checkReload); } } } } if (debugConfiguration.log && debugConfiguration.log.fileName) { check("The debug adapter can't write a log file in a remote workspace")(debugConfiguration.log.fileName); } if (debugConfiguration.request === 'launch') { if (debugConfiguration.file) { check("Firefox can't open a file in a remote workspace")(debugConfiguration.file); } if (debugConfiguration.profileDir) { check("Firefox can't have its profile in a remote workspace")(debugConfiguration.profileDir); } } } } ================================================ FILE: src/extension/eventBreakpointsProvider.ts ================================================ import * as vscode from 'vscode'; import { AvailableEvent, AvailableEventCategory } from '../common/customEvents'; export class EventBreakpointsProvider implements vscode.TreeDataProvider { private availableEvents: AvailableEventCategory[] = []; private readonly rootNode = new RootNode(this); private readonly treeDataChanged = new vscode.EventEmitter(); public readonly onDidChangeTreeData: vscode.Event; public readonly activeEventBreakpoints = new Set(); constructor() { this.onDidChangeTreeData = this.treeDataChanged.event; } setAvailableEvents(availableEvents: AvailableEventCategory[]) { this.availableEvents = availableEvents; this.treeDataChanged.fire(); } getAvailableEvents() { return this.availableEvents; } updateActiveEventBreakpoints(event: vscode.TreeCheckboxChangeEvent) { for (const [item, state] of event.items) { item.setChecked(state === vscode.TreeItemCheckboxState.Checked); } this.treeDataChanged.fire(); } getTreeItem(element: TreeNode): vscode.TreeItem | Thenable { return element.item; } getChildren(element?: TreeNode | undefined): vscode.ProviderResult { return (element ?? this.rootNode).getChildren?.() ?? []; } } interface TreeNode { item: vscode.TreeItem; setChecked(checked: boolean): void; getChildren?(): TreeNode[]; } class RootNode implements TreeNode { public readonly item: vscode.TreeItem; constructor(private readonly provider: EventBreakpointsProvider) { this.item = new vscode.TreeItem('', vscode.TreeItemCollapsibleState.Collapsed); } setChecked(checked: boolean): void {} getChildren(): TreeNode[] { return this.provider.getAvailableEvents()?.map(category => new CategoryNode(category, this.provider)) ?? []; } } class CategoryNode implements TreeNode { public readonly item: vscode.TreeItem; constructor( private readonly category: AvailableEventCategory, private readonly provider: EventBreakpointsProvider ) { this.item = new vscode.TreeItem(category.name, vscode.TreeItemCollapsibleState.Collapsed); this.item.checkboxState = this.category.events.every(event => this.provider.activeEventBreakpoints.has(event.id)) ? vscode.TreeItemCheckboxState.Checked : vscode.TreeItemCheckboxState.Unchecked; } setChecked(checked: boolean): void { for (const event of this.category.events) { if (checked) { this.provider.activeEventBreakpoints.add(event.id); } else { this.provider.activeEventBreakpoints.delete(event.id); } } } getChildren(): TreeNode[] { return this.category.events.map(event => new EventNode(event, this.provider)); } } class EventNode implements TreeNode { public readonly item: vscode.TreeItem; constructor( private readonly event: AvailableEvent, private readonly provider: EventBreakpointsProvider ) { this.item = new vscode.TreeItem(event.name); this.item.checkboxState = provider.activeEventBreakpoints.has(event.id) ? vscode.TreeItemCheckboxState.Checked : vscode.TreeItemCheckboxState.Unchecked; } setChecked(checked: boolean): void { if (checked) { this.provider.activeEventBreakpoints.add(this.event.id); } else { this.provider.activeEventBreakpoints.delete(this.event.id); } } } ================================================ FILE: src/extension/loadedScripts/fileNode.ts ================================================ import * as vscode from 'vscode'; import { NewSourceEventBody } from '../../common/customEvents'; import { TreeNode } from './treeNode'; import { NonLeafNode } from './nonLeafNode'; export class FileNode extends TreeNode { public constructor( filename: string, description: string | undefined, sourceInfo: NewSourceEventBody, parent: NonLeafNode, sessionId: string ) { super((filename.length > 0) ? filename : '(index)', parent, description, vscode.TreeItemCollapsibleState.None); this.treeItem.contextValue = 'file'; let pathOrUri: string; if (sourceInfo.path) { pathOrUri = sourceInfo.path; } else { pathOrUri = `debug:${encodeURIComponent(sourceInfo.url!)}?session=${encodeURIComponent(sessionId)}&ref=${sourceInfo.sourceId}`; } this.treeItem.command = { command: 'extension.firefox.openScript', arguments: [ pathOrUri ], title: '' } } public getChildren(): TreeNode[] { return []; } public getFullPath(): string { return this.parent!.getFullPath() + this.treeItem.label; } } ================================================ FILE: src/extension/loadedScripts/nonLeafNode.ts ================================================ import * as vscode from 'vscode'; import { NewSourceEventBody, ThreadStartedEventBody } from '../../common/customEvents'; import { TreeNode } from './treeNode'; import { FileNode } from './fileNode'; import { SessionNode } from './sessionNode'; export abstract class NonLeafNode extends TreeNode { protected children: (DirectoryNode | FileNode)[] = []; public constructor(label: string, parent: TreeNode) { super(label, parent); } public addSource( filename: string, path: string[], description: string | undefined, sourceInfo: NewSourceEventBody, sessionId: string ): TreeNode | undefined { if (path.length === 0) { // add the source file to this directory (not a subdirectory) this.addChild(new FileNode(filename, description, sourceInfo, this, sessionId)); return this; } // find the index (if it exists) of the child directory item whose path starts // with the same directory name as the path to be added let itemIndex = this.children.findIndex( (item) => ((item instanceof DirectoryNode) && (item.path[0] === path[0])) ); if (itemIndex < 0) { // there is no subdirectory that shares an initial path segment with the path to be added, // so we create a SourceDirectoryTreeItem for the path and add the source file to it let directoryItem = new DirectoryNode(path, this); directoryItem.addSource(filename, [], description, sourceInfo, sessionId); this.addChild(directoryItem); return this; } // the subdirectory item that shares an initial path segment with the path to be added let item = this.children[itemIndex]; // the length of the initial path segment that is equal let pathMatchLength = path.findIndex( (pathElement, index) => ((index >= item.path.length) || (item.path[index] !== pathElement)) ); if (pathMatchLength < 0) pathMatchLength = path.length; // the unmatched end segment of the path let pathRest = path.slice(pathMatchLength); if (pathMatchLength === item.path.length) { // the entire path of the subdirectory item is contained in the path of the file to be // added, so we add the file with the pathRest to the subdirectory item return item.addSource(filename, pathRest, description, sourceInfo, sessionId); } // only a part of the path of the subdirectory item is contained in the path of the file to // be added, so we split the subdirectory item into two and add the file to the first item item.split(pathMatchLength); item.addSource(filename, pathRest, description, sourceInfo, sessionId); return item; } public getChildren(): TreeNode[] { this.treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; return this.children; } /** * add a child item, respecting the sort order */ private addChild(newChild: DirectoryNode | FileNode): void { let index: number; if (newChild instanceof DirectoryNode) { index = this.children.findIndex( (child) => !((child instanceof DirectoryNode) && (child.treeItem.label! < newChild.treeItem.label!)) ); } else { index = this.children.findIndex( (child) => ((child instanceof FileNode) && (child.treeItem.label! >= newChild.treeItem.label!)) ); } if (index >= 0) { if (this.children[index].treeItem.label !== newChild.treeItem.label) { this.children.splice(index, 0, newChild); } } else { this.children.push(newChild); } } } export class ThreadNode extends NonLeafNode { public readonly id: number; public constructor(threadInfo: ThreadStartedEventBody, parent: SessionNode) { super(threadInfo.name, parent); this.id = threadInfo.id; this.treeItem.contextValue = 'thread'; } public removeSources(): TreeNode | undefined { this.children = []; return this; } } export class DirectoryNode extends NonLeafNode { public constructor(public path: string[], parent: TreeNode) { super(path.join('/'), parent); this.treeItem.contextValue = 'directory'; } /** * split this item into two items with this item representing the initial path segment of length * `atIndex` and the new child item representing the rest of the path */ public split(atIndex: number): void { let newChild = new DirectoryNode(this.path.slice(atIndex), this); newChild.children = this.children; newChild.children.map(grandChild => grandChild.parent = newChild); this.path.splice(atIndex); this.children = [ newChild ]; this.treeItem.label = this.path.join('/'); } public getFullPath(): string { return this.parent!.getFullPath() + this.treeItem.label + '/'; } } ================================================ FILE: src/extension/loadedScripts/provider.ts ================================================ import * as vscode from 'vscode'; import { ThreadStartedEventBody, NewSourceEventBody } from '../../common/customEvents'; import { TreeNode } from './treeNode'; import { RootNode } from './rootNode'; export class LoadedScriptsProvider implements vscode.TreeDataProvider { private readonly root = new RootNode(); private readonly treeDataChanged = new vscode.EventEmitter(); public readonly onDidChangeTreeData: vscode.Event; public constructor() { this.onDidChangeTreeData = this.treeDataChanged.event; } public getTreeItem(node: TreeNode): vscode.TreeItem { return node.treeItem; } public getChildren(node?: TreeNode): vscode.ProviderResult { let parent = (node || this.root); return parent.getChildren(); } public hasSession(sessionId: string) { return this.root.hasSession(sessionId); } public addSession(session: vscode.DebugSession) { let changedItem = this.root.addSession(session); this.sendTreeDataChangedEvent(changedItem); } public removeSession(sessionId: string) { let changedItem = this.root.removeSession(sessionId); this.sendTreeDataChangedEvent(changedItem); } public async addThread(threadInfo: ThreadStartedEventBody, sessionId: string) { let changedItem = await this.root.addThread(threadInfo, sessionId); this.sendTreeDataChangedEvent(changedItem); } public async removeThread(threadId: number, sessionId: string) { let changedItem = await this.root.removeThread(threadId, sessionId); this.sendTreeDataChangedEvent(changedItem); } public async addSource(sourceInfo: NewSourceEventBody, sessionId: string) { let changedItem = await this.root.addSource(sourceInfo, sessionId); this.sendTreeDataChangedEvent(changedItem); } public async removeSources(threadId: number, sessionId: string) { let changedItem = await this.root.removeSources(threadId, sessionId); this.sendTreeDataChangedEvent(changedItem); } public async getSourceUrls(sessionId: string): Promise { return this.root.getSourceUrls(sessionId); } private sendTreeDataChangedEvent(changedItem: TreeNode | undefined) { if (changedItem) { if (changedItem === this.root) { this.treeDataChanged.fire(); } else { this.treeDataChanged.fire(changedItem); } } } } ================================================ FILE: src/extension/loadedScripts/rootNode.ts ================================================ import * as vscode from 'vscode'; import { ThreadStartedEventBody, NewSourceEventBody } from '../../common/customEvents'; import { TreeNode } from './treeNode'; import { SessionNode } from './sessionNode'; import { DeferredMap } from '../../common/deferredMap'; export class RootNode extends TreeNode { private children = new DeferredMap(); private showSessions = false; public constructor() { super(''); this.treeItem.contextValue = 'root'; } private waitForSession(sessionId: string): Promise { return this.children.get(sessionId); } public hasSession(sessionId: string): boolean { return !!this.children.getExisting(sessionId); } public addSession(session: vscode.DebugSession): TreeNode | undefined { this.children.set(session.id, new SessionNode(session, this)); return this; } public removeSession(sessionId: string): TreeNode | undefined { this.children.delete(sessionId); return this; } public async addThread( threadInfo: ThreadStartedEventBody, sessionId: string ): Promise { const sessionNode = await this.waitForSession(sessionId); return this.fixChangedItem(sessionNode.addThread(threadInfo)); } public async removeThread( threadId: number, sessionId: string ): Promise { const sessionItem = await this.waitForSession(sessionId); return sessionItem ? this.fixChangedItem(sessionItem.removeThread(threadId)) : undefined; } public async addSource( sourceInfo: NewSourceEventBody, sessionId: string ): Promise { const sessionNode = await this.waitForSession(sessionId); return this.fixChangedItem(sessionNode.addSource(sourceInfo)); } public async removeSources(threadId: number, sessionId: string): Promise { const sessionItem = await this.waitForSession(sessionId); return sessionItem ? this.fixChangedItem(sessionItem.removeSources(threadId)) : undefined; } public async getSourceUrls(sessionId: string): Promise { const sessionNode = await this.waitForSession(sessionId); return sessionNode ? sessionNode.getSourceUrls() : undefined; } public getChildren(): TreeNode[] { this.treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; const existingChildren = this.children.getAllExisting(); if (this.showSessions || (existingChildren.length > 1)) { this.showSessions = true; return existingChildren; } else if (existingChildren.length == 1) { return existingChildren[0].getChildren(); } else { return []; } } private fixChangedItem(changedItem: TreeNode | undefined): TreeNode | undefined { if (!changedItem) return undefined; if (!this.showSessions && (changedItem instanceof SessionNode)) { return this; } else { return changedItem; } } } ================================================ FILE: src/extension/loadedScripts/sessionNode.ts ================================================ import * as vscode from 'vscode'; import { TreeNode } from './treeNode'; import { RootNode } from './rootNode'; import { ThreadNode } from './nonLeafNode'; import { ThreadStartedEventBody, NewSourceEventBody } from '../../common/customEvents'; export class SessionNode extends TreeNode { protected children: ThreadNode[] = []; private showThreads = false; private sourceUrls: string[] = []; public get id() { return this.session.id; } public constructor(private session: vscode.DebugSession, parent: RootNode) { super(session.name, parent); this.treeItem.contextValue = 'session'; } public addThread(threadInfo: ThreadStartedEventBody): TreeNode | undefined { if (!this.children.some((child) => (child.id === threadInfo.id))) { let index = this.children.findIndex((child) => (child.treeItem.label! > threadInfo.name)); if (index < 0) index = this.children.length; this.children.splice(index, 0, new ThreadNode(threadInfo, this)); return this; } else { return undefined; } } public removeThread(threadId: number): TreeNode | undefined { this.children = this.children.filter((child) => (child.id !== threadId)); return this; } public addSource(sourceInfo: NewSourceEventBody): TreeNode | undefined { if (!sourceInfo.url) return undefined; this.sourceUrls.push(sourceInfo.url); let threadItem = this.children.find((child) => (child.id === sourceInfo.threadId)); if (threadItem) { let path = splitURL(sourceInfo.url); let filename = path.pop()!; let description: string | undefined; if (sourceInfo.path) { description = sourceInfo.path; if (this.session.workspaceFolder) { const workspaceUri = this.session.workspaceFolder.uri; let workspacePath = (workspaceUri.scheme === 'file') ? workspaceUri.fsPath : workspaceUri.toString(); workspacePath += '/'; if (description.startsWith(workspacePath)) { description = description.substring(workspacePath.length); } } description = ` → ${description}`; } return this.fixChangedItem(threadItem.addSource(filename, path, description, sourceInfo, this.id)); } else { return undefined; } } public removeSources(threadId: number): TreeNode | undefined { this.sourceUrls = []; let threadItem = this.children.find((child) => (child.id === threadId)); return threadItem ? threadItem.removeSources() : undefined; } public getSourceUrls(): string[] { return this.sourceUrls; } public getChildren(): TreeNode[] { this.treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; if (this.showThreads || (this.children.length > 1)) { this.showThreads = true; return this.children; } else if (this.children.length == 1) { return this.children[0].getChildren(); } else { return []; } } private fixChangedItem(changedItem: TreeNode | undefined): TreeNode | undefined { if (!changedItem) return undefined; if (!this.showThreads && (changedItem instanceof ThreadNode)) { return this; } else { return changedItem; } } } /** * Split a URL with '/' as the separator, without splitting the origin or the search portion */ function splitURL(urlString: string): string[] { let originLength: number; let i = urlString.indexOf(':'); if (i >= 0) { i++; if (urlString[i] === '/') i++; if (urlString[i] === '/') i++; originLength = urlString.indexOf('/', i); } else { originLength = 0; } let searchStartIndex = urlString.indexOf('?', originLength); if (searchStartIndex < 0) { searchStartIndex = urlString.length; } let origin = urlString.substr(0, originLength); let search = urlString.substr(searchStartIndex); let path = urlString.substring(originLength, searchStartIndex); let result = path.split('/'); result[0] = origin + result[0]; result[result.length - 1] += search; return result; } ================================================ FILE: src/extension/loadedScripts/treeNode.ts ================================================ import * as vscode from 'vscode'; export abstract class TreeNode { public readonly treeItem: vscode.TreeItem; public constructor( label: string, public parent?: TreeNode, description?: string, collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.Collapsed ) { this.treeItem = new vscode.TreeItem(label, collapsibleState); this.treeItem.description = description; } public getFullPath(): string { return ''; } public abstract getChildren(): TreeNode[]; } ================================================ FILE: src/extension/main.ts ================================================ import * as vscode from 'vscode'; import isAbsoluteUrl from 'is-absolute-url'; import { LoadedScriptsProvider } from './loadedScripts/provider'; import { EventBreakpointsProvider } from './eventBreakpointsProvider'; import { ThreadStartedEventBody, ThreadExitedEventBody, NewSourceEventBody, RemoveSourcesEventBody, PopupAutohideEventBody, AvailableEventsEventBody } from '../common/customEvents'; import { addPathMapping, addNullPathMapping } from './addPathMapping'; import { PopupAutohideManager } from './popupAutohideManager'; import { DebugConfigurationProvider } from './debugConfigurationProvider'; import { createPathMappingForActiveTextEditor, createPathMappingForPath } from './pathMappingWizard'; export function activate(context: vscode.ExtensionContext) { const loadedScriptsProvider = new LoadedScriptsProvider(); const eventBreakpointsProvider = new EventBreakpointsProvider(); const popupAutohideManager = new PopupAutohideManager(sendCustomRequest); const debugConfigurationProvider = new DebugConfigurationProvider(); context.subscriptions.push(vscode.window.registerTreeDataProvider( 'extension.firefox.loadedScripts', loadedScriptsProvider )); const eventBreakpointsView = vscode.window.createTreeView( 'extension.firefox.eventBreakpoints', { treeDataProvider: eventBreakpointsProvider, manageCheckboxStateManually: true, showCollapseAll: true, } ); context.subscriptions.push(eventBreakpointsView); context.subscriptions.push(eventBreakpointsView.onDidChangeCheckboxState(e => { eventBreakpointsProvider.updateActiveEventBreakpoints(e); sendCustomRequest('setActiveEventBreakpoints', [...eventBreakpointsProvider.activeEventBreakpoints]); })); context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider( 'firefox', debugConfigurationProvider )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.reloadAddon', () => sendCustomRequest('reloadAddon') )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.toggleSkippingFile', (url) => sendCustomRequest('toggleSkippingFile', url) )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.openScript', openScript )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.addPathMapping', addPathMapping )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.addFilePathMapping', addPathMapping )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.addNullPathMapping', addNullPathMapping )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.addNullFilePathMapping', addNullPathMapping )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.enablePopupAutohide', () => popupAutohideManager.setPopupAutohide(true) )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.disablePopupAutohide', () => popupAutohideManager.setPopupAutohide(false) )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.togglePopupAutohide', () => popupAutohideManager.togglePopupAutohide() )); context.subscriptions.push(vscode.commands.registerCommand( 'extension.firefox.pathMappingWizard', () => createPathMappingForActiveTextEditor(loadedScriptsProvider) )); context.subscriptions.push(vscode.debug.onDidReceiveDebugSessionCustomEvent( (event) => onCustomEvent(event, loadedScriptsProvider, eventBreakpointsProvider, popupAutohideManager) )); context.subscriptions.push(vscode.debug.onDidStartDebugSession( (session) => onDidStartSession(session, loadedScriptsProvider) )); context.subscriptions.push(vscode.debug.onDidTerminateDebugSession( (session) => onDidTerminateSession(session, loadedScriptsProvider, popupAutohideManager) )); } async function sendCustomRequest(command: string, args?: any): Promise { await Promise.all([...activeFirefoxDebugSessions].map( session => session.customRequest(command, args) )); } const activeFirefoxDebugSessions = new Set(); function onDidStartSession( session: vscode.DebugSession, loadedScriptsProvider: LoadedScriptsProvider ) { if (session.type === 'firefox') { loadedScriptsProvider.addSession(session); activeFirefoxDebugSessions.add(session); } } function onDidTerminateSession( session: vscode.DebugSession, loadedScriptsProvider: LoadedScriptsProvider, popupAutohideManager: PopupAutohideManager ) { if (session.type === 'firefox') { loadedScriptsProvider.removeSession(session.id); activeFirefoxDebugSessions.delete(session); if (activeFirefoxDebugSessions.size === 0) { popupAutohideManager.disableButton(); } } } function onCustomEvent( event: vscode.DebugSessionCustomEvent, loadedScriptsProvider: LoadedScriptsProvider, eventBreakpointsProvider: EventBreakpointsProvider, popupAutohideManager: PopupAutohideManager ) { if (event.session.type === 'firefox') { switch (event.event) { case 'threadStarted': loadedScriptsProvider.addThread(event.body, event.session.id); break; case 'threadExited': loadedScriptsProvider.removeThread((event.body).id, event.session.id); break; case 'newSource': loadedScriptsProvider.addSource(event.body, event.session.id); break; case 'removeSources': loadedScriptsProvider.removeSources((event.body).threadId, event.session.id); break; case 'popupAutohide': popupAutohideManager.enableButton((event.body).popupAutohide); break; case 'unknownSource': createPathMappingForPath(event.body, event.session, loadedScriptsProvider); break; case 'availableEvents': eventBreakpointsProvider.setAvailableEvents(event.body); break; } } } async function openScript(pathOrUri: string) { let uri: vscode.Uri; if (isAbsoluteUrl(pathOrUri)) { uri = vscode.Uri.parse(pathOrUri); } else { uri = vscode.Uri.file(pathOrUri); } const doc = await vscode.workspace.openTextDocument(uri); vscode.window.showTextDocument(doc); } ================================================ FILE: src/extension/pathMappingWizard.ts ================================================ import path from 'path'; import vscode from 'vscode'; import { URL } from 'url'; import { LoadedScriptsProvider } from './loadedScripts/provider'; import { findLaunchConfig, addPathMappingToLaunchConfig, showLaunchConfig } from './addPathMapping'; interface PathMapping { url: string; path: string; } export async function createPathMappingForActiveTextEditor(loadedScriptsProvider: LoadedScriptsProvider) { const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showErrorMessage("There is no active text editor"); return; } const debugSession = vscode.debug.activeDebugSession; if (!debugSession) { vscode.window.showErrorMessage("There is no active debug session"); return; } if (debugSession.type !== 'firefox') { vscode.window.showErrorMessage("The active debug session is not of type \"firefox\""); return; } const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders) { vscode.window.showErrorMessage('There is no open folder'); return; } const launchConfigReference = findLaunchConfig(workspaceFolders, debugSession); if (!launchConfigReference) { vscode.window.showErrorMessage(`Couldn't find configuration for active debug session "${debugSession.name}"`); return; } const ffUrls = await loadedScriptsProvider.getSourceUrls(debugSession.id); if (!ffUrls) { vscode.window.showErrorMessage("Couldn't load the sources of the active debug session"); return; } if (ffUrls.length === 0) { vscode.window.showWarningMessage("Firefox didn't load any sources in the active debug session yet"); return; } const vscPath = vscodeUriToPath(editor.document.uri); const pathMapping = await createPathMapping(vscPath, ffUrls, debugSession.workspaceFolder); if (pathMapping) { const success = await addPathMappingToLaunchConfig(launchConfigReference, pathMapping.url, pathMapping.path); if (success) { await showLaunchConfig(launchConfigReference.workspaceFolder); vscode.window.showWarningMessage('Configuration was modified - please restart your debug session for the changes to take effect'); } } else { const vscFilename = editor.document.uri.path.split('/').pop()!; vscode.window.showWarningMessage(`Firefox hasn't loaded any file named "${vscFilename}"`); } } export async function createPathMappingForPath( vscPath: string, debugSession: vscode.DebugSession, loadedScriptsProvider: LoadedScriptsProvider ) { const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders) { return; } const launchConfigReference = findLaunchConfig(workspaceFolders, debugSession); if (!launchConfigReference) { return; } if (!loadedScriptsProvider.hasSession(debugSession.id)) { return; } const ffUrls = await loadedScriptsProvider.getSourceUrls(debugSession.id); if (!ffUrls || (ffUrls.length === 0)) { return; } const pathMapping = await createPathMapping(vscPath, ffUrls, debugSession.workspaceFolder); if (!pathMapping) { return; } const message = `This file's path (${vscPath}) isn't mapped to any url that was loaded by Firefox. ` + "Either this file hasn't been loaded by Firefox yet or " + "your debug configuration needs a pathMapping for this file - " + "do you think the file has already been loaded and want to let the " + "Path Mapping Wizard try to create a pathMapping for you?"; const yesOrNo = await vscode.window.showInformationMessage(message, 'Yes', 'No'); if (yesOrNo === 'Yes') { const success = await addPathMappingToLaunchConfig(launchConfigReference, pathMapping.url, pathMapping.path); if (success) { await showLaunchConfig(launchConfigReference.workspaceFolder); vscode.window.showWarningMessage('Configuration was modified - please restart your debug session for the changes to take effect'); } } } async function createPathMapping( vscPath: string, ffUrls: string[], workspaceFolder?: vscode.WorkspaceFolder ): Promise { const parsedFfUrls: URL[] = []; for (const ffUrl of ffUrls) { try { parsedFfUrls.push(new URL(ffUrl)); } catch {} } const bestMatch = findBestMatch(vscPath, parsedFfUrls); if (!bestMatch) return undefined; const pathMapping = await createPathMappingForMatch(vscPath, bestMatch, parsedFfUrls); if (workspaceFolder) { const workspaceFolderPath = vscodeUriToPath(workspaceFolder.uri); if (pathMapping.path.startsWith(workspaceFolderPath)) { pathMapping.path = '${workspaceFolder}' + pathMapping.path.substring(workspaceFolderPath.length); } } pathMapping.path = pathMapping.path.replace(/\\/g, '/'); return pathMapping; } function findBestMatch(vscPath: string, ffUrls: URL[]): URL | undefined { const vscPathSegments = vscodePathToUri(vscPath).path.split('/'); const vscFilename = vscPathSegments.pop()!; let bestMatch: URL | undefined; let bestScore = -1; for (const ffUrl of ffUrls) { const ffPathSegments = ffUrl.pathname.split('/'); const ffFilename = ffPathSegments.pop()!; if (ffFilename !== vscFilename) continue; let score = 0; while ((vscPathSegments.length > 0) && (ffPathSegments.length > 0)) { if (vscPathSegments.pop() === ffPathSegments.pop()) { score++; } else { break; } } if (score > bestScore) { bestMatch = ffUrl; bestScore = score; } } return bestMatch; } async function createPathMappingForMatch( vscPath: string, matchingFfUrl: URL, allFfUrls: URL[] ): Promise { let pathMapping: PathMapping = { url: matchingFfUrl.protocol + '//' + matchingFfUrl.host + matchingFfUrl.pathname, path: vscPath }; while (true) { const generalizedPathMapping = generalizePathMapping(pathMapping); if (!generalizedPathMapping || !await checkPathMapping(generalizedPathMapping, allFfUrls)) { return pathMapping; } pathMapping = generalizedPathMapping; } } function generalizePathMapping(pathMapping: PathMapping): PathMapping | undefined { const lastSegment = pathMapping.url.substring(pathMapping.url.lastIndexOf('/') + 1); const pathSep = vscodePathSep(pathMapping.path); if ((lastSegment === '') || !pathMapping.path.endsWith(pathSep + lastSegment)) { return undefined; } return { url: pathMapping.url.substring(0, pathMapping.url.length - lastSegment.length - 1), path: pathMapping.path.substring(0, pathMapping.path.length - lastSegment.length - 1) } } async function checkPathMapping(pathMapping: PathMapping, ffUrls: URL[]): Promise { for (let i = 0; i < ffUrls.length;) { const ffUrl = ffUrls[i]; const ffUrlWithoutQuery = ffUrl.protocol + '//' + ffUrl.host + ffUrl.pathname; const vscPath = applyPathMapping(ffUrlWithoutQuery, pathMapping); if (vscPath) { try { await vscode.workspace.fs.stat(vscodePathToUri(vscPath)); ffUrls.splice(i, 1); } catch { return false; } } else { i++; } } return true; } function applyPathMapping(ffUrl: string, pathMapping: PathMapping): string | undefined { if (ffUrl.startsWith(pathMapping.url)) { let vscPath = pathMapping.path + ffUrl.substring(pathMapping.url.length); return isWindowsAbsolutePath(vscPath) ? path.normalize(vscPath) : vscPath; } else { return undefined; } } const windowsAbsolutePathRegEx = /^[a-zA-Z]:\\/; function isWindowsAbsolutePath(path: string): boolean { return windowsAbsolutePathRegEx.test(path); } function vscodePathToUri(path: string): vscode.Uri { if (isWindowsAbsolutePath(path)) { return vscode.Uri.file(path); } else { return vscode.Uri.parse(path); } } export function vscodeUriToPath(uri: vscode.Uri): string { return (uri.scheme === 'file') ? uri.fsPath : uri.toString(); } function vscodePathSep(path: string): string { return isWindowsAbsolutePath(path) ? '\\' : '/'; } ================================================ FILE: src/extension/popupAutohideManager.ts ================================================ import * as vscode from 'vscode'; export class PopupAutohideManager { private button: vscode.StatusBarItem | undefined; constructor( private readonly sendCustomRequest: (command: string, args?: any) => Promise ) {} public async setPopupAutohide(popupAutohide: boolean): Promise { await this.sendCustomRequest('setPopupAutohide', popupAutohide.toString()); this.setButtonText(popupAutohide); } public async togglePopupAutohide(): Promise { const popupAutohide = await this.sendCustomRequest('togglePopupAutohide'); this.setButtonText(popupAutohide); } public enableButton(popupAutohide: boolean): void { if (!this.button) { this.button = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); this.button.command = 'extension.firefox.togglePopupAutohide'; this.button.text = ''; this.button.show(); } this.setButtonText(popupAutohide); } public disableButton(): void { if (this.button) { this.button.dispose(); this.button = undefined; } } private setButtonText(popupAutohide: boolean): void { if (this.button) { this.button.text = `Popup auto-hide ${popupAutohide ? 'enabled' : 'disabled'}`; } } } ================================================ FILE: src/test/setup.ts ================================================ import { config } from 'dotenv'; import fs from 'fs-extra'; config({ path: 'src/test/.env'}); export const mochaHooks = { async beforeAll() { if (process.env['FIREFOX_PROFILE_DIR'] && process.env['KEEP_PROFILE_CHANGES'] === 'true') { await fs.remove(process.env['FIREFOX_PROFILE_DIR']); } } }; ================================================ FILE: src/test/sourceMapUtil.ts ================================================ import * as path from 'path'; import * as fs from 'fs-extra'; import { Stream } from 'stream'; import * as assert from 'assert'; import { DebugClient } from '@vscode/debugadapter-testsupport'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as util from './util'; export async function testSourcemaps( dc: DebugClient, srcDir: string, breakpoint: DebugProtocol.SourceBreakpoint = { line: 7 } ): Promise { let fPath = path.join(srcDir, 'f.js'); let gPath = path.join(srcDir, 'g.js'); await util.setBreakpoints(dc, fPath, [breakpoint]); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateDelayed(dc, 'f()', 0)); let threadId = stoppedEvent.body.threadId!; await checkDebuggeeState(dc, threadId, fPath, 7, 'x', '2'); await util.runCommandAndReceiveStoppedEvent(dc, () => dc.stepInRequest({ threadId })); await checkDebuggeeState(dc, threadId, gPath, 5, 'y', '2'); await util.runCommandAndReceiveStoppedEvent(dc, () => dc.stepOutRequest({ threadId })); await checkDebuggeeState(dc, threadId, fPath, 8, 'x', '4'); await util.setBreakpoints(dc, gPath, [ 5 ]); await util.runCommandAndReceiveStoppedEvent(dc, () => dc.continueRequest({ threadId })); await checkDebuggeeState(dc, threadId, gPath, 5, 'y', '4'); } async function checkDebuggeeState( dc: DebugClient, threadId: number, sourcePath: string, line: number, variable: string, value: string ): Promise { let stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, line); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variables = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); assert.equal(util.findVariable(variables.body.variables, variable).value, value); } export async function copyFiles(sourceDir: string, targetDir: string, files: string[]): Promise { await Promise.all(files.map( (file) => fs.copy(path.join(sourceDir, file), path.join(targetDir, file)))); } export async function injectScriptTags(targetDir: string, scripts: string[]): Promise { let file = path.join(targetDir, 'index.html'); let content = await fs.readFile(file, 'utf8'); let scriptTags = scripts.map((script) => ``); content = content.replace('__SCRIPTS__', scriptTags.join('')); await fs.writeFile(file, content); } export function waitForStreamEnd(s: Stream): Promise { return new Promise((resolve) => { s.on('end', () => resolve()); }) } ================================================ FILE: src/test/testAccessorProperties.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; describe('Accessor properties: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); const SOURCE_PATH = path.join(TESTDATA_PATH, 'web/main.js'); afterEach(async function() { await dc.stop(); }); it('should show accessor properties', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let properties = await startAndGetProperties(dc, 98, 'getterAndSetter()'); assert.equal(util.findVariable(properties, 'getterProperty').value, 'Getter - expand to execute Getter'); assert.equal(util.findVariable(properties, 'setterProperty').value, 'Setter'); assert.equal(util.findVariable(properties, 'getterAndSetterProperty').value, 'Getter & Setter - expand to execute Getter'); }); it('should execute getters on demand', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let properties = await startAndGetProperties(dc, 98, 'getterAndSetter()'); let getterProperty = util.findVariable(properties, 'getterProperty'); let getterPropertyResponse = await dc.variablesRequest({ variablesReference: getterProperty.variablesReference }); let getterValue = util.findVariable(getterPropertyResponse.body.variables, 'Value from Getter').value; assert.equal(getterValue, '17'); let getterAndSetterProperty = util.findVariable(properties, 'getterAndSetterProperty'); let getterAndSetterPropertyResponse = await dc.variablesRequest({ variablesReference: getterAndSetterProperty.variablesReference }); let getterAndSetterValue = util.findVariable(getterAndSetterPropertyResponse.body.variables, 'Value from Getter').value; assert.equal(getterAndSetterValue, '23'); }); it('should execute nested getters', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let properties1 = await startAndGetProperties(dc, 98, 'getterAndSetter()'); let getterProperty1 = util.findVariable(properties1, 'nested'); let getterPropertyResponse1 = await dc.variablesRequest({ variablesReference: getterProperty1.variablesReference }); let getterValue1 = util.findVariable(getterPropertyResponse1.body.variables, 'Value from Getter'); let propertiesResponse2 = await dc.variablesRequest({ variablesReference: getterValue1.variablesReference }); let properties2 = propertiesResponse2.body.variables; let getterProperty2 = util.findVariable(properties2, 'z'); let getterPropertyResponse2 = await dc.variablesRequest({ variablesReference: getterProperty2.variablesReference }); let getterValue2 = util.findVariable(getterPropertyResponse2.body.variables, 'Value from Getter').value; assert.equal(getterValue2, '"foo"'); }); it('should show and execute getters lifted from prototypes', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { liftAccessorsFromPrototypes: 2 }); let properties1 = await startAndGetProperties(dc, 116, 'protoGetter()'); let getterProperty1 = util.findVariable(properties1, 'y'); let getterPropertyResponse1 = await dc.variablesRequest({ variablesReference: getterProperty1.variablesReference }); let getterValue1 = util.findVariable(getterPropertyResponse1.body.variables, 'Value from Getter').value; assert.equal(getterValue1, '"foo"'); let getterProperty2 = util.findVariable(properties1, 'z'); let getterPropertyResponse2 = await dc.variablesRequest({ variablesReference: getterProperty2.variablesReference }); let getterValue2 = util.findVariable(getterPropertyResponse2.body.variables, 'Value from Getter').value; assert.equal(getterValue2, '"bar"'); }); it('should only scan the configured number of prototypes for accessors to lift', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { liftAccessorsFromPrototypes: 1 }); let properties = await startAndGetProperties(dc, 116, 'protoGetter()'); util.findVariable(properties, 'y'); assert.throws(() => util.findVariable(properties, 'z')); }); async function startAndGetProperties(dc: DebugClient, bpLine: number, trigger: string): Promise { await util.setBreakpoints(dc, SOURCE_PATH, [ bpLine ]); util.evaluate(dc, trigger); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variablesResponse = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); let variable = util.findVariable(variablesResponse.body.variables, 'x'); let propertiesResponse = await dc.variablesRequest({ variablesReference: variable.variablesReference }); return propertiesResponse.body.variables; } }); ================================================ FILE: src/test/testConfigurationParser.ts ================================================ import * as os from 'os'; import { LaunchConfiguration, AttachConfiguration } from '../common/configuration'; import { parseConfiguration, NormalizedReloadConfiguration } from '../adapter/configuration'; import * as assert from 'assert'; import * as path from 'path'; import { isWindowsPlatform } from '../common/util'; describe('The configuration parser', function() { it('should create default values for a simple launch configuration', async function() { let filePath: string; let fileUrl: string; let tabFilter: string; if (isWindowsPlatform()) { filePath = 'c:\\Users\\user\\project\\index.html'; fileUrl = 'file:///c:/Users/user/project/index.html'; tabFilter = 'file:\/\/\/c:\/Users\/user\/project\/.*'; } else { filePath = '/home/user/project/index.html'; fileUrl = 'file:///home/user/project/index.html'; tabFilter = 'file:\/\/\/home\/user\/project\/.*'; } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: filePath }); assert.equal(parsedConfiguration.attach, undefined); assert.equal(parsedConfiguration.addon, undefined); assert.deepEqual(parsedConfiguration.filesToSkip, []); assert.equal(parsedConfiguration.reloadOnChange, undefined); assert.deepEqual(parsedConfiguration.tabFilter, { include: [ new RegExp(tabFilter) ], exclude: [] }); assert.equal(parsedConfiguration.showConsoleCallLocation, false); assert.equal(parsedConfiguration.liftAccessorsFromPrototypes, 0); assert.equal(parsedConfiguration.suggestPathMappingWizard, true); assert.ok(parsedConfiguration.launch!.firefoxExecutable); assert.equal([...parsedConfiguration.launch!.firefoxArgs].pop(), fileUrl); assert.equal(parsedConfiguration.launch!.port, 6000); assert.equal(parsedConfiguration.launch!.timeout, 5); assert.equal(parsedConfiguration.launch!.preferences['devtools.debugger.remote-enabled'], true); assert.ok(parsedConfiguration.launch!.profileDir); assert.equal(parsedConfiguration.launch!.srcProfileDir, undefined); assert.equal(parsedConfiguration.launch!.tmpDirs.length, 1); assert.equal(parsedConfiguration.launch!.tmpDirs[0], parsedConfiguration.launch!.profileDir); }); it('should create default values for a simple attach configuration', async function() { let parsedConfiguration = await parseConfiguration({ request: 'attach', url: 'https://mozilla.org/', webRoot: '/home/user/project' }); assert.equal(parsedConfiguration.launch, undefined); assert.equal(parsedConfiguration.addon, undefined); assert.deepEqual(parsedConfiguration.filesToSkip, []); assert.equal(parsedConfiguration.reloadOnChange, undefined); assert.deepEqual(parsedConfiguration.tabFilter, { include: [ /https:\/\/mozilla\.org\/.*/ ], exclude: [] }); assert.equal(parsedConfiguration.showConsoleCallLocation, false); assert.equal(parsedConfiguration.liftAccessorsFromPrototypes, 0); assert.equal(parsedConfiguration.suggestPathMappingWizard, true); assert.equal(parsedConfiguration.attach!.port, 6000); assert.equal(parsedConfiguration.attach!.host, 'localhost'); assert.equal(parsedConfiguration.attach!.reloadTabs, false); }); it('should require "file" or "url" to be set in a launch configuration', async function() { await assertPromiseRejects(parseConfiguration({ request: 'launch' }), 'You need to set either "file" or "url" in the launch configuration'); }); it('should require "file" to be an absolute path', async function() { await assertPromiseRejects(parseConfiguration({ request: 'launch', file: './index.html' }), 'The "file" property in the launch configuration has to be an absolute path'); }); it(`should require "webRoot" or "pathMappings" if "url" is specified in a launch configuration`, async function() { await assertPromiseRejects(parseConfiguration({ request: 'launch', url: 'https://mozilla.org/' }), `If you set "url" you also have to set "webRoot" or "pathMappings" in the launch configuration`); }); for (let request of [ 'launch', 'attach' ]) { it(`should require "webRoot" to be an absolute path in a ${request} configuration`, async function() { await assertPromiseRejects(parseConfiguration({ request, url: 'https://mozilla.org/', webRoot: './project' }), `The "webRoot" property in the ${request} configuration has to be an absolute path`); }); it(`should allow "url" without "webRoot" if "pathMappings" are specified in a ${request} configuration`, async function() { await parseConfiguration({ request, url: 'https://mozilla.org/', pathMappings: [{ url:'https://mozilla.org/', path: './project' }] }); }); } it('should require "url" if "webRoot" is specified in an attach configuration', async function() { await assertPromiseRejects(parseConfiguration({ request: 'attach', webRoot: '/home/user/project' }), 'If you set "webRoot" you also have to set "url" in the attach configuration'); }); it('should create a pathMapping for mapping "url" to "webRoot"', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org/', webRoot: '/home/user/project' }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://mozilla.org')!.path, '/home/user/project'); }); it('should strip a filename from the url and a trailing slash from the webRoot in the pathMapping', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org/index.html', webRoot: '/home/user/project/' }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://mozilla.org')!.path, '/home/user/project'); }); it('should not strip the hostname from the webRoot in the pathMapping', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org', webRoot: '/home/user/project' }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://mozilla.org')!.path, '/home/user/project'); }); it('should include a user-specified pathMapping in a launch configuration', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org/index.html', webRoot: '/home/user/project/', pathMappings: [{ url: 'https://static.mozilla.org', path: '/home/user/project/static' }] }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://static.mozilla.org')!.path, '/home/user/project/static'); }); it('should include a user-specified pathMapping in an attach configuration', async function() { let parsedConfiguration = await parseConfiguration({ request: 'attach', pathMappings: [{ url: 'https://static.mozilla.org', path: '/home/user/project/static' }] }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://static.mozilla.org')!.path, '/home/user/project/static'); }); it('should replace ${webRoot} in a user-specified pathMapping', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org/index.html', webRoot: '/home/user/project/', pathMappings: [{ url: 'https://static.mozilla.org', path: '${webRoot}/static' }] }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://static.mozilla.org')!.path, '/home/user/project/static'); }); it('should harmonize trailing slashes in user-specified pathMappings', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org/index.html', webRoot: '/home/user/project/', pathMappings: [{ url: 'https://static.mozilla.org', path: '${webRoot}/static/' }, { url: 'https://api.mozilla.org/', path: '${webRoot}/api' }] }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://static.mozilla.org/')!.path, '/home/user/project/static/'); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'https://api.mozilla.org/')!.path, '/home/user/project/api/'); }); it('should add default pathMappings for webpack if webRoot is defined', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org/index.html', webRoot: '/home/user/project/' }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'webpack:///~/')!.path, '/home/user/project/node_modules/'); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'webpack:///./~/')!.path, '/home/user/project/node_modules/'); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'webpack:///./')!.path, '/home/user/project/'); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === 'webpack:///src/')!.path, '/home/user/project/src/'); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === (isWindowsPlatform() ? 'webpack:///' : 'webpack://'))!.path, ''); }); it('should add only one default pathMapping for webpack if webRoot is not defined', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', }); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => mapping.url === (isWindowsPlatform() ? 'webpack:///' : 'webpack://'))!.path, ''); assert.equal(parsedConfiguration.pathMappings.find( (mapping) => ((typeof mapping.url === 'string') && mapping.url.startsWith('webpack') && (mapping.url.length > 11))), undefined); }); it('should create an attach configuration if "reAttach" is set to true', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reAttach: true }); assert.ok(parsedConfiguration.attach); }); { let launchConfig: LaunchConfiguration = { request: 'launch', file: '/home/user/project/index.html', reAttach: true }; let attachConfig: AttachConfiguration = { request: 'attach', url: 'https://mozilla.org/', webRoot: '/home/user/project' }; for (let config of [ launchConfig, attachConfig ]) { for (let reloadOnAttach of [ true, false ]) { it(`should set "reloadTabs" to ${reloadOnAttach} if "reloadOnAttach" is set to ${reloadOnAttach} in a ${config.request} configuration`, async function() { let parsedConfiguration = await parseConfiguration( Object.assign({ reloadOnAttach}, config)); assert.equal(parsedConfiguration.attach!.reloadTabs, reloadOnAttach); }); }} } it('should set "reloadTabs" to true if "reloadOnAttach" is not set in a launch configuration with "reAttach" set to true', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reAttach: true }); assert.equal(parsedConfiguration.attach!.reloadTabs, true); }); it('should create a corresponding NormalizedReloadConfiguration if "reloadOnChange" is set to a string', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange: '/home/user/project' }); assert.deepEqual(parsedConfiguration.reloadOnChange, { watch: [ '/home/user/project' ], ignore: [], debounce: 100 }); }); it('should create a corresponding NormalizedReloadConfiguration if "reloadOnChange" is set to a string array', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange: [ '/home/user/project/js', '/home/user/project/css' ] }); assert.deepEqual(parsedConfiguration.reloadOnChange, { watch: [ '/home/user/project/js', '/home/user/project/css' ], ignore: [], debounce: 100 }); }); it('should convert strings to string arrays and "debounce": false to "debounce": 0 in a detailed "reloadOnChange" configuration', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange: { watch: '/home/user/project/js', ignore: '/home/user/project/js/dummy.js', debounce: false } }); assert.deepEqual(parsedConfiguration.reloadOnChange, { watch: [ '/home/user/project/js' ], ignore: [ '/home/user/project/js/dummy.js' ], debounce: 0 }); }); it('should add defaults to a detailed "reloadOnChange" configuration', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange: { watch: [ '/home/user/project/js' ], } }); assert.deepEqual(parsedConfiguration.reloadOnChange, { watch: [ '/home/user/project/js' ], ignore: [], debounce: 100 }); }); it('should copy a normalized "reloadOnChange" configuration', async function() { let reloadOnChange: NormalizedReloadConfiguration = { watch: [ '/home/user/project/js', '/home/user/project/css' ], ignore: [ '/home/user/project/css/dummy.css' ], debounce: 200 } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange }); assert.deepEqual(parsedConfiguration.reloadOnChange, reloadOnChange); }); it('should convert windows-style directory separators for all globs provided to "reloadOnChange"', async function () { if (isWindowsPlatform()) { let reloadOnChangeNormalized: NormalizedReloadConfiguration = { watch: ['C:/Users/WinUser/Projects/project/scripts/**/*.js', '!C:/Users/WinUser/Projects/project/scripts/composer.js'], ignore: ['C:/Users/WinUser/Projects/project/scripts/cache/**/*.js'], debounce: 200 } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: 'C:\\Users\\WinUser\\Projects\\project/index.html', reloadOnChange: { watch: ['C:\\Users\\WinUser\\Projects\\project/scripts/**/*.js', '!C:\\Users\\WinUser\\Projects\\project/scripts/composer.js'], ignore: ['C:\\Users\\WinUser\\Projects\\project/scripts/cache/**/*.js'], debounce: 200 } }); assert.deepEqual(parsedConfiguration.reloadOnChange, reloadOnChangeNormalized); } else { let reloadOnChangeNormalized: NormalizedReloadConfiguration = { watch: ['/home/user/project/scripts/**/*.js', '!/home/user/project/scripts/composer.js'], ignore: ['/home/user/project/scripts/cache/**/*.js'], debounce: 200 } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange: { watch: ['/home/user/project/scripts/**/*.js', '!/home/user/project/scripts/composer.js'], ignore: ['/home/user/project/scripts/cache/**/*.js'], debounce: 200 } }); assert.deepEqual(parsedConfiguration.reloadOnChange, reloadOnChangeNormalized); } }); it('should convert windows-style directory separators for all globs provided to "reloadOnChange" in an array', async function () { if (isWindowsPlatform()) { let reloadOnChangeNormalized: NormalizedReloadConfiguration = { watch: ['C:/Users/WinUser/Projects/project/scripts/**/*.js', '!C:/Users/WinUser/Projects/project/scripts/composer.js'], ignore: [], debounce: 100 } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: 'C:\\Users\\WinUser\\Projects\\project/index.html', reloadOnChange: ['C:\\Users\\WinUser\\Projects\\project/scripts/**/*.js', '!C:\\Users\\WinUser\\Projects\\project/scripts/composer.js'] }); assert.deepEqual(parsedConfiguration.reloadOnChange, reloadOnChangeNormalized); } else { let reloadOnChangeNormalized: NormalizedReloadConfiguration = { watch: ['/home/user/project/scripts/**/*.js', '!/home/user/project/scripts/composer.js'], ignore: [], debounce: 100 } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange: ['/home/user/project/scripts/**/*.js', '!/home/user/project/scripts/composer.js'] }); assert.deepEqual(parsedConfiguration.reloadOnChange, reloadOnChangeNormalized); } }); it('should convert windows-style directory separators for the single glob provided to "reloadOnChange"', async function () { if (isWindowsPlatform()) { let reloadOnChangeNormalized: NormalizedReloadConfiguration = { watch: ['C:/Users/WinUser/Projects/project/scripts/**/*.js'], ignore: [], debounce: 100 } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: 'C:\\Users\\WinUser\\Projects\\project/index.html', reloadOnChange: 'C:\\Users\\WinUser\\Projects\\project/scripts/**/*.js' }); assert.deepEqual(parsedConfiguration.reloadOnChange, reloadOnChangeNormalized); } else { let reloadOnChangeNormalized: NormalizedReloadConfiguration = { watch: ['/home/user/project/scripts/**/*.js'], ignore: [], debounce: 100 } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reloadOnChange: '/home/user/project/scripts/**/*.js' }); assert.deepEqual(parsedConfiguration.reloadOnChange, reloadOnChangeNormalized); } }); it('should parse "skipFiles"', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', skipFiles: [ '/home/user/project/libs/**/*' ] }); assert.equal(parsedConfiguration.filesToSkip.length, 1); }); it('should create a corresponding ParsedTabFilterConfiguration if "tabFilter" is set to a string', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', tabFilter: 'http://localhost:3000' }); assert.deepEqual(parsedConfiguration.tabFilter, { include: [ /^http:\/\/localhost:3000$/ ], exclude: [] }); }); it('should create a corresponding ParsedTabFilterConfiguration if "tabFilter" is set to a string array', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', tabFilter: [ 'http://localhost:3000', 'about:newtab' ] }); assert.deepEqual(parsedConfiguration.tabFilter, { include: [ /^http:\/\/localhost:3000$/, /^about:newtab$/ ], exclude: [] }); }); it('should add "exclude" to a detailed "tabFilter" containing only "include"', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', tabFilter: { include: '*localhost*' } }); assert.deepEqual(parsedConfiguration.tabFilter, { include: [ /^.*localhost.*$/ ], exclude: [] }); }); it('should add "include" to a detailed "tabFilter" containing only "exclude"', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', tabFilter: { exclude: 'https://developer.mozilla.org/*' } }); assert.deepEqual(parsedConfiguration.tabFilter, { include: [ /.*/ ], exclude: [ /^https:\/\/developer\.mozilla\.org\/.*$/ ] }); }); it('should copy a detailed "tabFilter"', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', tabFilter: { include: [ '*localhost*' ], exclude: [ 'https://developer.mozilla.org/*' ] } }); assert.deepEqual(parsedConfiguration.tabFilter, { include: [ /^.*localhost.*$/ ], exclude: [ /^https:\/\/developer\.mozilla\.org\/.*$/ ] }); }); it('should copy the "showConsoleCallLocation" value', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', showConsoleCallLocation: true }); assert.equal(parsedConfiguration.showConsoleCallLocation, true); }); it('should copy the "liftAccessorsFromPrototypes" value', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', liftAccessorsFromPrototypes: 1 }); assert.equal(parsedConfiguration.liftAccessorsFromPrototypes, 1); }); it('should copy the "suggestPathMappingWizard" value', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', suggestPathMappingWizard: false }); assert.equal(parsedConfiguration.suggestPathMappingWizard, false); }); it('should not allow both "profile" and "profileDir" to be specified', async function() { await assertPromiseRejects(parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', profile: 'default', profileDir: '/home/user/firefoxProfile' }), 'You can set either "profile" or "profileDir", but not both'); }); it('should not allow "keepProfileChanges" if neither "profile" nor "profileDir" is set', async function() { await assertPromiseRejects(parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', keepProfileChanges: true, }), 'To enable "keepProfileChanges" you need to set either "profile" or "profileDir"'); }); for (let keepProfileChanges of [ undefined, false ]) { it(`should copy "profileDir" to "srcProfileDir" if "keepProfileChanges" is ${keepProfileChanges}`, async function() { let profileDir = '/home/user/project/ff-profile'; let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', profileDir, keepProfileChanges }); assert.equal(parsedConfiguration.launch!.srcProfileDir, profileDir); assert.ok(parsedConfiguration.launch!.profileDir); assert.notEqual(parsedConfiguration.launch!.profileDir, profileDir); }); } it('should copy "profileDir" to "profileDir" if "keepProfileChanges" is true', async function() { let profileDir = '/home/user/project/ff-profile'; let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reAttach: true, profileDir, keepProfileChanges: true }); assert.equal(parsedConfiguration.launch!.profileDir, profileDir); assert.equal(parsedConfiguration.launch!.srcProfileDir, undefined); }); it('should use the specified tmpDir', async function() { const tmpDir = '/home/user/tmp'; const parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', tmpDir }); assert.ok(parsedConfiguration.launch!.profileDir.startsWith(path.join(tmpDir + '/vscode-firefox-debug-profile-'))); }); it('should parse user-specified Firefox preferences', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', preferences: { 'my.boolean': true, 'my.number': 17, 'my.string': 'foo', 'devtools.debugger.remote-enabled': null } }); let parsedPreferences = parsedConfiguration.launch!.preferences; assert.equal(parsedPreferences['my.boolean'], true); assert.equal(parsedPreferences['my.number'], 17); assert.equal(parsedPreferences['my.string'], 'foo'); assert.equal(parsedPreferences['devtools.debugger.remote-enabled'], undefined); }); it('should copy "port" from a launch configuration', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', reAttach: true, port: 7000 }); assert.equal(parsedConfiguration.attach!.port, 7000); assert.equal(parsedConfiguration.launch!.port, 7000); assert.ok(parsedConfiguration.launch!.firefoxArgs.indexOf('6000') < 0); assert.ok(parsedConfiguration.launch!.firefoxArgs.indexOf('7000') >= 0); }); it('should copy "timeout" from a launch configuration', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', timeout: 10 }); assert.equal(parsedConfiguration.launch!.timeout, 10); }); it('should add user-specified "firefoxArgs"', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', file: '/home/user/project/index.html', firefoxArgs: [ '-private' ] }); assert.ok(parsedConfiguration.launch!.firefoxArgs.indexOf('-private') >= 0); }); it('should allow installing WebExtensions that don\'t specify an ID in their manifest', async function() { await parseConfiguration({ request: 'launch', addonPath: path.join(__dirname, '../../testdata/webExtension2/addOn') }); }); it('should add pathMappings for WebExtension debugging', async function() { let addonPath = path.join(__dirname, '../../testdata/webExtension/addOn'); let parsedConfiguration = await parseConfiguration({ request: 'launch', addonPath }); assert.equal(parsedConfiguration.pathMappings.find( (pathMapping) => (typeof pathMapping.url === 'object') && (pathMapping.url.source === '^moz-extension:\\/\\/[0-9a-f-]*(\\/.*)$'))!.path, addonPath); assert.equal(parsedConfiguration.pathMappings.find( (pathMapping) => (typeof pathMapping.url === 'object') && (pathMapping.url.source === '^jar:file:.*\\/extensions\\/%7B12345678-1234-1234-1234-123456781234%7D.xpi!(\\/.*)$'))!.path, addonPath); }); it('should default to "about:blank" as the start page for addon debugging', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', addonPath: path.join(__dirname, '../../testdata/webExtension/addOn') }); assert.equal([...parsedConfiguration.launch!.firefoxArgs].pop(), 'about:blank'); }); it('should allow setting "file" to define the start page for addon debugging', async function() { let filePath: string; let fileUrl: string; if (isWindowsPlatform()) { filePath = 'c:\\Users\\user\\project\\index.html'; fileUrl = 'file:///c:/Users/user/project/index.html'; } else { filePath = '/home/user/project/index.html'; fileUrl = 'file:///home/user/project/index.html'; } let parsedConfiguration = await parseConfiguration({ request: 'launch', file: filePath, addonPath: path.join(__dirname, '../../testdata/webExtension/addOn') }); assert.equal([...parsedConfiguration.launch!.firefoxArgs].pop(), fileUrl); }); it('should allow setting "url" to define the start page for addon debugging', async function() { let parsedConfiguration = await parseConfiguration({ request: 'launch', url: 'https://mozilla.org', webRoot: '/home/user/project', addonPath: path.join(__dirname, '../../testdata/webExtension/addOn') }); assert.equal([...parsedConfiguration.launch!.firefoxArgs].pop(), 'https://mozilla.org'); }); }); async function assertPromiseRejects(promise: Promise, reason: string): Promise { try { await promise; } catch(err) { assert.equal(err, reason); return; } throw new Error('The promise was resolved but should have been rejected'); } ================================================ FILE: src/test/testConsole.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; import { isWindowsPlatform } from '../common/util'; describe('Debug console: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); afterEach(async function() { await dc.stop(); }); it('should forward messages from the browser console to vscode', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); util.evaluate(dc, 'console.log("log")'); let outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'stdout'); assert.equal(outputEvent.body.output.trim(), 'log'); util.evaluate(dc, 'console.debug("debug")'); outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'stdout'); assert.equal(outputEvent.body.output.trim(), 'debug'); util.evaluate(dc, 'console.info("info")'); outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'stdout'); assert.equal(outputEvent.body.output.trim(), 'info'); util.evaluate(dc, 'console.warn("warn")'); outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'console'); assert.equal(outputEvent.body.output.trim(), 'warn'); util.evaluate(dc, 'console.error("error")'); outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'stderr'); assert.equal(outputEvent.body.output.trim(), 'error'); util.evaluate(dc, 'console.log("foo",2)'); outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'stdout'); assert.equal(outputEvent.body.output.trim(), 'foo 2'); util.evaluate(dc, 'console.log({"foo":"bar"})'); outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'stdout'); assert.notEqual(outputEvent.body.variablesReference, undefined); let vars = await dc.variablesRequest({ variablesReference: outputEvent.body.variablesReference! }); assert.equal(vars.body.variables.length, 1); assert.equal(vars.body.variables[0].name, 'arguments'); vars = await dc.variablesRequest({ variablesReference: vars.body.variables[0].variablesReference }); assert.equal(vars.body.variables.length, 1); assert.equal(vars.body.variables[0].name, 'arg0'); vars = await dc.variablesRequest({ variablesReference: vars.body.variables[0].variablesReference }); assert.equal(vars.body.variables.length, 2); assert.equal(util.findVariable(vars.body.variables, 'foo').value, '"bar"'); }); it('should send error messages from the browser to vscode', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); dc.setExceptionBreakpointsRequest({ filters: [] }); util.evaluateDelayed(dc, 'foo.bar', 0); let outputEvent = await dc.waitForEvent('output'); let category = outputEvent.body.category; assert.equal((category === 'stdout') || (category === 'stderr'), true); assert.equal(outputEvent.body.output.trim(), 'ReferenceError: foo is not defined'); util.evaluateDelayed(dc, 'eval("foo(")', 0); outputEvent = await dc.waitForEvent('output'); category = outputEvent.body.category; assert.equal((category === 'stdout') || (category === 'stderr'), true); assert.equal(outputEvent.body.output.trim(), 'SyntaxError: expected expression, got end of script'); util.evaluateDelayed(dc, 'throw new Error("Something went wrong")', 0); outputEvent = await dc.waitForEvent('output'); category = outputEvent.body.category; assert.equal((category === 'stdout') || (category === 'stderr'), true); assert.equal(outputEvent.body.output.trim(), 'Error: Something went wrong'); }); it('should append the console call location to console messages', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { showConsoleCallLocation: true }); let expectedMessageEnding = 'testdata/web/main.js:80:10)'; if (isWindowsPlatform()) { expectedMessageEnding = expectedMessageEnding.replace(/\//g, '\\'); } util.evaluate(dc, 'log("foo")'); let outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.output.substr(0, 5), 'foo ('); assert.ok(outputEvent.body.output.endsWith(expectedMessageEnding + '\n')); util.evaluate(dc, 'log("foo","bar")'); outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.output.substr(0, 9), 'foo bar ('); assert.ok(outputEvent.body.output.endsWith(expectedMessageEnding + '\n')); util.evaluate(dc, 'log({"foo":"bar"})'); outputEvent = await dc.waitForEvent('output'); assert.notEqual(outputEvent.body.variablesReference, undefined); assert.equal(outputEvent.body.output.substr(0, 14), '{foo: "bar"} ('); assert.ok(outputEvent.body.output.endsWith(expectedMessageEnding + '\n')); }); it('should offer code completions in the debugging console', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let completionsResult = await dc.completionsRequest({ text: 'v', column: 2 }); let completions = completionsResult.body.targets.map(item => item.label); assert.ok(completions.length >= 3); assert.ok(completions.indexOf('valueOf') >= 0); assert.ok(completions.indexOf('values') >= 0); assert.ok(completions.indexOf('vars') >= 0); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 12 ]); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'vars()')); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId }); let frameId = stackTrace.body.stackFrames[0].id; completionsResult = await dc.completionsRequest({ frameId, text: 'n', column: 2 }); completions = completionsResult.body.targets.map(item => item.label); assert.ok(completions.length >= 6); assert.ok(completions.indexOf('name') >= 0); assert.ok(completions.indexOf('navigator') >= 0); assert.ok(completions.indexOf('netscape') >= 0); assert.ok(completions.indexOf('noop') >= 0); assert.ok(completions.indexOf('num1') >= 0); assert.ok(completions.indexOf('num2') >= 0); }); }); ================================================ FILE: src/test/testDataBreakpoints.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import * as path from 'path'; import * as assert from 'assert'; import * as util from './util'; describe('Data breakpoints: The debug adapter', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); afterEach(async function() { await dc.stop(); }); it('should add a data breakpoint and hit it', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); const sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await setupDataBreakpoint(sourcePath); // check that we hit the data breakpoint const stoppedEvent = await util.receiveStoppedEvent(dc); const stackTraceResponse = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); assert.strictEqual(stackTraceResponse.body.stackFrames[0].source!.path, sourcePath); assert.strictEqual(stackTraceResponse.body.stackFrames[0].line, 121); }); it('should remove and re-add a data breakpoint and hit it', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); const sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); const dataId = await setupDataBreakpoint(sourcePath); let stoppedEvent = await util.receiveStoppedEvent(dc); await dc.continueRequest({ threadId: stoppedEvent.body.threadId }); // set a regular breakpoint after the data breakpoint and remove the data breakpoint await util.setBreakpoints(dc, sourcePath, [ 122 ], true); await dc.setDataBreakpointsRequest({ breakpoints: [] }); // check that we don't hit the data breakpoint anymore util.evaluate(dc, 'inc(obj)'); stoppedEvent = await util.receiveStoppedEvent(dc); let stackTraceResponse = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); assert.strictEqual(stackTraceResponse.body.stackFrames[0].source!.path, sourcePath); assert.strictEqual(stackTraceResponse.body.stackFrames[0].line, 122); await dc.continueRequest({ threadId: stoppedEvent.body.threadId }); // re-add the data breakpoint await dc.setDataBreakpointsRequest({ breakpoints: [ { dataId } ] }); // check that we hit the data breakpoint again util.evaluate(dc, 'inc(obj)'); stoppedEvent = await util.receiveStoppedEvent(dc); stackTraceResponse = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); assert.strictEqual(stackTraceResponse.body.stackFrames[0].source!.path, sourcePath); assert.strictEqual(stackTraceResponse.body.stackFrames[0].line, 121); }); async function setupDataBreakpoint(sourcePath: string): Promise { // set a regular breakpoint and hit it await util.setBreakpoints(dc, sourcePath, [ 120 ], true); util.evaluate(dc, 'inc(obj)'); const stoppedEvent = await util.receiveStoppedEvent(dc); // find the object in the top scope const stackTraceResponse = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); const frameId = stackTraceResponse.body.stackFrames[0].id; const scopes = await dc.scopesRequest({ frameId }); const scopeVariablesReference = scopes.body.scopes[0].variablesReference; const variablesResponse = await dc.variablesRequest({ variablesReference: scopeVariablesReference }); const variable = util.findVariable(variablesResponse.body.variables, 'o'); // set a data breakpoint on the object's `x` property const dbpInfoResponse = await dc.dataBreakpointInfoRequest({ variablesReference: variable.variablesReference, name: 'x' }); assert.strictEqual(dbpInfoResponse.body.description, 'x'); assert.deepStrictEqual(dbpInfoResponse.body.accessTypes, [ 'read', 'write' ]); const dataId = dbpInfoResponse.body.dataId!; assert.strictEqual(!!dataId, true); await dc.setDataBreakpointsRequest({ breakpoints: [ { dataId, accessType: 'write' } ] }); dc.continueRequest({ threadId: stoppedEvent.body.threadId }); return dataId; } }); ================================================ FILE: src/test/testDebugAddons.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; import { delay } from '../common/util'; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); describe('Addons: The debugger', function() { let dc: DebugClient; afterEach(async function() { await dc.stop(); }); it(`should debug a WebExtension`, async function() { dc = await util.initDebugClientForAddon(TESTDATA_PATH); await debugWebExtension(dc); }); it(`should show log messages from WebExtensions`, async function() { dc = await util.initDebugClientForAddon(TESTDATA_PATH); await util.setConsoleThread(dc, await util.findTabThread(dc)); util.evaluate(dc, 'putMessage("bar")'); let outputEvent = await dc.waitForEvent('output'); assert.equal(outputEvent.body.category, 'stdout'); assert.equal(outputEvent.body.output.trim(), 'foo: bar'); }); it(`should debug a WebExtension without an ID if it is installed using RDP`, async function() { dc = await util.initDebugClientForAddon(TESTDATA_PATH, { addonDirectory: 'webExtension2' }); await debugWebExtension(dc, 'webExtension2'); }); }); async function debugWebExtension(dc: DebugClient, addonDirectory = 'webExtension') { let backgroundScriptPath = path.join(TESTDATA_PATH, addonDirectory, 'addOn/backgroundscript.js'); await util.setBreakpoints(dc, backgroundScriptPath, [ 2 ]); let contentScriptPath = path.join(TESTDATA_PATH, addonDirectory, 'addOn/contentscript.js'); await util.setBreakpoints(dc, contentScriptPath, [ 6 ]); await delay(500); await util.setConsoleThread(dc, await util.findTabThread(dc)); util.evaluate(dc, 'putMessage("bar")'); let stoppedEvent = await util.receiveStoppedEvent(dc); let contentThreadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId: contentThreadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, contentScriptPath); dc.continueRequest({ threadId: contentThreadId }); stoppedEvent = await util.receiveStoppedEvent(dc); let addOnThreadId = stoppedEvent.body.threadId!; stackTrace = await dc.stackTraceRequest({ threadId: addOnThreadId }); assert.notEqual(contentThreadId, addOnThreadId); } ================================================ FILE: src/test/testDebugWebWorkers.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; describe('Webworkers: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); beforeEach(async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); }); afterEach(async function() { await dc.stop(); }); it('should debug a WebWorker', async function() { let mainSourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, mainSourcePath, [ 55 ]); let workerSourcePath = path.join(TESTDATA_PATH, 'web/worker.js'); let workerBreakpointsResponse = await util.setBreakpoints(dc, workerSourcePath, [ 2 ], false); let workerBreakpoint = workerBreakpointsResponse.body.breakpoints[0]; assert.equal(workerBreakpoint.verified, false); util.evaluateDelayed(dc, 'startWorker()', 0); let breakpointEvent = await util.receiveBreakpointEvent(dc); assert.equal(breakpointEvent.body.breakpoint.id, workerBreakpoint.id); assert.equal(breakpointEvent.body.breakpoint.verified, true); assert.equal(breakpointEvent.body.breakpoint.line, 2); util.evaluateDelayed(dc, 'callWorker()', 0); let stoppedEvent = await util.receiveStoppedEvent(dc); let workerThreadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId: workerThreadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, workerSourcePath); dc.continueRequest({ threadId: workerThreadId }); stoppedEvent = await util.receiveStoppedEvent(dc); let mainThreadId = stoppedEvent.body.threadId!; stackTrace = await dc.stackTraceRequest({ threadId: mainThreadId }); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variables = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); assert.notEqual(mainThreadId, workerThreadId); assert.equal(stackTrace.body.stackFrames[0].source!.path, mainSourcePath); assert.equal(util.findVariable(variables.body.variables, 'received').value, '"bar"'); }); }); ================================================ FILE: src/test/testEvaluate.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; describe('Evaluate: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); beforeEach(async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); }); afterEach(async function() { await dc.stop(); }); it('should evaluate watches while running', async function() { let evalResult = await dc.evaluateRequest({ expression: 'obj.x + 7', context: 'watch' }); assert.equal(evalResult.body.result, '24'); }); it('should evaluate watches on different stackframes', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let evalResult = await dc.evaluateRequest({ expression: 'n*2', context: 'watch', frameId: stackTrace.body.stackFrames[0].id }); assert.equal(evalResult.body.result, '2'); evalResult = await dc.evaluateRequest({ expression: 'n*2', context: 'watch', frameId: stackTrace.body.stackFrames[1].id }); assert.equal(evalResult.body.result, '4'); }); it('should skip over breakpoints when evaluating watches while running', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 8, 25 ]); dc.evaluateRequest({ expression: 'factorial(3)', context: 'watch' }); await util.assertPromiseTimeout(util.receiveStoppedEvent(dc), 200); }); it('should skip over breakpoints when evaluating watches while paused', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 8, 25 ]); util.evaluate(dc, 'vars()'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); dc.evaluateRequest({ expression: 'factorial(3)', context: 'watch', frameId: stackTrace.body.stackFrames[0].id }); await util.assertPromiseTimeout(util.receiveStoppedEvent(dc), 200); }); it('should inspect watches evaluated while running', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'watch' }); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); it('should inspect watches evaluated while paused', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'watch', frameId: stackTrace.body.stackFrames[0].id }); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); it('should inspect watches (evaluated while running) after running other evaluations', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'watch' }); await dc.evaluateRequest({ expression: 'obj.x', context: 'watch' }); await dc.evaluateRequest({ expression: 'obj.y', context: 'repl' }); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); it('should inspect watches (evaluated while paused) after running other evaluations', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'watch', frameId: stackTrace.body.stackFrames[0].id }); await dc.evaluateRequest({ expression: 'n', context: 'watch', frameId: stackTrace.body.stackFrames[0].id }); await dc.evaluateRequest({ expression: 'obj.y', context: 'repl', frameId: stackTrace.body.stackFrames[0].id }); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); it('should evaluate console expressions on different stackframes', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let evalResult = await dc.evaluateRequest({ expression: 'n*2', context: 'repl', frameId: stackTrace.body.stackFrames[0].id }); assert.equal(evalResult.body.result, '2'); evalResult = await dc.evaluateRequest({ expression: 'n*2', context: 'repl', frameId: stackTrace.body.stackFrames[1].id }); assert.equal(evalResult.body.result, '4'); }); it('should evaluate console expressions while the thread is running', async function() { let evalResult = await dc.evaluateRequest({ expression: 'obj.x', context: 'repl' }); assert.equal(evalResult.body.result, '17'); }); it('should hit breakpoints when evaluating console expressions while running', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 8, 25 ]); dc.evaluateRequest({ expression: 'factorial(3)', context: 'repl' }); await util.receiveStoppedEvent(dc); }); it('should inspect console evaluation results after breaking', async function() { let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'repl' }); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); await util.receiveStoppedEvent(dc); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); it('should inspect console evaluation results after running other evaluations', async function() { let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'repl' }); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); await dc.evaluateRequest({ expression: 'n*2', context: 'watch', frameId: stackTrace.body.stackFrames[0].id }); await dc.evaluateRequest({ expression: 'obj.y', context: 'repl', frameId: stackTrace.body.stackFrames[0].id }); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); it('should inspect console evaluation results after stepping', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'repl', frameId: stackTrace.body.stackFrames[0].id }); dc.stepOutRequest({ threadId: stoppedEvent.body.threadId! }); await util.receiveStoppedEvent(dc); dc.stepOutRequest({ threadId: stoppedEvent.body.threadId! }); await util.receiveStoppedEvent(dc); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); it('should inspect console evaluation results after resuming', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let evalResult = await dc.evaluateRequest({ expression: 'obj', context: 'repl', frameId: stackTrace.body.stackFrames[0].id }); await dc.continueRequest({ threadId: stoppedEvent.body.threadId! }); let inspectResult = await dc.variablesRequest({ variablesReference: evalResult.body.variablesReference }); assert.equal(util.findVariable(inspectResult.body.variables, 'x').value, '17'); }); }); ================================================ FILE: src/test/testGulpSourceMaps.ts ================================================ import * as os from 'os'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as uuid from 'uuid'; import * as util from './util'; import * as sourceMapUtil from './sourceMapUtil'; import gulp from 'gulp'; import nop from 'gulp-nop'; import sourcemaps from 'gulp-sourcemaps'; import uglify from 'gulp-uglify'; import rename from 'gulp-rename'; import concat from 'gulp-concat'; import mapSources from '@gulp-sourcemaps/map-sources'; import { DebugClient } from '@vscode/debugadapter-testsupport'; const TESTDATA_PATH = path.join(__dirname, '../../testdata/web/sourceMaps/scripts'); describe('Gulp sourcemaps: The debugger', function() { let dc: DebugClient | undefined; let targetDir: string | undefined; afterEach(async function() { if (dc) { await dc.stop(); dc = undefined; } if (targetDir) { await fs.remove(targetDir); targetDir = undefined; } }); for (let minifyScripts of [false, true]) { for (let bundleScripts of [false, true]) { for (let embedSourceMap of [false, true]) { for (let separateBuildDir of [false, true]) { // we need to apply at least one transformation, otherwise no sourcemap will be generated if (!minifyScripts && !bundleScripts) continue; const transformations: string[] = []; if (minifyScripts) transformations.push('minified'); if (bundleScripts) transformations.push('bundled'); let descr = `should map ${transformations.join(', ')} scripts ` + `to their original sources in ${separateBuildDir ? 'a different' : 'the same'} directory ` + `using an ${embedSourceMap ? 'embedded' : 'external'} source-map`; it(descr, async function() { const targetPaths = await prepareTargetDir(bundleScripts, separateBuildDir); targetDir = targetPaths.targetDir; await build(targetPaths.buildDir, minifyScripts, bundleScripts, embedSourceMap, separateBuildDir); dc = await util.initDebugClient('', true, { file: path.join(targetPaths.buildDir, 'index.html') }); await sourceMapUtil.testSourcemaps(dc, targetPaths.srcDir); }); }}}} }); interface TargetPaths { targetDir: string; srcDir: string; buildDir: string; } async function prepareTargetDir( bundle: boolean, separateBuildDir: boolean ): Promise { let targetDir = path.join(os.tmpdir(), `vscode-firefox-debug-test-${uuid.v4()}`); await fs.mkdir(targetDir); let scriptTags = bundle ? ['bundle.js'] : ['f.min.js', 'g.min.js']; if (!separateBuildDir) { await sourceMapUtil.copyFiles(TESTDATA_PATH, targetDir, ['index.html', 'f.js', 'g.js']); await sourceMapUtil.injectScriptTags(targetDir, scriptTags); return { targetDir, srcDir: targetDir, buildDir: targetDir }; } else { let srcDir = path.join(targetDir, 'src'); await fs.mkdir(srcDir); let buildDir = path.join(targetDir, 'build'); await fs.mkdir(buildDir); await sourceMapUtil.copyFiles(TESTDATA_PATH, srcDir, ['f.js', 'g.js']); await sourceMapUtil.copyFiles(TESTDATA_PATH, buildDir, ['index.html']); await sourceMapUtil.injectScriptTags(buildDir, scriptTags); return { targetDir, srcDir, buildDir }; } } function build( buildDir: string, minifyScripts: boolean, bundleScripts: boolean, embedSourceMap: boolean, separateBuildDir: boolean ): Promise { return sourceMapUtil.waitForStreamEnd( gulp.src(path.join(buildDir, separateBuildDir ? '../src/*.js' : '*.js')) .pipe(sourcemaps.init()) .pipe(minifyScripts ? uglify({ mangle: false, compress: { sequences: false } }) : nop()) .pipe(bundleScripts ? concat('bundle.js') : rename((path) => { path.basename += '.min'; })) .pipe(mapSources((srcPath) => separateBuildDir ? '../src/' + srcPath : srcPath)) .pipe(sourcemaps.write(embedSourceMap ? undefined : '.', { includeContent: false })) .pipe(gulp.dest(buildDir))); } ================================================ FILE: src/test/testHitBreakpoints.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; import { DebugProtocol } from '@vscode/debugprotocol'; import { delay } from '../common/util'; describe('Hitting breakpoints: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); beforeEach(async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); }); afterEach(async function() { await dc.stop(); }); it('should hit a breakpoint', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 3 ]); util.evaluateCloaked(dc, 'noop()'); let stoppedEvent = await util.receiveStoppedEvent(dc); assert.equal(stoppedEvent.body.allThreadsStopped, false); assert.equal(stoppedEvent.body.reason, 'breakpoint'); }); it('should hit a breakpoint in an evaluateRequest', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 3 ]); util.evaluate(dc, 'noop()'); let stoppedEvent = await util.receiveStoppedEvent(dc); assert.equal(stoppedEvent.body.allThreadsStopped, false); assert.equal(stoppedEvent.body.reason, 'breakpoint'); }); it('should hit an uncaught exception breakpoint', async function() { await dc.setExceptionBreakpointsRequest({filters: [ 'uncaught' ]}); util.evaluateCloaked(dc, 'throwUncaughtException()'); let stoppedEvent = await util.receiveStoppedEvent(dc); assert.equal(stoppedEvent.body.allThreadsStopped, false); assert.equal(stoppedEvent.body.reason, 'exception'); }); it('should not hit an uncaught exception breakpoint triggered by a debugger eval', async function() { await dc.setExceptionBreakpointsRequest({filters: [ 'uncaught' ]}); util.evaluate(dc, 'throwUncaughtException()'); await util.assertPromiseTimeout(util.receiveStoppedEvent(dc), 1000); }); it('should not hit an uncaught exception breakpoint when those are disabled', async function() { await dc.setExceptionBreakpointsRequest({filters: []}); util.evaluateCloaked(dc, 'throwUncaughtException()'); await util.assertPromiseTimeout(util.receiveStoppedEvent(dc), 1000); }); it('should hit a caught exception breakpoint', async function() { await dc.setExceptionBreakpointsRequest({filters: [ 'all' ]}); util.evaluateCloaked(dc, 'throwAndCatchException()'); let stoppedEvent = await util.receiveStoppedEvent(dc); assert.equal(stoppedEvent.body.allThreadsStopped, false); assert.equal(stoppedEvent.body.reason, 'exception'); }); it('should not hit a caught exception breakpoint triggered by a debugger eval', async function() { await dc.setExceptionBreakpointsRequest({filters: [ 'all' ]}); util.evaluate(dc, 'throwAndCatchException()'); await util.assertPromiseTimeout(util.receiveStoppedEvent(dc), 1000); }); it('should not hit a caught exception breakpoint when those are disabled', async function() { await dc.setExceptionBreakpointsRequest({filters: [ 'uncaught' ]}); util.evaluateCloaked(dc, 'throwAndCatchException()'); await util.assertPromiseTimeout(util.receiveStoppedEvent(dc), 1000); }); it('should break on a debugger statement', async function() { await dc.setExceptionBreakpointsRequest({filters: [ 'debugger' ]}); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'loadScript("debuggerStatement.js")')); assert.equal(stoppedEvent.body.allThreadsStopped, false); assert.equal(stoppedEvent.body.reason, 'debuggerStatement'); await dc.continueRequest({ threadId: stoppedEvent.body.threadId }); stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'debuggerStatement()')); assert.equal(stoppedEvent.body.allThreadsStopped, false); assert.equal(stoppedEvent.body.reason, 'debuggerStatement'); }); it('should not break on a debugger statement when those are disabled', async function() { await dc.setExceptionBreakpointsRequest({filters: []}); let stoppedPromise = util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'loadScript("debuggerStatement.js")')); await util.assertPromiseTimeout(stoppedPromise, 500); stoppedPromise = util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'debuggerStatement()')); await util.assertPromiseTimeout(stoppedPromise, 500); }); it('should not hit a breakpoint after it has been removed', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 8, 10 ]); util.evaluateCloaked(dc, 'vars()'); let stoppedEvent = await util.receiveStoppedEvent(dc); let threadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].line, 8); await util.setBreakpoints(dc, sourcePath, [ 12 ]); await delay(100); await util.runCommandAndReceiveStoppedEvent(dc, () => dc.continueRequest({ threadId })); stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].line, 12); }); it('should skip a breakpoint until its hit count is reached', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ { line: 24, hitCondition: '4' } ]); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'factorial(5)') ); let threadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId }); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variablesResponse = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); let variables = variablesResponse.body.variables; assert.equal(util.findVariable(variables, 'n').value, '2'); }); it('should show the output from logpoints', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ { line: 24, logMessage: 'factorial({n})' } ]); util.evaluate(dc, 'factorial(3)'); const outputEvents = await util.collectOutputEvents(dc, 3); assert.equal(outputEvents[0].body.output.trimRight(), 'factorial(3)'); assert.equal(outputEvents[1].body.output.trimRight(), 'factorial(2)'); assert.equal(outputEvents[2].body.output.trimRight(), 'factorial(1)'); }); it('should support objects in the output from logpoints', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ { line: 24, logMessage: 'obj is {obj}' } ]); util.evaluate(dc, 'factorial(1)'); const outputEvent = await dc.waitForEvent('output') as DebugProtocol.OutputEvent; assert.equal(outputEvent.body.output.trim(), 'obj is {x: 17, y: Object}'); assert.notEqual(outputEvent.body.variablesReference, undefined); const args = await dc.variablesRequest({ variablesReference: outputEvent.body.variablesReference! }); assert.equal(args.body.variables.length, 1); const argsVar = util.findVariable(args.body.variables, 'arguments'); assert.equal(argsVar.value.trim(), 'obj is {x: 17, y: Object}'); let vars = await dc.variablesRequest({ variablesReference: argsVar.variablesReference }); const objVar = util.findVariable(vars.body.variables, 'arg1'); assert.notEqual(objVar.variablesReference, undefined); vars = await dc.variablesRequest({ variablesReference: objVar.variablesReference! }); assert.equal(util.findVariable(vars.body.variables, 'x').value, '17'); assert.equal(util.findVariable(vars.body.variables, 'y').value, '{z: "xyz"}'); }); }); ================================================ FILE: src/test/testInspectVariables.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; import { delay } from '../common/util'; describe('Inspecting variables: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); beforeEach(async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); }); afterEach(async function() { await dc.stop(); }); it('should inspect variables of different types in different scopes', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 19 ]); util.evaluate(dc, 'vars({ key: "value" })'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variablesResponse = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); let variables = variablesResponse.body.variables; assert.equal(util.findVariable(variables, 'str2').value, '"foo"'); assert.equal(util.findVariable(variables, 'undef').value, 'undefined'); assert.equal(util.findVariable(variables, 'nul').value, 'null'); assert.equal(util.findVariable(variables, 'sym1').value, 'Symbol(Local Symbol)'); assert.equal(util.findVariable(variables, 'sym2').value, 'Symbol(Global Symbol)'); assert.equal(util.findVariable(variables, 'sym3').value, 'Symbol(Symbol.iterator)'); variablesResponse = await dc.variablesRequest({ variablesReference: util.findVariable(variables, 'this').variablesReference }); variables = variablesResponse.body.variables; assert.equal(util.findVariable(variables, 'scrollX').value, '0'); variablesResponse = await dc.variablesRequest({ variablesReference: scopes.body.scopes[1].variablesReference }); variables = variablesResponse.body.variables; assert.equal(util.findVariable(variables, 'bool1').value, 'false'); assert.equal(util.findVariable(variables, 'bool2').value, 'true'); assert.equal(util.findVariable(variables, 'num1').value, '0'); assert.equal(util.findVariable(variables, 'num2').value, '120'); assert.equal(util.findVariable(variables, 'str1').value, '""'); variablesResponse = await dc.variablesRequest({ variablesReference: scopes.body.scopes[2].variablesReference }); let variable = util.findVariable(variablesResponse.body.variables, 'arg')!; assert.equal(variable.value, '{key: "value", Symbol(Local Symbol): "Symbol-keyed 1", Symbol(Symbol.iterator): "Symbol-keyed 2"}'); variablesResponse = await dc.variablesRequest({ variablesReference: variable.variablesReference }); assert.equal(util.findVariable(variablesResponse.body.variables, 'key').value, '"value"'); assert.equal(util.findVariable(variablesResponse.body.variables, 'Symbol(Local Symbol)').value, '"Symbol-keyed 1"'); assert.equal(util.findVariable(variablesResponse.body.variables, 'Symbol(Symbol.iterator)').value, '"Symbol-keyed 2"'); }); it('should inspect variables in different stackframes', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluateDelayed(dc, 'factorial(4)', 0); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); for (let i = 0; i < 4; i++) { let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[i].id }); let variables = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); assert.equal(util.findVariable(variables.body.variables, 'n').value, i + 1); } }); it('should inspect return values on stepping out', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 71 ]); util.evaluate(dc, 'doEval(17)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let threadId = stoppedEvent.body.threadId!; await delay(100); // TODO await dc.stepOutRequest({ threadId }); await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variables = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); assert.equal(util.findVariable(variables.body.variables, 'Return value').value, 17); }); it.skip('should inspect return values on stepping out of recursive functions', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 25 ]); util.evaluate(dc, 'factorial(4)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let threadId = stoppedEvent.body.threadId!; for (let i = 0; i < 4; i++) { await dc.stepOutRequest({ threadId }); await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variables = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); assert.equal(util.findVariable(variables.body.variables, 'Return value').value, factorial(i + 1)); } }); it('should set variables', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 11 ]); util.evaluate(dc, 'vars()'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let frameId = stackTrace.body.stackFrames[0].id; let scopes = await dc.scopesRequest({ frameId }); let variablesReference = scopes.body.scopes[0].variablesReference; let variables = await dc.variablesRequest({ variablesReference }); assert.equal(util.findVariable(variables.body.variables, 'num1').value, '0'); let result = await dc.evaluateRequest({ context: 'repl', frameId, expression: 'num1' }); assert.equal(result.body.result, '0'); await dc.setVariableRequest({ variablesReference, name: 'num1', value: '7' }); result = await dc.evaluateRequest({ context: 'repl', frameId, expression: 'num1' }); assert.equal(result.body.result, '7'); }); it('should inspect variables from the stack after running evaluations', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 8 ]); util.evaluate(dc, 'vars({foo:{bar:"baz"}})'); let stoppedEvent = await util.receiveStoppedEvent(dc); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let frameId = stackTrace.body.stackFrames[0].id; let scopes = await dc.scopesRequest({ frameId }); await dc.evaluateRequest({ expression: 'obj.x', context: 'watch', frameId }); await dc.evaluateRequest({ expression: 'obj.y', context: 'repl', frameId }); let variables = await dc.variablesRequest({ variablesReference: scopes.body.scopes[1].variablesReference }); let arg = util.findVariable(variables.body.variables, 'arg'); variables = await dc.variablesRequest({ variablesReference: arg.variablesReference }); let foo = util.findVariable(variables.body.variables, 'foo'); variables = await dc.variablesRequest({ variablesReference: foo.variablesReference }); let bar = util.findVariable(variables.body.variables, 'bar'); assert.equal(bar.value, '"baz"'); }); it('should return the same variables if the variablesRequest is issued twice', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 112 ]); util.evaluate(dc, 'protoGetter().y'); const stoppedEvent = await util.receiveStoppedEvent(dc); const stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); const frameId = stackTrace.body.stackFrames[0].id; const scopes = await dc.scopesRequest({ frameId }); const variablesReference = scopes.body.scopes[0].variablesReference; const variables1 = await dc.variablesRequest({ variablesReference }); const variables2 = await dc.variablesRequest({ variablesReference }); assert.deepStrictEqual(variables1.body.variables, variables2.body.variables); }); }); function factorial(n: number): number { if (n <= 1) { return 1; } else { return n * factorial(n - 1); } } ================================================ FILE: src/test/testSetBreakpoints.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; import { delay } from '../common/util'; describe('Setting breakpoints: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); afterEach(async function() { await dc.stop(); }); it('should provide breakpoint locations in sources without sourcemaps', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); const sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); const locations = await dc.customRequest('breakpointLocations', { source: { path: sourcePath }, line: 17 }); assert.deepStrictEqual(locations.body.breakpoints, [ { line: 17, column: 14 }, { line: 17, column: 20 }, { line: 17, column: 49 }, { line: 17, column: 56 }, { line: 17, column: 89 } ]); }); it('should eventually verify a breakpoint set on a loaded file', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 3 ], false); let breakpointId = setBreakpointsResponse.body.breakpoints[0].id; assert.equal(setBreakpointsResponse.body.breakpoints.length, 1); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, false); assert.equal(setBreakpointsResponse.body.breakpoints[0].line, 3); let ev = await util.receiveBreakpointEvent(dc); assert.equal(ev.body.reason, 'changed'); assert.equal(ev.body.breakpoint.id, breakpointId); assert.equal(ev.body.breakpoint.verified, true); assert.equal(ev.body.breakpoint.line, 3); }); it('should eventually move and verify a breakpoint set on a loaded file', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 2 ], false); let breakpointId = setBreakpointsResponse.body.breakpoints[0].id; assert.equal(setBreakpointsResponse.body.breakpoints.length, 1); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, false); assert.equal(setBreakpointsResponse.body.breakpoints[0].line, 2); let ev = await util.receiveBreakpointEvent(dc); assert.equal(ev.body.reason, 'changed'); assert.equal(ev.body.breakpoint.id, breakpointId); assert.equal(ev.body.breakpoint.verified, true); assert.equal(ev.body.breakpoint.line, 3); }); it('should eventually verify a breakpoint set before the page is loaded', async function() { dc = await util.initDebugClient(TESTDATA_PATH, false); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 3 ], false); assert.equal(setBreakpointsResponse.body.breakpoints.length, 1); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, false); let breakpointId = setBreakpointsResponse.body.breakpoints[0].id; let ev = await util.receiveBreakpointEvent(dc); assert.equal(ev.body.reason, 'changed'); assert.equal(ev.body.breakpoint.id, breakpointId); assert.equal(ev.body.breakpoint.verified, true); assert.equal(ev.body.breakpoint.line, 3); }); it('should eventually move and verify a breakpoint set before the page is loaded', async function() { dc = await util.initDebugClient(TESTDATA_PATH, false); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 2 ], false); assert.equal(setBreakpointsResponse.body.breakpoints.length, 1); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, false); let breakpointId = setBreakpointsResponse.body.breakpoints[0].id; let ev = await util.receiveBreakpointEvent(dc); assert.equal(ev.body.reason, 'changed'); assert.equal(ev.body.breakpoint.id, breakpointId); assert.equal(ev.body.breakpoint.verified, true); assert.equal(ev.body.breakpoint.line, 3); }); it('should eventually verify a breakpoint set on a dynamically loaded script', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/dlscript.js'); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 3 ], false); assert.equal(setBreakpointsResponse.body.breakpoints.length, 1); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, false); let breakpointId = setBreakpointsResponse.body.breakpoints[0].id; util.evaluate(dc, 'loadScript("dlscript.js")'); let ev = await util.receiveBreakpointEvent(dc); assert.equal(ev.body.reason, 'changed'); assert.equal(ev.body.breakpoint.id, breakpointId); assert.equal(ev.body.breakpoint.verified, true); assert.equal(ev.body.breakpoint.line, 3); }); it('should keep old breakpoints verified when setting new ones', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 3 ]); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 3, 8 ], false); assert.equal(setBreakpointsResponse.body.breakpoints.length, 2); assert.equal(setBreakpointsResponse.body.breakpoints[0].line, 3); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, true); assert.equal(setBreakpointsResponse.body.breakpoints[1].line, 8); assert.equal(setBreakpointsResponse.body.breakpoints[1].verified, false); }); it('should handle multiple setBreakpointsRequests in quick succession', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); util.setBreakpoints(dc, sourcePath, [ 11 ], false); util.setBreakpoints(dc, sourcePath, [ 10, 8 ], false); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 9, 10 ], false); assert.equal(setBreakpointsResponse.body.breakpoints.length, 2); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, false); assert.equal(setBreakpointsResponse.body.breakpoints[1].verified, false); await delay(200); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'vars()') ); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); assert.equal(stackTrace.body.stackFrames[0].line, 9); }); it('should remove a breakpoint', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); let setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [ 3 ], false); assert.equal(setBreakpointsResponse.body.breakpoints.length, 1); assert.equal(setBreakpointsResponse.body.breakpoints[0].verified, false); setBreakpointsResponse = await util.setBreakpoints(dc, sourcePath, [], false); assert.equal(setBreakpointsResponse.body.breakpoints.length, 0); }); it('should add a condition to an already set breakpoint', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ { line: 24 } ]); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'factorial(5)') ); let threadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId }); let scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); let variablesResponse = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); let variables = variablesResponse.body.variables; assert.equal(util.findVariable(variables, 'n').value, '5'); await util.setBreakpoints(dc, sourcePath, [ { line: 24, condition: 'n === 2' } ]); await dc.continueRequest({ threadId }); await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'factorial(5)') ); stackTrace = await dc.stackTraceRequest({ threadId }); scopes = await dc.scopesRequest({ frameId: stackTrace.body.stackFrames[0].id }); variablesResponse = await dc.variablesRequest({ variablesReference: scopes.body.scopes[0].variablesReference }); variables = variablesResponse.body.variables; assert.equal(util.findVariable(variables, 'n').value, '2'); }); it('should add a logMessage to an already set breakpoint', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ { line: 24 } ]); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluate(dc, 'factorial(0)') ); let threadId = stoppedEvent.body.threadId!; await dc.continueRequest({ threadId }); await util.setBreakpoints(dc, sourcePath, [ { line: 24, logMessage: 'factorial({n})' } ]); util.evaluate(dc, 'factorial(3)'); const outputEvents = await util.collectOutputEvents(dc, 3); assert.equal(outputEvents[0].body.output.trimRight(), 'factorial(3)'); assert.equal(outputEvents[1].body.output.trimRight(), 'factorial(2)'); assert.equal(outputEvents[2].body.output.trimRight(), 'factorial(1)'); }); }); ================================================ FILE: src/test/testSkipFiles.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; import { delay, isWindowsPlatform } from '../common/util'; describe('Skipping files: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); afterEach(async function() { await dc.stop(); }); it('should skip exceptions in blackboxed files', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { skipFiles: [ '**/skip.js' ] }); await dc.setExceptionBreakpointsRequest({filters: [ 'all' ]}); await delay(100); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'try{ testSkipFiles() }catch(e){}')); let stacktrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, path.join(TESTDATA_PATH, 'web/main.js')); assert.equal(stacktrace.body.stackFrames[0].line, 76); }); it('should skip exceptions in blackboxed files thrown immediately after loading', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { skipFiles: [ '**/exception.js' ] }); await dc.setExceptionBreakpointsRequest({filters: [ 'all' ]}); let mainFilePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, mainFilePath, [ 3 ]); await delay(100); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'loadScript("exception.js")')); let stacktrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, path.join(TESTDATA_PATH, 'web/main.js')); assert.equal(stacktrace.body.stackFrames[0].line, 3); }); it(`should skip exceptions in blackboxed source-mapped files thrown immediately after loading`, async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { skipFiles: [ '**/exception-sourcemap.ts' ] }); await dc.setExceptionBreakpointsRequest({filters: [ 'all' ]}); let mainFilePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, mainFilePath, [ 3 ]); await delay(100); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'loadScript("exception-sourcemap.js")')); let stacktrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, path.join(TESTDATA_PATH, 'web/main.js')); assert.equal(stacktrace.body.stackFrames[0].line, 3); }); it('should skip breakpoints in blackboxed files', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { skipFiles: [ '**/skip.js' ] }); await dc.setExceptionBreakpointsRequest({filters: []}); let skipFilePath = path.join(TESTDATA_PATH, 'web/skip.js'); await util.setBreakpoints(dc, skipFilePath, [ 2 ]); let mainFilePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, mainFilePath, [ 76 ]); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'try{ testSkipFiles() }catch(e){}')); let stacktrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, mainFilePath); assert.equal(stacktrace.body.stackFrames[0].line, 76); }); it('should toggle skipping files that were not skipped by the configuration', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); await dc.setExceptionBreakpointsRequest({filters: []}); let skipFilePath = path.join(TESTDATA_PATH, 'web/skip.js'); await util.setBreakpoints(dc, skipFilePath, [ 2 ]); let mainFilePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, mainFilePath, [ 76 ]); let skipFileUrl = isWindowsPlatform() ? 'file:///' + skipFilePath.replace(/\\/g, '/') : 'file://' + skipFilePath; await dc.customRequest('toggleSkippingFile', skipFileUrl); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'try{ testSkipFiles() }catch(e){}')); let threadId = stoppedEvent.body.threadId; let stacktrace = await dc.stackTraceRequest({ threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, mainFilePath); assert.equal(stacktrace.body.stackFrames[0].line, 76); await dc.customRequest('toggleSkippingFile', skipFileUrl); await dc.continueRequest({ threadId }); stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'try{ testSkipFiles() }catch(e){}')); stacktrace = await dc.stackTraceRequest({ threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, skipFilePath); assert.equal(stacktrace.body.stackFrames[0].line, 2); }); it('should toggle skipping files that were skipped by the configuration', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true, { skipFiles: [ '**/skip.js' ] }); await dc.setExceptionBreakpointsRequest({filters: []}); let skipFilePath = path.join(TESTDATA_PATH, 'web/skip.js'); await util.setBreakpoints(dc, skipFilePath, [ 2 ]); let mainFilePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, mainFilePath, [ 76 ]); let skipFileUrl = isWindowsPlatform() ? 'file:///' + skipFilePath.replace(/\\/g, '/') : 'file://' + skipFilePath; await dc.customRequest('toggleSkippingFile', skipFileUrl); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'try{ testSkipFiles() }catch(e){}')); let threadId = stoppedEvent.body.threadId; let stacktrace = await dc.stackTraceRequest({ threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, skipFilePath); assert.equal(stacktrace.body.stackFrames[0].line, 2); await dc.customRequest('toggleSkippingFile', skipFileUrl); await dc.continueRequest({ threadId }); stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'try{ testSkipFiles() }catch(e){}')); stacktrace = await dc.stackTraceRequest({ threadId }); assert.equal(stacktrace.body.stackFrames[0].source!.path, mainFilePath); assert.equal(stacktrace.body.stackFrames[0].line, 76); }); it('should send a StoppedEvent with the same reason to VSCode after a skipFile toggle', async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); let skipFilePath = path.join(TESTDATA_PATH, 'web/skip.js'); let mainFilePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, mainFilePath, [ 75 ]); let stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => util.evaluateCloaked(dc, 'try{ testSkipFiles() }catch(e){}')); assert.equal((stoppedEvent).body.reason, 'breakpoint'); let skipFileUrl = isWindowsPlatform() ? 'file:///' + skipFilePath.replace(/\\/g, '/') : 'file://' + skipFilePath; stoppedEvent = await util.runCommandAndReceiveStoppedEvent(dc, () => dc.customRequest('toggleSkippingFile', skipFileUrl)); assert.equal((stoppedEvent).body.reason, 'breakpoint'); }) }); ================================================ FILE: src/test/testSourceActorCollection.ts ================================================ import assert from 'assert'; import { SourceActorCollection } from '../adapter/adapter/source'; import { ISourceActorProxy } from '../adapter/firefox/actorProxy/source'; import { MappedLocation } from '../adapter/location'; import { delay } from '../common/util'; class FakeSourceActorProxy implements ISourceActorProxy { public readonly url = null; public readonly source = { actor: this.name, url: null, generatedUrl: null, introductionUrl: null, isBlackBoxed: false, isPrettyPrinted: false, isSourceMapped: false, sourceMapURL: null, }; constructor(public readonly name: string) {} getBreakableLines(): Promise { throw new Error('Method not implemented.'); } getBreakableLocations(line: number): Promise { throw new Error('Method not implemented.'); } fetchSource(): Promise { throw new Error('Method not implemented.'); } setBlackbox(blackbox: boolean): Promise { throw new Error('Method not implemented.'); } dispose(): void { throw new Error('Method not implemented.'); } } const a1 = new FakeSourceActorProxy('fakeSourceActor1'); const a2 = new FakeSourceActorProxy('fakeSourceActor2'); async function getName(actor: ISourceActorProxy) { return actor.name; } async function getNameOrFail(actor: ISourceActorProxy) { if (actor === a1) { throw { from: actor.name, error: 'noSuchActor' } } return actor.name; } describe('SourceActorCollection: runWithSomeActor', function() { it('should work', async function() { const collection = new SourceActorCollection(a1); const result = await collection.runWithSomeActor(getName); assert.equal(result, a1.name); }); it('should work after the first actor was replaced', async function() { const collection = new SourceActorCollection(a1); collection.remove(a1); collection.add(a2); const result = await collection.runWithSomeActor(getName); assert.equal(result, a2.name); }); it('should work while the first actor is being replaced', async function() { const collection = new SourceActorCollection(a1); collection.remove(a1); const resultPromise = collection.runWithSomeActor(getName); await delay(1); collection.add(a2); assert.equal(await resultPromise, a2.name); }); it('should work after a second actor was removed', async function() { const collection = new SourceActorCollection(a1); collection.add(a2); collection.remove(a2); const result = await collection.runWithSomeActor(getName); assert.equal(result, a1.name); }); it('should work when an actor was destroyed before another was added', async function() { const collection = new SourceActorCollection(a1); const resultPromise = collection.runWithSomeActor(getNameOrFail); await delay(1); collection.add(a2); assert.equal(await resultPromise, a2.name); }); it('should work when an actor was destroyed after another was added', async function() { const collection = new SourceActorCollection(a1); collection.add(a2); const resultPromise = collection.runWithSomeActor(getNameOrFail); assert.equal(await resultPromise, a2.name); }); it('should rethrow actor exceptions', async function() { const collection = new SourceActorCollection(a1); try { await collection.runWithSomeActor(actor => actor.fetchSource()); } catch (err: any) { assert.equal(err.message, "Method not implemented."); return; } assert.fail(); }); }); ================================================ FILE: src/test/testStepThrough.ts ================================================ import { DebugClient } from '@vscode/debugadapter-testsupport'; import * as path from 'path'; import * as util from './util'; import * as assert from 'assert'; import { delay } from '../common/util'; describe('Stepping: The debugger', function() { let dc: DebugClient; const TESTDATA_PATH = path.join(__dirname, '../../testdata'); beforeEach(async function() { dc = await util.initDebugClient(TESTDATA_PATH, true); }); afterEach(async function() { await dc.stop(); }); it('should step over', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 11 ]); util.evaluateDelayed(dc, 'vars()', 0); let stoppedEvent = await util.receiveStoppedEvent(dc); let threadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 11); dc.nextRequest({ threadId }); await util.receiveStoppedEvent(dc); stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 12); }); it('should step in', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 27 ]); util.evaluate(dc, 'factorial(3)'); let stoppedEvent = await util.receiveStoppedEvent(dc); let threadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 27); dc.stepInRequest({ threadId }); await util.receiveStoppedEvent(dc); stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 24); }); it('should step out', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 3 ]); util.evaluate(dc, 'vars()'); let stoppedEvent = await util.receiveStoppedEvent(dc); let threadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 3); dc.stepOutRequest({ threadId }); await util.receiveStoppedEvent(dc); stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 21); }); it('should restart a frame', async function() { let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 11 ]); util.evaluate(dc, 'vars()'); let stoppedEvent = await util.receiveStoppedEvent(dc); let threadId = stoppedEvent.body.threadId!; let stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 11); const frameId = stackTrace.body.stackFrames[0].id; dc.restartFrameRequest({ frameId }); await util.receiveStoppedEvent(dc); stackTrace = await dc.stackTraceRequest({ threadId }); assert.equal(stackTrace.body.stackFrames[0].source!.path, sourcePath); assert.equal(stackTrace.body.stackFrames[0].line, 8); }); it('should step into an eval()', async function() { let evalExpr = 'factorial(5)'; let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 71 ]); dc.evaluateRequest({ expression: `doEval('${evalExpr}')`, context: 'repl' }); let stoppedEvent = await util.receiveStoppedEvent(dc); dc.stepInRequest({ threadId: stoppedEvent.body.threadId! }); await util.receiveStoppedEvent(dc); await delay(100); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let source = await dc.sourceRequest({ sourceReference: stackTrace.body.stackFrames[0].source!.sourceReference! }); assert.equal(source.body.content, evalExpr); }); it('should fetch long eval() expressions', async function() { let evalExpr = 'factorial(1);'; for (let i = 0; i < 12; i++) { evalExpr = evalExpr + evalExpr; } let sourcePath = path.join(TESTDATA_PATH, 'web/main.js'); await util.setBreakpoints(dc, sourcePath, [ 71 ]); dc.evaluateRequest({ expression: `doEval('${evalExpr}')`, context: 'repl' }); let stoppedEvent = await util.receiveStoppedEvent(dc); dc.stepInRequest({ threadId: stoppedEvent.body.threadId! }); await util.receiveStoppedEvent(dc); await delay(100); let stackTrace = await dc.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); let source = await dc.sourceRequest({ sourceReference: stackTrace.body.stackFrames[0].source!.sourceReference! }); assert.equal(source.body.content, evalExpr); }); }); ================================================ FILE: src/test/testTerminateAndCleanup.ts ================================================ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs-extra'; import * as assert from 'assert'; import * as uuid from 'uuid'; import * as util from './util'; import { delay } from '../common/util'; import { FirefoxDebugSession } from '../adapter/firefoxDebugSession'; import { parseConfiguration } from '../adapter/configuration'; describe('Terminate and cleanup: The debugger', function() { const TESTDATA_PATH = path.join(__dirname, '../../testdata'); it('should eventually delete the temporary profile after terminating Firefox', async function() { if (process.env['KEEP_PROFILE_CHANGES'] === 'true') { this.skip(); return; } const tmpDir = path.join(os.tmpdir(), `vscode-firefox-debug-test-${uuid.v4()}`); const dc = await util.initDebugClient(TESTDATA_PATH, true, { tmpDir }); // check that the temporary profile has been created assert.ok((await fs.readdir(tmpDir)).length > 0); await dc.stop(); // check repeatedly if the temporary profile has been deleted and fail after 5 seconds if it hasn't var startTime = Date.now(); while ((Date.now() - startTime) < 5000) { await delay(200); if ((await fs.readdir(tmpDir)).length === 0) { await fs.rmdir(tmpDir); return; } } throw new Error("The temporary profile hasn't been deleted after 5 seconds"); }); it('should eventually delete the temporary profile after a detached Firefox process was terminated', async function() { if (os.platform() === 'darwin') { this.skip(); return; } const tmpDir = path.join(os.tmpdir(), `vscode-firefox-debug-test-${uuid.v4()}`); const dc = await util.initDebugClient(TESTDATA_PATH, true, { tmpDir, reAttach: true }); // check that the temporary profile has been created assert.ok((await fs.readdir(tmpDir)).length > 0); await dc.stop(); // attach to Firefox again and terminate it using the Terminator WebExtension const parsedConfig = await parseConfiguration({ request: 'attach' }); const ds = new FirefoxDebugSession(parsedConfig, () => undefined); await ds.start(); ds.addonsActor!.installAddon(path.resolve(__dirname, '../../dist/terminator')); // check repeatedly if the temporary profile has been deleted and fail after 5 seconds if it hasn't var startTime = Date.now(); while ((Date.now() - startTime) < 5000) { await delay(200); if ((await fs.readdir(tmpDir)).length === 0) { await fs.rmdir(tmpDir); return; } } throw new Error("The temporary profile hasn't been deleted after 5 seconds"); }); }); ================================================ FILE: src/test/testWebpackSourceMaps.ts ================================================ import * as os from 'os'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as uuid from 'uuid'; import * as assert from 'assert'; import * as util from './util'; import * as sourceMapUtil from './sourceMapUtil'; import webpack from 'webpack'; import TerserPlugin from 'terser-webpack-plugin'; import { DebugClient } from '@vscode/debugadapter-testsupport'; const TESTDATA_PATH = path.join(__dirname, '../../testdata/web/sourceMaps/modules'); describe('Webpack sourcemaps: The debugger', function() { let dc: DebugClient | undefined; let targetDir: string | undefined; afterEach(async function() { if (dc) { await dc.stop(); dc = undefined; } if (targetDir) { await fs.remove(targetDir); targetDir = undefined; } }); for (const devtool of [ 'source-map', 'inline-source-map', 'nosources-source-map', 'inline-nosources-source-map', 'eval-source-map', 'eval-cheap-source-map', 'eval-cheap-module-source-map', 'eval-nosources-source-map', 'eval-nosources-cheap-source-map', 'eval-nosources-cheap-module-source-map', ] satisfies Devtool[]) { const description = `should map webpack-bundled modules with devtool "${devtool}" to their original sources`; const isEvalSourcemap = devtool.includes('eval'); const isCheapSourcemap = devtool.includes('cheap'); it(description, async function() { let targetDir = await prepareTargetDir(); await build(targetDir, devtool); dc = await util.initDebugClient('', true, { file: path.join(targetDir, 'index.html'), pathMappings: [{ url: 'webpack:///', path: targetDir + '/' }] }); // test breakpoint locations if the devtool provides column breakpoints if (!isCheapSourcemap) { const breakpointLocations = await dc.customRequest('breakpointLocations', { source: { path: path.join(targetDir, 'f.js') }, line: 7 }); assert.deepStrictEqual(breakpointLocations.body.breakpoints, [ { line: 7, column: isEvalSourcemap ? 1 : 2 }, { line: 7, column: 6 } ]); } const breakpoint = { line: 7, column: isEvalSourcemap ? 1 : 6 }; await sourceMapUtil.testSourcemaps(dc, targetDir, breakpoint); }); } }); async function prepareTargetDir(): Promise { let targetDir = path.join(await fs.realpath(os.tmpdir()), `vscode-firefox-debug-test-${uuid.v4()}`); await fs.mkdir(targetDir); await sourceMapUtil.copyFiles(TESTDATA_PATH, targetDir, ['index.html', 'f.js', 'g.js']); return targetDir; } type Devtool = `${'inline-' | 'hidden-' | 'eval-' | ''}${'nosources-' | ''}${'cheap-module-' | 'cheap-' | ''}source-map`; function build(targetDir: string, devtool: Devtool): Promise { return new Promise((resolve, reject) => { webpack({ context: targetDir, entry: './f.js', output: { path: targetDir, filename: 'bundle.js' }, devtool: devtool, optimization: { minimizer: [new TerserPlugin({ terserOptions: { mangle: false } })], } }, (err, stats) => { if (err) { reject(err); } else { resolve(); } }); }) } ================================================ FILE: src/test/util.ts ================================================ import { delay, isWindowsPlatform } from '../common/util'; import { DebugClient } from '@vscode/debugadapter-testsupport'; import { DebugProtocol } from '@vscode/debugprotocol'; import { LaunchConfiguration } from '../common/configuration'; import * as path from 'path'; import * as net from 'net'; export async function initDebugClient( testDataPath: string, waitForPageLoadedEvent: boolean, extraLaunchArgs?: {} ): Promise { await waitForUnoccupiedPort(6000, 1000); let dc = new DebugClient('node', './dist/adapter.bundle.js', 'firefox'); let launchArgs: LaunchConfiguration = { request: 'launch', file: path.join(testDataPath, 'web/index.html') }; processEnvironmentVariables(launchArgs); if (extraLaunchArgs !== undefined) { launchArgs = Object.assign(launchArgs, extraLaunchArgs); } const pageLoadedPromise = receivePageLoadedEvent(dc); await dc.start(); await Promise.all([ dc.launch(launchArgs), dc.configurationSequence() ]); if (waitForPageLoadedEvent) { await pageLoadedPromise; } return dc; } export async function initDebugClientForAddon( testDataPath: string, options?: { delayedNavigation?: boolean, addonDirectory?: string } ): Promise { await waitForUnoccupiedPort(6000, 1000); let addonPath: string; if (options && options.addonDirectory) { addonPath = path.join(testDataPath, `${options.addonDirectory}/addOn`); } else { addonPath = path.join(testDataPath, `webExtension/addOn`); } let dcArgs: LaunchConfiguration = { request: 'launch', addonPath }; if (options && options.delayedNavigation) { dcArgs.file = path.join(testDataPath, `web/index.html`); } else { dcArgs.file = path.join(testDataPath, `webExtension/index.html`); } processEnvironmentVariables(dcArgs); let dc = new DebugClient('node', './dist/adapter.bundle.js', 'firefox'); const pageLoadedPromise = receivePageLoadedEvent(dc); await dc.start(); await Promise.all([ dc.launch(dcArgs), dc.waitForEvent('initialized', 20000) ]); dc.setExceptionBreakpointsRequest({ filters: [] }); await pageLoadedPromise; if (options && options.delayedNavigation) { await setConsoleThread(dc, await findTabThread(dc)); let filePath = path.join(testDataPath, `webExtension/index.html`); let fileUrl = isWindowsPlatform() ? 'file:///' + filePath.replace(/\\/g, '/') : 'file://' + filePath; await evaluate(dc, `location="${fileUrl}"`); await receivePageLoadedEvent(dc); } return dc; } function processEnvironmentVariables(launchArgs: LaunchConfiguration) { if (process.env['FIREFOX_EXECUTABLE']) { launchArgs.firefoxExecutable = process.env['FIREFOX_EXECUTABLE']; } if (process.env['FIREFOX_PROFILE']) { launchArgs.profile = process.env['FIREFOX_PROFILE']; } if (process.env['FIREFOX_PROFILE_DIR']) { launchArgs.profileDir = process.env['FIREFOX_PROFILE_DIR']; } if (process.env['KEEP_PROFILE_CHANGES'] === 'true') { launchArgs.keepProfileChanges = true; } if (process.env['HEADLESS'] === 'true') { launchArgs.firefoxArgs = ['-headless']; } } async function waitForUnoccupiedPort(port: number, timeout: number) { if (!await isPortOccupied(port)) { return; } for (let i = 0; i < timeout / 100; i++) { await delay(100); if (!await isPortOccupied(port)) { return; } } throw new Error(`Port ${port} is occupied`); } async function isPortOccupied(port: number): Promise { const server = net.createServer(); const occupied = await new Promise((resolve, reject) => { server.on('error', (err: any) => { if (err.code === 'EADDRINUSE') { resolve(true); } else { reject(err); } }); server.on('listening', () => resolve(false)); server.listen(port); }); await new Promise(resolve => server.close(() => resolve())); return occupied; } export async function receivePageLoadedEvent(dc: DebugClient, lenient: boolean = false): Promise { let ev = await dc.waitForEvent('output', 10000); let outputMsg = ev.body.output.trim(); if (outputMsg.substr(0, 6) !== 'Loaded') { if (lenient) { await receivePageLoadedEvent(dc, true); } else { throw new Error(`Wrong output message '${outputMsg}'`); } } } export async function setBreakpoints( dc: DebugClient, sourcePath: string, breakpoints: number[] | DebugProtocol.SourceBreakpoint[], waitForVerification = true ): Promise { let sourceBreakpoints: DebugProtocol.SourceBreakpoint[]; if ((breakpoints.length > 0) && (typeof breakpoints[0] === 'number')) { sourceBreakpoints = (breakpoints).map(line => { return { line }; }) } else { sourceBreakpoints = breakpoints; } const result = await dc.setBreakpointsRequest({ source: { path: sourcePath }, breakpoints: sourceBreakpoints }); if (waitForVerification) { let unverified = result.body.breakpoints.filter(breakpoint => !breakpoint.verified).length; if (unverified > 0) { await new Promise(resolve => { dc.on('breakpoint', () => { unverified--; if (unverified === 0) { resolve(); } }); }); } } return result; } export function receiveBreakpointEvent(dc: DebugClient): Promise { return dc.waitForEvent('breakpoint', 10000); } export function receiveStoppedEvent(dc: DebugClient): Promise { return dc.waitForEvent('stopped', 10000); } export function collectOutputEvents(dc: DebugClient, count: number): Promise { return new Promise(resolve => { const outputEvents: DebugProtocol.OutputEvent[] = []; function listener(event: DebugProtocol.OutputEvent) { outputEvents.push(event); if (outputEvents.length >= count) { dc.removeListener('output', listener); resolve(outputEvents); } } dc.addListener('output', listener); }); } export async function runCommandAndReceiveStoppedEvent(dc: DebugClient, command: () => void): Promise { let stoppedEventPromise = dc.waitForEvent('stopped', 10000); command(); return await stoppedEventPromise; } export function evaluate(dc: DebugClient, js: string): Promise { return dc.evaluateRequest({ context: 'repl', expression: js }); } export function evaluateDelayed(dc: DebugClient, js: string, delay: number): Promise { js = `setTimeout(function() { ${js} }, ${delay})`; return evaluate(dc, js); } export function evaluateCloaked(dc: DebugClient, js: string): Promise { js = js.replace("'", "\\'"); js = `eval('setTimeout(function() { ${js} }, 0)')`; return dc.evaluateRequest({ context: 'repl', expression: js }); } export async function assertPromiseTimeout(promise: Promise, timeout: number): Promise { let promiseResolved = await Promise.race([ promise.then(() => true), delay(timeout).then(() => false) ]); if (promiseResolved) { throw new Error(`The Promise was resolved within ${timeout}ms`); } } export function findVariable(variables: DebugProtocol.Variable[], varName: string): DebugProtocol.Variable { for (var i = 0; i < variables.length; i++) { if (variables[i].name === varName) { return variables[i]; } } throw new Error(`Variable '${varName}' not found`); } export async function findTabThread(dc: DebugClient): Promise { let threadsPresponse = await dc.threadsRequest(); for (let thread of threadsPresponse.body.threads) { if (thread.name.startsWith('Tab')) { return thread.id; } } throw new Error('Couldn\'t find a tab thread'); } export async function setConsoleThread(dc: DebugClient, threadId: number): Promise { try { // TODO explain (only used for side effect that the "active" thread is set) // await dc.stackTraceRequest({ threadId }); // await dc.pauseRequest({ threadId }); await dc.continueRequest({ threadId }); } catch(e) {} } ================================================ FILE: src/typings/gulp-nop.d.ts ================================================ declare module "gulp-nop" { function nop(): NodeJS.ReadWriteStream; namespace nop {} export = nop; } ================================================ FILE: src/typings/map-sources.d.ts ================================================ declare module "@gulp-sourcemaps/map-sources" { function mapSources(mapFn: (sourcePath: string, file: any) => string): NodeJS.ReadWriteStream; namespace mapSources {} export = mapSources; } ================================================ FILE: testdata/web/debuggerStatement.js ================================================ debugger; function debuggerStatement() { debugger; } ================================================ FILE: testdata/web/dlscript.js ================================================ function dynTest() { console.log("Test"); } ================================================ FILE: testdata/web/exception-sourcemap.js ================================================ try { throw new Error(); } catch (e) { noop(); } //# sourceMappingURL=exception-sourcemap.js.map ================================================ FILE: testdata/web/exception-sourcemap.ts ================================================ declare function noop(): any; try { throw new Error(); } catch(e) { noop(); } ================================================ FILE: testdata/web/exception.js ================================================ try { throw new Error(); } catch(e) { noop(); } ================================================ FILE: testdata/web/index.html ================================================

Test

================================================ FILE: testdata/web/main.js ================================================ function noop() { let dummy = 0; } function vars(arg) { let bool1 = false; let bool2 = true; let num1 = 0; let num2 = factorial(5); let str1 = ''; { let str2 = 'foo'; let undef = undefined; let nul = null; let sym1 = Symbol('Local Symbol'); let sym2 = Symbol.for('Global Symbol'); let sym3 = Symbol.iterator; if (arg) { arg[sym1] = 'Symbol-keyed 1'; arg[sym3] = 'Symbol-keyed 2'; } noop(); } } function factorial(n) { if (n <= 1) { return 1; } else { return n * factorial(n - 1); } } function loadScript(url) { var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url; head.appendChild(script); } function throwUncaughtException() { throw new Error('TestException'); } function throwAndCatchException() { try { throw new Error('TestException'); } catch(e) {} } var worker; function startWorker() { worker = new Worker('worker.js'); worker.onmessage = function(e) { let received = e.data; noop(); } } function callWorker() { worker.postMessage({ foo: 'bar' }); } var obj = { x: 17, y: { z: 'xyz' } } function doEval(expr) { return eval(expr); } function testSkipFiles() { dummyFunc(); throwError(); } function log(...x) { console.log(...x); } function getterAndSetter() { let y = { _z: 'foo', get z() { return this._z; } }; let x = { gp: 17, gsp: 23, _y: y, get getterProperty() { return this.gp; }, set setterProperty(val) {}, get getterAndSetterProperty() { return this.gsp; }, set getterAndSetterProperty(val) {}, get nested() { return this._y; } }; return x; } class ProtoGetterBase { constructor() { this._z = 'bar'; } get z() { return this._z; } } class ProtoGetter extends ProtoGetterBase { constructor() { super(); this._y = 'foo'; } get y() { return this._y; } } function protoGetter() { var x = new ProtoGetter(); return x; } function inc(o) { console.log(`Old: ${o.x}`); o.x++; console.log(`New: ${o.x}`); } ================================================ FILE: testdata/web/skip.js ================================================ function dummyFunc() { let dummy = 0; } function throwError() { throw new Error(); } ================================================ FILE: testdata/web/sourceMaps/modules/f.js ================================================ import { g } from './g'; // Test function f() { var x = 1; x = 2; x = g(x); x = g(x); } window.f = f; ================================================ FILE: testdata/web/sourceMaps/modules/g.js ================================================ /** * Test */ export function g(y) { return y * y; } ================================================ FILE: testdata/web/sourceMaps/modules/index.html ================================================

Source-map Test

================================================ FILE: testdata/web/sourceMaps/scripts/f.js ================================================ /** * Test */ function f() { var x = 1; x = 2; x = g(x); x = g(x); } ================================================ FILE: testdata/web/sourceMaps/scripts/g.js ================================================ /** * Test */ function g(y) { return y * y; } ================================================ FILE: testdata/web/sourceMaps/scripts/index.html ================================================ __SCRIPTS__

Source-map Test

================================================ FILE: testdata/web/tsconfig.json ================================================ { "compilerOptions": { "sourceMap": true }, "files": [ "exception-sourcemap.ts" ] } ================================================ FILE: testdata/web/worker.js ================================================ onmessage = function(e) { postMessage(e.data.foo); } ================================================ FILE: testdata/webExtension/addOn/backgroundscript.js ================================================ chrome.runtime.onMessage.addListener((msg) => { console.log('foo: ' + msg.foo); }); ================================================ FILE: testdata/webExtension/addOn/contentscript.js ================================================ let messageBox = document.getElementById('messageBox'); setInterval(() => { let message = messageBox.textContent; if (message.length > 0) { messageBox.textContent = ''; chrome.runtime.sendMessage({ "foo": message }); } }, 200); ================================================ FILE: testdata/webExtension/addOn/manifest.json ================================================ { "description": "A WebExtension used for testing vscode-firefox-debug", "manifest_version": 2, "name": "Test", "version": "1.0", "homepage_url": "https://github.com/firefox-devtools/vscode-firefox-debug", "applications": { "gecko": { "id": "{12345678-1234-1234-1234-123456781234}" } }, "background": { "scripts": [ "backgroundscript.js" ] }, "content_scripts": [ { "matches": [ "file:///*" ], "js": [ "contentscript.js" ] } ] } ================================================ FILE: testdata/webExtension/index.html ================================================

WebExtension Test

================================================ FILE: testdata/webExtension2/addOn/backgroundscript.js ================================================ chrome.runtime.onMessage.addListener((msg) => { console.log('foo: ' + msg.foo); }); ================================================ FILE: testdata/webExtension2/addOn/contentscript.js ================================================ let messageBox = document.getElementById('messageBox'); setInterval(() => { let message = messageBox.textContent; if (message.length > 0) { messageBox.textContent = ''; chrome.runtime.sendMessage({ "foo": message }); } }, 200); ================================================ FILE: testdata/webExtension2/addOn/manifest.json ================================================ { "description": "A WebExtension used for testing vscode-firefox-debug", "manifest_version": 2, "name": "Test", "version": "1.0", "homepage_url": "https://github.com/firefox-devtools/vscode-firefox-debug", "background": { "scripts": [ "backgroundscript.js" ] }, "content_scripts": [ { "matches": [ "file:///*" ], "js": [ "contentscript.js" ] } ] } ================================================ FILE: testdata/webExtension2/index.html ================================================

WebExtension Test

================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es2015", "lib": [ "es2015" ], "module": "commonjs", "noEmit": true, "strict": true, "noImplicitReturns": true, "esModuleInterop": true, "isolatedModules": true, "skipLibCheck": true }, "include": [ "src/adapter/firefoxDebugAdapter.ts", "src/adapter/util/forkedLauncher.ts", "src/adapter/firefox/protocol.d.ts", "src/extension/main.ts", "src/typings/*.d.ts", "src/test/test*.ts" ] } ================================================ FILE: webpack.config.js ================================================ const path = require('path'); const CopyPlugin = require('copy-webpack-plugin'); module.exports = { context: path.resolve(__dirname, 'src'), entry: { extension: './extension/main.ts', adapter: './adapter/firefoxDebugAdapter.ts', launcher: './adapter/util/forkedLauncher.ts' }, resolve: { extensions: [ '.js', '.ts' ] }, module: { rules: [ { test: /\.ts$/, loader: 'babel-loader' } ] }, externals: { vscode: 'commonjs vscode', fsevents: 'commonjs fsevents' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', libraryTarget: 'commonjs2', devtoolModuleFilenameTemplate: '../src/[resource-path]' }, target: 'node', node: { __dirname: false }, plugins: [ new CopyPlugin({ patterns: [ path.resolve(__dirname, 'node_modules/source-map/lib/mappings.wasm'), path.resolve(__dirname, 'LICENSE') ] }) ], devtool: 'source-map' };