Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for conpty terminal handoff #725

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,20 @@ export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
useConpty?: boolean;
useConptyDll?: boolean;
conptyInheritCursor?: boolean;
conptyHandoff?: IConptyHandoffHandles;
}

export interface IPtyOpenOptions {
cols?: number;
rows?: number;
encoding?: string | null;
}

export interface IConptyHandoffHandles {
input?: number;
output?: number;
signal?: number;
ref?: number;
server?: number; // ConPty process
client?: number; // Shell process
}
9 changes: 5 additions & 4 deletions src/native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/shared/conout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

export interface IWorkerData {
conoutPipeName: string;
conoutFD?: number;
}

export const enum ConoutWorkerMessage {
Expand Down
70 changes: 68 additions & 2 deletions src/win/conpty.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@

#define NODE_ADDON_API_DISABLE_DEPRECATED
#include <node_api.h>
#include <uv.h>
#include <assert.h>
#include <Shlwapi.h> // PathCombine, PathIsRelative
#include <sstream>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <io.h>
#include <fcntl.h>
#include <Windows.h>
#include <strsafe.h>
#include "path_util.h"
Expand All @@ -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

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<HANDLE>(info[0].As<Napi::Number>().Int64Value());
HANDLE hOut = reinterpret_cast<HANDLE>(info[1].As<Napi::Number>().Int64Value());
HANDLE hSig = reinterpret_cast<HANDLE>(info[2].As<Napi::Number>().Int64Value());
HANDLE hRef = reinterpret_cast<HANDLE>(info[3].As<Napi::Number>().Int64Value());
HANDLE hServerProcess = reinterpret_cast<HANDLE>(info[4].As<Napi::Number>().Int64Value());
HANDLE hClientProcess = reinterpret_cast<HANDLE>(info[5].As<Napi::Number>().Int64Value());
Napi::Function exitCallback = info[6].As<Napi::Function>();

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
*/
Expand All @@ -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;
};

Expand Down
5 changes: 3 additions & 2 deletions src/windowsConoutConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ export class ConoutConnection implements IDisposable {
public get onReady(): IEvent<void> { 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) => {
Expand Down
72 changes: 52 additions & 20 deletions src/windowsPtyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -118,24 +144,30 @@ 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);
});
this._outSocket.on('connect', () => {
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,
writable: true
});
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;
}
}
Expand Down
31 changes: 17 additions & 14 deletions src/windowsTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -114,9 +120,6 @@ export class WindowsTerminal extends Terminal {

});

this._file = file;
this._name = name;

this._readable = true;
this._writable = true;

Expand Down
Loading
Loading