diff --git a/src/index.ts b/src/index.ts index 53d0322e5..964377668 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ * Copyright (c) 2018, Microsoft Corporation (MIT License). */ -import { ITerminal, IPtyOpenOptions, IPtyForkOptions, IWindowsPtyForkOptions } from './interfaces'; +import { ITerminal, IPtyOpenOptions, IPtyForkOptions, IWindowsPtyForkOptions, IConptyHandoffHandles } from './interfaces'; import { ArgvOrCommandLine } from './types'; let terminalCtor: any; @@ -44,6 +44,14 @@ export function open(options: IPtyOpenOptions): ITerminal { return terminalCtor.open(options); } +export function handoff(handoff: IConptyHandoffHandles, opt?: IWindowsPtyForkOptions): ITerminal { + opt = opt || {}; + opt.useConpty = true; + opt.useConptyDll = true; + opt.conptyHandoff = handoff; + return new terminalCtor(undefined, undefined, opt); +} + /** * Expose the native API when not Windows, note that this is not public API and * could be removed at any time. diff --git a/src/interfaces.ts b/src/interfaces.ts index 207bf15d5..521e543b0 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -121,6 +121,7 @@ export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { useConpty?: boolean; useConptyDll?: boolean; conptyInheritCursor?: boolean; + conptyHandoff?: IConptyHandoffHandles; } export interface IPtyOpenOptions { @@ -128,3 +129,12 @@ export interface IPtyOpenOptions { rows?: number; encoding?: string | null; } + +export interface IConptyHandoffHandles { + input?: number; + output?: number; + signal?: number; + ref?: number; + server?: number; // ConPty process + client?: number; // Shell process +} diff --git a/src/native.d.ts b/src/native.d.ts index 973aca57d..719c1d17b 100644 --- a/src/native.d.ts +++ b/src/native.d.ts @@ -8,6 +8,7 @@ interface IConptyNative { resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void; clear(ptyId: number, useConptyDll: boolean): void; kill(ptyId: number, useConptyDll: boolean): void; + handoff(input: number, output: number, signal: number, ref: number, server: number, client: number, onExitCallback: (exitCode: number) => void): IWinptyProcess; } interface IWinptyNative { @@ -28,15 +29,15 @@ interface IUnixNative { interface IConptyProcess { pty: number; fd: number; - conin: string; - conout: string; + conin: string | number; + conout: string | number; } interface IWinptyProcess { pty: number; fd: number; - conin: string; - conout: string; + conin: string | number; + conout: string | number; pid: number; innerPid: number; } diff --git a/src/shared/conout.ts b/src/shared/conout.ts index 7a7e05f85..f63b1ba49 100644 --- a/src/shared/conout.ts +++ b/src/shared/conout.ts @@ -4,6 +4,7 @@ export interface IWorkerData { conoutPipeName: string; + conoutFD?: number; } export const enum ConoutWorkerMessage { diff --git a/src/win/conpty.cc b/src/win/conpty.cc index b4b33d10b..ac0cb40db 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -12,6 +12,7 @@ #define NODE_ADDON_API_DISABLE_DEPRECATED #include +#include #include #include // PathCombine, PathIsRelative #include @@ -19,6 +20,8 @@ #include #include #include +#include +#include #include #include #include "path_util.h" @@ -34,6 +37,7 @@ typedef HRESULT (__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn, HANDLE typedef HRESULT (__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize); typedef HRESULT (__stdcall *PFNCLEARPSEUDOCONSOLE)(HPCON hpc); typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc); +typedef HRESULT (__stdcall *PFNPACKPSEUDOCONSOLE)(HANDLE hProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC); #endif @@ -105,8 +109,10 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { // Calling DisconnectNamedPipes here or in PtyKill results in a crash, // ref https://github.com/microsoft/node-pty/issues/512, // so we only call CloseHandle for now. - CloseHandle(baton->hIn); - CloseHandle(baton->hOut); + if (baton->hIn) + CloseHandle(baton->hIn); + if (baton->hOut) + CloseHandle(baton->hOut); auto status = tsfn.BlockingCall(exit_event, callback); // In main thread switch (status) { @@ -552,6 +558,65 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) { return env.Undefined(); } +static Napi::Value PtyHandoff(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 7 || + !info[0].IsNumber() || + !info[1].IsNumber() || + !info[2].IsNumber() || + !info[3].IsNumber() || + !info[4].IsNumber() || + !info[5].IsNumber() || + !info[6].IsFunction()) { + throw Napi::Error::New(env, "Usage: pty.handoff(input, output, signal, ref, server, client, exitCallback)"); + } + + HANDLE hIn = reinterpret_cast(info[0].As().Int64Value()); + HANDLE hOut = reinterpret_cast(info[1].As().Int64Value()); + HANDLE hSig = reinterpret_cast(info[2].As().Int64Value()); + HANDLE hRef = reinterpret_cast(info[3].As().Int64Value()); + HANDLE hServerProcess = reinterpret_cast(info[4].As().Int64Value()); + HANDLE hClientProcess = reinterpret_cast(info[5].As().Int64Value()); + Napi::Function exitCallback = info[6].As(); + + HPCON hpc = nullptr; + + HANDLE hLibrary = LoadConptyDll(info, true); + if (hLibrary != nullptr) + { + PFNPACKPSEUDOCONSOLE const pfnPackPseudoConsole = (PFNPACKPSEUDOCONSOLE)GetProcAddress( + (HMODULE)hLibrary, + "ConptyPackPseudoConsole"); + if (pfnPackPseudoConsole) + { + pfnPackPseudoConsole(hServerProcess, hRef, hSig, &hpc); + } + } + + if (!hpc) { + throw Napi::Error::New(env, "Failed to handoff conpty"); + } + + const int ptyId = InterlockedIncrement(&ptyCounter); + pty_baton* handle = new pty_baton(ptyId, nullptr, nullptr, hpc); + handle->hShell = hClientProcess; + ptyHandles.insert(ptyHandles.end(), handle); + + SetupExitCallback(env, exitCallback, handle); + + Napi::Object marshal = Napi::Object::New(env); + marshal.Set("pty", Napi::Number::New(env, ptyId)); + marshal.Set("fd", Napi::Number::New(env, -1)); + marshal.Set("conin", Napi::Number::New(env, uv_open_osfhandle(hIn))); + marshal.Set("conout", Napi::Number::New(env, uv_open_osfhandle(hOut))); + marshal.Set("innerPid", Napi::Number::New(env, GetProcessId(hClientProcess))); + marshal.Set("pid", Napi::Number::New(env, GetProcessId(hServerProcess))); + + return marshal; +} + /** * Init */ @@ -562,6 +627,7 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { exports.Set("resize", Napi::Function::New(env, PtyResize)); exports.Set("clear", Napi::Function::New(env, PtyClear)); exports.Set("kill", Napi::Function::New(env, PtyKill)); + exports.Set("handoff", Napi::Function::New(env, PtyHandoff)); return exports; }; diff --git a/src/windowsConoutConnection.ts b/src/windowsConoutConnection.ts index de0b6b498..7f338f5b2 100644 --- a/src/windowsConoutConnection.ts +++ b/src/windowsConoutConnection.ts @@ -37,9 +37,10 @@ export class ConoutConnection implements IDisposable { public get onReady(): IEvent { return this._onReady.event; } constructor( - private _conoutPipeName: string + private _conoutPipeName: string, + conoutFD: number | undefined, ) { - const workerData: IWorkerData = { conoutPipeName: _conoutPipeName }; + const workerData: IWorkerData = { conoutPipeName: _conoutPipeName, conoutFD }; const scriptPath = __dirname.replace('node_modules.asar', 'node_modules.asar.unpacked'); this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData }); this._worker.on('message', (message: ConoutWorkerMessage) => { diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index 5e8c062db..37d8e96a0 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -11,10 +11,22 @@ import { Socket } from 'net'; import { ArgvOrCommandLine } from './types'; import { fork } from 'child_process'; import { ConoutConnection } from './windowsConoutConnection'; +import { IConptyHandoffHandles } from './interfaces'; let conptyNative: IConptyNative; let winptyNative: IWinptyNative; +export interface IWindowsPtyAgentOptions { + file?: string; + args?: ArgvOrCommandLine; + env?: string[]; + cwd?: string; + cols?: number; + rows?: number; + + handoff?: IConptyHandoffHandles; +} + /** * The amount of time to wait for additional data after the conpty shell process has exited before * shutting down the socket. The timer will be reset if a new data event comes in after the timer @@ -46,12 +58,7 @@ export class WindowsPtyAgent { public get pty(): number { return this._pty; } constructor( - file: string, - args: ArgvOrCommandLine, - env: string[], - cwd: string, - cols: number, - rows: number, + opts: IWindowsPtyAgentOptions, debug: boolean, private _useConpty: boolean | undefined, private _useConptyDll: boolean = false, @@ -91,20 +98,39 @@ export class WindowsPtyAgent { } this._ptyNative = this._useConpty ? conptyNative : winptyNative; - // Sanitize input variable. - cwd = path.resolve(cwd); + const handoff = !!opts.handoff; + let commandLine: string, cwd: string, env: string[]; + let term: IConptyProcess | IWinptyProcess; - // Compose command line - const commandLine = argsToCommandLine(file, args); + if (handoff) { + if (!this._useConpty || !this._useConptyDll) { + throw new Error('Terminal handoff requires conpty and conpty dll'); + } + + const { input, output, signal, ref, server, client } = opts.handoff!; + + term = (this._ptyNative as IConptyNative).handoff(input!, output!, signal!, ref!, server!, client!, c => this._$onProcessExit(c)); // borrow IWinptyProcess - // Open pty session. - let term: IConptyProcess | IWinptyProcess; - if (this._useConpty) { - term = (this._ptyNative as IConptyNative).startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor, this._useConptyDll); - } 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; + } else { + const { file, args, cols, rows } = opts; + env = opts.env!; + + // Sanitize input variable. + cwd = path.resolve(opts.cwd!); + + // Compose command line + commandLine = argsToCommandLine(file!, args!); + + // Open pty session. + if (this._useConpty) { + term = (this._ptyNative as IConptyNative).startProcess(file!, cols!, rows!, debug, this._generatePipeName(), conptyInheritCursor, this._useConptyDll); + } 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; + } } // Not available on windows. @@ -118,7 +144,13 @@ export class WindowsPtyAgent { this._outSocket = new Socket(); this._outSocket.setEncoding('utf8'); // The conout socket must be ready out on another thread to avoid deadlocks - this._conoutSocketWorker = new ConoutConnection(term.conout); + let { conout } = term; + let conoutFD: number | undefined; + if (typeof conout === 'number') { + conoutFD = conout; + conout = "\\\\.\\pipe\\" + this._generatePipeName() + "-out"; + } + this._conoutSocketWorker = new ConoutConnection(conout, conoutFD); this._conoutSocketWorker.onReady(() => { this._conoutSocketWorker.connectSocket(this._outSocket); }); @@ -126,7 +158,7 @@ export class WindowsPtyAgent { this._outSocket.emit('ready_datapipe'); }); - const inSocketFD = fs.openSync(term.conin, 'w'); + const inSocketFD = typeof term.conin === 'number' ? term.conin : fs.openSync(term.conin, 'w'); this._inSocket = new Socket({ fd: inSocketFD, readable: false, @@ -134,8 +166,8 @@ export class WindowsPtyAgent { }); this._inSocket.setEncoding('utf8'); - if (this._useConpty) { - const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, c => this._$onProcessExit(c)); + if (this._useConpty && !handoff) { + const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine!, cwd!, env!, c => this._$onProcessExit(c)); this._innerPid = connect.pid; } } diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts index 2ef9feddb..8ac762f24 100644 --- a/src/windowsTerminal.ts +++ b/src/windowsTerminal.ts @@ -6,7 +6,7 @@ import { Socket } from 'net'; import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal'; -import { WindowsPtyAgent } from './windowsPtyAgent'; +import { WindowsPtyAgent, IWindowsPtyAgentOptions } from './windowsPtyAgent'; import { IPtyOpenOptions, IWindowsPtyForkOptions } from './interfaces'; import { ArgvOrCommandLine } from './types'; import { assign } from './utils'; @@ -25,21 +25,27 @@ export class WindowsTerminal extends Terminal { this._checkType('args', args, 'string', true); // Initialize arguments - args = args || []; - file = file || DEFAULT_FILE; opt = opt || {}; - opt.env = opt.env || process.env; + + const agentOpt: IWindowsPtyAgentOptions = {}; + if (opt.conptyHandoff) { + agentOpt.handoff = opt.conptyHandoff; + this._name = opt.name || DEFAULT_NAME; + } else { + agentOpt.args = args || []; + this._file = agentOpt.file = file || DEFAULT_FILE; + const env = assign({}, opt.env || process.env); + this._name = opt.name || env.TERM || DEFAULT_NAME; + agentOpt.env = this._parseEnv(env); + agentOpt.cwd = opt.cwd || process.cwd(); + } if (opt.encoding) { console.warn('Setting encoding on Windows is not supported'); } - const env = assign({}, opt.env); - this._cols = opt.cols || DEFAULT_COLS; - this._rows = opt.rows || DEFAULT_ROWS; - const cwd = opt.cwd || process.cwd(); - const name = opt.name || env.TERM || DEFAULT_NAME; - const parsedEnv = this._parseEnv(env); + this._cols = agentOpt.cols = opt.cols || DEFAULT_COLS; + this._rows = agentOpt.rows = opt.rows || DEFAULT_ROWS; // If the terminal is ready this._isReady = false; @@ -48,7 +54,7 @@ export class WindowsTerminal extends Terminal { this._deferreds = []; // Create new termal. - this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor); + this._agent = new WindowsPtyAgent(agentOpt, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor); this._socket = this._agent.outSocket; // Not available until `ready` event emitted. @@ -114,9 +120,6 @@ export class WindowsTerminal extends Terminal { }); - this._file = file; - this._name = name; - this._readable = true; this._writable = true; diff --git a/src/worker/conoutSocketWorker.ts b/src/worker/conoutSocketWorker.ts index cedd86fe2..cf7639a7f 100644 --- a/src/worker/conoutSocketWorker.ts +++ b/src/worker/conoutSocketWorker.ts @@ -6,10 +6,14 @@ 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(); +const { conoutPipeName, conoutFD } = workerData as IWorkerData; +const conoutSocket = new Socket(conoutFD ? { + fd: conoutFD, + readable: true, + writable: false +} : undefined); conoutSocket.setEncoding('utf8'); -conoutSocket.connect(conoutPipeName, () => { +const onConnect = () => { const server = createServer(workerSocket => { conoutSocket.pipe(workerSocket); }); @@ -19,4 +23,9 @@ conoutSocket.connect(conoutPipeName, () => { throw new Error('worker_threads parentPort is null'); } parentPort.postMessage(ConoutWorkerMessage.READY); -}); +}; +if (conoutFD) { + onConnect(); +} else { + conoutSocket.connect(conoutPipeName, onConnect); +} diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index 7a14ec647..fed34b4f0 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -17,6 +17,8 @@ declare module 'node-pty' { */ export function spawn(file: string, args: string[] | string, options: IPtyForkOptions | IWindowsPtyForkOptions): IPty; + export function handoff(handoff: IConptyHandoffHandles, opt?: IWindowsPtyForkOptions): IPty; + export interface IBasePtyForkOptions { /** @@ -110,6 +112,15 @@ declare module 'node-pty' { conptyInheritCursor?: boolean; } + export interface IConptyHandoffHandles { + input?: number; + output?: number; + signal?: number; + ref?: number; + server?: number; // ConPty process + client?: number; // Shell process + } + /** * An interface representing a pseudoterminal, on Windows this is emulated via the winpty library. */