| | |
| | |
| | |
| | |
| | |
| |
|
| | import * as os from 'os'; |
| | import * as path from 'path'; |
| | import { Socket } from 'net'; |
| | import { ArgvOrCommandLine } from './types'; |
| | import { fork } from 'child_process'; |
| |
|
| | let conptyNative: IConptyNative; |
| | let winptyNative: IWinptyNative; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const FLUSH_DATA_INTERVAL = 20; |
| |
|
| | |
| | |
| | |
| | |
| | export class WindowsPtyAgent { |
| | private _inSocket: Socket; |
| | private _outSocket: Socket; |
| | private _pid: number; |
| | private _innerPid: number; |
| | private _innerPidHandle: number; |
| | private _closeTimeout: NodeJS.Timer; |
| | private _exitCode: number | undefined; |
| |
|
| | private _fd: any; |
| | private _pty: number; |
| | private _ptyNative: IConptyNative | IWinptyNative; |
| |
|
| | public get inSocket(): Socket { return this._inSocket; } |
| | public get outSocket(): Socket { return this._outSocket; } |
| | public get fd(): any { return this._fd; } |
| | public get innerPid(): number { return this._innerPid; } |
| | public get pty(): number { return this._pty; } |
| |
|
| | constructor( |
| | file: string, |
| | args: ArgvOrCommandLine, |
| | env: string[], |
| | cwd: string, |
| | cols: number, |
| | rows: number, |
| | debug: boolean, |
| | private _useConpty: boolean | undefined, |
| | conptyInheritCursor: boolean = false |
| | ) { |
| | if (this._useConpty === undefined || this._useConpty === true) { |
| | this._useConpty = this._getWindowsBuildNumber() >= 18309; |
| | } |
| | if (this._useConpty) { |
| | if (!conptyNative) { |
| | try { |
| | conptyNative = require('../build/Release/conpty.node'); |
| | } catch (outerError) { |
| | try { |
| | conptyNative = require('../build/Debug/conpty.node'); |
| | } catch (innerError) { |
| | console.error('innerError', innerError); |
| | |
| | throw outerError; |
| | } |
| | } |
| | } |
| | } else { |
| | if (!winptyNative) { |
| | try { |
| | winptyNative = require('../build/Release/pty.node'); |
| | } catch (outerError) { |
| | try { |
| | winptyNative = require('../build/Debug/pty.node'); |
| | } catch (innerError) { |
| | console.error('innerError', innerError); |
| | |
| | throw outerError; |
| | } |
| | } |
| | } |
| | } |
| | this._ptyNative = this._useConpty ? conptyNative : winptyNative; |
| |
|
| | |
| | cwd = path.resolve(cwd); |
| |
|
| | |
| | const commandLine = argsToCommandLine(file, args); |
| |
|
| | |
| | let term: IConptyProcess | IWinptyProcess; |
| | if (this._useConpty) { |
| | term = (this._ptyNative as IConptyNative).startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor); |
| | } else { |
| | term = (this._ptyNative as IWinptyNative).startProcess(file, commandLine, env, cwd, cols, rows, debug); |
| | this._pid = (term as IWinptyProcess).pid; |
| | this._innerPid = (term as IWinptyProcess).innerPid; |
| | this._innerPidHandle = (term as IWinptyProcess).innerPidHandle; |
| | } |
| |
|
| | |
| | this._fd = term.fd; |
| |
|
| | |
| | |
| | this._pty = term.pty; |
| |
|
| | |
| | this._outSocket = new Socket(); |
| | this._outSocket.setEncoding('utf8'); |
| | this._outSocket.connect(term.conout, () => { |
| | |
| |
|
| | |
| | this._outSocket.emit('ready_datapipe'); |
| | }); |
| |
|
| | this._inSocket = new Socket(); |
| | this._inSocket.setEncoding('utf8'); |
| | this._inSocket.connect(term.conin); |
| | |
| |
|
| | if (this._useConpty) { |
| | const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, c => this._$onProcessExit(c) |
| | ); |
| | this._innerPid = connect.pid; |
| | } |
| | } |
| |
|
| | public resize(cols: number, rows: number): void { |
| | if (this._useConpty) { |
| | if (this._exitCode !== undefined) { |
| | throw new Error('Cannot resize a pty that has already exited'); |
| | } |
| | this._ptyNative.resize(this._pty, cols, rows); |
| | return; |
| | } |
| | this._ptyNative.resize(this._pid, cols, rows); |
| | } |
| |
|
| | public kill(): void { |
| | this._inSocket.readable = false; |
| | this._inSocket.writable = false; |
| | this._outSocket.readable = false; |
| | this._outSocket.writable = false; |
| | |
| | if (this._useConpty) { |
| | this._getConsoleProcessList().then(consoleProcessList => { |
| | consoleProcessList.forEach((pid: number) => { |
| | try { |
| | process.kill(pid); |
| | } catch (e) { |
| | |
| | } |
| | }); |
| | (this._ptyNative as IConptyNative).kill(this._pty); |
| | }); |
| | } else { |
| | (this._ptyNative as IWinptyNative).kill(this._pid, this._innerPidHandle); |
| | |
| | |
| | |
| | |
| | |
| | |
| | const processList: number[] = (this._ptyNative as IWinptyNative).getProcessList(this._pid); |
| | processList.forEach(pid => { |
| | try { |
| | process.kill(pid); |
| | } catch (e) { |
| | |
| | } |
| | }); |
| | } |
| | } |
| |
|
| | private _getConsoleProcessList(): Promise<number[]> { |
| | return new Promise<number[]>(resolve => { |
| | const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [ this._innerPid.toString() ]); |
| | agent.on('message', message => { |
| | clearTimeout(timeout); |
| | resolve(message.consoleProcessList); |
| | }); |
| | const timeout = setTimeout(() => { |
| | |
| | agent.kill(); |
| | resolve([ this._innerPid ]); |
| | }, 5000); |
| | }); |
| | } |
| |
|
| | public get exitCode(): number { |
| | if (this._useConpty) { |
| | return this._exitCode; |
| | } |
| | return (this._ptyNative as IWinptyNative).getExitCode(this._innerPidHandle); |
| | } |
| |
|
| | private _getWindowsBuildNumber(): number { |
| | const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release()); |
| | let buildNumber: number = 0; |
| | if (osVersion && osVersion.length === 4) { |
| | buildNumber = parseInt(osVersion[3]); |
| | } |
| | return buildNumber; |
| | } |
| |
|
| | private _generatePipeName(): string { |
| | return `conpty-${Math.random() * 10000000}`; |
| | } |
| |
|
| | |
| | |
| | |
| | private _$onProcessExit(exitCode: number): void { |
| | this._exitCode = exitCode; |
| | this._flushDataAndCleanUp(); |
| | this._outSocket.on('data', () => this._flushDataAndCleanUp()); |
| | } |
| |
|
| | private _flushDataAndCleanUp(): void { |
| | if (this._closeTimeout) { |
| | clearTimeout(this._closeTimeout); |
| | } |
| | this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL); |
| | } |
| |
|
| | private _cleanUpProcess(): void { |
| | this._inSocket.readable = false; |
| | this._inSocket.writable = false; |
| | this._outSocket.readable = false; |
| | this._outSocket.writable = false; |
| | this._outSocket.destroy(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | export function argsToCommandLine(file: string, args: ArgvOrCommandLine): string { |
| | if (isCommandLine(args)) { |
| | if (args.length === 0) { |
| | return file; |
| | } |
| | return `${argsToCommandLine(file, [])} ${args}`; |
| | } |
| | const argv = [file]; |
| | Array.prototype.push.apply(argv, args); |
| | let result = ''; |
| | for (let argIndex = 0; argIndex < argv.length; argIndex++) { |
| | if (argIndex > 0) { |
| | result += ' '; |
| | } |
| | const arg = argv[argIndex]; |
| | |
| | const hasLopsidedEnclosingQuote = xOr((arg[0] !== '"'), (arg[arg.length - 1] !== '"')); |
| | const hasNoEnclosingQuotes = ((arg[0] !== '"') && (arg[arg.length - 1] !== '"')); |
| | const quote = |
| | arg === '' || |
| | (arg.indexOf(' ') !== -1 || |
| | arg.indexOf('\t') !== -1) && |
| | ((arg.length > 1) && |
| | (hasLopsidedEnclosingQuote || hasNoEnclosingQuotes)); |
| | if (quote) { |
| | result += '\"'; |
| | } |
| | let bsCount = 0; |
| | for (let i = 0; i < arg.length; i++) { |
| | const p = arg[i]; |
| | if (p === '\\') { |
| | bsCount++; |
| | } else if (p === '"') { |
| | result += repeatText('\\', bsCount * 2 + 1); |
| | result += '"'; |
| | bsCount = 0; |
| | } else { |
| | result += repeatText('\\', bsCount); |
| | bsCount = 0; |
| | result += p; |
| | } |
| | } |
| | if (quote) { |
| | result += repeatText('\\', bsCount * 2); |
| | result += '\"'; |
| | } else { |
| | result += repeatText('\\', bsCount); |
| | } |
| | } |
| | return result; |
| | } |
| |
|
| | function isCommandLine(args: ArgvOrCommandLine): args is string { |
| | return typeof args === 'string'; |
| | } |
| |
|
| | function repeatText(text: string, count: number): string { |
| | let result = ''; |
| | for (let i = 0; i < count; i++) { |
| | result += text; |
| | } |
| | return result; |
| | } |
| |
|
| | function xOr(arg1: boolean, arg2: boolean): boolean { |
| | return ((arg1 && !arg2) || (!arg1 && arg2)); |
| | } |
| |
|