From e1bfea5b63c81291d8c552a8b42500e66338ae61 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 12 Dec 2019 18:03:04 +0100 Subject: [PATCH] Add candidate finding to ports view Part of #81388 --- .../api/browser/mainThreadTunnelService.ts | 5 +- .../workbench/api/common/extHost.protocol.ts | 3 +- .../api/common/extHostTunnelService.ts | 40 +---- src/vs/workbench/api/node/extHost.services.ts | 3 +- .../api/node/extHostTunnelService.ts | 149 ++++++++++++++++++ .../contrib/remote/browser/tunnelView.ts | 35 ++-- .../extensions/worker/extHost.services.ts | 2 - .../remote/common/remoteExplorerService.ts | 32 ++-- 8 files changed, 198 insertions(+), 71 deletions(-) create mode 100644 src/vs/workbench/api/node/extHostTunnelService.ts diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index afab749949551..1e3204481079f 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -10,7 +10,6 @@ import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remo @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService implements MainThreadTunnelServiceShape { - // @ts-ignore private readonly _proxy: ExtHostTunnelServiceShape; constructor( @@ -36,6 +35,10 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { return Promise.resolve(this.remoteExplorerService.addDetected(tunnels)); } + async $registerCandidateFinder(): Promise { + this.remoteExplorerService.registerCandidateFinder(() => this._proxy.$findCandidatePorts()); + } + dispose(): void { // } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f37e0e96236fe..a8e142ccd151b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -782,6 +782,7 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions): Promise; $closeTunnel(remotePort: number): Promise; $addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[]): Promise; + $registerCandidateFinder(): Promise; } // -- extension host @@ -1400,7 +1401,7 @@ export interface ExtHostStorageShape { export interface ExtHostTunnelServiceShape { - + $findCandidatePorts(): Promise<{ port: number, detail: string }[]>; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 5b13798e29491..55492233e4335 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtHostTunnelServiceShape, MainThreadTunnelServiceShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import * as vscode from 'vscode'; -import { Disposable } from 'vs/base/common/lifecycle'; export interface TunnelOptions { remote: { port: number, host: string }; @@ -28,39 +26,3 @@ export interface IExtHostTunnelService extends ExtHostTunnelServiceShape { } export const IExtHostTunnelService = createDecorator('IExtHostTunnelService'); - - -export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { - readonly _serviceBrand: undefined; - private readonly _proxy: MainThreadTunnelServiceShape; - - constructor( - @IExtHostRpcService extHostRpc: IExtHostRpcService - ) { - super(); - this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); - } - async makeTunnel(forward: TunnelOptions): Promise { - const tunnel = await this._proxy.$openTunnel(forward); - if (tunnel) { - const disposableTunnel: vscode.Tunnel = { - remote: tunnel.remote, - localAddress: tunnel.localAddress, - dispose: () => { - return this._proxy.$closeTunnel(tunnel.remote.port); - } - }; - this._register(disposableTunnel); - return disposableTunnel; - } - return undefined; - } - - async addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): Promise { - if (tunnels) { - return this._proxy.$addDetected(tunnels); - } - } - -} - diff --git a/src/vs/workbench/api/node/extHost.services.ts b/src/vs/workbench/api/node/extHost.services.ts index 7dd3169394c67..326a1c9cb8d7e 100644 --- a/src/vs/workbench/api/node/extHost.services.ts +++ b/src/vs/workbench/api/node/extHost.services.ts @@ -26,7 +26,8 @@ import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionS import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; -import { IExtHostTunnelService, ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; +import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; +import { ExtHostTunnelService } from 'vs/workbench/api/node/extHostTunnelService'; // register singleton services registerSingleton(ILogService, ExtHostLogService); diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts new file mode 100644 index 0000000000000..4cd66bcef67c6 --- /dev/null +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MainThreadTunnelServiceShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import * as vscode from 'vscode'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { URI } from 'vs/base/common/uri'; +import { exec } from 'child_process'; +import * as resources from 'vs/base/common/resources'; +import * as fs from 'fs'; +import { isLinux } from 'vs/base/common/platform'; +import { IExtHostTunnelService, TunnelOptions } from 'vs/workbench/api/common/extHostTunnelService'; + +export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { + readonly _serviceBrand: undefined; + private readonly _proxy: MainThreadTunnelServiceShape; + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @IExtHostInitDataService initData: IExtHostInitDataService + ) { + super(); + this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); + if (initData.remote.isRemote && initData.remote.authority) { + this.registerCandidateFinder(); + } + } + async makeTunnel(forward: TunnelOptions): Promise { + const tunnel = await this._proxy.$openTunnel(forward); + if (tunnel) { + const disposableTunnel: vscode.Tunnel = { + remote: tunnel.remote, + localAddress: tunnel.localAddress, + dispose: () => { + return this._proxy.$closeTunnel(tunnel.remote.port); + } + }; + this._register(disposableTunnel); + return disposableTunnel; + } + return undefined; + } + + async addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): Promise { + if (tunnels) { + return this._proxy.$addDetected(tunnels); + } + } + + registerCandidateFinder(): Promise { + return this._proxy.$registerCandidateFinder(); + } + + async $findCandidatePorts(): Promise<{ port: number, detail: string }[]> { + if (!isLinux) { + return []; + } + + const ports: { port: number, detail: string }[] = []; + const tcp: string = fs.readFileSync('/proc/net/tcp', 'utf8'); + const tcp6: string = fs.readFileSync('/proc/net/tcp6', 'utf8'); + const procSockets: string = await (new Promise(resolve => { + exec('ls -l /proc/[0-9]*/fd/[0-9]* | grep socket:', (error, stdout, stderr) => { + resolve(stdout); + }); + })); + + const procChildren = fs.readdirSync('/proc'); + const processes: { pid: number, cwd: string, cmd: string }[] = []; + for (let childName of procChildren) { + try { + const pid: number = Number(childName); + const childUri = resources.joinPath(URI.file('/proc'), childName); + const childStat = fs.statSync(childUri.fsPath); + if (childStat.isDirectory() && !isNaN(pid)) { + const cwd = fs.readlinkSync(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = fs.readFileSync(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8').replace(/\0/g, ' '); + processes.push({ pid, cwd, cmd }); + } + } catch (e) { + // + } + } + + const connections: { socket: number, ip: string, port: number }[] = this.loadListeningPorts(tcp, tcp6); + const sockets = this.getSockets(procSockets); + + const socketMap = sockets.reduce((m, socket) => { + m[socket.socket] = socket; + return m; + }, {} as Record); + const processMap = processes.reduce((m, process) => { + m[process.pid] = process; + return m; + }, {} as Record); + + connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { + const command = processMap[socketMap[socket].pid].cmd; + if (!command.match('.*\.vscode\-server\-[a-zA-Z]+\/bin.*') && (command.indexOf('out/vs/server/main.js') === -1)) { + ports.push({ port, detail: processMap[socketMap[socket].pid].cmd }); + } + }); + + return ports; + } + + private getSockets(stdout: string) { + const lines = stdout.trim().split('\n'); + return lines.map(line => { + const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; + return { + pid: parseInt(match[1], 10), + socket: parseInt(match[2], 10) + }; + }); + } + + private loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { + const table = ([] as Record[]).concat(...stdouts.map(this.loadConnectionTable)); + return [ + ...new Map( + table.filter(row => row.st === '0A') + .map(row => { + const address = row.local_address.split(':'); + return { + socket: parseInt(row.inode, 10), + ip: address[0], + port: parseInt(address[1], 16) + }; + }).map(port => [port.port, port]) + ).values() + ]; + } + + private loadConnectionTable(stdout: string): Record[] { + const lines = stdout.trim().split('\n'); + const names = lines.shift()!.trim().split(/\s+/) + .filter(name => name !== 'rx_queue' && name !== 'tm->when'); + const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { + obj[names[i] || i] = value; + return obj; + }, {} as Record)); + return table; + } +} diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 8077d54908213..c61a3d2661ed6 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -52,8 +52,8 @@ export interface ITunnelViewModel { onForwardedPortsChanged: Event; readonly forwarded: TunnelItem[]; readonly detected: TunnelItem[]; - readonly candidates: TunnelItem[]; - readonly groups: ITunnelGroup[]; + readonly candidates: Promise; + groups(): Promise; } export class TunnelViewModel extends Disposable implements ITunnelViewModel { @@ -70,7 +70,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { this._register(this.model.onPortName(() => this._onForwardedPortsChanged.fire())); } - get groups(): ITunnelGroup[] { + async groups(): Promise { const groups: ITunnelGroup[] = []; if (this.model.forwarded.size > 0) { groups.push({ @@ -86,8 +86,8 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { items: this.detected }); } - const candidates = this.candidates; - if (this.candidates.length > 0) { + const candidates = await this.candidates; + if (candidates.length > 0) { groups.push({ label: nls.localize('remote.tunnelsView.candidates', "Candidates"), tunnelType: TunnelType.Candidate, @@ -113,17 +113,16 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } - get candidates(): TunnelItem[] { - const candidates: TunnelItem[] = []; - const values = this.model.candidates.values(); - let iterator = values.next(); - while (!iterator.done) { - if (!this.model.forwarded.has(iterator.value.remote) && !this.model.detected.has(iterator.value.remote)) { - candidates.push(new TunnelItem(TunnelType.Candidate, iterator.value.remote, iterator.value.localAddress, false, undefined, iterator.value.description)); - } - iterator = values.next(); - } - return candidates; + get candidates(): Promise { + return this.model.candidates.then(values => { + const candidates: TunnelItem[] = []; + values.forEach(value => { + if (!this.model.forwarded.has(value.port) && !this.model.detected.has(value.port)) { + candidates.push(new TunnelItem(TunnelType.Candidate, value.port, undefined, false, undefined, value.detail)); + } + }); + return candidates; + }); } dispose() { @@ -312,7 +311,7 @@ class TunnelDataSource implements IAsyncDataSourceelement).items) { @@ -332,7 +331,7 @@ enum TunnelType { interface ITunnelGroup { tunnelType: TunnelType; label: string; - items?: ITunnelItem[]; + items?: ITunnelItem[] | Promise; } interface ITunnelItem { diff --git a/src/vs/workbench/services/extensions/worker/extHost.services.ts b/src/vs/workbench/services/extensions/worker/extHost.services.ts index de59a4481478e..8a65101aa4ec3 100644 --- a/src/vs/workbench/services/extensions/worker/extHost.services.ts +++ b/src/vs/workbench/services/extensions/worker/extHost.services.ts @@ -21,7 +21,6 @@ import { ExtHostExtensionService } from 'vs/workbench/api/worker/extHostExtensio import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService'; -import { IExtHostTunnelService, ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; // register singleton services registerSingleton(ILogService, ExtHostLogService); @@ -34,7 +33,6 @@ registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors); registerSingleton(IExtHostStorage, ExtHostStorage); registerSingleton(IExtHostExtensionService, ExtHostExtensionService); registerSingleton(IExtHostSearch, ExtHostSearch); -registerSingleton(IExtHostTunnelService, ExtHostTunnelService); // register services that only throw errors function NotImplementedProxy(name: ServiceIdentifier): { new(): T } { diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index f1dd74ca8fa4f..01f41b09a1c03 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -29,13 +29,14 @@ export interface Tunnel { export class TunnelModel extends Disposable { readonly forwarded: Map; readonly detected: Map; - readonly candidates: Map; private _onForwardPort: Emitter = new Emitter(); public onForwardPort: Event = this._onForwardPort.event; private _onClosePort: Emitter = new Emitter(); public onClosePort: Event = this._onClosePort.event; private _onPortName: Emitter = new Emitter(); public onPortName: Event = this._onPortName.event; + private _candidateFinder: (() => Promise<{ port: number, detail: string }[]>) | undefined; + constructor( @ITunnelService private readonly tunnelService: ITunnelService ) { @@ -54,11 +55,7 @@ export class TunnelModel extends Disposable { }); this.detected = new Map(); - this.candidates = new Map(); this._register(this.tunnelService.onTunnelOpened(tunnel => { - if (this.candidates.has(tunnel.tunnelRemotePort)) { - this.candidates.delete(tunnel.tunnelRemotePort); - } if (!this.forwarded.has(tunnel.tunnelRemotePort) && tunnel.localAddress) { this.forwarded.set(tunnel.tunnelRemotePort, { remote: tunnel.tunnelRemotePort, @@ -122,6 +119,17 @@ export class TunnelModel extends Disposable { }); }); } + + registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void { + this._candidateFinder = finder; + } + + get candidates(): Promise<{ port: number, detail: string }[]> { + if (this._candidateFinder) { + return this._candidateFinder(); + } + return Promise.resolve([]); + } } export interface IRemoteExplorerService { @@ -136,6 +144,7 @@ export interface IRemoteExplorerService { forward(remote: number, local?: number, name?: string): Promise; close(remote: number): Promise; addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): void; + registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void; } export interface HelpInformation { @@ -180,7 +189,7 @@ class RemoteExplorerService implements IRemoteExplorerService { public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; private _helpInformation: HelpInformation[] = []; private _tunnelModel: TunnelModel; - private editable: { remote: number | undefined, data: IEditableData } | undefined; + private _editable: { remote: number | undefined, data: IEditableData } | undefined; private readonly _onDidChangeEditable: Emitter = new Emitter(); public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; @@ -253,16 +262,21 @@ class RemoteExplorerService implements IRemoteExplorerService { setEditable(remote: number | undefined, data: IEditableData | null): void { if (!data) { - this.editable = undefined; + this._editable = undefined; } else { - this.editable = { remote, data }; + this._editable = { remote, data }; } this._onDidChangeEditable.fire(remote); } getEditableData(remote: number | undefined): IEditableData | undefined { - return this.editable && this.editable.remote === remote ? this.editable.data : undefined; + return this._editable && this._editable.remote === remote ? this._editable.data : undefined; } + + registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void { + this.tunnelModel.registerCandidateFinder(finder); + } + } registerSingleton(IRemoteExplorerService, RemoteExplorerService, true);