src/vs/workbench/api/node/extHostTunnelService.ts

419 LOC · 225 covered · 194 uncovered · 33 ranges · 7 concepts · 7 introducers · 6 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/platform/remote/common/managedSocket.ts · 145 LOCcommon/managedSocket.tssrc/vs/platform/remote/common/remoteAgentConnection.ts · 839 LOCcommon/remoteAgentConnec…src/vs/platform/remote/common/remoteSocketFactoryService.ts · 60 LOCcommon/remoteSocketFacto…src/vs/platform/sign/common/sign.ts · 22 LOCcommon/sign.tssrc/vs/platform/tunnel/node/tunnelService.ts · 265 LOCnode/tunnelService.tssrc/vs/workbench/services/remote/common/tunnelModel.ts · 1042 LOCcommon/tunnelModel.tsextHostTunnelService.ts ×3 · 22 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×2 · 21 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×4 · 38 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×2 · 23 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×1 · 9 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×1 · 17 introduced LOCextHostTunnelService.ts …remoteAgentConnection.ts ×53 · 823 introduced LOCremoteAgentConnection.ts…extHostTunnelService.test|title=ExtHostTunnelService findPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService findPorts|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService getSockets|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService getSockets|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService loadConnectionTable|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService loadConnectionTable|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService loadListeningPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService loadListeningPorts|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService parseIpAddress|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService parseIpAddress|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService tryFindRootPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService tryFindRootPorts|occurrence=1extHostTunnelService.tes…Focused file · src/vs/workbench/api/node/extHostTunnelService.ts · 419 LOCnode/extHostTunnelServic…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- remoteAgentConnection.ts ×53
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import * as fs from 'fs';
7 > import { exec } from 'child_process';
8 > import { VSBuffer } from '../../../base/common/buffer.js';
9 > import { Emitter } from '../../../base/common/event.js';
10 > import { DisposableStore } from '../../../base/common/lifecycle.js';
11 > import { MovingAverage } from '../../../base/common/numbers.js';
12 > import { isLinux } from '../../../base/common/platform.js';
13 > import * as resources from '../../../base/common/resources.js';
14 > import { URI } from '../../../base/common/uri.js';
15 > import * as pfs from '../../../base/node/pfs.js';
16 > import { ISocket, SocketCloseEventType } from '../../../base/parts/ipc/common/ipc.net.js';
17 > import { ILogService } from '../../../platform/log/common/log.js';
18 > import { ManagedSocket, RemoteSocketHalf, connectManagedSocket } from '../../../platform/remote/common/managedSocket.js';
19 > import { ManagedRemoteConnection } from '../../../platform/remote/common/remoteAuthorityResolver.js';
20 > import { ISignService } from '../../../platform/sign/common/sign.js';
21 > import { isAllInterfaces, isLocalhost } from '../../../platform/tunnel/common/tunnel.js';
22 > import { NodeRemoteTunnel } from '../../../platform/tunnel/node/tunnelService.js';
23 > import { IExtHostInitDataService } from '../common/extHostInitDataService.js';
24 > import { IExtHostRpcService } from '../common/extHostRpcService.js';
25 > import { ExtHostTunnelService } from '../common/extHostTunnelService.js';
26 > import { CandidatePort, parseAddress } from '../../services/remote/common/tunnelModel.js';
27 > import * as vscode from 'vscode';
28 > import { IExtHostConfiguration } from '../common/extHostConfiguration.js';
29 >
30 > export function getSockets(stdout: string): Record<string, { pid: number; socket: number }> {
31 > const lines = stdout.trim().split('\n'); extHostTunnelService.ts ×1
32 > const mapped: { pid: number; socket: number }[] = [];
33 > lines.forEach(line => {
34 > const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!;
35 > if (match && match.length >= 3) {
36 > mapped.push({
37 > pid: parseInt(match[1], 10),
38 > socket: parseInt(match[2], 10)
39 > });
40 > }
41 > });
42 > const socketMap = mapped.reduce((m: Record<string, typeof mapped[0]>, socket) => {
43 > m[socket.socket] = socket;
44 > return m;
45 > }, {});
46 > return socketMap;
47 > }
49 > export function loadListeningPorts(...stdouts: string[]): { socket: number; ip: string; port: number }[] {
50 > const table = ([] as Record<string, string>[]).concat(...stdouts.map(loadConnectionTable)); extHostTunnelService.ts ×2
51 > return [
52 > ...new Map(
53 > table.filter(row => row.st === '0A')
54 > .map(row => {
55 > const address = row.local_address.split(':');
56 > return {
57 > socket: parseInt(row.inode, 10),
58 > ip: parseIpAddress(address[0]),
59 > port: parseInt(address[1], 16)
60 > };
61 > }).map(port => [port.ip + ':' + port.port, port])
62 > ).values()
63 > ];
64 > }
66 > export function parseIpAddress(hex: string): string {
67 > let result = ''; extHostTunnelService.ts ×2
68 > if (hex.length === 8) {
69 > for (let i = hex.length - 2; i >= 0; i -= 2) { extHostTunnelService.ts ×2
70 > result += parseInt(hex.substr(i, 2), 16);
71 > if (i !== 0) {
72 > result += '.';
73 > }
74 > }
76 > // Nice explanation of host format in tcp6 file: https://serverfault.com/questions/592574/why-does-proc-net-tcp6-represents-1-as-1000
77 > for (let i = 0; i < hex.length; i += 8) {
78 > const word = hex.substring(i, i + 8);
79 > let subWord = '';
80 > for (let j = 8; j >= 2; j -= 2) {
81 > subWord += word.substring(j - 2, j);
82 > if ((j === 6) || (j === 2)) {
83 > // Trim leading zeros
84 > subWord = parseInt(subWord, 16).toString(16);
85 > result += `${subWord}`;
86 > subWord = '';
87 > if (i + j !== hex.length - 6) {
88 > result += ':';
89 > }
90 > }
91 > }
92 > }
93 > }
94 > return result;
95 > }
97 > export function loadConnectionTable(stdout: string): Record<string, string>[] {
98 > const lines = stdout.trim().split('\n'); extHostTunnelService.ts ×1
99 > const names = lines.shift()!.trim().split(/\s+/)
100 > .filter(name => name !== 'rx_queue' && name !== 'tm->when');
101 > const table = lines.map(line => line.trim().split(/\s+/).reduce((obj: Record<string, string>, value, i) => {
102 > obj[names[i] || i] = value;
103 > return obj;
104 > }, {}));
105 > return table;
106 > }
108 > function knownExcludeCmdline(command: string): boolean { extHostTunnelService.ts ×3
109 > if (command.length > 500) {
110 return false;
111 }
112 > return !!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) extHostTunnelService.ts ×3
113 > || (command.indexOf('out/server-main.js') !== -1)
114 > || (command.indexOf('_productName=VSCode') !== -1);
115 > }
117 > export function getRootProcesses(stdout: string) {
118 > const lines = stdout.trim().split('\n'); extHostTunnelService.ts ×4
119 > const mapped: { pid: number; cmd: string; ppid: number }[] = [];
120 > lines.forEach(line => {
121 > const match = /^\d+\s+\D+\s+root\s+(\d+)\s+(\d+).+\d+\:\d+\:\d+\s+(.+)$/.exec(line)!;
122 > if (match && match.length >= 4) {
123 > mapped.push({
124 > pid: parseInt(match[1], 10),
125 > ppid: parseInt(match[2]),
126 > cmd: match[3]
127 > });
128 > }
129 > });
130 > return mapped;
131 > }
133 > export async function findPorts(connections: { socket: number; ip: string; port: number }[], socketMap: Record<string, { pid: number; socket: number }>, processes: { pid: number; cwd: string; cmd: string }[]): Promise<CandidatePort[]> { extHostTunnelService.ts ×3
134 > const processMap = processes.reduce((m: Record<string, typeof processes[0]>, process) => {
135 > m[process.pid] = process;
136 > return m;
137 > }, {});
138 >
139 > const ports: CandidatePort[] = [];
140 > connections.forEach(({ socket, ip, port }) => {
141 > const pid = socketMap[socket] ? socketMap[socket].pid : undefined;
142 > const command: string | undefined = pid ? processMap[pid]?.cmd : undefined;
143 > if (pid && command && !knownExcludeCmdline(command)) {
144 > ports.push({ host: ip, port, detail: command, pid });
145 > }
146 > });
147 > return ports;
148 > }
150 > export function tryFindRootPorts(connections: { socket: number; ip: string; port: number }[], rootProcessesStdout: string, previousPorts: Map<number, CandidatePort & { ppid: number }>): Map<number, CandidatePort & { ppid: number }> {
151 > const ports: Map<number, CandidatePort & { ppid: number }> = new Map(); extHostTunnelService.ts ×4
152 > const rootProcesses = getRootProcesses(rootProcessesStdout);
153 >
154 > for (const connection of connections) {
155 > const previousPort = previousPorts.get(connection.port);
156 > if (previousPort) {
157 ports.set(connection.port, previousPort);
158 continue;
159 }
160 > const rootProcessMatch = rootProcesses.find((value) => value.cmd.includes(`${connection.port}`)); extHostTunnelService.ts ×4
161 > if (rootProcessMatch) {
162 > let bestMatch = rootProcessMatch;
163 > // There are often several processes that "look" like they could match the port.
164 > // The one we want is usually the child of the other. Find the most child process.
165 > let mostChild: { pid: number; cmd: string; ppid: number } | undefined;
166 > do {
167 > mostChild = rootProcesses.find(value => value.ppid === bestMatch.pid);
168 > if (mostChild) {
169 > bestMatch = mostChild;
170 > }
171 > } while (mostChild);
172 > ports.set(connection.port, { host: connection.ip, port: connection.port, pid: bestMatch.pid, detail: bestMatch.cmd, ppid: bestMatch.ppid });
173 > } else {
174 ports.set(connection.port, { host: connection.ip, port: connection.port, ppid: Number.MAX_VALUE });
175 }
177 >
178 > return ports;
179 > }
181 > export class NodeExtHostTunnelService extends ExtHostTunnelService {
182 > private _initialCandidates: CandidatePort[] | undefined = undefined;
183 > private _foundRootPorts: Map<number, CandidatePort & { ppid: number }> = new Map();
184 > private _candidateFindingEnabled: boolean = false;
185 >
186 > constructor(
187 @IExtHostRpcService extHostRpc: IExtHostRpcService,
188 @IExtHostInitDataService private readonly initData: IExtHostInitDataService,
189 @ILogService logService: ILogService,
190 @ISignService private readonly signService: ISignService,
191 @IExtHostConfiguration private readonly configurationService: IExtHostConfiguration,
192 ) {
193 super(extHostRpc, initData, logService);
194 if (isLinux && initData.remote.isRemote && initData.remote.authority) {
195 this._proxy.$setRemoteTunnelService(process.pid);
196 this.setInitialCandidates();
197 }
198 }
200 > override async $registerCandidateFinder(enable: boolean): Promise<void> {
201 if (enable && this._candidateFindingEnabled) {
202 // already enabled
203 return;
204 }
205
206 this._candidateFindingEnabled = enable;
207 let oldPorts: { host: string; port: number; detail?: string }[] | undefined = undefined;
208
209 // If we already have found initial candidates send those immediately.
210 if (this._initialCandidates) {
211 oldPorts = this._initialCandidates;
212 await this._proxy.$onFoundNewCandidates(this._initialCandidates);
213 }
214
215 // Regularly scan to see if the candidate ports have changed.
216 const movingAverage = new MovingAverage();
217 let scanCount = 0;
218 while (this._candidateFindingEnabled) {
219 const startTime = new Date().getTime();
220 const newPorts = (await this.findCandidatePorts()).filter(candidate => (isLocalhost(candidate.host) || isAllInterfaces(candidate.host)));
221 this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) found candidate ports ${newPorts.map(port => port.port).join(', ')}`);
222 const timeTaken = new Date().getTime() - startTime;
223 this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) candidate port scan took ${timeTaken} ms.`);
224 // Do not count the first few scans towards the moving average as they are likely to be slower.
225 if (scanCount++ > 3) {
226 movingAverage.update(timeTaken);
227 }
228 if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) {
229 oldPorts = newPorts;
230 await this._proxy.$onFoundNewCandidates(oldPorts);
231 }
232 const delay = this.calculateDelay(movingAverage.value);
233 this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) next candidate port scan in ${delay} ms.`);
234 await (new Promise<void>(resolve => setTimeout(() => resolve(), delay)));
235 }
236 }
238 > private calculateDelay(movingAverage: number) {
239 // Some local testing indicated that the moving average might be between 50-100 ms.
240 return Math.max(movingAverage * 20, 2000);
241 }
243 > private async setInitialCandidates(): Promise<void> {
244 this._initialCandidates = await this.findCandidatePorts();
245 this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) Initial candidates found: ${this._initialCandidates.map(c => c.port).join(', ')}`);
246 }
248 > private async findCandidatePorts(): Promise<CandidatePort[]> {
249 let tcp: string = '';
250 let tcp6: string = '';
251 try {
252 tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8');
253 tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8');
254 } catch (e) {
255 // File reading error. No additional handling needed.
256 }
257 const connections: { socket: number; ip: string; port: number }[] = loadListeningPorts(tcp, tcp6);
258
259 const procSockets: string = await (new Promise(resolve => {
260 exec('ls -l /proc/[0-9]*/fd/[0-9]* | grep socket:', (error, stdout, stderr) => {
261 resolve(stdout);
262 });
263 }));
264 const socketMap = getSockets(procSockets);
265
266 const procChildren = await pfs.Promises.readdir('/proc');
267 const processes: {
268 pid: number; cwd: string; cmd: string;
269 }[] = [];
270 for (const childName of procChildren) {
271 try {
272 const pid: number = Number(childName);
273 const childUri = resources.joinPath(URI.file('/proc'), childName);
274 const childStat = await fs.promises.stat(childUri.fsPath);
275 if (childStat.isDirectory() && !isNaN(pid)) {
276 const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath);
277 const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8');
278 processes.push({ pid, cwd, cmd });
279 }
280 } catch (e) {
281 //
282 }
283 }
284
285 const unFoundConnections: { socket: number; ip: string; port: number }[] = [];
286 const filteredConnections = connections.filter((connection => {
287 const foundConnection = socketMap[connection.socket];
288 if (!foundConnection) {
289 unFoundConnections.push(connection);
290 }
291 return foundConnection;
292 }));
293
294 const foundPorts = findPorts(filteredConnections, socketMap, processes);
295 let heuristicPorts: CandidatePort[] | undefined;
296 this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) number of possible root ports ${unFoundConnections.length}`);
297 if (unFoundConnections.length > 0) {
298 const rootProcesses: string = await (new Promise(resolve => {
299 exec('ps -F -A -l | grep root', (error, stdout, stderr) => {
300 resolve(stdout);
301 });
302 }));
303 this._foundRootPorts = tryFindRootPorts(unFoundConnections, rootProcesses, this._foundRootPorts);
304 heuristicPorts = Array.from(this._foundRootPorts.values());
305 this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) heuristic ports ${heuristicPorts.map(heuristicPort => heuristicPort.port).join(', ')}`);
306
307 }
308 return foundPorts.then(foundCandidates => {
309 if (heuristicPorts) {
310 return foundCandidates.concat(heuristicPorts);
311 } else {
312 return foundCandidates;
313 }
314 });
315 }
317 > private async defaultTunnelHost(): Promise<string> {
318 const settingValue = (await this.configurationService.getConfigProvider()).getConfiguration('remote').get('localPortHost');
319 return (!settingValue || settingValue === 'localhost') ? '127.0.0.1' : '0.0.0.0';
320 }
322 > protected override makeManagedTunnelFactory(authority: vscode.ManagedResolvedAuthority): vscode.RemoteAuthorityResolver['tunnelFactory'] {
323 return async (tunnelOptions) => {
324 const t = new NodeRemoteTunnel(
325 {
326 commit: this.initData.commit,
327 quality: this.initData.quality,
328 logService: this.logService,
329 ipcLogger: null,
330 // services and address providers have stubs since we don't need
331 // the connection identification that the renderer process uses
332 remoteSocketFactoryService: {
333 _serviceBrand: undefined,
334 async connect(_connectTo: ManagedRemoteConnection, path: string, query: string, debugLabel: string): Promise<ISocket> {
335 const result = await authority.makeConnection();
336 return ExtHostManagedSocket.connect(result, path, query, debugLabel);
337 },
338 register() {
339 throw new Error('not implemented');
340 },
341 },
342 addressProvider: {
343 getAddress() {
344 return Promise.resolve({
345 connectTo: new ManagedRemoteConnection(0),
346 connectionToken: authority.connectionToken,
347 });
348 },
349 },
350 signService: this.signService,
351 },
352 await this.defaultTunnelHost(),
353 tunnelOptions.remoteAddress.host || 'localhost',
354 tunnelOptions.remoteAddress.port,
355 tunnelOptions.localAddressPort,
356 );
357
358 await t.waitForReady();
359
360 const disposeEmitter = new Emitter<void>();
361
362 return {
363 localAddress: parseAddress(t.localAddress) ?? t.localAddress,
364 remoteAddress: { port: t.tunnelRemotePort, host: t.tunnelRemoteHost },
365 onDidDispose: disposeEmitter.event,
366 dispose: () => {
367 t.dispose();
368 disposeEmitter.fire();
369 disposeEmitter.dispose();
370 },
371 };
372 };
373 }
375 >
376 > export class ExtHostManagedSocket extends ManagedSocket {
377 > public static connect(
378 > passing: vscode.ManagedMessagePassing,
379 > path: string, query: string, debugLabel: string,
380 > ): Promise<ExtHostManagedSocket> {
381 > const d = new DisposableStore();
382 > const half: RemoteSocketHalf = {
383 > onClose: d.add(new Emitter()),
384 > onData: d.add(new Emitter()),
385 > onEnd: d.add(new Emitter()),
386 > };
387 >
388 > d.add(passing.onDidReceiveMessage(d => half.onData.fire(VSBuffer.wrap(d))));
389 > d.add(passing.onDidEnd(() => half.onEnd.fire()));
390 > d.add(passing.onDidClose(error => half.onClose.fire({
391 > type: SocketCloseEventType.NodeSocketCloseEvent,
392 > error,
393 > hadError: !!error
394 > })));
395 >
396 > const socket = new ExtHostManagedSocket(passing, debugLabel, half);
397 > socket._register(d);
398 > return connectManagedSocket(socket, path, query, debugLabel, half);
399 > }
400 >
401 > constructor(
402 private readonly passing: vscode.ManagedMessagePassing,
403 debugLabel: string,
404 half: RemoteSocketHalf,
405 ) {
406 super(debugLabel, half);
407 }
409 > public override write(buffer: VSBuffer): void {
410 this.passing.send(buffer.buffer);
411 }
412 > protected override closeRemote(): void { remoteAgentConnection.ts ×53
413 this.passing.end();
414 }
416 > public override async drain(): Promise<void> {
417 await this.passing.drain?.();
418 }