Skip to content

Commit

Permalink
Merge pull request #415 from microsoft/hang_fix
Browse files Browse the repository at this point in the history
Host conout socket in a worker
  • Loading branch information
Tyriar authored Feb 9, 2021
2 parents c5ccb80 + dcc96be commit b532aef
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 30 deletions.
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,14 @@ npm run build

## Dependencies

### Linux/Ubuntu
Node.JS 12+ or Electron 8+ is required to use `node-pty`.

```
### Linux (apt)

```sh
sudo apt install -y make python build-essential
```

The following are also needed:

- Node.JS 10+

### macOS

Xcode is needed to compile the sources, this can be installed from the App Store.
Expand All @@ -102,7 +100,6 @@ npm install --global --production windows-build-tools
The following are also needed:

- [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk) - only the "Desktop C++ Apps" components are needed to be installed
- Node.JS 10+

## Debugging

Expand Down
8 changes: 1 addition & 7 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ jobs:
vmImage: 'ubuntu-16.04'
strategy:
matrix:
node_10_x:
node_version: 10.x
node_12_x:
node_version: 12.x
steps:
Expand All @@ -33,8 +31,6 @@ jobs:
vmImage: 'macOS-10.15'
strategy:
matrix:
node_10_x:
node_version: 10.x
node_12_x:
node_version: 12.x
steps:
Expand All @@ -57,8 +53,6 @@ jobs:
vmImage: 'vs2017-win2016'
strategy:
matrix:
node_10_x:
node_version: 10.x
node_12_x:
node_version: 12.x
steps:
Expand Down Expand Up @@ -87,7 +81,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
versionSpec: '12.x'
displayName: 'Install Node.js'
- script: |
npm i
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"devDependencies": {
"@types/mocha": "^7.0.2",
"@types/node": "8",
"@types/node": "12",
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.27.0",
"cross-env": "^5.1.4",
Expand Down
15 changes: 15 additions & 0 deletions src/shared/conout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2020, Microsoft Corporation (MIT License).
*/

export interface IWorkerData {
conoutPipeName: string;
}

export const enum ConoutWorkerMessage {
READY = 1
}

export function getWorkerPipeName(conoutPipeName: string): string {
return `${conoutPipeName}-worker`;
}
1 change: 0 additions & 1 deletion src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ export abstract class Terminal implements ITerminal {
public abstract get slave(): Socket;

protected _close(): void {
this._socket.writable = false;
this._socket.readable = false;
this.write = () => {};
this.end = () => {};
Expand Down
78 changes: 78 additions & 0 deletions src/windowsConoutConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2020, Microsoft Corporation (MIT License).
*/

import { Worker } from 'worker_threads';
import { Socket } from 'net';
import { IDisposable } from './types';
import { IWorkerData, ConoutWorkerMessage, getWorkerPipeName } from './shared/conout';
import { join } from 'path';
import { IEvent, EventEmitter2 } from './eventEmitter2';

/**
* The amount of time to wait for additional data after the conpty shell process has exited before
* shutting down the worker and sockets. The timer will be reset if a new data event comes in after
* the timer has started.
*/
const FLUSH_DATA_INTERVAL = 1000;

/**
* Connects to and manages the lifecycle of the conout socket. This socket must be drained on
* another thread in order to avoid deadlocks where Conpty waits for the out socket to drain
* when `ClosePseudoConsole` is called. This happens when data is being written to the terminal when
* the pty is closed.
*
* See also:
* - https://github.com/microsoft/node-pty/issues/375
* - https://github.com/microsoft/vscode/issues/76548
* - https://github.com/microsoft/terminal/issues/1810
* - https://docs.microsoft.com/en-us/windows/console/closepseudoconsole
*/
export class ConoutConnection implements IDisposable {
private _worker: Worker;
private _drainTimeout: NodeJS.Timeout;
private _isDisposed: boolean = false;

private _onReady = new EventEmitter2<void>();
public get onReady(): IEvent<void> { return this._onReady.event; }

constructor(
private _conoutPipeName: string
) {
const workerData: IWorkerData = { conoutPipeName: _conoutPipeName };
this._worker = new Worker(join(__dirname, 'worker/conoutSocketWorker.js'), { workerData });
this._worker.on('message', (message: ConoutWorkerMessage) => {
switch (message) {
case ConoutWorkerMessage.READY:
this._onReady.fire();
return;
default:
console.warn('Unexpected ConoutWorkerMessage', message);
}
});
}

dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
// Drain all data from the socket before closing
this._drainDataAndClose();
}

connectSocket(socket: Socket): void {
socket.connect(getWorkerPipeName(this._conoutPipeName));
}

private _drainDataAndClose(): void {
if (this._drainTimeout) {
clearTimeout(this._drainTimeout);
}
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);
}

private async _destroySocket(): Promise<void> {
await this._worker.terminate();
}
}
20 changes: 10 additions & 10 deletions src/windowsPtyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as path from 'path';
import { Socket } from 'net';
import { ArgvOrCommandLine } from './types';
import { fork } from 'child_process';
import { ConoutConnection } from './windowsConoutConnection';

let conptyNative: IConptyNative;
let winptyNative: IWinptyNative;
Expand All @@ -18,7 +19,7 @@ let winptyNative: IWinptyNative;
* shutting down the socket. The timer will be reset if a new data event comes in after the timer
* has started.
*/
const FLUSH_DATA_INTERVAL = 20;
const FLUSH_DATA_INTERVAL = 1000;

/**
* This agent sits between the WindowsTerminal class and provides a common interface for both conpty
Expand All @@ -32,6 +33,7 @@ export class WindowsPtyAgent {
private _innerPidHandle: number;
private _closeTimeout: NodeJS.Timer;
private _exitCode: number | undefined;
private _conoutSocketWorker: ConoutConnection;

private _fd: any;
private _pty: number;
Expand Down Expand Up @@ -115,17 +117,18 @@ export class WindowsPtyAgent {
// Create terminal pipe IPC channel and forward to a local unix socket.
this._outSocket = new Socket();
this._outSocket.setEncoding('utf8');
this._outSocket.connect(term.conout, () => {
// TODO: Emit event on agent instead of socket?

// Emit ready event.
// The conout socket must be ready out on another thread to avoid deadlocks
this._conoutSocketWorker = new ConoutConnection(term.conout);
this._conoutSocketWorker.onReady(() => {
this._conoutSocketWorker.connectSocket(this._outSocket);
});
this._outSocket.on('connect', () => {
this._outSocket.emit('ready_datapipe');
});

this._inSocket = new Socket();
this._inSocket.setEncoding('utf8');
this._inSocket.connect(term.conin);
// TODO: Wait for ready event?

if (this._useConpty) {
const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, c => this._$onProcessExit(c));
Expand All @@ -146,9 +149,7 @@ export class WindowsPtyAgent {

public kill(): void {
this._inSocket.readable = false;
this._inSocket.writable = false;
this._outSocket.readable = false;
this._outSocket.writable = false;
// Tell the agent to kill the pty, this releases handles to the process
if (this._useConpty) {
this._getConsoleProcessList().then(consoleProcessList => {
Expand Down Expand Up @@ -178,6 +179,7 @@ export class WindowsPtyAgent {
}
});
}
this._conoutSocketWorker.dispose();
}

private _getConsoleProcessList(): Promise<number[]> {
Expand Down Expand Up @@ -233,9 +235,7 @@ export class WindowsPtyAgent {

private _cleanUpProcess(): void {
this._inSocket.readable = false;
this._inSocket.writable = false;
this._outSocket.readable = false;
this._outSocket.writable = false;
this._outSocket.destroy();
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/worker/conoutSocketWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2020, Microsoft Corporation (MIT License).
*/

import { parentPort, workerData } from 'worker_threads';
import { Socket, createServer } from 'net';
import { ConoutWorkerMessage, IWorkerData, getWorkerPipeName } from '../shared/conout';

const conoutPipeName = (workerData as IWorkerData).conoutPipeName;
const conoutSocket = new Socket();
conoutSocket.setEncoding('utf8');
conoutSocket.connect(conoutPipeName, () => {
const server = createServer(workerSocket => {
conoutSocket.pipe(workerSocket);
});
server.listen(getWorkerPipeName(conoutPipeName));
parentPort.postMessage(ConoutWorkerMessage.READY);
});
33 changes: 33 additions & 0 deletions test/spam-close.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// This test creates a pty periodically, spamming it with echo calls and killing it shortly after.
// It's a test case for https://github.com/microsoft/node-pty/issues/375, the script will hang
// when it show this bug instead of continuing to create more processes.

var os = require('os');
var pty = require('..');

var isWindows = os.platform() === 'win32';
var shell = isWindows ? 'cmd.exe' : 'bash';

let i = 0;

setInterval(() => {
console.log(`creating pty ${++i}`);
var ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 80,
rows: 26,
cwd: isWindows ? process.env.USERPROFILE : process.env.HOME,
env: Object.assign({ TEST: "Environment vars work" }, process.env),
useConpty: true
});

ptyProcess.onData(data => console.log(` data: ${data.replace(/\x1b|\n|\r/g, '_')}`));

setInterval(() => {
ptyProcess.write('echo foo\r'.repeat(50));
}, 10);
setTimeout(() => {
console.log(` killing ${ptyProcess.pid}...`);
ptyProcess.kill();
}, 100);
}, 1200);
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce"
integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==

"@types/node@8":
version "8.10.49"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.49.tgz#f331afc5efed0796798e5591d6e0ece636969b7b"
integrity sha512-YX30JVx0PvSmJ3Eqr74fYLGeBxD+C7vIL20ek+GGGLJeUbVYRUW3EzyAXpIRA0K8c8o0UWqR/GwEFYiFoz1T8w==
"@types/node@12":
version "12.12.45"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.45.tgz#33d550d6da243652004b00cbf4f15997456a38e3"
integrity sha512-9w50wqeS0qQH9bo1iIRcQhDXRxoDzyAqCL5oJG+Nuu7cAoe6omGo+YDE0spAGK5sPrdLDhQLbQxq0DnxyndPKA==

"@typescript-eslint/eslint-plugin@^2.27.0":
version "2.27.0"
Expand Down

0 comments on commit b532aef

Please sign in to comment.