|
1 | | -// Copyright (c) Microsoft Corporation. All rights reserved. |
2 | | -// Licensed under the MIT License. |
3 | | - |
4 | | -import * as vscode from 'vscode'; |
5 | | -import TelemetryReporter from '@vscode/extension-telemetry'; |
6 | | -import * as path from 'path'; |
7 | | -import * as fs from 'fs'; |
8 | | -import { CDPTarget } from './cdpTarget'; |
9 | | -import { fixRemoteWebSocket, getListOfTargets, getRemoteEndpointSettings, IRemoteTargetJson, isLocalResource, SETTINGS_STORE_NAME } from './utils'; |
10 | | -import { IncomingMessage } from 'http'; |
11 | | -import * as https from 'https'; |
12 | | -import { LaunchConfigManager } from './launchConfigManager'; |
13 | | - |
14 | | -export class CDPTargetsProvider implements vscode.TreeDataProvider<CDPTarget> { |
15 | | - readonly onDidChangeTreeData: vscode.Event<CDPTarget | null>; |
16 | | - readonly changeDataEvent: vscode.EventEmitter<CDPTarget | null>; |
17 | | - private extensionPath: string; |
18 | | - private telemetryReporter: Readonly<TelemetryReporter>; |
19 | | - |
20 | | - constructor(context: vscode.ExtensionContext, telemetryReporter: Readonly<TelemetryReporter>) { |
21 | | - this.changeDataEvent = new vscode.EventEmitter<CDPTarget | null>(); |
22 | | - this.onDidChangeTreeData = this.changeDataEvent.event; |
23 | | - this.extensionPath = context.extensionPath; |
24 | | - this.telemetryReporter = telemetryReporter; |
25 | | - } |
26 | | - |
27 | | - getTreeItem(element: CDPTarget): vscode.TreeItem { |
28 | | - return element; |
29 | | - } |
30 | | - |
31 | | - async getChildren(element?: CDPTarget): Promise<CDPTarget[]> { |
32 | | - let targets: CDPTarget[] = []; |
33 | | - |
34 | | - const willShowWorkers = vscode.workspace.getConfiguration(SETTINGS_STORE_NAME).get('showWorkers'); |
35 | | - |
36 | | - if (!element) { |
37 | | - // Get a list of the targets available |
38 | | - const { hostname, port, useHttps } = getRemoteEndpointSettings(); |
39 | | - const responseArray = await getListOfTargets(hostname, port, useHttps); |
40 | | - if (Array.isArray(responseArray)) { |
41 | | - await this.clearFaviconResourceDirectory(); |
42 | | - if (responseArray.length > 0) { |
43 | | - const responseIconPromiseArray: Array<Promise<IRemoteTargetJson>> = []; |
44 | | - responseArray.forEach((target: IRemoteTargetJson) => { |
45 | | - const actualTarget = fixRemoteWebSocket(hostname, port, target); |
46 | | - if (actualTarget.type === 'page' || actualTarget.type === 'iframe') { |
47 | | - responseIconPromiseArray.push(this.downloadFaviconFromSitePromise(actualTarget)); |
48 | | - } else if ((actualTarget.type !== 'service_worker' && actualTarget.type !== 'shared_worker') || willShowWorkers) { |
49 | | - targets.push(new CDPTarget(actualTarget, '', this.extensionPath)); |
50 | | - } |
51 | | - }); |
52 | | - |
53 | | - const iconResultsArray = await Promise.all(responseIconPromiseArray); |
54 | | - for (const actualTarget of iconResultsArray) { |
55 | | - if (isLocalResource(actualTarget.faviconUrl)) { |
56 | | - targets.push(new CDPTarget(actualTarget, '', this.extensionPath, actualTarget.faviconUrl)); |
57 | | - } else { |
58 | | - targets.push(new CDPTarget(actualTarget, '', this.extensionPath)); |
59 | | - } |
60 | | - } |
61 | | - } |
62 | | - } else { |
63 | | - this.telemetryReporter.sendTelemetryEvent('view/error/no_json_array'); |
64 | | - } |
65 | | - |
66 | | - // Sort the targets by type and then title, but keep 'page' types at the top |
67 | | - // since those are the ones most likely to be the ones the user wants. |
68 | | - targets.sort((a: CDPTarget, b: CDPTarget) => { |
69 | | - if (a.targetJson.type === b.targetJson.type) { |
70 | | - return a.targetJson.title < b.targetJson.title ? -1 : 1; |
71 | | - } if (a.targetJson.type === 'page') { |
72 | | - return -1; |
73 | | - } if (b.targetJson.type === 'page') { |
74 | | - return 1; |
75 | | - } |
76 | | - return a.targetJson.type < b.targetJson.type ? -1 : 1; |
77 | | - |
78 | | - }); |
79 | | - } else { |
80 | | - // Just expand the element to show its properties |
81 | | - targets = element.getChildren(); |
82 | | - } |
83 | | - |
84 | | - return targets; |
85 | | - } |
86 | | - |
87 | | - refresh(): void { |
88 | | - this.changeDataEvent.fire(null); |
89 | | - void this.clearFaviconResourceDirectory(); |
90 | | - LaunchConfigManager.instance.updateLaunchConfig(); |
91 | | - } |
92 | | - |
93 | | - async clearFaviconResourceDirectory(): Promise<void> { |
94 | | - const directory = path.join(this.extensionPath, 'resources', 'favicons'); |
95 | | - const files = await fs.promises.readdir(directory); |
96 | | - for (const file of files) { |
97 | | - const fileString = file.toString(); |
98 | | - if (fileString !== '.gitkeep') { |
99 | | - await fs.promises.unlink(path.join(directory, fileString)); |
100 | | - } |
101 | | - } |
102 | | - } |
103 | | - |
104 | | - downloadFaviconFromSitePromise(actualTarget: IRemoteTargetJson) : Promise<IRemoteTargetJson> { |
105 | | - if (!actualTarget.url || !actualTarget.url.startsWith('https')) { |
106 | | - return Promise.resolve(actualTarget); |
107 | | - } |
108 | | - const faviconRegex = /((?:\/\/|\.)([^.]*)\.[^.^/]+\/).*/; |
109 | | - |
110 | | - // Example regex match: https://learn.microsoft.com/en-us/microsoft-edge/ |
111 | | - // urlMatch[0] = .microsoft.com/en-us/microsoft-edge/ |
112 | | - // urlMatch[1] = .microsoft.com/ |
113 | | - // urlMatch[2] = microsoft |
114 | | - const urlMatch = faviconRegex.exec(actualTarget.url); |
115 | | - let filename; |
116 | | - if (urlMatch) { |
117 | | - filename = `${urlMatch[2]}Favicon.ico`; |
118 | | - } else { |
119 | | - return Promise.resolve(actualTarget); |
120 | | - } |
121 | | - |
122 | | - // Replacing ".microsoft.com/en-us/microsoft-edge/" with ".microsoft.com/favicon.ico" |
123 | | - const faviconUrl = actualTarget.url.replace(faviconRegex, '$1favicon.ico'); |
124 | | - |
125 | | - const filePath = path.join(this.extensionPath, 'resources', 'favicons', filename); |
126 | | - |
127 | | - const promise = new Promise<IRemoteTargetJson>((resolve, reject) => { |
128 | | - https.get(faviconUrl, (response: IncomingMessage) => { |
129 | | - if (response.headers['content-type'] && response.headers['content-type'].includes('icon')) { |
130 | | - const buffer: Uint8Array[] = []; |
131 | | - response.on('data', (data: Uint8Array) => { |
132 | | - buffer.push(data); |
133 | | - }); |
134 | | - |
135 | | - // eslint-disable-next-line @typescript-eslint/no-misused-promises |
136 | | - response.on('end', async () => { |
137 | | - try { |
138 | | - await fs.promises.writeFile(filePath, Buffer.concat(buffer)); |
139 | | - actualTarget.faviconUrl = filePath; |
140 | | - resolve(actualTarget); |
141 | | - } catch { |
142 | | - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors |
143 | | - reject(actualTarget); |
144 | | - } |
145 | | - }); |
146 | | - } else { |
147 | | - resolve(actualTarget); |
148 | | - } |
149 | | - }); |
150 | | - }); |
151 | | - |
152 | | - const timeout = new Promise<IRemoteTargetJson>(resolve => { |
153 | | - const id = setTimeout(() => { |
154 | | - clearTimeout(id); |
155 | | - resolve(actualTarget); |
156 | | - }, 1000); |
157 | | - }); |
158 | | - |
159 | | - // If it takes over a second to download, we will resolve null and use default icons. |
160 | | - return Promise.race([promise, timeout]); |
161 | | - } |
162 | | -} |
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +import * as vscode from 'vscode'; |
| 5 | +import TelemetryReporter from '@vscode/extension-telemetry'; |
| 6 | +import * as path from 'path'; |
| 7 | +import * as fs from 'fs'; |
| 8 | +import { CDPTarget } from './cdpTarget'; |
| 9 | +import { fixRemoteWebSocket, getListOfTargets, getRemoteEndpointSettings, IRemoteTargetJson, isLocalResource, SETTINGS_STORE_NAME } from './utils'; |
| 10 | +import { IncomingMessage } from 'http'; |
| 11 | +import * as https from 'https'; |
| 12 | +import { LaunchConfigManager } from './launchConfigManager'; |
| 13 | + |
| 14 | +export class CDPTargetsProvider implements vscode.TreeDataProvider<CDPTarget> { |
| 15 | + readonly onDidChangeTreeData: vscode.Event<CDPTarget | null>; |
| 16 | + readonly changeDataEvent: vscode.EventEmitter<CDPTarget | null>; |
| 17 | + private extensionPath: string; |
| 18 | + private telemetryReporter: Readonly<TelemetryReporter>; |
| 19 | + |
| 20 | + constructor(context: vscode.ExtensionContext, telemetryReporter: Readonly<TelemetryReporter>) { |
| 21 | + this.changeDataEvent = new vscode.EventEmitter<CDPTarget | null>(); |
| 22 | + this.onDidChangeTreeData = this.changeDataEvent.event; |
| 23 | + this.extensionPath = context.extensionPath; |
| 24 | + this.telemetryReporter = telemetryReporter; |
| 25 | + } |
| 26 | + |
| 27 | + getTreeItem(element: CDPTarget): vscode.TreeItem { |
| 28 | + return element; |
| 29 | + } |
| 30 | + |
| 31 | + async getChildren(element?: CDPTarget): Promise<CDPTarget[]> { |
| 32 | + let targets: CDPTarget[] = []; |
| 33 | + |
| 34 | + const willShowWorkers = vscode.workspace.getConfiguration(SETTINGS_STORE_NAME).get('showWorkers'); |
| 35 | + |
| 36 | + if (!element) { |
| 37 | + // Get a list of the targets available |
| 38 | + const { hostname, port, useHttps } = getRemoteEndpointSettings(); |
| 39 | + const responseArray = await getListOfTargets(hostname, port, useHttps); |
| 40 | + if (Array.isArray(responseArray)) { |
| 41 | + await this.clearFaviconResourceDirectory(); |
| 42 | + if (responseArray.length > 0) { |
| 43 | + const responseIconPromiseArray: Array<Promise<IRemoteTargetJson>> = []; |
| 44 | + responseArray.forEach((target: IRemoteTargetJson) => { |
| 45 | + const actualTarget = fixRemoteWebSocket(hostname, port, target); |
| 46 | + if (actualTarget.type === 'page' || actualTarget.type === 'iframe') { |
| 47 | + responseIconPromiseArray.push(this.downloadFaviconFromSitePromise(actualTarget)); |
| 48 | + } else if ((actualTarget.type !== 'service_worker' && actualTarget.type !== 'shared_worker') || willShowWorkers) { |
| 49 | + targets.push(new CDPTarget(actualTarget, '', this.extensionPath)); |
| 50 | + } |
| 51 | + }); |
| 52 | + |
| 53 | + const iconResultsArray = await Promise.all(responseIconPromiseArray); |
| 54 | + for (const actualTarget of iconResultsArray) { |
| 55 | + if (isLocalResource(actualTarget.faviconUrl)) { |
| 56 | + targets.push(new CDPTarget(actualTarget, '', this.extensionPath, actualTarget.faviconUrl)); |
| 57 | + } else { |
| 58 | + targets.push(new CDPTarget(actualTarget, '', this.extensionPath)); |
| 59 | + } |
| 60 | + } |
| 61 | + } |
| 62 | + } else { |
| 63 | + this.telemetryReporter.sendTelemetryEvent('view/error/no_json_array'); |
| 64 | + } |
| 65 | + |
| 66 | + // Sort the targets by type and then title, but keep 'page' types at the top |
| 67 | + // since those are the ones most likely to be the ones the user wants. |
| 68 | + targets.sort((a: CDPTarget, b: CDPTarget) => { |
| 69 | + if (a.targetJson.type === b.targetJson.type) { |
| 70 | + return a.targetJson.title < b.targetJson.title ? -1 : 1; |
| 71 | + } if (a.targetJson.type === 'page') { |
| 72 | + return -1; |
| 73 | + } if (b.targetJson.type === 'page') { |
| 74 | + return 1; |
| 75 | + } |
| 76 | + return a.targetJson.type < b.targetJson.type ? -1 : 1; |
| 77 | + |
| 78 | + }); |
| 79 | + } else { |
| 80 | + // Just expand the element to show its properties |
| 81 | + targets = element.getChildren(); |
| 82 | + } |
| 83 | + |
| 84 | + return targets; |
| 85 | + } |
| 86 | + |
| 87 | + refresh(): void { |
| 88 | + this.changeDataEvent.fire(null); |
| 89 | + void this.clearFaviconResourceDirectory(); |
| 90 | + LaunchConfigManager.instance.updateLaunchConfig(); |
| 91 | + } |
| 92 | + |
| 93 | + async clearFaviconResourceDirectory(): Promise<void> { |
| 94 | + const directory = path.join(this.extensionPath, 'resources', 'favicons'); |
| 95 | + const files = await fs.promises.readdir(directory); |
| 96 | + for (const file of files) { |
| 97 | + const fileString = file.toString(); |
| 98 | + if (fileString !== '.gitkeep') { |
| 99 | + await fs.promises.unlink(path.join(directory, fileString)); |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + downloadFaviconFromSitePromise(actualTarget: IRemoteTargetJson) : Promise<IRemoteTargetJson> { |
| 105 | + if (!actualTarget.url || !actualTarget.url.startsWith('https')) { |
| 106 | + return Promise.resolve(actualTarget); |
| 107 | + } |
| 108 | + const faviconRegex = /((?:\/\/|\.)([^\.]*)\.[^\.^\/]+\/).*/; |
| 109 | + |
| 110 | + // Example regex match: https://learn.microsoft.com/en-us/microsoft-edge/ |
| 111 | + // urlMatch[0] = .microsoft.com/en-us/microsoft-edge/ |
| 112 | + // urlMatch[1] = .microsoft.com/ |
| 113 | + // urlMatch[2] = microsoft |
| 114 | + const urlMatch = faviconRegex.exec(actualTarget.url); |
| 115 | + let filename; |
| 116 | + if (urlMatch) { |
| 117 | + filename = `${urlMatch[2]}Favicon.ico`; |
| 118 | + } else { |
| 119 | + return Promise.resolve(actualTarget); |
| 120 | + } |
| 121 | + |
| 122 | + // Replacing ".microsoft.com/en-us/microsoft-edge/" with ".microsoft.com/favicon.ico" |
| 123 | + const faviconUrl = actualTarget.url.replace(faviconRegex, '$1favicon.ico'); |
| 124 | + |
| 125 | + const filePath = path.join(this.extensionPath, 'resources', 'favicons', filename); |
| 126 | + |
| 127 | + const promise = new Promise<IRemoteTargetJson>((resolve, reject) => { |
| 128 | + https.get(faviconUrl, (response: IncomingMessage) => { |
| 129 | + if (response.headers['content-type'] && response.headers['content-type'].includes('icon')) { |
| 130 | + const buffer: Uint8Array[] = []; |
| 131 | + response.on('data', (data: Uint8Array) => { |
| 132 | + buffer.push(data); |
| 133 | + }); |
| 134 | + |
| 135 | + // eslint-disable-next-line @typescript-eslint/no-misused-promises |
| 136 | + response.on('end', async () => { |
| 137 | + try { |
| 138 | + await fs.promises.writeFile(filePath, Buffer.concat(buffer)); |
| 139 | + actualTarget.faviconUrl = filePath; |
| 140 | + resolve(actualTarget); |
| 141 | + } catch { |
| 142 | + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors |
| 143 | + reject(actualTarget); |
| 144 | + } |
| 145 | + }); |
| 146 | + } else { |
| 147 | + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors |
| 148 | + resolve(actualTarget); |
| 149 | + } |
| 150 | + }); |
| 151 | + }); |
| 152 | + |
| 153 | + const timeout = new Promise<IRemoteTargetJson>(resolve => { |
| 154 | + const id = setTimeout(() => { |
| 155 | + clearTimeout(id); |
| 156 | + resolve(actualTarget); |
| 157 | + }, 1000); |
| 158 | + }); |
| 159 | + |
| 160 | + // If it takes over a second to download, we will resolve null and use default icons. |
| 161 | + return Promise.race([promise, timeout]); |
| 162 | + } |
| 163 | +} |
0 commit comments