Skip to content

Commit

Permalink
Add candidate finding to ports view
Browse files Browse the repository at this point in the history
Part of #81388
  • Loading branch information
alexr00 committed Dec 12, 2019
1 parent d0c8e7a commit e1bfea5
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 71 deletions.
5 changes: 4 additions & 1 deletion src/vs/workbench/api/browser/mainThreadTunnelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -36,6 +35,10 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape {
return Promise.resolve(this.remoteExplorerService.addDetected(tunnels));
}

async $registerCandidateFinder(): Promise<void> {
this.remoteExplorerService.registerCandidateFinder(() => this._proxy.$findCandidatePorts());
}

dispose(): void {
//
}
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ export interface MainThreadTunnelServiceShape extends IDisposable {
$openTunnel(tunnelOptions: TunnelOptions): Promise<TunnelDto | undefined>;
$closeTunnel(remotePort: number): Promise<void>;
$addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[]): Promise<void>;
$registerCandidateFinder(): Promise<void>;
}

// -- extension host
Expand Down Expand Up @@ -1400,7 +1401,7 @@ export interface ExtHostStorageShape {


export interface ExtHostTunnelServiceShape {

$findCandidatePorts(): Promise<{ port: number, detail: string }[]>;
}

// --- proxy identifiers
Expand Down
40 changes: 1 addition & 39 deletions src/vs/workbench/api/common/extHostTunnelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -28,39 +26,3 @@ export interface IExtHostTunnelService extends ExtHostTunnelServiceShape {
}

export const IExtHostTunnelService = createDecorator<IExtHostTunnelService>('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<vscode.Tunnel | undefined> {
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<void> {
if (tunnels) {
return this._proxy.$addDetected(tunnels);
}
}

}

3 changes: 2 additions & 1 deletion src/vs/workbench/api/node/extHost.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
149 changes: 149 additions & 0 deletions src/vs/workbench/api/node/extHostTunnelService.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.Tunnel | undefined> {
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<void> {
if (tunnels) {
return this._proxy.$addDetected(tunnels);
}
}

registerCandidateFinder(): Promise<void> {
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<string, typeof sockets[0]>);
const processMap = processes.reduce((m, process) => {
m[process.pid] = process;
return m;
}, {} as Record<string, typeof processes[0]>);

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<string, string>[]).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<string, string>[] {
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<string, string>));
return table;
}
}
35 changes: 17 additions & 18 deletions src/vs/workbench/contrib/remote/browser/tunnelView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export interface ITunnelViewModel {
onForwardedPortsChanged: Event<void>;
readonly forwarded: TunnelItem[];
readonly detected: TunnelItem[];
readonly candidates: TunnelItem[];
readonly groups: ITunnelGroup[];
readonly candidates: Promise<TunnelItem[]>;
groups(): Promise<ITunnelGroup[]>;
}

export class TunnelViewModel extends Disposable implements ITunnelViewModel {
Expand All @@ -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<ITunnelGroup[]> {
const groups: ITunnelGroup[] = [];
if (this.model.forwarded.size > 0) {
groups.push({
Expand All @@ -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,
Expand All @@ -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<TunnelItem[]> {
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() {
Expand Down Expand Up @@ -312,7 +311,7 @@ class TunnelDataSource implements IAsyncDataSource<ITunnelViewModel, ITunnelItem

getChildren(element: ITunnelViewModel | ITunnelItem | ITunnelGroup) {
if (element instanceof TunnelViewModel) {
return element.groups;
return element.groups();
} else if (element instanceof TunnelItem) {
return [];
} else if ((<ITunnelGroup>element).items) {
Expand All @@ -332,7 +331,7 @@ enum TunnelType {
interface ITunnelGroup {
tunnelType: TunnelType;
label: string;
items?: ITunnelItem[];
items?: ITunnelItem[] | Promise<ITunnelItem[]>;
}

interface ITunnelItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<T>(name: ServiceIdentifier<T>): { new(): T } {
Expand Down
Loading

0 comments on commit e1bfea5

Please sign in to comment.