Rewrite terminal.cpp

Rewrites the Terminal class as a GDExtension to be used directly in
Godot without a terminal.gd proxy.

Breaks a lot of things in its current state (e.g. signals and other
functions have not be implemented yet), but does add support for
transparent colors and true color inversion. It also seems to
be about 4x faster (FPS-wise) than the old version with some basic
stress testing.

Old source code has been moved to a different directory to be copied
over and/or rewritten piece by piece.
This commit is contained in:
Leroy Hopson 2024-02-07 00:01:14 +13:00
parent 7d2e22530e
commit a849423096
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
29 changed files with 1431 additions and 857 deletions

View file

@ -0,0 +1,433 @@
/**
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
* Copyright (c) 2021, Leroy Hopson (MIT License).
*
* pty.cc:
* This file is responsible for starting processes
* with pseudo-terminal file descriptors.
*/
// node versions lower than 10 define this as 0x502 which disables many of the
// definitions needed to compile
#include <node_version.h>
#if NODE_MODULE_VERSION <= 57
#define _WIN32_WINNT 0x600
#endif
#include "path_util.h"
#include <Shlwapi.h> // PathCombine, PathIsRelative
#include <Windows.h>
#include <iostream>
#include <nan.h>
#include <sstream>
#include <string>
#include <strsafe.h>
#include <vector>
extern "C" void init(v8::Local<v8::Object>);
// Taken from the RS5 Windows SDK, but redefined here in case we're targeting <=
// 17134
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
ProcThreadAttributeValue(22, FALSE, TRUE, FALSE)
typedef VOID *HPCON;
typedef HRESULT(__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn,
HANDLE hOut, DWORD dwFlags,
HPCON *phpcon);
typedef HRESULT(__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize);
typedef void(__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc);
#endif
struct pty_baton {
int id;
HANDLE hIn;
HANDLE hOut;
HPCON hpc;
HANDLE hShell;
HANDLE hWait;
Nan::Callback cb;
uv_async_t async;
uv_thread_t tid;
pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc)
: id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc){};
};
static std::vector<pty_baton *> ptyHandles;
static volatile LONG ptyCounter;
static pty_baton *get_pty_baton(int id) {
for (size_t i = 0; i < ptyHandles.size(); ++i) {
pty_baton *ptyHandle = ptyHandles[i];
if (ptyHandle->id == id) {
return ptyHandle;
}
}
return nullptr;
}
template <typename T>
std::vector<T> vectorFromString(const std::basic_string<T> &str) {
return std::vector<T>(str.begin(), str.end());
}
void throwNanError(const Nan::FunctionCallbackInfo<v8::Value> *info,
const char *text, const bool getLastError) {
std::stringstream errorText;
errorText << text;
if (getLastError) {
errorText << ", error code: " << GetLastError();
}
Nan::ThrowError(errorText.str().c_str());
(*info).GetReturnValue().SetUndefined();
}
// Returns a new server named pipe. It has not yet been connected.
bool createDataServerPipe(bool write, std::wstring kind, HANDLE *hServer,
std::wstring &name, const std::wstring &pipeName) {
*hServer = INVALID_HANDLE_VALUE;
name = L"\\\\.\\pipe\\" + pipeName + L"-" + kind;
const DWORD winOpenMode =
PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND |
FILE_FLAG_FIRST_PIPE_INSTANCE /* | FILE_FLAG_OVERLAPPED */;
SECURITY_ATTRIBUTES sa = {};
sa.nLength = sizeof(sa);
*hServer = CreateNamedPipeW(name.c_str(),
/*dwOpenMode=*/winOpenMode,
/*dwPipeMode=*/PIPE_TYPE_BYTE |
PIPE_READMODE_BYTE | PIPE_WAIT,
/*nMaxInstances=*/1,
/*nOutBufferSize=*/0,
/*nInBufferSize=*/0,
/*nDefaultTimeOut=*/30000, &sa);
return *hServer != INVALID_HANDLE_VALUE;
}
HRESULT CreateNamedPipesAndPseudoConsole(COORD size, DWORD dwFlags,
HANDLE *phInput, HANDLE *phOutput,
HPCON *phPC, std::wstring &inName,
std::wstring &outName,
const std::wstring &pipeName) {
HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
bool fLoadedDll = hLibrary != nullptr;
if (fLoadedDll) {
PFNCREATEPSEUDOCONSOLE const pfnCreate =
(PFNCREATEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,
"CreatePseudoConsole");
if (pfnCreate) {
if (phPC == NULL || phInput == NULL || phOutput == NULL) {
return E_INVALIDARG;
}
bool success =
createDataServerPipe(true, L"in", phInput, inName, pipeName);
if (!success) {
return HRESULT_FROM_WIN32(GetLastError());
}
success =
createDataServerPipe(false, L"out", phOutput, outName, pipeName);
if (!success) {
return HRESULT_FROM_WIN32(GetLastError());
}
return pfnCreate(size, *phInput, *phOutput, dwFlags, phPC);
} else {
// Failed to find CreatePseudoConsole in kernel32. This is likely because
// the user is not running a build of Windows that supports that API.
// We should fall back to winpty in this case.
return HRESULT_FROM_WIN32(GetLastError());
}
}
// Failed to find kernel32. This is realy unlikely - honestly no idea how
// this is even possible to hit. But if it does happen, fall back to
// winpty.
return HRESULT_FROM_WIN32(GetLastError());
}
static NAN_METHOD(PtyStartProcess) {
Nan::HandleScope scope;
v8::Local<v8::Object> marshal;
std::wstring inName, outName;
BOOL fSuccess = FALSE;
std::unique_ptr<wchar_t[]> mutableCommandline;
PROCESS_INFORMATION _piClient{};
if (info.Length() != 6 || !info[0]->IsString() || !info[1]->IsNumber() ||
!info[2]->IsNumber() || !info[3]->IsBoolean() || !info[4]->IsString() ||
!info[5]->IsBoolean()) {
Nan::ThrowError("Usage: pty.startProcess(file, cols, rows, debug, "
"pipeName, inheritCursor)");
return;
}
const std::wstring filename(path_util::to_wstring(Nan::Utf8String(info[0])));
const SHORT cols = info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust();
const SHORT rows = info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust();
const bool debug = Nan::To<bool>(info[3]).FromJust();
const std::wstring pipeName(path_util::to_wstring(Nan::Utf8String(info[4])));
const bool inheritCursor = Nan::To<bool>(info[5]).FromJust();
// use environment 'Path' variable to determine location of
// the relative path that we have recieved (e.g cmd.exe)
std::wstring shellpath;
if (::PathIsRelativeW(filename.c_str())) {
shellpath = path_util::get_shell_path(filename.c_str());
} else {
shellpath = filename;
}
std::string shellpath_(shellpath.begin(), shellpath.end());
if (shellpath.empty() || !path_util::file_exists(shellpath)) {
std::stringstream why;
why << "File not found: " << shellpath_;
Nan::ThrowError(why.str().c_str());
return;
}
HANDLE hIn, hOut;
HPCON hpc;
HRESULT hr = CreateNamedPipesAndPseudoConsole(
{cols, rows}, inheritCursor ? 1 /*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0,
&hIn, &hOut, &hpc, inName, outName, pipeName);
// Restore default handling of ctrl+c
SetConsoleCtrlHandler(NULL, FALSE);
// Set return values
marshal = Nan::New<v8::Object>();
if (SUCCEEDED(hr)) {
// We were able to instantiate a conpty
const int ptyId = InterlockedIncrement(&ptyCounter);
Nan::Set(marshal, Nan::New<v8::String>("pty").ToLocalChecked(),
Nan::New<v8::Number>(ptyId));
ptyHandles.insert(ptyHandles.end(), new pty_baton(ptyId, hIn, hOut, hpc));
} else {
Nan::ThrowError("Cannot launch conpty");
return;
}
Nan::Set(marshal, Nan::New<v8::String>("fd").ToLocalChecked(),
Nan::New<v8::Number>(-1));
{
std::string coninPipeNameStr(inName.begin(), inName.end());
Nan::Set(marshal, Nan::New<v8::String>("conin").ToLocalChecked(),
Nan::New<v8::String>(coninPipeNameStr).ToLocalChecked());
std::string conoutPipeNameStr(outName.begin(), outName.end());
Nan::Set(marshal, Nan::New<v8::String>("conout").ToLocalChecked(),
Nan::New<v8::String>(conoutPipeNameStr).ToLocalChecked());
}
info.GetReturnValue().Set(marshal);
}
VOID CALLBACK OnProcessExitWinEvent(_In_ PVOID context,
_In_ BOOLEAN TimerOrWaitFired) {
pty_baton *baton = static_cast<pty_baton *>(context);
// Fire OnProcessExit
uv_async_send(&baton->async);
}
static void OnProcessExit(uv_async_t *async) {
Nan::HandleScope scope;
pty_baton *baton = static_cast<pty_baton *>(async->data);
UnregisterWait(baton->hWait);
// Get exit code
DWORD exitCode = 0;
GetExitCodeProcess(baton->hShell, &exitCode);
// Call function
v8::Local<v8::Value> args[1] = {Nan::New<v8::Number>(exitCode)};
Nan::AsyncResource asyncResource("node-pty.callback");
baton->cb.Call(1, args, &asyncResource);
// Clean up
baton->cb.Reset();
}
static NAN_METHOD(PtyConnect) {
Nan::HandleScope scope;
// If we're working with conpty's we need to call ConnectNamedPipe here AFTER
// the Socket has attempted to connect to the other end, then actually
// spawn the process here.
std::stringstream errorText;
BOOL fSuccess = FALSE;
if (info.Length() != 5 || !info[0]->IsNumber() || !info[1]->IsString() ||
!info[2]->IsString() || !info[3]->IsArray() || !info[4]->IsFunction()) {
Nan::ThrowError("Usage: pty.connect(id, cmdline, cwd, env, exitCallback)");
return;
}
const int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
const std::wstring cmdline(path_util::to_wstring(Nan::Utf8String(info[1])));
const std::wstring cwd(path_util::to_wstring(Nan::Utf8String(info[2])));
const v8::Local<v8::Array> envValues = info[3].As<v8::Array>();
const v8::Local<v8::Function> exitCallback =
v8::Local<v8::Function>::Cast(info[4]);
// Prepare command line
std::unique_ptr<wchar_t[]> mutableCommandline =
std::make_unique<wchar_t[]>(cmdline.length() + 1);
HRESULT hr = StringCchCopyW(mutableCommandline.get(), cmdline.length() + 1,
cmdline.c_str());
// Prepare cwd
std::unique_ptr<wchar_t[]> mutableCwd =
std::make_unique<wchar_t[]>(cwd.length() + 1);
hr = StringCchCopyW(mutableCwd.get(), cwd.length() + 1, cwd.c_str());
// Prepare environment
std::wstring env;
if (!envValues.IsEmpty()) {
std::wstringstream envBlock;
for (uint32_t i = 0; i < envValues->Length(); i++) {
std::wstring envValue(path_util::to_wstring(
Nan::Utf8String(Nan::Get(envValues, i).ToLocalChecked())));
envBlock << envValue << L'\0';
}
envBlock << L'\0';
env = envBlock.str();
}
auto envV = vectorFromString(env);
LPWSTR envArg = envV.empty() ? nullptr : envV.data();
// Fetch pty handle from ID and start process
pty_baton *handle = get_pty_baton(id);
BOOL success = ConnectNamedPipe(handle->hIn, nullptr);
success = ConnectNamedPipe(handle->hOut, nullptr);
// Attach the pseudoconsole to the client application we're creating
STARTUPINFOEXW siEx{0};
siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW);
siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
siEx.StartupInfo.hStdError = nullptr;
siEx.StartupInfo.hStdInput = nullptr;
siEx.StartupInfo.hStdOutput = nullptr;
SIZE_T size = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
BYTE *attrList = new BYTE[size];
siEx.lpAttributeList =
reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList);
fSuccess =
InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size);
if (!fSuccess) {
return throwNanError(&info, "InitializeProcThreadAttributeList failed",
true);
}
fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
handle->hpc, sizeof(HPCON), NULL, NULL);
if (!fSuccess) {
return throwNanError(&info, "UpdateProcThreadAttribute failed", true);
}
PROCESS_INFORMATION piClient{};
fSuccess = !!CreateProcessW(
nullptr, mutableCommandline.get(),
nullptr, // lpProcessAttributes
nullptr, // lpThreadAttributes
false, // bInheritHandles VERY IMPORTANT that this is false
EXTENDED_STARTUPINFO_PRESENT |
CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags
envArg, // lpEnvironment
mutableCwd.get(), // lpCurrentDirectory
&siEx.StartupInfo, // lpStartupInfo
&piClient // lpProcessInformation
);
if (!fSuccess) {
return throwNanError(&info, "Cannot create process", true);
}
// Update handle
handle->hShell = piClient.hProcess;
handle->cb.Reset(exitCallback);
handle->async.data = handle;
// Setup OnProcessExit callback
uv_async_init(uv_default_loop(), &handle->async, OnProcessExit);
// Setup Windows wait for process exit event
RegisterWaitForSingleObject(&handle->hWait, piClient.hProcess,
OnProcessExitWinEvent, (PVOID)handle, INFINITE,
WT_EXECUTEONLYONCE);
// Return
v8::Local<v8::Object> marshal = Nan::New<v8::Object>();
Nan::Set(marshal, Nan::New<v8::String>("pid").ToLocalChecked(),
Nan::New<v8::Number>(piClient.dwProcessId));
info.GetReturnValue().Set(marshal);
}
void ConPTY::resize(int id, int cols, int rows) {
// SHORT cols = info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust();
// SHORT rows = info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust();
const pty_baton *handle = get_pty_baton(id);
HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
bool fLoadedDll = hLibrary != nullptr;
if (fLoadedDll) {
PFNRESIZEPSEUDOCONSOLE const pfnResizePseudoConsole =
(PFNRESIZEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,
"ResizePseudoConsole");
if (pfnResizePseudoConsole) {
COORD size = {cols, rows};
pfnResizePseudoConsole(handle->hpc, size);
}
}
}
void ConPTY::kill(int id) {
const pty_baton *handle = get_pty_baton(id);
HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
bool fLoadedDll = hLibrary != nullptr;
if (fLoadedDll) {
PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole =
(PFNCLOSEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,
"ClosePseudoConsole");
if (pfnClosePseudoConsole) {
pfnClosePseudoConsole(handle->hpc);
}
}
CloseHandle(handle->hShell);
return info.GetReturnValue().SetUndefined();
}
/**
* Init
*/
void ConPTY::_register_methods() {
register_method("_init", &ConPTY::_init);
register_method("start_process", &ConPTY::start_process);
register_method("connect_to_named_pipe", &ConPTY::connect_to_named_pipe);
register_method("resize", &ConPTY::resize);
register_method("kill", &ConPTY::kill);
};
void ConPTY::_init() {}

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2021 Leroy Hopson <godot-xterm@leroy.geek.nz>
// SPDX-License-Identifier: MIT
#ifndef GODOT_XTERM_CONPTY_H
#define GODOT_XTERM_CONPTY_H
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/callable.hpp>
namespace godot {
class ConPTY : public RefCounted {
GDCLASS(ConPTY, RefCounted)
public:
// Array fork(String file,
// int _ignored, /* FIXME: For some reason Pipe throws
// ENOTSOCK in read callback if args (or another
// non-empty, non-zero) value is in this position. */
// PoolStringArray args, PoolStringArray env, String cwd, int
// cols, int rows, int uid, int gid, bool utf8, Ref<FuncRef>
// on_exit);
// Array open(int cols, int rows);
void resize(int id, int cols, int rows);
void kill(int id);
// String process(int fd, String tty);
void _init();
protected:
static void _bind_methods();
};
} // namespace godot
#endif // GODOT_XTERM_CONPTY_H

View file

@ -0,0 +1,43 @@
/**
* Copyright (c) 2019, Microsoft Corporation (MIT License).
*/
#include <nan.h>
#include <windows.h>
static NAN_METHOD(ApiConsoleProcessList) {
if (info.Length() != 1 || !info[0]->IsNumber()) {
Nan::ThrowError("Usage: getConsoleProcessList(shellPid)");
return;
}
const SHORT pid = info[0]->Uint32Value(Nan::GetCurrentContext()).FromJust();
if (!FreeConsole()) {
Nan::ThrowError("FreeConsole failed");
}
if (!AttachConsole(pid)) {
Nan::ThrowError("AttachConsole failed");
}
auto processList = std::vector<DWORD>(64);
auto processCount =
GetConsoleProcessList(&processList[0], processList.size());
if (processList.size() < processCount) {
processList.resize(processCount);
processCount = GetConsoleProcessList(&processList[0], processList.size());
}
FreeConsole();
v8::Local<v8::Array> result = Nan::New<v8::Array>();
for (DWORD i = 0; i < processCount; i++) {
Nan::Set(result, i, Nan::New<v8::Number>(processList[i]));
}
info.GetReturnValue().Set(result);
}
extern "C" void init(v8::Local<v8::Object> target) {
Nan::HandleScope scope;
Nan::SetMethod(target, "getConsoleProcessList", ApiConsoleProcessList);
};
NODE_MODULE(pty, init);

View file

@ -0,0 +1,73 @@
/**
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
#include <Shlwapi.h> // PathCombine
#include <nan.h>
#include "path_util.h"
namespace path_util {
const wchar_t *to_wstring(const Nan::Utf8String &str) {
const char *bytes = *str;
unsigned int sizeOfStr = MultiByteToWideChar(CP_UTF8, 0, bytes, -1, NULL, 0);
wchar_t *output = new wchar_t[sizeOfStr];
MultiByteToWideChar(CP_UTF8, 0, bytes, -1, output, sizeOfStr);
return output;
}
bool file_exists(std::wstring filename) {
DWORD attr = ::GetFileAttributesW(filename.c_str());
if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) {
return false;
}
return true;
}
// cmd.exe -> C:\Windows\system32\cmd.exe
std::wstring get_shell_path(std::wstring filename) {
std::wstring shellpath;
if (file_exists(filename)) {
return shellpath;
}
wchar_t buffer_[MAX_ENV];
int read = ::GetEnvironmentVariableW(L"Path", buffer_, MAX_ENV);
if (!read) {
return shellpath;
}
std::wstring delimiter = L";";
size_t pos = 0;
std::vector<std::wstring> paths;
std::wstring buffer(buffer_);
while ((pos = buffer.find(delimiter)) != std::wstring::npos) {
paths.push_back(buffer.substr(0, pos));
buffer.erase(0, pos + delimiter.length());
}
const wchar_t *filename_ = filename.c_str();
for (int i = 0; i < paths.size(); ++i) {
std::wstring path = paths[i];
wchar_t searchPath[MAX_PATH];
::PathCombineW(searchPath, const_cast<wchar_t *>(path.c_str()), filename_);
if (searchPath == NULL) {
continue;
}
if (file_exists(searchPath)) {
shellpath = searchPath;
break;
}
}
return shellpath;
}
} // namespace path_util

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
#ifndef NODE_PTY_PATH_UTIL_H_
#define NODE_PTY_PATH_UTIL_H_
#include <nan.h>
#define MAX_ENV 65536
namespace path_util {
const wchar_t *to_wstring(const Nan::Utf8String &str);
bool file_exists(std::wstring filename);
std::wstring get_shell_path(std::wstring filename);
} // namespace path_util
#endif // NODE_PTY_PATH_UTIL_H_

View file

@ -0,0 +1,320 @@
/**
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*
* pty.cc:
* This file is responsible for starting processes
* with pseudo-terminal file descriptors.
*/
#include <Shlwapi.h> // PathCombine, PathIsRelative
#include <iostream>
#include <nan.h>
#include <sstream>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>
#include <winpty.h>
#include "path_util.h"
/**
* Misc
*/
extern "C" void init(v8::Local<v8::Object>);
#define WINPTY_DBG_VARIABLE TEXT("WINPTYDBG")
/**
* winpty
*/
static std::vector<winpty_t *> ptyHandles;
static volatile LONG ptyCounter;
/**
* Helpers
*/
static winpty_t *get_pipe_handle(int handle) {
for (size_t i = 0; i < ptyHandles.size(); ++i) {
winpty_t *ptyHandle = ptyHandles[i];
int current = (int)winpty_agent_process(ptyHandle);
if (current == handle) {
return ptyHandle;
}
}
return nullptr;
}
static bool remove_pipe_handle(int handle) {
for (size_t i = 0; i < ptyHandles.size(); ++i) {
winpty_t *ptyHandle = ptyHandles[i];
if ((int)winpty_agent_process(ptyHandle) == handle) {
winpty_free(ptyHandle);
ptyHandles.erase(ptyHandles.begin() + i);
ptyHandle = nullptr;
return true;
}
}
return false;
}
void throw_winpty_error(const char *generalMsg, winpty_error_ptr_t error_ptr) {
std::stringstream why;
std::wstring msg(winpty_error_msg(error_ptr));
std::string msg_(msg.begin(), msg.end());
why << generalMsg << ": " << msg_;
Nan::ThrowError(why.str().c_str());
winpty_error_free(error_ptr);
}
static NAN_METHOD(PtyGetExitCode) {
Nan::HandleScope scope;
if (info.Length() != 1 || !info[0]->IsNumber()) {
Nan::ThrowError("Usage: pty.getExitCode(pidHandle)");
return;
}
DWORD exitCode = 0;
GetExitCodeProcess(
(HANDLE)info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(),
&exitCode);
info.GetReturnValue().Set(Nan::New<v8::Number>(exitCode));
}
static NAN_METHOD(PtyGetProcessList) {
Nan::HandleScope scope;
if (info.Length() != 1 || !info[0]->IsNumber()) {
Nan::ThrowError("Usage: pty.getProcessList(pid)");
return;
}
int pid = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
winpty_t *pc = get_pipe_handle(pid);
if (pc == nullptr) {
info.GetReturnValue().Set(Nan::New<v8::Array>(0));
return;
}
int processList[64];
const int processCount = 64;
int actualCount =
winpty_get_console_process_list(pc, processList, processCount, nullptr);
v8::Local<v8::Array> result = Nan::New<v8::Array>(actualCount);
for (uint32_t i = 0; i < actualCount; i++) {
Nan::Set(result, i, Nan::New<v8::Number>(processList[i]));
}
info.GetReturnValue().Set(result);
}
static NAN_METHOD(PtyStartProcess) {
Nan::HandleScope scope;
if (info.Length() != 7 || !info[0]->IsString() || !info[1]->IsString() ||
!info[2]->IsArray() || !info[3]->IsString() || !info[4]->IsNumber() ||
!info[5]->IsNumber() || !info[6]->IsBoolean()) {
Nan::ThrowError(
"Usage: pty.startProcess(file, cmdline, env, cwd, cols, rows, debug)");
return;
}
std::stringstream why;
const wchar_t *filename = path_util::to_wstring(Nan::Utf8String(info[0]));
const wchar_t *cmdline = path_util::to_wstring(Nan::Utf8String(info[1]));
const wchar_t *cwd = path_util::to_wstring(Nan::Utf8String(info[3]));
// create environment block
std::wstring env;
const v8::Local<v8::Array> envValues = v8::Local<v8::Array>::Cast(info[2]);
if (!envValues.IsEmpty()) {
std::wstringstream envBlock;
for (uint32_t i = 0; i < envValues->Length(); i++) {
std::wstring envValue(path_util::to_wstring(
Nan::Utf8String(Nan::Get(envValues, i).ToLocalChecked())));
envBlock << envValue << L'\0';
}
env = envBlock.str();
}
// use environment 'Path' variable to determine location of
// the relative path that we have recieved (e.g cmd.exe)
std::wstring shellpath;
if (::PathIsRelativeW(filename)) {
shellpath = path_util::get_shell_path(filename);
} else {
shellpath = filename;
}
std::string shellpath_(shellpath.begin(), shellpath.end());
if (shellpath.empty() || !path_util::file_exists(shellpath)) {
why << "File not found: " << shellpath_;
Nan::ThrowError(why.str().c_str());
goto cleanup;
}
int cols = info[4]->Int32Value(Nan::GetCurrentContext()).FromJust();
int rows = info[5]->Int32Value(Nan::GetCurrentContext()).FromJust();
bool debug = Nan::To<bool>(info[6]).FromJust();
// Enable/disable debugging
SetEnvironmentVariable(WINPTY_DBG_VARIABLE,
debug ? "1" : NULL); // NULL = deletes variable
// Create winpty config
winpty_error_ptr_t error_ptr = nullptr;
winpty_config_t *winpty_config = winpty_config_new(0, &error_ptr);
if (winpty_config == nullptr) {
throw_winpty_error("Error creating WinPTY config", error_ptr);
goto cleanup;
}
winpty_error_free(error_ptr);
// Set pty size on config
winpty_config_set_initial_size(winpty_config, cols, rows);
// Start the pty agent
winpty_t *pc = winpty_open(winpty_config, &error_ptr);
winpty_config_free(winpty_config);
if (pc == nullptr) {
throw_winpty_error("Error launching WinPTY agent", error_ptr);
goto cleanup;
}
winpty_error_free(error_ptr);
// Save pty struct for later use
ptyHandles.insert(ptyHandles.end(), pc);
// Create winpty spawn config
winpty_spawn_config_t *config = winpty_spawn_config_new(
WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, shellpath.c_str(), cmdline, cwd,
env.c_str(), &error_ptr);
if (config == nullptr) {
throw_winpty_error("Error creating WinPTY spawn config", error_ptr);
goto cleanup;
}
winpty_error_free(error_ptr);
// Spawn the new process
HANDLE handle = nullptr;
BOOL spawnSuccess =
winpty_spawn(pc, config, &handle, nullptr, nullptr, &error_ptr);
winpty_spawn_config_free(config);
if (!spawnSuccess) {
throw_winpty_error("Unable to start terminal process", error_ptr);
goto cleanup;
}
winpty_error_free(error_ptr);
// Set return values
v8::Local<v8::Object> marshal = Nan::New<v8::Object>();
Nan::Set(marshal, Nan::New<v8::String>("innerPid").ToLocalChecked(),
Nan::New<v8::Number>((int)GetProcessId(handle)));
Nan::Set(marshal, Nan::New<v8::String>("innerPidHandle").ToLocalChecked(),
Nan::New<v8::Number>((int)handle));
Nan::Set(marshal, Nan::New<v8::String>("pid").ToLocalChecked(),
Nan::New<v8::Number>((int)winpty_agent_process(pc)));
Nan::Set(marshal, Nan::New<v8::String>("pty").ToLocalChecked(),
Nan::New<v8::Number>(InterlockedIncrement(&ptyCounter)));
Nan::Set(marshal, Nan::New<v8::String>("fd").ToLocalChecked(),
Nan::New<v8::Number>(-1));
{
LPCWSTR coninPipeName = winpty_conin_name(pc);
std::wstring coninPipeNameWStr(coninPipeName);
std::string coninPipeNameStr(coninPipeNameWStr.begin(),
coninPipeNameWStr.end());
Nan::Set(marshal, Nan::New<v8::String>("conin").ToLocalChecked(),
Nan::New<v8::String>(coninPipeNameStr).ToLocalChecked());
LPCWSTR conoutPipeName = winpty_conout_name(pc);
std::wstring conoutPipeNameWStr(conoutPipeName);
std::string conoutPipeNameStr(conoutPipeNameWStr.begin(),
conoutPipeNameWStr.end());
Nan::Set(marshal, Nan::New<v8::String>("conout").ToLocalChecked(),
Nan::New<v8::String>(conoutPipeNameStr).ToLocalChecked());
}
info.GetReturnValue().Set(marshal);
goto cleanup;
cleanup:
delete filename;
delete cmdline;
delete cwd;
}
static NAN_METHOD(PtyResize) {
Nan::HandleScope scope;
if (info.Length() != 3 || !info[0]->IsNumber() || !info[1]->IsNumber() ||
!info[2]->IsNumber()) {
Nan::ThrowError("Usage: pty.resize(pid, cols, rows)");
return;
}
int handle = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
int cols = info[1]->Int32Value(Nan::GetCurrentContext()).FromJust();
int rows = info[2]->Int32Value(Nan::GetCurrentContext()).FromJust();
winpty_t *pc = get_pipe_handle(handle);
if (pc == nullptr) {
Nan::ThrowError("The pty doesn't appear to exist");
return;
}
BOOL success = winpty_set_size(pc, cols, rows, nullptr);
if (!success) {
Nan::ThrowError("The pty could not be resized");
return;
}
return info.GetReturnValue().SetUndefined();
}
static NAN_METHOD(PtyKill) {
Nan::HandleScope scope;
if (info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsNumber()) {
Nan::ThrowError("Usage: pty.kill(pid, innerPidHandle)");
return;
}
int handle = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
HANDLE innerPidHandle =
(HANDLE)info[1]->Int32Value(Nan::GetCurrentContext()).FromJust();
winpty_t *pc = get_pipe_handle(handle);
if (pc == nullptr) {
Nan::ThrowError("Pty seems to have been killed already");
return;
}
assert(remove_pipe_handle(handle));
CloseHandle(innerPidHandle);
return info.GetReturnValue().SetUndefined();
}
/**
* Init
*/
extern "C" void init(v8::Local<v8::Object> target) {
Nan::HandleScope scope;
Nan::SetMethod(target, "startProcess", PtyStartProcess);
Nan::SetMethod(target, "resize", PtyResize);
Nan::SetMethod(target, "kill", PtyKill);
Nan::SetMethod(target, "getExitCode", PtyGetExitCode);
Nan::SetMethod(target, "getProcessList", PtyGetProcessList);
};
NODE_MODULE(pty, init);