Skip to content
This repository has been archived by the owner on Dec 6, 2022. It is now read-only.

Fix race condition when killing Chrome on Windows when disconnecting #703

Merged
merged 2 commits into from
Jul 18, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 34 additions & 18 deletions src/chromeDebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,32 +312,17 @@ export class ChromeDebugAdapter extends CoreDebugAdapter {
super.onResumed();
}

public disconnect(args: DebugProtocol.DisconnectArguments): void {
public async disconnect(args: DebugProtocol.DisconnectArguments): Promise<void> {
const hadTerminated = this._hasTerminated;

// Disconnect before killing Chrome, because running "taskkill" when it's paused sometimes doesn't kill it
super.disconnect(args);

if ( (this._chromeProc || (!this._chromeProc && this._chromePID)) && !hadTerminated) {
if ( (this._chromeProc || this._chromePID) && !hadTerminated) {
// Only kill Chrome if the 'disconnect' originated from vscode. If we previously terminated
// due to Chrome shutting down, or devtools taking over, don't kill Chrome.
if (coreUtils.getPlatform() === coreUtils.Platform.Windows && this._chromePID) {
let taskkillCmd = `taskkill /PID ${this._chromePID}`;
logger.log(`Killing Chrome process by pid: ${taskkillCmd}`);
try {
// Run synchronously because this process may be killed before exec() would run
execSync(taskkillCmd);
} catch (e) {
// Can fail if Chrome was already open, and the process with _chromePID is gone.
// Or if it already shut down for some reason.
}
// execSync above may succeed, but Chrome still might not shut down, for example if the web page promts the user about unsaved changes.
// In that case, we need to use /F to force shutdown, but we risk Chrome not shutting down correctly.
taskkillCmd = `taskkill /F /PID ${this._chromePID}`;
logger.log(`Killing Chrome process by pid (using force in case the first attempt failed): ${taskkillCmd}`);
try {
execSync(taskkillCmd);
} catch (e) {}
await this.killChromeOnWindows(this._chromePID);
} else if (this._chromeProc) {
logger.log('Killing Chrome process');
this._chromeProc.kill('SIGINT');
Expand All @@ -347,6 +332,37 @@ export class ChromeDebugAdapter extends CoreDebugAdapter {
this._chromeProc = null;
}

private async killChromeOnWindows(chromePID: number): Promise<void> {
Copy link
Contributor

@digeff digeff Jul 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have enough telemetry to figure out if this method/logic is working as expected?

let taskkillCmd = `taskkill /PID ${chromePID}`;
logger.log(`Killing Chrome process by pid: ${taskkillCmd}`);
try {
execSync(taskkillCmd);
} catch (e) {
// The command will fail if process was not found. This can be safely ignored.
}

for (let i = 0 ; i < 10; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use for this for loop? How is this better than using a timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the pattern in findNewlyLaunchedChromeProcess() at the bottom of the file. It will try up to 10 times and wait 200ms between tries. Total time will be 2000ms in addition to command execution time. It does use a timeout to wait.

I think it's readable but I am open to changing it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine but you could also use the retryAsync helper in core's utils.ts if you want.

// Check to see if the process is still running
let tasklistOutput = execSync(`tasklist /FI "PID eq ${chromePID}"`).toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we log this execution too?

if (!tasklistOutput.includes(chromePID.toString())) {
Copy link
Contributor

@digeff digeff Jul 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that the chromePID is a substring of another process PID? Is it possible that the output of the command is not in the format that we expect?

Copy link
Contributor Author

@mrcrane mrcrane Jul 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When tasklist finds the process it will return output that looks like
Image Name PID Session Name Session# Mem Usage
========== === ============ ======== =========
chrome.exe 16116 Console 1 273,300 K

When tasklist doesn't find the process it outputs
INFO: No tasks are running which match the specified criteria.

In both cases the exit code is 0

Because the tasklist is run with a PID filter it will only return one or zero processes. Any process that with a PID that is a superstring will be filtered out.

I implemented with a search of PID in the output because it seemed like the most robust way to differentiate between success and failure outputs. Searching for the presence of the PID is independent of the format.

Let me know if you have any better suggestions. I can add comments as documentation for what is going on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to use csv output which is slightly better, but still not perfect.

return;
}

// Give the process some time to close gracefully
logger.log(`Chrome process with pid ${chromePID} is still alive, waiting...`)
await new Promise<void>((resolve) => {
setTimeout(resolve, 200);
});
}

// At this point we can assume the process won't close on its own, so force kill it
let taskkillForceCmd = `taskkill /F /PID ${chromePID}`;
logger.log(`Killing Chrome process timed out. Killing again using force: ${taskkillForceCmd}`);
try {
execSync(taskkillForceCmd);
} catch (e) {}
}

/**
* Opt-in event called when the 'reload' button in the debug widget is pressed
*/
Expand Down