├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo.gif ├── demo2.gif ├── icon.png ├── package-lock.json ├── package.json ├── postBuildStep.ts ├── src ├── common │ └── styles.css ├── extension.ts ├── host │ ├── host.ts │ ├── mainHost.ts │ ├── mainMessaging.ts │ ├── messaging.ts │ ├── patch │ │ └── inspectorContentPolicy.ts │ └── toolsHost.ts ├── telemetry.ts └── utils.ts ├── tsconfig.json ├── tslint.json └── webpack.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .browse.VC.db 3 | npm-debug.log 4 | *.vsix 5 | 6 | lib/ 7 | node_modules/ 8 | /out/ 9 | typings/ 10 | /.vs 11 | /src/.vs/src/v16/.suo 12 | /src/.vs 13 | /package/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 16 | "preLaunchTask": "npm: build" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsc.autoDetect": "off" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | } 13 | }, 14 | { 15 | "type": "npm", 16 | "script": "lint", 17 | "problemMatcher": [ 18 | "$tsc" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/*.ts 2 | **/tsconfig.json 3 | **/tslint.json 4 | .gitignore 5 | .vscode/** 6 | src/** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.7 2 | * Fixed copying text from the devtools - [#23](https://github.com/BlankSourceCode/vscode-devtools/issues/23) 3 | 4 | ## 0.0.6 5 | * Fixed issue with the DevTools showing blank in latest versions of VS Code. - [#18](https://github.com/BlankSourceCode/vscode-devtools/issues/18) 6 | 7 | ## 0.0.5 8 | * Added attach config to attach to an already existing chrome instance - [#10](https://github.com/CodeMooseUS/vscode-devtools/issues/10) 9 | * Downgraded event-stream npm package due to security issue - [info](https://code.visualstudio.com/blogs/2018/11/26/event-stream) 10 | 11 | ## 0.0.4 12 | * Fixed a crash due to sourcemaps - [#8](https://github.com/CodeMooseUS/vscode-devtools/issues/8) 13 | 14 | ## 0.0.3 15 | * Fixed an issue with telemetry not updating user count correctly 16 | 17 | ## 0.0.2 18 | * Added devtools settings persistence - [#1](https://github.com/CodeMooseUS/vscode-devtools/issues/1) 19 | * Any settings that you change from within the devtools themselves will now be there next time you open the devtools. 20 | * This includes changing the devtools theme which auto reloads the tools. 21 | * Added anonymous telemetry reporting to see which functions need implementing next based on usage. 22 | 23 | ## 0.0.1 24 | * Initial preview release 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 James Lissiak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS PROJECT IS NO LONGER UNDER ACTIVE DEVELOPMENT 2 | 3 | This was a project that I took on in my spare time to show the potential of having the DevTool embedded directly in VS Code. Unfortunately I no longer have the time to keep this up to date with all the new changes in Chrome, so I am archiving the project and will be removing it from the VS Code marketplace. 4 | 5 | However, I think the experiment was a success, and it worked so well in fact, that there is now an officially supported version from Microsoft for the Edge DevTools, which you should check out at the following links: 6 | 7 | [Microsoft Edge Developer Tools for Visual Studio Code](https://github.com/microsoft/vscode-edge-devtools) 8 | 9 | [Microsoft Edge Developer Tools integration in VS Code](https://docs.microsoft.com/en-us/microsoft-edge/visual-studio-code/microsoft-edge-devtools-extension#browser-debugging-with-microsoft-edge-devtools-integration-in-visual-studio-code) 10 | 11 | You can install the Edge version from the [VS Code marketplace](https://marketplace.visualstudio.com/items?itemName=ms-edgedevtools.vscode-edge-devtools) 12 | 13 | Thanks to everyone that installed/used/filed issues for this project, and I hope it was useful while it lasted. 14 | 15 | # VSCode DevTools for Chrome 16 | 17 | A VSCode extension to host the chrome devtools inside of a webview. 18 | 19 | 20 | 21 |

22 | 23 | Marketplace badge 24 | 25 |

26 | 27 | ## Attaching to a running chrome instance: 28 | ![Demo1](demo.gif) 29 | 30 | ## Launching a 'debugger for chrome' project and using screencast: 31 | ![Demo2](demo2.gif) 32 | 33 | # Using the extension 34 | 35 | ## Launching as a Debugger 36 | You can launch the Chrome DevTools hosted in VS Code like you would a debugger, by using a launch.json config file. However, the Chrome DevTools aren't a debugger and any breakpoints set in VS Code won't be hit, you can of course use the script debugger in Chrome DevTools. 37 | 38 | To do this in your `launch.json` add a new debug config with two parameters. 39 | - `type` - The name of the debugger which must be `devtools-for-chrome`. Required. 40 | - `url` - The url to launch Chrome at. Optional. 41 | - `file` - The local file path to launch Chrome at. Optional. 42 | - `request` - Whether a new tab in Chrome should be opened `launch` or to use an exsisting tab `attach` matched on URL. Optional. 43 | - `name` - A friendly name to show in the VS Code UI. Required. 44 | ``` 45 | { 46 | "version": "0.1.0", 47 | "configurations": [ 48 | { 49 | "type": "devtools-for-chrome", 50 | "request": "launch", 51 | "name": "Launch Chrome DevTools", 52 | "file": "${workspaceFolder}/index.html" 53 | }, 54 | { 55 | "type": "devtools-for-chrome", 56 | "request": "attach", 57 | "name": "Attach Chrome DevTools", 58 | "url": "http://localhost:8000/" 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | ## Launching Chrome manually 65 | - Start chrome with no extensions and remote-debugging enabled on port 9222: 66 | - `chrome.exe --disable-extensions --remote-debugging-port=9222` 67 | - Open the devtools inside VS Code: 68 | - Run the command - `DevTools for Chrome: Attach to a target` 69 | - Select a target from the drop down 70 | 71 | ## Launching Chrome via the extension 72 | - Start chrome: 73 | - Run the command - `DevTools for Chrome: Launch Chrome and then attach to a target` 74 | - Navigate to whatever page you want 75 | - Open the devtools inside VS Code: 76 | - Select a target from the drop down 77 | 78 | 79 | # Known Issues 80 | - Prototyping stage 81 | - Having the DevTools in a non-foreground tab can cause issues while debugging 82 | - This is due to VS Code suspending script execution of non-foreground webviews 83 | - The workaround is to put the DevTools in a split view tab so that they are always visible while open 84 | - Chrome browser extensions can sometimes cause the webview to terminate 85 | 86 | # Developing the extension itself 87 | 88 | - Start chrome with remote-debugging enabled on port 9222 89 | - `chrome.exe --disable-extensions --remote-debugging-port=9222` 90 | - Run the extension 91 | - `npm install` 92 | - `npm run watch` or `npm run build` 93 | - Open the folder in VSCode 94 | - `F5` to start debugging 95 | - Open the devtools 96 | - Run the command - `DevTools for Chrome: Attach to a target` 97 | - Select a target from the drop down 98 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlankSourceCode/vscode-devtools/5907fa959ad861ea2d5018ed44f77bff0e549975/demo.gif -------------------------------------------------------------------------------- /demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlankSourceCode/vscode-devtools/5907fa959ad861ea2d5018ed44f77bff0e549975/demo2.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlankSourceCode/vscode-devtools/5907fa959ad861ea2d5018ed44f77bff0e549975/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-devtools-for-chrome", 3 | "displayName": "DevTools for Chrome", 4 | "description": "Open the chrome devtools as a dockable webview", 5 | "version": "0.0.7", 6 | "preview": true, 7 | "license": "SEE LICENSE IN LICENSE", 8 | "publisher": "codemooseus", 9 | "icon": "icon.png", 10 | "aiKey": "3ec4c1b4-542f-4adf-830a-a4b04370fa3f", 11 | "homepage": "https://github.com/CodeMooseUS/vscode-devtools/blob/master/README.md", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/CodeMooseUS/vscode-devtools" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/CodeMooseUS/vscode-devtools/issues" 18 | }, 19 | "galleryBanner": { 20 | "color": "#5c2d91", 21 | "theme": "dark" 22 | }, 23 | "keywords": [ 24 | "browser", 25 | "console", 26 | "debugger", 27 | "dom", 28 | "remote" 29 | ], 30 | "engines": { 31 | "vscode": "^1.34.0" 32 | }, 33 | "categories": [ 34 | "Other" 35 | ], 36 | "activationEvents": [ 37 | "onCommand:devtools-for-chrome.launch", 38 | "onCommand:devtools-for-chrome.attach", 39 | "onWebviewPanel:devtools-for-chrome", 40 | "onDebug" 41 | ], 42 | "main": "./out/extension", 43 | "contributes": { 44 | "commands": [ 45 | { 46 | "command": "devtools-for-chrome.launch", 47 | "title": "Launch Chrome and then attach to a target", 48 | "category": "DevTools for Chrome" 49 | }, 50 | { 51 | "command": "devtools-for-chrome.attach", 52 | "title": "Attach to a target", 53 | "category": "DevTools for Chrome" 54 | } 55 | ], 56 | "debuggers": [ 57 | { 58 | "type": "devtools-for-chrome", 59 | "label": "DevTools for Chrome", 60 | "configurationAttributes": { 61 | "launch": { 62 | "properties": { 63 | "url": { 64 | "type": "string", 65 | "description": "Absolute uri to launch.", 66 | "default": "http://localhost:8080" 67 | }, 68 | "file": { 69 | "type": "string", 70 | "description": "File path to launch.", 71 | "default": "${workspaceFolder}/index.html" 72 | }, 73 | "chromePath": { 74 | "type": "string", 75 | "description": "Absolute path to the Chrome instance to launch.", 76 | "default": "" 77 | } 78 | } 79 | }, 80 | "attach": { 81 | "properties": { 82 | "url": { 83 | "type": "string", 84 | "description": "Absolute uri to launch.", 85 | "default": "http://localhost:8080" 86 | }, 87 | "file": { 88 | "type": "string", 89 | "description": "File path to launch.", 90 | "default": "${workspaceFolder}/index.html" 91 | }, 92 | "chromePath": { 93 | "type": "string", 94 | "description": "Absolute path to the Chrome instance to launch.", 95 | "default": "" 96 | } 97 | } 98 | } 99 | } 100 | } 101 | ], 102 | "configuration": { 103 | "title": "DevTools for Chrome", 104 | "type": "object", 105 | "properties": { 106 | "vscode-devtools-for-chrome.hostname": { 107 | "type": "string", 108 | "default": "localhost", 109 | "description": "The hostname on which to search for remote debuggable chrome instances" 110 | }, 111 | "vscode-devtools-for-chrome.port": { 112 | "type": "number", 113 | "default": 9222, 114 | "description": "The port on which to search for remote debuggable chrome instances" 115 | }, 116 | "vscode-devtools-for-chrome.chromePath": { 117 | "type": "string", 118 | "default": null, 119 | "description": "The path to Chrome to be used rather than searching for the default" 120 | } 121 | } 122 | } 123 | }, 124 | "scripts": { 125 | "package": "npx vsce package --out vscode-devtools.vsix", 126 | "vscode:prepublish": "npm run build", 127 | "build": "npm run build-wp && npm run build-post", 128 | "build-wp": "webpack", 129 | "build-post": "npx ts-node postBuildStep.ts", 130 | "build-watch": "npm run build && npm run watch", 131 | "watch": "npm run watch-wp", 132 | "watch-wp": "webpack --watch" 133 | }, 134 | "dependencies": { 135 | "applicationinsights": "1.6.0", 136 | "ws": "7.2.1" 137 | }, 138 | "devDependencies": { 139 | "@types/fs-extra": "8.0.1", 140 | "@types/node": "13.5.0", 141 | "@types/shelljs": "0.8.6", 142 | "@types/ws": "7.2.0", 143 | "@types/vscode": "1.34.0", 144 | "@types/applicationinsights": "0.20.0", 145 | "chrome-devtools-frontend": "1.0.602557", 146 | "fs-extra": "8.1.0", 147 | "shelljs": "0.8.3", 148 | "ts-node": "8.6.2", 149 | "ts-loader": "6.0.4", 150 | "tslint": "6.0.0", 151 | "typescript": "3.7.5", 152 | "vsce": "1.71.0", 153 | "vscode": "1.1.37", 154 | "webpack": "4.41.5", 155 | "webpack-cli": "3.3.10" 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /postBuildStep.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as fse from "fs-extra"; 5 | import path from "path"; 6 | import { applyContentSecurityPolicyPatch } from "./src/host/patch/inspectorContentPolicy"; 7 | 8 | async function copyFile(srcDir: string, outDir: string, name: string) { 9 | await fse.copy( 10 | path.join(srcDir, name), 11 | path.join(outDir, name), 12 | ); 13 | } 14 | 15 | async function copyStaticFiles() { 16 | // Copy the static css file to the out directory 17 | const commonSrcDir = "./src/common/"; 18 | const commonOutDir = "./out/common/"; 19 | await fse.ensureDir(commonOutDir); 20 | await copyFile(commonSrcDir, commonOutDir, "styles.css"); 21 | 22 | const toolsSrcDir = 23 | `node_modules/chrome-devtools-frontend/front_end/`; 24 | if (!isDirectory(toolsSrcDir)) { 25 | throw new Error(`Could not find Chrome DevTools path at '${toolsSrcDir}'. ` + 26 | "Did you run npm install?"); 27 | } 28 | 29 | // Copy the devtools to the out directory 30 | const toolsOutDir = "./out/tools/front_end/"; 31 | await fse.ensureDir(toolsOutDir); 32 | await fse.copy(toolsSrcDir, toolsOutDir); 33 | 34 | // Patch older versions of the webview with our workarounds 35 | await patchFilesForWebView(toolsOutDir); 36 | } 37 | 38 | async function patchFilesForWebView(toolsOutDir: string) { 39 | // Release file versions 40 | await patchFileForWebView("inspector.html", toolsOutDir, true, [ 41 | applyContentSecurityPolicyPatch, 42 | ]); 43 | 44 | // Debug file versions 45 | 46 | } 47 | 48 | async function patchFileForWebView( 49 | filename: string, 50 | dir: string, 51 | isRelease: boolean, 52 | patches: Array<(content: string, isRelease?: boolean) => string>) { 53 | const file = path.join(dir, filename); 54 | 55 | // Ignore missing files 56 | if (!await fse.pathExists(file)) { 57 | return; 58 | } 59 | 60 | // Read in the file 61 | let content = (await fse.readFile(file)).toString(); 62 | 63 | // Apply each patch in order 64 | patches.forEach((patchFunction) => { 65 | content = patchFunction(content, isRelease); 66 | }); 67 | 68 | // Write out the final content 69 | await fse.writeFile(file, content); 70 | } 71 | 72 | function isDirectory(fullPath: string) { 73 | try { 74 | return fse.statSync(fullPath).isDirectory(); 75 | } catch { 76 | return false; 77 | } 78 | } 79 | 80 | copyStaticFiles(); 81 | -------------------------------------------------------------------------------- /src/common/styles.css: -------------------------------------------------------------------------------- 1 | html, body, iframe { 2 | height: 100%; 3 | width: 100%; 4 | position: absolute; 5 | padding: 0; 6 | margin: 0; 7 | overflow: hidden; 8 | } 9 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import WebSocket from 'ws'; 4 | import TelemetryReporter from './telemetry'; 5 | import QuickPickItem = vscode.QuickPickItem; 6 | import * as utils from './utils'; 7 | import packageJson from "../package.json"; 8 | 9 | interface IPackageInfo { 10 | name: string; 11 | version: string; 12 | aiKey: string; 13 | } 14 | 15 | const debuggerType: string = 'devtools-for-chrome'; 16 | const defaultUrl: string = 'about:blank'; 17 | let telemetryReporter: TelemetryReporter; 18 | 19 | export function activate(context: vscode.ExtensionContext) { 20 | 21 | const packageInfo = getPackageInfo(context); 22 | if (packageInfo && vscode.env.machineId !== 'someValue.machineId') { 23 | // Use the real telemetry reporter 24 | telemetryReporter = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); 25 | } else { 26 | // Fallback to a fake telemetry reporter 27 | telemetryReporter = new DebugTelemetryReporter(); 28 | } 29 | context.subscriptions.push(telemetryReporter); 30 | 31 | context.subscriptions.push(vscode.commands.registerCommand('devtools-for-chrome.launch', async () => { 32 | launch(context); 33 | })); 34 | 35 | context.subscriptions.push(vscode.commands.registerCommand('devtools-for-chrome.attach', async () => { 36 | attach(context, /* viaConfig= */ false, defaultUrl); 37 | })); 38 | 39 | vscode.debug.registerDebugConfigurationProvider(debuggerType, { 40 | provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken): vscode.ProviderResult { 41 | return Promise.resolve([{ 42 | type: debuggerType, 43 | name: 'Launch Chrome against localhost', 44 | request: 'launch', 45 | url: 'http://localhost:8080' 46 | }]); 47 | }, 48 | 49 | resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult { 50 | if (config && config.type === debuggerType) { 51 | const targetUri: string = utils.getUrlFromConfig(folder, config); 52 | if (config.request && config.request.localeCompare('attach', 'en', { sensitivity: 'base' }) === 0) { 53 | attach(context, /* viaConfig= */ true, targetUri); 54 | telemetryReporter.sendTelemetryEvent('launch/command/attach'); 55 | } else if (config.request && config.request.localeCompare('launch', 'en', { sensitivity: 'base' }) === 0) { 56 | launch(context, targetUri, config.chromePath); 57 | telemetryReporter.sendTelemetryEvent('launch/command/launch'); 58 | } 59 | } else { 60 | vscode.window.showErrorMessage('No supported launch config was found.'); 61 | telemetryReporter.sendTelemetryEvent('launch/error/config_not_found'); 62 | } 63 | return; 64 | } 65 | }); 66 | } 67 | 68 | async function launch(context: vscode.ExtensionContext, launchUrl?: string, chromePathFromLaunchConfig?: string) { 69 | const viaConfig = !!(launchUrl || chromePathFromLaunchConfig); 70 | const telemetryProps = { viaConfig: `${viaConfig}` }; 71 | telemetryReporter.sendTelemetryEvent('launch', telemetryProps); 72 | 73 | const { hostname, port } = getSettings(); 74 | const portFree = await utils.isPortFree(hostname, port); 75 | if (portFree) { 76 | const settings = vscode.workspace.getConfiguration('vscode-devtools-for-chrome'); 77 | const pathToChrome = settings.get('chromePath') as string || chromePathFromLaunchConfig || utils.getPathToChrome(); 78 | 79 | if (!pathToChrome || !utils.existsSync(pathToChrome)) { 80 | vscode.window.showErrorMessage('Chrome was not found. Chrome must be installed for this extension to function. If you have Chrome installed at a custom location you can specify it in the \'chromePath\' setting.'); 81 | telemetryReporter.sendTelemetryEvent('launch/error/chrome_not_found', telemetryProps); 82 | return; 83 | } 84 | 85 | utils.launchLocalChrome(pathToChrome, port, defaultUrl); 86 | } 87 | 88 | const target = JSON.parse(await utils.getURL(`http://${hostname}:${port}/json/new?${launchUrl}`)); 89 | 90 | if (!target || !target.webSocketDebuggerUrl || target.webSocketDebuggerUrl === '') { 91 | vscode.window.showErrorMessage(`Could not find the launched Chrome tab: (${launchUrl}).`); 92 | telemetryReporter.sendTelemetryEvent('launch/error/tab_not_found', telemetryProps); 93 | attach(context, viaConfig, defaultUrl); 94 | } else { 95 | DevToolsPanel.createOrShow(context, target.webSocketDebuggerUrl); 96 | } 97 | } 98 | 99 | async function attach(context: vscode.ExtensionContext, viaConfig: boolean, targetUrl: string) { 100 | const telemetryProps = { viaConfig: `${viaConfig}` }; 101 | telemetryReporter.sendTelemetryEvent('attach', telemetryProps); 102 | 103 | const { hostname, port } = getSettings(); 104 | const responseArray = await getListOfTargets(hostname, port); 105 | if (Array.isArray(responseArray)) { 106 | telemetryReporter.sendTelemetryEvent('attach/list', telemetryProps, { targetCount: responseArray.length }); 107 | 108 | if (responseArray.length === 0) { 109 | vscode.window.showErrorMessage(`Could not find any targets for attaching.\nDid you remember to run Chrome with '--remote-debugging-port=9222'?`); 110 | return; 111 | } 112 | 113 | const items: QuickPickItem[] = []; 114 | 115 | responseArray.forEach(i => { 116 | i = utils.fixRemoteUrl(hostname, port, i); 117 | items.push({ 118 | label: i.title, 119 | description: i.url, 120 | detail: i.webSocketDebuggerUrl 121 | }); 122 | }); 123 | 124 | let targetWebsocketUrl = ''; 125 | if (typeof targetUrl === 'string' && targetUrl.length > 0 && targetUrl !== defaultUrl) { 126 | const matches = items.filter(i => i.description && targetUrl.localeCompare(i.description, 'en', { sensitivity: 'base' }) === 0); 127 | if (matches && matches.length > 0 ) { 128 | targetWebsocketUrl = matches[0].detail || ''; 129 | } else { 130 | vscode.window.showErrorMessage(`Couldn't attach to ${targetUrl}.`); 131 | } 132 | } 133 | 134 | if (targetWebsocketUrl && targetWebsocketUrl.length > 0) { 135 | DevToolsPanel.createOrShow(context, targetWebsocketUrl as string); 136 | } else { 137 | vscode.window.showQuickPick(items).then((selection) => { 138 | if (selection) { 139 | DevToolsPanel.createOrShow(context, selection.detail as string); 140 | } 141 | }); 142 | } 143 | } else { 144 | telemetryReporter.sendTelemetryEvent('attach/error/no_json_array', telemetryProps); 145 | } 146 | } 147 | 148 | function getSettings(): { hostname: string, port: number } { 149 | const settings = vscode.workspace.getConfiguration('vscode-devtools-for-chrome'); 150 | const hostname = settings.get('hostname') as string || 'localhost'; 151 | const port = settings.get('port') as number || 9222; 152 | 153 | return { hostname, port }; 154 | } 155 | 156 | function getPackageInfo(context: vscode.ExtensionContext): IPackageInfo { 157 | if (packageJson) { 158 | return { 159 | name: packageJson.name, 160 | version: packageJson.version, 161 | aiKey: packageJson.aiKey 162 | }; 163 | } 164 | return undefined as any as IPackageInfo; 165 | } 166 | 167 | async function getListOfTargets(hostname: string, port: number, useHttps: boolean = false): Promise> { 168 | const checkDiscoveryEndpoint = (uri: string) => { 169 | return utils.getURL(uri, { headers: { Host: "localhost" } }); 170 | }; 171 | 172 | const protocol = (useHttps ? "https" : "http"); 173 | 174 | let jsonResponse = ""; 175 | for (const endpoint of ["/json/list", "/json"]) { 176 | try { 177 | jsonResponse = await checkDiscoveryEndpoint(`${protocol}://${hostname}:${port}${endpoint}`); 178 | if (jsonResponse) { 179 | break; 180 | } 181 | } catch { 182 | // Do nothing 183 | } 184 | } 185 | 186 | let result: any[]; 187 | try { 188 | result = JSON.parse(jsonResponse); 189 | } catch { 190 | result = []; 191 | } 192 | return result; 193 | } 194 | 195 | class DevToolsPanel { 196 | private static currentPanel: DevToolsPanel | undefined; 197 | private readonly _panel: vscode.WebviewPanel; 198 | private readonly _context: vscode.ExtensionContext; 199 | private readonly _extensionPath: string; 200 | private readonly _targetUrl: string; 201 | private _socket: WebSocket | undefined = undefined; 202 | private _isConnected: boolean = false; 203 | private _messages: any[] = []; 204 | private _disposables: vscode.Disposable[] = []; 205 | 206 | public static createOrShow(context: vscode.ExtensionContext, targetUrl: string) { 207 | const column = vscode.ViewColumn.Beside; 208 | 209 | if (DevToolsPanel.currentPanel) { 210 | DevToolsPanel.currentPanel._panel.reveal(column); 211 | } else { 212 | const panel = vscode.window.createWebviewPanel('devtools-for-chrome', 'DevTools', column, { 213 | enableScripts: true, 214 | enableCommandUris: true, 215 | retainContextWhenHidden: true 216 | }); 217 | 218 | DevToolsPanel.currentPanel = new DevToolsPanel(panel, context, targetUrl); 219 | } 220 | } 221 | 222 | public static revive(panel: vscode.WebviewPanel, context: vscode.ExtensionContext, targetUrl: string) { 223 | DevToolsPanel.currentPanel = new DevToolsPanel(panel, context, targetUrl); 224 | } 225 | 226 | private constructor(panel: vscode.WebviewPanel, context: vscode.ExtensionContext, targetUrl: string) { 227 | this._panel = panel; 228 | this._context = context; 229 | this._extensionPath = context.extensionPath; 230 | this._targetUrl = targetUrl; 231 | 232 | this._update(); 233 | 234 | // Handle closing 235 | this._panel.onDidDispose(() => { 236 | this.dispose(); 237 | }, undefined, this._disposables); 238 | 239 | // Handle view change 240 | this._panel.onDidChangeViewState(e => { 241 | if (this._panel.visible) { 242 | this._update(); 243 | } 244 | }, undefined, this._disposables); 245 | 246 | // Handle messages from the webview 247 | this._panel.webview.onDidReceiveMessage(message => { 248 | this._onMessageFromWebview(message); 249 | }, undefined, this._disposables); 250 | } 251 | 252 | public dispose() { 253 | DevToolsPanel.currentPanel = undefined; 254 | 255 | this._panel.dispose(); 256 | this._disposeSocket(); 257 | 258 | while (this._disposables.length) { 259 | const x = this._disposables.pop(); 260 | if (x) { 261 | x.dispose(); 262 | } 263 | } 264 | } 265 | 266 | private _disposeSocket() { 267 | if (this._socket) { 268 | // Reset the socket since the devtools have been reloaded 269 | telemetryReporter.sendTelemetryEvent('websocket/dispose'); 270 | const s = this._socket as any; 271 | s.onopen = undefined; 272 | s.onmessage = undefined; 273 | s.onerror = undefined; 274 | s.onclose = undefined; 275 | this._socket.close(); 276 | this._socket = undefined; 277 | } 278 | } 279 | 280 | private _onMessageFromWebview(message: string) { 281 | if (message === 'ready') { 282 | if (this._socket) { 283 | telemetryReporter.sendTelemetryEvent('websocket/reconnect'); 284 | } 285 | this._disposeSocket(); 286 | } else if (message.substr(0, 10) === 'telemetry:') { 287 | return this._sendTelemetryMessage(message.substr(10)); 288 | } else if (message.substr(0, 9) === 'getState:') { 289 | return this._getDevtoolsState(); 290 | } else if (message.substr(0, 9) === 'setState:') { 291 | return this._setDevtoolsState(message.substr(9)); 292 | } else if (message.substr(0, 7) === 'getUrl:') { 293 | return this._getDevtoolsUrl(message.substr(7)); 294 | } else if (message.substr(0, 7) === 'copyText:') { 295 | return this._copyText(message.substr(9)); 296 | } 297 | 298 | if (!this._socket) { 299 | // First message, so connect a real websocket to the target 300 | this._connectToTarget(); 301 | } else if (!this._isConnected) { 302 | // DevTools are sending a message before the real websocket has finished opening so cache it 303 | this._messages.push(message); 304 | } else { 305 | // Websocket ready so send the message directly 306 | this._socket.send(message); 307 | } 308 | } 309 | 310 | private _connectToTarget() { 311 | const url = this._targetUrl; 312 | 313 | // Create the websocket 314 | this._socket = new WebSocket(url); 315 | this._socket.onopen = this._onOpen.bind(this); 316 | this._socket.onmessage = this._onMessage.bind(this); 317 | this._socket.onerror = this._onError.bind(this); 318 | this._socket.onclose = this._onClose.bind(this); 319 | } 320 | 321 | private _onOpen() { 322 | this._isConnected = true; 323 | // Tell the devtools that the real websocket was opened 324 | telemetryReporter.sendTelemetryEvent('websocket/open'); 325 | this._panel.webview.postMessage('open'); 326 | 327 | if (this._socket) { 328 | // Forward any cached messages onto the real websocket 329 | for (const message of this._messages) { 330 | this._socket.send(message); 331 | } 332 | this._messages = []; 333 | } 334 | } 335 | 336 | private _onMessage(message: any) { 337 | if (this._isConnected) { 338 | // Forward the message onto the devtools 339 | this._panel.webview.postMessage(message.data); 340 | } 341 | } 342 | 343 | private _onError() { 344 | if (this._isConnected) { 345 | // Tell the devtools that there was a connection error 346 | telemetryReporter.sendTelemetryEvent('websocket/error'); 347 | this._panel.webview.postMessage('error'); 348 | } 349 | } 350 | 351 | private _onClose() { 352 | if (this._isConnected) { 353 | // Tell the devtools that the real websocket was closed 354 | telemetryReporter.sendTelemetryEvent('websocket/close'); 355 | this._panel.webview.postMessage('close'); 356 | } 357 | this._isConnected = false; 358 | } 359 | 360 | private _sendTelemetryMessage(message: string) { 361 | const telemetry = JSON.parse(message); 362 | telemetryReporter.sendTelemetryEvent(telemetry.name, telemetry.properties, telemetry.metrics); 363 | } 364 | 365 | private _getDevtoolsState() { 366 | const allPrefsKey = 'devtools-preferences'; 367 | const allPrefs: any = this._context.workspaceState.get(allPrefsKey) || 368 | { 369 | uiTheme: '"dark"', 370 | screencastEnabled: false 371 | }; 372 | this._panel.webview.postMessage(`preferences:${JSON.stringify(allPrefs)}`); 373 | } 374 | 375 | private _setDevtoolsState(message: string) { 376 | // Parse the preference from the message and store it 377 | const pref = JSON.parse(message) as { name: string, value: string }; 378 | 379 | const allPrefsKey = 'devtools-preferences'; 380 | const allPrefs: any = this._context.workspaceState.get(allPrefsKey) || {}; 381 | allPrefs[pref.name] = pref.value; 382 | this._context.workspaceState.update(allPrefsKey, allPrefs); 383 | } 384 | 385 | private async _getDevtoolsUrl(message: string) { 386 | // Parse the request from the message and store it 387 | const request = JSON.parse(message) as { id: number, url: string }; 388 | 389 | let content = ''; 390 | try { 391 | content = await utils.getURL(request.url); 392 | } catch (ex) { 393 | content = ''; 394 | } 395 | 396 | this._panel.webview.postMessage(`setUrl:${JSON.stringify({ id: request.id, content })}`); 397 | } 398 | 399 | private async _copyText(message: string) { 400 | // Parse the request from the message and store it 401 | const request = JSON.parse(message) as { text: string }; 402 | vscode.env.clipboard.writeText(request.text); 403 | } 404 | 405 | private _update() { 406 | this._panel.webview.html = this._getHtmlForWebview(); 407 | } 408 | 409 | private _getHtmlForWebview() { 410 | const htmlPath = vscode.Uri.file(path.join(this._extensionPath, 'out/tools/front_end', 'inspector.html')); 411 | const htmlUri = htmlPath.with({ scheme: 'vscode-resource' }); 412 | 413 | const scriptPath = vscode.Uri.file(path.join(this._extensionPath, 'out', 'host', 'messaging.bundle.js')); 414 | const scriptUri = scriptPath.with({ scheme: 'vscode-resource' }); 415 | 416 | const stylesPath = vscode.Uri.file(path.join(this._extensionPath, 'out', 'common', 'styles.css')); 417 | const stylesUri = stylesPath.with({ scheme: 'vscode-resource' }); 418 | 419 | return ` 420 | 421 | 422 | 423 | 424 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | `; 437 | } 438 | } 439 | 440 | class DebugTelemetryReporter extends TelemetryReporter { 441 | constructor() { 442 | super('extensionId', 'extensionVersion', 'key'); 443 | } 444 | 445 | public sendTelemetryEvent(name: string, properties?: any, measurements?: any) { 446 | console.log(`${name}: ${JSON.stringify(properties)}, ${JSON.stringify(properties)}`); 447 | } 448 | 449 | public dispose(): Promise { 450 | return Promise.resolve(); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/host/host.ts: -------------------------------------------------------------------------------- 1 | import { ToolsHost, ToolsResourceLoader, ToolsWebSocket, IRuntimeResourceLoader } from "./toolsHost"; 2 | 3 | export interface IDevToolsWindow extends Window { 4 | InspectorFrontendHost: ToolsHost; 5 | WebSocket: typeof ToolsWebSocket; 6 | ResourceLoaderOverride: ToolsResourceLoader; 7 | Root: IRoot; 8 | _importScriptPathPrefix: string; 9 | } 10 | 11 | export interface IRoot { 12 | Runtime: IRuntimeResourceLoader; 13 | } 14 | 15 | export function initialize(dtWindow: IDevToolsWindow) { 16 | if (!dtWindow) { 17 | return; 18 | } 19 | 20 | // Create a mock sessionStorage since it doesn't exist in data url but the devtools use it 21 | const sessionStorage = {}; 22 | Object.defineProperty(dtWindow, "sessionStorage", { 23 | get() { return sessionStorage; }, 24 | set() { /* NO-OP */ }, 25 | }); 26 | 27 | // Prevent the devtools from using localStorage since it doesn't exist in data uris 28 | Object.defineProperty(dtWindow, "localStorage", { 29 | get() { return undefined; }, 30 | set() { /* NO-OP */ }, 31 | }); 32 | 33 | // Setup the global objects that must exist at load time 34 | dtWindow.InspectorFrontendHost = new ToolsHost(); 35 | dtWindow.WebSocket = ToolsWebSocket; 36 | 37 | // Listen for messages from the extension and forward to the tools 38 | dtWindow.addEventListener("message", (e) => { 39 | if (e.data.substr(0, 12) === 'preferences:') { 40 | dtWindow.InspectorFrontendHost.fireGetStateCallback(e.data.substr(12)); 41 | } else if (e.data.substr(0, 7) === 'setUrl:') { 42 | dtWindow.ResourceLoaderOverride.resolveUrlRequest(e.data.substr(7)); 43 | } 44 | }, true); 45 | 46 | dtWindow.addEventListener("DOMContentLoaded", () => { 47 | dtWindow.ResourceLoaderOverride = new ToolsResourceLoader(dtWindow); 48 | dtWindow._importScriptPathPrefix = dtWindow._importScriptPathPrefix.replace("null", "vscode-resource:"); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/host/mainHost.ts: -------------------------------------------------------------------------------- 1 | import { IDevToolsWindow, initialize } from "./host"; 2 | 3 | initialize(window as any as IDevToolsWindow); 4 | -------------------------------------------------------------------------------- /src/host/mainMessaging.ts: -------------------------------------------------------------------------------- 1 | import { initializeMessaging } from "./messaging"; 2 | 3 | initializeMessaging(); 4 | -------------------------------------------------------------------------------- /src/host/messaging.ts: -------------------------------------------------------------------------------- 1 | declare const acquireVsCodeApi: () => any; 2 | 3 | export function initializeMessaging() { 4 | const vscode = acquireVsCodeApi(); 5 | 6 | let toolsWindow: Window | null; 7 | 8 | window.addEventListener("DOMContentLoaded", () => { 9 | toolsWindow = (document.getElementById("host") as HTMLIFrameElement).contentWindow; 10 | }); 11 | 12 | window.addEventListener("message", (messageEvent) => { 13 | // Both windows now have a "null" origin so we need to distiguish direction based on protocol, 14 | // which will throw an exception when it is from the devtools x-domain window. 15 | // See: https://blog.mattbierner.com/vscode-webview-web-learnings/ 16 | let sendToDevTools = false; 17 | try { 18 | sendToDevTools = (messageEvent.source as Window).location.protocol === "data:"; 19 | } catch { /* NO-OP */ } 20 | 21 | if (!sendToDevTools) { 22 | // Pass the message onto the extension 23 | vscode.postMessage(messageEvent.data); 24 | } else if (toolsWindow) { 25 | // Pass the message onto the devtools 26 | toolsWindow.postMessage(messageEvent.data, "*"); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/host/patch/inspectorContentPolicy.ts: -------------------------------------------------------------------------------- 1 | export function applyContentSecurityPolicyPatch(content: string) { 2 | return content 3 | .replace( 4 | /script-src\s*'self'/g, 5 | `script-src vscode-resource: 'self'`) 6 | .replace( 7 | ``, 8 | ``, 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/host/toolsHost.ts: -------------------------------------------------------------------------------- 1 | export interface IRuntimeResourceLoader { 2 | loadResourcePromise: (url: string) => Promise; 3 | } 4 | 5 | export class ToolsHost { 6 | private _getStateCallback: ((prefs: any) => void) | undefined = undefined; 7 | 8 | public getPreferences(callback: (prefs: any) => void) { 9 | // Load the preference via the extension workspaceState 10 | this._getStateCallback = callback; 11 | window.parent.postMessage('getState:', '*'); 12 | } 13 | 14 | public setPreference(name: string, value: string) { 15 | // Save the preference via the extension workspaceState 16 | window.parent.postMessage(`setState:${JSON.stringify({ name, value })}`, '*'); 17 | } 18 | 19 | public recordEnumeratedHistogram(actionName: string, actionCode: number, bucketSize: number) { 20 | // Inform the extension of the chrome telemetry event 21 | const telemetry = { 22 | name: `devtools/${actionName}`, 23 | properties: {}, 24 | metrics: {} 25 | }; 26 | if (actionName === 'DevTools.InspectElement') { 27 | (telemetry.metrics as any)[`${actionName}.duration`] = actionCode; 28 | } else { 29 | (telemetry.properties as any)[`${actionName}.actionCode`] = actionCode; 30 | } 31 | window.parent.postMessage(`telemetry:${JSON.stringify(telemetry)}`, '*'); 32 | } 33 | 34 | public copyText(text: string) { 35 | window.parent.postMessage(`copyText:${JSON.stringify({ text })}`, '*'); 36 | } 37 | 38 | public fireGetStateCallback(state: string) { 39 | const prefs = JSON.parse(state); 40 | if (this._getStateCallback) { 41 | this._getStateCallback(prefs); 42 | } 43 | } 44 | } 45 | 46 | export class ToolsWebSocket { 47 | constructor(url: string) { 48 | window.addEventListener('message', messageEvent => { 49 | if (messageEvent.data && messageEvent.data[0] !== '{') { 50 | // Extension websocket control messages 51 | switch (messageEvent.data) { 52 | case 'error': 53 | (this as any).onerror(); 54 | break; 55 | 56 | case 'close': 57 | (this as any).onclose(); 58 | break; 59 | 60 | case 'open': 61 | (this as any).onopen(); 62 | break; 63 | } 64 | } else { 65 | // Messages from the websocket 66 | (this as any).onmessage(messageEvent); 67 | } 68 | }); 69 | 70 | // Inform the extension that we are ready to recieve messages 71 | window.parent.postMessage('ready', '*'); 72 | } 73 | 74 | public send(message: string) { 75 | // Forward the message to the extension 76 | window.parent.postMessage(message, '*'); 77 | } 78 | } 79 | 80 | export class ToolsResourceLoader { 81 | private _window: Window; 82 | private _realLoadResource: (url: string) => Promise; 83 | private _urlLoadNextId: number; 84 | private _urlLoadResolvers: Map void>; 85 | 86 | constructor(dtWindow: Window) { 87 | this._window = dtWindow; 88 | this._realLoadResource = (this._window as any).Runtime.loadResourcePromise; 89 | this._urlLoadNextId = 0; 90 | this._urlLoadResolvers = new Map(); 91 | (this._window as any).Runtime.loadResourcePromise = this.loadResource.bind(this); 92 | } 93 | 94 | public resolveUrlRequest(message: string) { 95 | // Parse the request from the message and store it 96 | const response = JSON.parse(message) as { id: number, content: string }; 97 | 98 | if (this._urlLoadResolvers.has(response.id)) { 99 | const callback = this._urlLoadResolvers.get(response.id); 100 | if (callback) { 101 | callback(response.content); 102 | } 103 | this._urlLoadResolvers.delete(response.id); 104 | } 105 | } 106 | 107 | private async loadResource(url: string): Promise { 108 | if (url === 'sources/module.json') { 109 | // Override the paused event revealer so that hitting a bp will not switch to the sources tab 110 | const content = await this._realLoadResource(url); 111 | return content.replace(/{[^}]+DebuggerPausedDetailsRevealer[^}]+},/gm, ''); 112 | } if (url.substr(0, 7) === 'http://' || url.substr(0, 8) === 'https://') { 113 | // Forward the cross domain request over to the extension 114 | return new Promise((resolve: (url: string) => void, reject) => { 115 | const id = this._urlLoadNextId++; 116 | this._urlLoadResolvers.set(id, resolve); 117 | window.parent.postMessage(`getUrl:${JSON.stringify({ id, url })}`, '*'); 118 | }); 119 | } else { 120 | return this._realLoadResource(url); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/telemetry.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | // Edited to fix the user id reporting 5 | 6 | 'use strict'; 7 | 8 | process.env['APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL'] = 'true'; 9 | 10 | import * as appInsights from 'applicationinsights'; 11 | import * as fs from 'fs'; 12 | import * as os from 'os'; 13 | import * as path from 'path'; 14 | import * as vscode from 'vscode'; 15 | 16 | export default class TelemetryReporter extends vscode.Disposable { 17 | private appInsightsClient: appInsights.TelemetryClient | undefined; 18 | private userOptIn: boolean = false; 19 | private toDispose: vscode.Disposable[] = []; 20 | 21 | private static TELEMETRY_CONFIG_ID = 'telemetry'; 22 | private static TELEMETRY_CONFIG_ENABLED_ID = 'enableTelemetry'; 23 | 24 | private logStream: fs.WriteStream | undefined; 25 | 26 | constructor(private extensionId: string, private extensionVersion: string, key: string) { 27 | super(() => this.toDispose.forEach((d) => d && d.dispose())); 28 | let logFilePath = process.env['VSCODE_LOGS'] || ''; 29 | if (logFilePath && extensionId && process.env['VSCODE_LOG_LEVEL'] === 'trace') { 30 | logFilePath = path.join(logFilePath, `${extensionId}.txt`); 31 | this.logStream = fs.createWriteStream(logFilePath, { flags: 'a', encoding: 'utf8', autoClose: true }); 32 | } 33 | this.updateUserOptIn(key); 34 | this.toDispose.push(vscode.workspace.onDidChangeConfiguration(() => this.updateUserOptIn(key))); 35 | } 36 | 37 | private updateUserOptIn(key: string): void { 38 | const config = vscode.workspace.getConfiguration(TelemetryReporter.TELEMETRY_CONFIG_ID); 39 | if (this.userOptIn !== config.get(TelemetryReporter.TELEMETRY_CONFIG_ENABLED_ID, true)) { 40 | this.userOptIn = config.get(TelemetryReporter.TELEMETRY_CONFIG_ENABLED_ID, true); 41 | if (this.userOptIn) { 42 | this.createAppInsightsClient(key); 43 | } else { 44 | this.dispose(); 45 | } 46 | } 47 | } 48 | 49 | private createAppInsightsClient(key: string) { 50 | // Check if another instance is already initialized 51 | if (appInsights.defaultClient) { 52 | this.appInsightsClient = new appInsights.TelemetryClient(key); 53 | // No other way to enable offline mode 54 | this.appInsightsClient.channel.setUseDiskRetryCaching(true); 55 | } else { 56 | appInsights.setup(key) 57 | .setAutoCollectRequests(false) 58 | .setAutoCollectPerformance(false) 59 | .setAutoCollectExceptions(false) 60 | .setAutoCollectDependencies(false) 61 | .setAutoDependencyCorrelation(false) 62 | .setAutoCollectConsole(false) 63 | .setUseDiskRetryCaching(true) 64 | .start(); 65 | this.appInsightsClient = appInsights.defaultClient; 66 | } 67 | 68 | this.appInsightsClient.commonProperties = this.getCommonProperties(); 69 | 70 | // Add the user and session id's to the context so that the analytics will correctly assign users 71 | if (vscode && vscode.env) { 72 | this.appInsightsClient.context.tags[this.appInsightsClient.context.keys.userId] = vscode.env.machineId; 73 | this.appInsightsClient.context.tags[this.appInsightsClient.context.keys.sessionId] = vscode.env.sessionId; 74 | } 75 | 76 | // Check if it's an Asimov key to change the endpoint 77 | if (key && key.indexOf('AIF-') === 0) { 78 | this.appInsightsClient.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; 79 | } 80 | } 81 | 82 | // __GDPR__COMMON__ "common.os" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } 83 | // __GDPR__COMMON__ "common.platformversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } 84 | // __GDPR__COMMON__ "common.extname" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } 85 | // __GDPR__COMMON__ "common.extversion" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } 86 | // __GDPR__COMMON__ "common.vscodemachineid" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } 87 | // __GDPR__COMMON__ "common.vscodesessionid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } 88 | // __GDPR__COMMON__ "common.vscodeversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } 89 | private getCommonProperties(): { [key: string]: string } { 90 | // tslint:disable-next-line:no-null-keyword 91 | const commonProperties = Object.create(null); 92 | commonProperties['common.os'] = os.platform(); 93 | commonProperties['common.platformversion'] = (os.release() || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); 94 | commonProperties['common.extname'] = this.extensionId; 95 | commonProperties['common.extversion'] = this.extensionVersion; 96 | if (vscode && vscode.env) { 97 | commonProperties['common.vscodemachineid'] = vscode.env.machineId; 98 | commonProperties['common.vscodesessionid'] = vscode.env.sessionId; 99 | commonProperties['common.vscodeversion'] = vscode.version; 100 | } 101 | return commonProperties; 102 | } 103 | 104 | public sendTelemetryEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }): void { 105 | if (this.userOptIn && eventName && this.appInsightsClient) { 106 | this.appInsightsClient.trackEvent({ 107 | name: `${this.extensionId}/${eventName}`, 108 | properties: properties, 109 | measurements: measurements 110 | }); 111 | 112 | if (this.logStream) { 113 | this.logStream.write(`telemetry/${eventName} ${JSON.stringify({ properties, measurements })}\n`); 114 | } 115 | } 116 | } 117 | 118 | public dispose(): Promise { 119 | const flushEventsToLogger = new Promise(resolve => { 120 | if (!this.logStream) { 121 | return resolve(void 0); 122 | } 123 | this.logStream.on('finish', resolve); 124 | this.logStream.end(); 125 | }); 126 | 127 | const flushEventsToAI = new Promise(resolve => { 128 | if (this.appInsightsClient) { 129 | this.appInsightsClient.flush({ 130 | callback: () => { 131 | // All data flushed 132 | this.appInsightsClient = undefined; 133 | resolve(void 0); 134 | } 135 | }); 136 | } else { 137 | resolve(void 0); 138 | } 139 | }); 140 | return Promise.all([flushEventsToAI, flushEventsToLogger]); 141 | } 142 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as http from 'http'; 4 | import * as https from 'https'; 5 | import * as net from 'net'; 6 | import * as os from 'os'; 7 | import * as path from 'path'; 8 | import * as url from 'url'; 9 | import * as vscode from 'vscode'; 10 | 11 | export function getURL(aUrl: string, options: https.RequestOptions = {}): Promise { 12 | return new Promise((resolve, reject) => { 13 | const parsedUrl = url.parse(aUrl); 14 | const get = parsedUrl.protocol === 'https:' ? https.get : http.get; 15 | options = { 16 | rejectUnauthorized: false, 17 | ...parsedUrl, 18 | ...options 19 | }; 20 | 21 | get(options, (response) => { 22 | let responseData = ''; 23 | response.on('data', chunk => { 24 | responseData += chunk.toString(); 25 | }); 26 | response.on('end', () => { 27 | // Sometimes the 'error' event is not fired. Double check here. 28 | if (response.statusCode === 200) { 29 | resolve(responseData); 30 | } else { 31 | reject(new Error(responseData.trim())); 32 | } 33 | }); 34 | }).on('error', e => { 35 | reject(e); 36 | }); 37 | }); 38 | } 39 | 40 | export function fixRemoteUrl(remoteAddress: string, remotePort: number, target: any): any { 41 | if (target.webSocketDebuggerUrl) { 42 | const addressMatch = target.webSocketDebuggerUrl.match(/ws:\/\/([^/]+)\/?/); 43 | if (addressMatch) { 44 | const replaceAddress = `${remoteAddress}:${remotePort}`; 45 | target.webSocketDebuggerUrl = target.webSocketDebuggerUrl.replace(addressMatch[1], replaceAddress); 46 | } 47 | } 48 | return target; 49 | } 50 | 51 | 52 | export const enum Platform { 53 | Windows, OSX, Linux 54 | } 55 | 56 | export function getPlatform(): Platform { 57 | const platform = os.platform(); 58 | return platform === 'darwin' ? Platform.OSX : 59 | platform === 'win32' ? Platform.Windows : 60 | Platform.Linux; 61 | } 62 | 63 | export function existsSync(path: string): boolean { 64 | try { 65 | fs.statSync(path); 66 | return true; 67 | } catch (e) { 68 | return false; 69 | } 70 | } 71 | 72 | export function launchLocalChrome(chromePath: string, chromePort: number, targetUrl: string) { 73 | const chromeArgs = [ 74 | '--disable-extensions', 75 | `--remote-debugging-port=${chromePort}` 76 | ]; 77 | 78 | const chromeProc = cp.spawn(chromePath, chromeArgs, { 79 | stdio: 'ignore', 80 | detached: true 81 | }); 82 | 83 | chromeProc.unref(); 84 | } 85 | 86 | export async function isPortFree(host: string, port: number): Promise { 87 | return new Promise((resolve) => { 88 | const server = net.createServer(); 89 | 90 | server.on('error', () => resolve(false)); 91 | server.listen(port, host); 92 | 93 | server.on('listening', () => { 94 | server.close(); 95 | server.unref(); 96 | }); 97 | 98 | server.on('close', () => resolve(true)); 99 | }); 100 | } 101 | 102 | const WIN_APPDATA = process.env.LOCALAPPDATA || '/'; 103 | const DEFAULT_CHROME_PATH = { 104 | LINUX: '/usr/bin/google-chrome', 105 | OSX: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', 106 | WIN: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 107 | WIN_LOCALAPPDATA: path.join(WIN_APPDATA, 'Google\\Chrome\\Application\\chrome.exe'), 108 | WINx86: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 109 | }; 110 | 111 | export function getPathToChrome(): string { 112 | const platform = getPlatform(); 113 | if (platform === Platform.OSX) { 114 | return existsSync(DEFAULT_CHROME_PATH.OSX) ? DEFAULT_CHROME_PATH.OSX : ''; 115 | } else if (platform === Platform.Windows) { 116 | if (existsSync(DEFAULT_CHROME_PATH.WINx86)) { 117 | return DEFAULT_CHROME_PATH.WINx86; 118 | } else if (existsSync(DEFAULT_CHROME_PATH.WIN)) { 119 | return DEFAULT_CHROME_PATH.WIN; 120 | } else if (existsSync(DEFAULT_CHROME_PATH.WIN_LOCALAPPDATA)) { 121 | return DEFAULT_CHROME_PATH.WIN_LOCALAPPDATA; 122 | } else { 123 | return ''; 124 | } 125 | } else { 126 | return existsSync(DEFAULT_CHROME_PATH.LINUX) ? DEFAULT_CHROME_PATH.LINUX : ''; 127 | } 128 | } 129 | 130 | export function pathToFileURL(absPath: string, normalize?: boolean): string { 131 | if (normalize) { 132 | absPath = path.normalize(absPath); 133 | absPath = forceForwardSlashes(absPath); 134 | } 135 | 136 | absPath = (absPath.startsWith('/') ? 'file://' : 'file:///') + absPath; 137 | return encodeURI(absPath); 138 | } 139 | 140 | export function forceForwardSlashes(aUrl: string): string { 141 | return aUrl 142 | .replace(/\\\//g, '/') // Replace \/ (unnecessarily escaped forward slash) 143 | .replace(/\\/g, '/'); 144 | } 145 | 146 | export function getUrlFromConfig(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration): string { 147 | let outUrlString = ''; 148 | 149 | if (config.file && folder) { 150 | outUrlString = config.file; 151 | outUrlString = outUrlString.replace('${workspaceFolder}', folder.uri.path); 152 | outUrlString = pathToFileURL(outUrlString); 153 | } else if (config.url) { 154 | outUrlString = config.url; 155 | } 156 | 157 | return outUrlString; 158 | } 159 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es2017", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "out", 10 | "skipLibCheck": true, 11 | "noUnusedLocals": true, 12 | "strict": true, 13 | "resolveJsonModule": true 14 | }, 15 | "include": [ 16 | "./**/*" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space", 7 | "check-uppercase" 8 | ], 9 | "indent": [ 10 | true, 11 | "spaces", 12 | 4 13 | ], 14 | "one-line": [ 15 | true, 16 | "check-else", 17 | "check-finally", 18 | "check-catch", 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "ordered-imports": [ 23 | true 24 | ], 25 | "no-var-keyword": true, 26 | "quotemark": [ 27 | true, 28 | "single", 29 | "avoid-escape", 30 | "avoid-template" 31 | ], 32 | "semicolon": [ 33 | true, 34 | "always" 35 | ], 36 | "whitespace": [ 37 | true, 38 | "check-branch", 39 | "check-decl", 40 | "check-operator", 41 | "check-module", 42 | "check-separator", 43 | "check-type" 44 | ], 45 | "typedef-whitespace": [ 46 | true, 47 | { 48 | "call-signature": "nospace", 49 | "index-signature": "nospace", 50 | "parameter": "nospace", 51 | "property-declaration": "nospace", 52 | "variable-declaration": "nospace" 53 | }, 54 | { 55 | "call-signature": "onespace", 56 | "index-signature": "onespace", 57 | "parameter": "onespace", 58 | "property-declaration": "onespace", 59 | "variable-declaration": "onespace" 60 | } 61 | ], 62 | "no-internal-module": true, 63 | "no-trailing-whitespace": true, 64 | "no-null-keyword": true, 65 | "prefer-const": true, 66 | "triple-equals": true 67 | } 68 | } -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | const commonConfig = { 4 | devtool: "source-map", 5 | mode: "development", 6 | module: { 7 | rules: [ 8 | { 9 | exclude: /node_modules/, 10 | test: /\.tsx?$/, 11 | use: "ts-loader", 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: [".tsx", ".ts", ".js"], 17 | }, 18 | }; 19 | 20 | module.exports = [ 21 | { 22 | ...commonConfig, 23 | entry: { 24 | host: "./src/host/mainHost.ts", 25 | messaging: "./src/host/mainMessaging.ts", 26 | }, 27 | name: "host", 28 | output: { 29 | filename: "[name].bundle.js", 30 | path: path.resolve(__dirname, "out/host"), 31 | }, 32 | }, 33 | { 34 | ...commonConfig, 35 | entry: { 36 | extension: "./src/extension.ts", 37 | }, 38 | externals: { 39 | vscode: "commonjs vscode", 40 | }, 41 | name: "extension", 42 | output: { 43 | devtoolModuleFilenameTemplate: "../[resource-path]", 44 | filename: "[name].js", 45 | libraryTarget: "commonjs2", 46 | path: path.resolve(__dirname, "out"), 47 | }, 48 | stats: "errors-only", // Bug ws package includes dev-dependencies which webpack will report as warnings 49 | target: "node", 50 | }, 51 | ]; 52 | --------------------------------------------------------------------------------