Skip to content

Commit 1b77d76

Browse files
authored
chore: add pid tracker for browser (#2264)
1 parent 5d5a044 commit 1b77d76

7 files changed

Lines changed: 115 additions & 43 deletions

File tree

src/build/dapCustom.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,12 @@ const dapCustom: JSONSchema4 = {
340340
},
341341
},
342342
},
343+
}, {
344+
properties: {
345+
pid: {
346+
type: 'number',
347+
},
348+
},
343349
}),
344350

345351
...makeRequest(

src/common/pidLiveness.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { IDisposable } from './disposable';
6+
7+
/**
8+
* Utility to poll a PID for liveness and notify when it exits.
9+
*/
10+
export class PidLiveness implements IDisposable {
11+
private timer?: NodeJS.Timeout;
12+
13+
constructor(
14+
public readonly pid: number,
15+
private readonly onExit: (killed: boolean) => void,
16+
pollInterval: number,
17+
) {
18+
this.updateInterval(pollInterval);
19+
}
20+
21+
public dispose() {
22+
clearInterval(this.timer);
23+
}
24+
25+
public updateInterval(interval = 200) {
26+
if (this.timer) {
27+
clearInterval(this.timer);
28+
}
29+
30+
this.timer = setInterval(() => {
31+
if (!isProcessAlive(this.pid)) {
32+
this.onExit(true);
33+
this.dispose();
34+
}
35+
}, interval);
36+
}
37+
}
38+
39+
function isProcessAlive(processId: number) {
40+
try {
41+
process.kill(processId, 0);
42+
return true;
43+
} catch {
44+
return false;
45+
}
46+
}

src/dap/api.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export namespace Dap {
9090
output(params: OutputEventParams): void;
9191

9292
/**
93-
* The event indicates that some information about a breakpoint has changed.
93+
* The event indicates that some information about a breakpoint has changed. While debug adapters may notify the clients of `changed` breakpoints using this event, clients should continue to use the breakpoint's original properties when updating a source's breakpoints in the `breakpoint` request.
9494
*/
9595
breakpoint(params: BreakpointEventParams): void;
9696

@@ -1300,7 +1300,7 @@ export namespace Dap {
13001300
): Promise<OutputEventParams>;
13011301

13021302
/**
1303-
* The event indicates that some information about a breakpoint has changed.
1303+
* The event indicates that some information about a breakpoint has changed. While debug adapters may notify the clients of `changed` breakpoints using this event, clients should continue to use the breakpoint's original properties when updating a source's breakpoints in the `breakpoint` request.
13041304
*/
13051305
on(request: 'breakpoint', handler: (params: BreakpointEventParams) => void): void;
13061306
off(request: 'breakpoint', handler: (params: BreakpointEventParams) => void): void;
@@ -2919,6 +2919,7 @@ export namespace Dap {
29192919
}
29202920

29212921
export interface LaunchUnelevatedResult {
2922+
pid?: number;
29222923
}
29232924

29242925
export interface LaunchVSCodeParams {

src/targets/browser/launcher.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
ChildProcessBrowserProcess,
2626
IBrowserProcess,
2727
NonTrackedBrowserProcess,
28+
PidTrackedBrowserProcess,
2829
} from './spawn/browserProcess';
2930
import { retryGetBrowserEndpoint } from './spawn/endpoints';
3031
import { launchUnelevatedChrome } from './unelevatedChome';
@@ -104,13 +105,15 @@ export async function launch(
104105
clientCapabilities.supportsLaunchUnelevatedProcessRequest && options.launchUnelevated
105106
);
106107
if (launchUnelevated && typeof actualConnection === 'number' && actualConnection !== 0) {
107-
await launchUnelevatedChrome(
108+
const response = await launchUnelevatedChrome(
108109
dap,
109110
executablePath,
110111
browserArguments.toArray(),
111112
cancellationToken,
112113
);
113-
browserProcess = new NonTrackedBrowserProcess(logger);
114+
browserProcess = response.pid
115+
? new PidTrackedBrowserProcess(response.pid, logger)
116+
: new NonTrackedBrowserProcess(logger);
114117
} else {
115118
logger.info(LogTag.RuntimeLaunch, `Launching Chrome from ${executablePath}`);
116119

src/targets/browser/spawn/browserProcess.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TaskCancelledError } from '../../../common/cancellation';
1313
import { DisposableList } from '../../../common/disposable';
1414
import { EventEmitter } from '../../../common/events';
1515
import { ILogger } from '../../../common/logging';
16+
import { PidLiveness } from '../../../common/pidLiveness';
1617
import { delay } from '../../../common/promiseUtil';
1718
import { KillBehavior } from '../../../configuration';
1819
import { killTree } from '../../node/killTree';
@@ -116,6 +117,42 @@ export class NonTrackedBrowserProcess implements IBrowserProcess {
116117
}
117118
}
118119

120+
/**
121+
* Browser process tracked using PID liveness polling (used for unelevated launches).
122+
*/
123+
export class PidTrackedBrowserProcess implements IBrowserProcess {
124+
public readonly stderr = undefined;
125+
public readonly stdout = undefined;
126+
127+
private readonly exitEmitter = new EventEmitter<number>();
128+
public readonly onExit = this.exitEmitter.event;
129+
130+
private readonly errorEmitter = new EventEmitter<Error>();
131+
public readonly onError = this.errorEmitter.event;
132+
133+
private readonly liveness: PidLiveness;
134+
135+
constructor(private readonly pid: number, private readonly logger: ILogger) {
136+
this.liveness = new PidLiveness(pid, () => this.exitEmitter.fire(0), 1000);
137+
}
138+
139+
public async transport(
140+
options: ITransportOptions,
141+
cancellationToken: CancellationToken,
142+
): Promise<ITransport> {
143+
return inspectWsConnection(this.logger, this, options, cancellationToken);
144+
}
145+
146+
public kill(): void {
147+
try {
148+
process.kill(this.pid);
149+
} catch {
150+
// ignore
151+
}
152+
this.liveness.updateInterval(200);
153+
}
154+
}
155+
119156
/**
120157
* Browser process
121158
*/

src/targets/browser/unelevatedChome.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export async function launchUnelevatedChrome(
1010
chromePath: string,
1111
chromeArgs: string[],
1212
cancellationToken: CancellationToken,
13-
): Promise<void> {
13+
): Promise<{ pid?: number }> {
1414
const response = dap.launchUnelevatedRequest({
1515
process: chromePath,
1616
args: chromeArgs,
1717
});
1818

19-
await timeoutPromise(response, cancellationToken, 'Could not launch browser unelevated');
19+
return await timeoutPromise(response, cancellationToken, 'Could not launch browser unelevated');
2020
}

src/targets/node/program.ts

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { ChildProcess } from 'child_process';
66
import { ILogger } from '../../common/logging';
7+
import { PidLiveness } from '../../common/pidLiveness';
78
import { KillBehavior } from '../../configuration';
89
import Dap from '../../dap/api';
910
import { IStopMetadata } from '../targets';
@@ -140,15 +141,19 @@ export class TerminalProcess implements IProgram {
140141
resolve({ code: 0, killed });
141142
}),
142143
);
143-
private loop?: { timer: NodeJS.Timeout; processId: number };
144+
private liveness?: PidLiveness;
144145

145146
constructor(
146147
private readonly terminalResult: Dap.RunInTerminalResult,
147148
private readonly logger: ILogger,
148149
private readonly killBehavior: KillBehavior,
149150
) {
150151
if (terminalResult.processId) {
151-
this.startPollLoop(terminalResult.processId);
152+
this.liveness = new PidLiveness(
153+
terminalResult.processId,
154+
this.onStopped,
155+
TerminalProcess.terminationPollInterval,
156+
);
152157
}
153158
}
154159

@@ -158,9 +163,7 @@ export class TerminalProcess implements IProgram {
158163
return; // to avoid any races
159164
}
160165

161-
if (!this.loop) {
162-
this.startPollLoop(processId);
163-
}
166+
this.liveness ??= new PidLiveness(processId, this.onStopped, 1000);
164167
}
165168

166169
public stop(): Promise<IStopMetadata> {
@@ -171,46 +174,22 @@ export class TerminalProcess implements IProgram {
171174

172175
// If we're already polling some process ID, kill it and accelerate polling
173176
// so we can confirm it's dead quickly.
174-
if (this.loop) {
175-
killTree(this.loop.processId, this.logger, this.killBehavior);
176-
this.startPollLoop(this.loop.processId, TerminalProcess.killConfirmInterval);
177+
if (this.liveness) {
178+
killTree(this.liveness.pid, this.logger, this.killBehavior);
179+
this.liveness.updateInterval(TerminalProcess.killConfirmInterval);
177180
} else if (this.terminalResult.shellProcessId) {
178181
// If we had a shell process ID, well, that's good enough.
179182
killTree(this.terminalResult.shellProcessId, this.logger, this.killBehavior);
180-
this.startPollLoop(this.terminalResult.shellProcessId, TerminalProcess.killConfirmInterval);
183+
this.liveness = new PidLiveness(
184+
this.terminalResult.shellProcessId,
185+
this.onStopped,
186+
TerminalProcess.killConfirmInterval,
187+
);
181188
} else {
182189
// Otherwise, we can't do anything. Pretend like we did.
183190
this.onStopped(true);
184191
}
185192

186193
return this.stopped;
187194
}
188-
189-
private startPollLoop(processId: number, interval = TerminalProcess.terminationPollInterval) {
190-
if (this.loop) {
191-
clearInterval(this.loop.timer);
192-
}
193-
194-
const loop = {
195-
processId,
196-
timer: setInterval(() => {
197-
if (!isProcessAlive(processId)) {
198-
clearInterval(loop.timer);
199-
this.onStopped(true);
200-
}
201-
}, interval),
202-
};
203-
204-
this.loop = loop;
205-
}
206-
}
207-
208-
function isProcessAlive(processId: number) {
209-
try {
210-
// kill with signal=0 just test for whether the proc is alive. It throws if not.
211-
process.kill(processId, 0);
212-
return true;
213-
} catch {
214-
return false;
215-
}
216195
}

0 commit comments

Comments
 (0)