├── .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 |
24 |
25 |
26 |
27 | ## Attaching to a running chrome instance:
28 | 
29 |
30 | ## Launching a 'debugger for chrome' project and using screencast:
31 | 
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 |
--------------------------------------------------------------------------------