Skip to content

Commit

Permalink
Try to find ports with a root process (#119341)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexr00 authored Mar 19, 2021
1 parent 421d67e commit 1b7470b
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 60 deletions.
105 changes: 91 additions & 14 deletions src/vs/workbench/api/node/extHostTunnelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ExtensionTunnel implements vscode.Tunnel {
}
}

export function getSockets(stdout: string): { pid: number, socket: number }[] {
export function getSockets(stdout: string): Record<string, { pid: number; socket: number; }> {
const lines = stdout.trim().split('\n');
const mapped: { pid: number, socket: number }[] = [];
lines.forEach(line => {
Expand All @@ -51,7 +51,11 @@ export function getSockets(stdout: string): { pid: number, socket: number }[] {
});
}
});
return mapped;
const socketMap = mapped.reduce((m, socket) => {
m[socket.socket] = socket;
return m;
}, {} as Record<string, typeof mapped[0]>);
return socketMap;
}

export function loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] {
Expand Down Expand Up @@ -108,29 +112,68 @@ function knownExcludeCmdline(command: string): boolean {
|| (command.indexOf('_productName=VSCode') !== -1);
}

export async function findPorts(tcp: string, tcp6: string, procSockets: string, processes: { pid: number, cwd: string, cmd: string }[]): Promise<CandidatePort[]> {
const connections: { socket: number, ip: string, port: number }[] = loadListeningPorts(tcp, tcp6);
const sockets = getSockets(procSockets);
export function getRootProcesses(stdout: string) {
const lines = stdout.trim().split('\n');
const mapped: { pid: number, cmd: string, ppid: number }[] = [];
lines.forEach(line => {
const match = /^\d+\s+\D+\s+root\s+(\d+)\s+(\d+).+\d+\:\d+\:\d+\s+(.+)$/.exec(line)!;
if (match && match.length >= 4) {
mapped.push({
pid: parseInt(match[1], 10),
ppid: parseInt(match[2]),
cmd: match[3]
});
}
});
return mapped;
}

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

const ports: CandidatePort[] = [];
connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => {
const command: string | undefined = processMap[socketMap[socket].pid]?.cmd;
if (command && !knownExcludeCmdline(command)) {
ports.push({ host: ip, port, detail: command, pid: socketMap[socket].pid });
connections.forEach(({ socket, ip, port }) => {
const pid = socketMap[socket] ? socketMap[socket].pid : undefined;
const command: string | undefined = pid ? processMap[pid]?.cmd : undefined;
if (pid && command && !knownExcludeCmdline(command)) {
ports.push({ host: ip, port, detail: command, pid });
}
});
return ports;
}

export function tryFindRootPorts(connections: { socket: number, ip: string, port: number }[], rootProcessesStdout: string, previousPorts: Map<number, CandidatePort & { ppid: number }>): Map<number, CandidatePort & { ppid: number }> {
const ports: Map<number, CandidatePort & { ppid: number }> = new Map();
const rootProcesses = getRootProcesses(rootProcessesStdout);

for (const connection of connections) {
const previousPort = previousPorts.get(connection.port);
if (previousPort) {
ports.set(connection.port, previousPort);
continue;
}
const rootProcessMatch = rootProcesses.find((value) => value.cmd.includes(`${connection.port}`));
if (rootProcessMatch) {
let bestMatch = rootProcessMatch;
// There are often several processes that "look" like they could match the port.
// The one we want is usually the child of the other. Find the most child process.
let mostChild: { pid: number, cmd: string, ppid: number } | undefined;
do {
mostChild = rootProcesses.find(value => value.ppid === bestMatch.pid);
if (mostChild) {
bestMatch = mostChild;
}
} while (mostChild);
ports.set(connection.port, { host: connection.ip, port: connection.port, pid: bestMatch.pid, detail: bestMatch.cmd, ppid: bestMatch.ppid });
}
}

return ports;
}

export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService {
readonly _serviceBrand: undefined;
private readonly _proxy: MainThreadTunnelServiceShape;
Expand All @@ -140,6 +183,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
private _onDidChangeTunnels: Emitter<void> = new Emitter<void>();
onDidChangeTunnels: vscode.Event<void> = this._onDidChangeTunnels.event;
private _candidateFindingEnabled: boolean = false;
private _foundRootPorts: Map<number, CandidatePort & { ppid: number }> = new Map();

private _providerHandleCounter: number = 0;
private _portAttributesProviders: Map<number, { provider: vscode.PortAttributesProvider, selector: PortAttributesProviderSelector }> = new Map();
Expand Down Expand Up @@ -320,11 +364,14 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
} catch (e) {
// File reading error. No additional handling needed.
}
const connections: { socket: number, ip: string, port: number }[] = loadListeningPorts(tcp, tcp6);

const procSockets: string = await (new Promise(resolve => {
exec('ls -l /proc/[0-9]*/fd/[0-9]* | grep socket:', (error, stdout, stderr) => {
resolve(stdout);
});
}));
const socketMap = getSockets(procSockets);

const procChildren = await pfs.readdir('/proc');
const processes: {
Expand All @@ -344,6 +391,36 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
//
}
}
return findPorts(tcp, tcp6, procSockets, processes);

const unFoundConnections: { socket: number, ip: string, port: number }[] = [];
const filteredConnections = connections.filter((connection => {
const foundConnection = socketMap[connection.socket];
if (!foundConnection) {
unFoundConnections.push(connection);
}
return foundConnection;
}));

const foundPorts = findPorts(filteredConnections, socketMap, processes);
let heuristicPorts: CandidatePort[] | undefined;
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) number of possible root ports ${unFoundConnections.length}`);
if (unFoundConnections.length > 0) {
const rootProcesses: string = await (new Promise(resolve => {
exec('ps -F -A -l | grep root', (error, stdout, stderr) => {
resolve(stdout);
});
}));
this._foundRootPorts = tryFindRootPorts(unFoundConnections, rootProcesses, this._foundRootPorts);
heuristicPorts = Array.from(this._foundRootPorts.values());
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) heuristic ports ${heuristicPorts.join(', ')}`);

}
return foundPorts.then(foundCandidates => {
if (heuristicPorts) {
return foundCandidates.concat(heuristicPorts);
} else {
return foundCandidates;
}
});
}
}
107 changes: 61 additions & 46 deletions src/vs/workbench/test/node/api/extHostTunnelService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import { findPorts, getSockets, loadConnectionTable, loadListeningPorts } from 'vs/workbench/api/node/extHostTunnelService';
import { findPorts, getRootProcesses, getSockets, loadConnectionTable, loadListeningPorts, tryFindRootPorts } from 'vs/workbench/api/node/extHostTunnelService';

const tcp =
` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
Expand Down Expand Up @@ -215,52 +214,68 @@ const processes: { pid: number, cwd: string, cmd: string }[] = [
}
];

if (platform.isLinux) {
suite('ExtHostTunnelService', () => {
test('getSockets', function () {
const result = getSockets(procSockets);
assert.equal(result.length, 78);
// 4412 is the pid fo the http-server in the test data
assert.notEqual(result.find(value => value.pid === 4412), undefined);
});
const psStdOut =
`4 S root 1 0 0 80 0 - 596 - 1440 2 14:41 ? 00:00:00 /bin/sh -c echo Container started ; trap "exit 0" 15; while sleep 1 & wait $!; do :; done
4 S root 14 0 0 80 0 - 596 - 764 4 14:41 ? 00:00:00 /bin/sh
4 S root 40 0 0 80 0 - 596 - 700 4 14:41 ? 00:00:00 /bin/sh
4 S root 513 380 0 80 0 - 2476 - 3404 1 14:41 pts/1 00:00:00 sudo npx http-server -p 5000
4 S root 514 513 0 80 0 - 165439 - 41380 5 14:41 pts/1 00:00:00 http-server
0 S root 1052 1 0 80 0 - 573 - 752 5 14:43 ? 00:00:00 sleep 1
0 S node 1056 329 0 80 0 - 596 do_wai 764 10 14:43 ? 00:00:00 /bin/sh -c ps -F -A -l | grep root
0 S node 1058 1056 0 80 0 - 770 pipe_w 888 9 14:43 ? 00:00:00 grep root`;

test('loadConnectionTable', function () {
const result = loadConnectionTable(tcp);
assert.equal(result.length, 6);
assert.deepEqual(result[0], {
10: '1',
11: '0000000010173312',
12: '100',
13: '0',
14: '0',
15: '10',
16: '0',
inode: '2335214',
local_address: '00000000:0BBA',
rem_address: '00000000:0000',
retrnsmt: '00000000',
sl: '0:',
st: '0A',
timeout: '0',
tr: '00:00000000',
tx_queue: '00000000:00000000',
uid: '1000'
});
});
suite('ExtHostTunnelService', () => {
test('getSockets', function () {
const result = getSockets(procSockets);
assert.strictEqual(Object.keys(result).length, 75);
// 4412 is the pid of the http-server in the test data
assert.notStrictEqual(Object.keys(result).find(key => result[key].pid === 4412), undefined);
});

test('loadListeningPorts', function () {
const result = loadListeningPorts(tcp, tcp6);
// There should be 7 based on the input data. One of them should be 3002.
assert.equal(result.length, 7);
assert.notEqual(result.find(value => value.port === 3002), undefined);
test('loadConnectionTable', function () {
const result = loadConnectionTable(tcp);
assert.strictEqual(result.length, 6);
assert.deepStrictEqual(result[0], {
10: '1',
11: '0000000010173312',
12: '100',
13: '0',
14: '0',
15: '10',
16: '0',
inode: '2335214',
local_address: '00000000:0BBA',
rem_address: '00000000:0000',
retrnsmt: '00000000',
sl: '0:',
st: '0A',
timeout: '0',
tr: '00:00000000',
tx_queue: '00000000:00000000',
uid: '1000'
});
});

test('findPorts', async function () {
const result = await findPorts(tcp, tcp6, procSockets, processes);
assert.equal(result.length, 1);
assert.equal(result[0].host, '0.0.0.0');
assert.equal(result[0].port, 3002);
assert.equal(result[0].detail, 'http-server');
});
test('loadListeningPorts', function () {
const result = loadListeningPorts(tcp, tcp6);
// There should be 7 based on the input data. One of them should be 3002.
assert.strictEqual(result.length, 7);
assert.notStrictEqual(result.find(value => value.port === 3002), undefined);
});

test('tryFindRootPorts', function () {
const rootProcesses = getRootProcesses(psStdOut);
assert.strictEqual(rootProcesses.length, 6);
const result = tryFindRootPorts([{ socket: 1000, ip: '127.0.0.1', port: 5000 }], psStdOut, new Map());
assert.strictEqual(result.size, 1);
assert.strictEqual(result.get(5000)?.pid, 514);
});

test('findPorts', async function () {
const result = await findPorts(loadListeningPorts(tcp, tcp6), getSockets(procSockets), processes);
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].host, '0.0.0.0');
assert.strictEqual(result[0].port, 3002);
assert.strictEqual(result[0].detail, 'http-server');
});
}
});

0 comments on commit 1b7470b

Please sign in to comment.