|
|
|
|
|
|
|
|
|
|
|
|
|
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)); |
|
} |
|
|