2 * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
3 * Copyright (c) 2016, Daniel Imms (MIT License).
4 * Copyright (c) 2018, Microsoft Corporation (MIT License).
7 * This file is responsible for starting processes
8 * with pseudo-terminal file descriptors.
11 // node versions lower than 10 define this as 0x502 which disables many of the definitions needed to compile
12 #include <node_version.h>
13 #if NODE_MODULE_VERSION <= 57
14 #define _WIN32_WINNT 0x600
19 #include <Shlwapi.h> // PathCombine, PathIsRelative
25 #include "path_util.h"
27 extern "C" void init(v8::Local<v8::Object>);
29 // Taken from the RS5 Windows SDK, but redefined here in case we're targeting <= 17134
30 #ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
31 #define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
32 ProcThreadAttributeValue(22, FALSE, TRUE, FALSE)
35 typedef HRESULT (__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn, HANDLE hOut, DWORD dwFlags, HPCON* phpcon);
36 typedef HRESULT (__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize);
37 typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc);
53 pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {};
56 static std::vector<pty_baton*> ptyHandles;
57 static volatile LONG ptyCounter;
59 static pty_baton* get_pty_baton(int id) {
60 for (size_t i = 0; i < ptyHandles.size(); ++i) {
61 pty_baton* ptyHandle = ptyHandles[i];
62 if (ptyHandle->id == id) {
70 std::vector<T> vectorFromString(const std::basic_string<T> &str) {
71 return std::vector<T>(str.begin(), str.end());
74 void throwNanError(const Nan::FunctionCallbackInfo<v8::Value>* info, const char* text, const bool getLastError) {
75 std::stringstream errorText;
78 errorText << ", error code: " << GetLastError();
80 Nan::ThrowError(errorText.str().c_str());
81 (*info).GetReturnValue().SetUndefined();
84 // Returns a new server named pipe. It has not yet been connected.
85 bool createDataServerPipe(bool write,
89 const std::wstring &pipeName)
91 *hServer = INVALID_HANDLE_VALUE;
93 name = L"\\\\.\\pipe\\" + pipeName + L"-" + kind;
95 const DWORD winOpenMode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE/* | FILE_FLAG_OVERLAPPED */;
97 SECURITY_ATTRIBUTES sa = {};
98 sa.nLength = sizeof(sa);
100 *hServer = CreateNamedPipeW(
102 /*dwOpenMode=*/winOpenMode,
103 /*dwPipeMode=*/PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
105 /*nOutBufferSize=*/0,
107 /*nDefaultTimeOut=*/30000,
110 return *hServer != INVALID_HANDLE_VALUE;
113 HRESULT CreateNamedPipesAndPseudoConsole(COORD size,
118 std::wstring& inName,
119 std::wstring& outName,
120 const std::wstring& pipeName)
122 HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
123 bool fLoadedDll = hLibrary != nullptr;
126 PFNCREATEPSEUDOCONSOLE const pfnCreate = (PFNCREATEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "CreatePseudoConsole");
129 if (phPC == NULL || phInput == NULL || phOutput == NULL)
134 bool success = createDataServerPipe(true, L"in", phInput, inName, pipeName);
137 return HRESULT_FROM_WIN32(GetLastError());
139 success = createDataServerPipe(false, L"out", phOutput, outName, pipeName);
142 return HRESULT_FROM_WIN32(GetLastError());
144 return pfnCreate(size, *phInput, *phOutput, dwFlags, phPC);
148 // Failed to find CreatePseudoConsole in kernel32. This is likely because
149 // the user is not running a build of Windows that supports that API.
150 // We should fall back to winpty in this case.
151 return HRESULT_FROM_WIN32(GetLastError());
155 // Failed to find kernel32. This is realy unlikely - honestly no idea how
156 // this is even possible to hit. But if it does happen, fall back to winpty.
157 return HRESULT_FROM_WIN32(GetLastError());
160 static NAN_METHOD(PtyStartProcess) {
161 Nan::HandleScope scope;
163 v8::Local<v8::Object> marshal;
164 std::wstring inName, outName;
165 BOOL fSuccess = FALSE;
166 std::unique_ptr<wchar_t[]> mutableCommandline;
167 PROCESS_INFORMATION _piClient{};
169 if (info.Length() != 6 ||
170 !info[0]->IsString() ||
171 !info[1]->IsNumber() ||
172 !info[2]->IsNumber() ||
173 !info[3]->IsBoolean() ||
174 !info[4]->IsString() ||
175 !info[5]->IsBoolean()) {
176 Nan::ThrowError("Usage: pty.startProcess(file, cols, rows, debug, pipeName, inheritCursor)");
180 const std::wstring filename(path_util::to_wstring(Nan::Utf8String(info[0])));
181 const SHORT cols = info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust();
182 const SHORT rows = info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust();
183 const bool debug = Nan::To<bool>(info[3]).FromJust();
184 const std::wstring pipeName(path_util::to_wstring(Nan::Utf8String(info[4])));
185 const bool inheritCursor = Nan::To<bool>(info[5]).FromJust();
187 // use environment 'Path' variable to determine location of
188 // the relative path that we have recieved (e.g cmd.exe)
189 std::wstring shellpath;
190 if (::PathIsRelativeW(filename.c_str())) {
191 shellpath = path_util::get_shell_path(filename.c_str());
193 shellpath = filename;
196 std::string shellpath_(shellpath.begin(), shellpath.end());
198 if (shellpath.empty() || !path_util::file_exists(shellpath)) {
199 std::stringstream why;
200 why << "File not found: " << shellpath_;
201 Nan::ThrowError(why.str().c_str());
207 HRESULT hr = CreateNamedPipesAndPseudoConsole({cols, rows}, inheritCursor ? 1/*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0, &hIn, &hOut, &hpc, inName, outName, pipeName);
209 // Restore default handling of ctrl+c
210 SetConsoleCtrlHandler(NULL, FALSE);
213 marshal = Nan::New<v8::Object>();
216 // We were able to instantiate a conpty
217 const int ptyId = InterlockedIncrement(&ptyCounter);
218 Nan::Set(marshal, Nan::New<v8::String>("pty").ToLocalChecked(), Nan::New<v8::Number>(ptyId));
219 ptyHandles.insert(ptyHandles.end(), new pty_baton(ptyId, hIn, hOut, hpc));
221 Nan::ThrowError("Cannot launch conpty");
225 Nan::Set(marshal, Nan::New<v8::String>("fd").ToLocalChecked(), Nan::New<v8::Number>(-1));
227 std::string coninPipeNameStr(inName.begin(), inName.end());
228 Nan::Set(marshal, Nan::New<v8::String>("conin").ToLocalChecked(), Nan::New<v8::String>(coninPipeNameStr).ToLocalChecked());
230 std::string conoutPipeNameStr(outName.begin(), outName.end());
231 Nan::Set(marshal, Nan::New<v8::String>("conout").ToLocalChecked(), Nan::New<v8::String>(conoutPipeNameStr).ToLocalChecked());
233 info.GetReturnValue().Set(marshal);
236 VOID CALLBACK OnProcessExitWinEvent(
238 _In_ BOOLEAN TimerOrWaitFired) {
239 pty_baton *baton = static_cast<pty_baton*>(context);
241 // Fire OnProcessExit
242 uv_async_send(&baton->async);
245 static void OnProcessExit(uv_async_t *async) {
246 Nan::HandleScope scope;
247 pty_baton *baton = static_cast<pty_baton*>(async->data);
249 UnregisterWait(baton->hWait);
253 GetExitCodeProcess(baton->hShell, &exitCode);
256 v8::Local<v8::Value> args[1] = {
257 Nan::New<v8::Number>(exitCode)
260 Nan::AsyncResource asyncResource("node-pty.callback");
261 baton->cb.Call(1, args, &asyncResource);
266 static NAN_METHOD(PtyConnect) {
267 Nan::HandleScope scope;
269 // If we're working with conpty's we need to call ConnectNamedPipe here AFTER
270 // the Socket has attempted to connect to the other end, then actually
271 // spawn the process here.
273 std::stringstream errorText;
274 BOOL fSuccess = FALSE;
276 if (info.Length() != 5 ||
277 !info[0]->IsNumber() ||
278 !info[1]->IsString() ||
279 !info[2]->IsString() ||
280 !info[3]->IsArray() ||
281 !info[4]->IsFunction()) {
282 Nan::ThrowError("Usage: pty.connect(id, cmdline, cwd, env, exitCallback)");
286 const int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
287 const std::wstring cmdline(path_util::to_wstring(Nan::Utf8String(info[1])));
288 const std::wstring cwd(path_util::to_wstring(Nan::Utf8String(info[2])));
289 const v8::Local<v8::Array> envValues = info[3].As<v8::Array>();
290 const v8::Local<v8::Function> exitCallback = v8::Local<v8::Function>::Cast(info[4]);
292 // Prepare command line
293 std::unique_ptr<wchar_t[]> mutableCommandline = std::make_unique<wchar_t[]>(cmdline.length() + 1);
294 HRESULT hr = StringCchCopyW(mutableCommandline.get(), cmdline.length() + 1, cmdline.c_str());
297 std::unique_ptr<wchar_t[]> mutableCwd = std::make_unique<wchar_t[]>(cwd.length() + 1);
298 hr = StringCchCopyW(mutableCwd.get(), cwd.length() + 1, cwd.c_str());
300 // Prepare environment
302 if (!envValues.IsEmpty()) {
303 std::wstringstream envBlock;
304 for(uint32_t i = 0; i < envValues->Length(); i++) {
305 std::wstring envValue(path_util::to_wstring(Nan::Utf8String(Nan::Get(envValues, i).ToLocalChecked())));
306 envBlock << envValue << L'\0';
309 env = envBlock.str();
311 auto envV = vectorFromString(env);
312 LPWSTR envArg = envV.empty() ? nullptr : envV.data();
314 // Fetch pty handle from ID and start process
315 pty_baton* handle = get_pty_baton(id);
317 BOOL success = ConnectNamedPipe(handle->hIn, nullptr);
318 success = ConnectNamedPipe(handle->hOut, nullptr);
320 // Attach the pseudoconsole to the client application we're creating
321 STARTUPINFOEXW siEx{0};
322 siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW);
323 siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
324 siEx.StartupInfo.hStdError = nullptr;
325 siEx.StartupInfo.hStdInput = nullptr;
326 siEx.StartupInfo.hStdOutput = nullptr;
329 InitializeProcThreadAttributeList(NULL, 1, 0, &size);
330 BYTE *attrList = new BYTE[size];
331 siEx.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList);
333 fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size);
335 return throwNanError(&info, "InitializeProcThreadAttributeList failed", true);
337 fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList,
339 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
345 return throwNanError(&info, "UpdateProcThreadAttribute failed", true);
348 PROCESS_INFORMATION piClient{};
349 fSuccess = !!CreateProcessW(
351 mutableCommandline.get(),
352 nullptr, // lpProcessAttributes
353 nullptr, // lpThreadAttributes
354 false, // bInheritHandles VERY IMPORTANT that this is false
355 EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags
356 envArg, // lpEnvironment
357 mutableCwd.get(), // lpCurrentDirectory
358 &siEx.StartupInfo, // lpStartupInfo
359 &piClient // lpProcessInformation
362 return throwNanError(&info, "Cannot create process", true);
366 handle->hShell = piClient.hProcess;
367 handle->cb.Reset(exitCallback);
368 handle->async.data = handle;
370 // Setup OnProcessExit callback
371 uv_async_init(uv_default_loop(), &handle->async, OnProcessExit);
373 // Setup Windows wait for process exit event
374 RegisterWaitForSingleObject(&handle->hWait, piClient.hProcess, OnProcessExitWinEvent, (PVOID)handle, INFINITE, WT_EXECUTEONLYONCE);
377 v8::Local<v8::Object> marshal = Nan::New<v8::Object>();
378 Nan::Set(marshal, Nan::New<v8::String>("pid").ToLocalChecked(), Nan::New<v8::Number>(piClient.dwProcessId));
379 info.GetReturnValue().Set(marshal);
382 static NAN_METHOD(PtyResize) {
383 Nan::HandleScope scope;
385 if (info.Length() != 3 ||
386 !info[0]->IsNumber() ||
387 !info[1]->IsNumber() ||
388 !info[2]->IsNumber()) {
389 Nan::ThrowError("Usage: pty.resize(id, cols, rows)");
393 int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
394 SHORT cols = info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust();
395 SHORT rows = info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust();
397 const pty_baton* handle = get_pty_baton(id);
399 HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
400 bool fLoadedDll = hLibrary != nullptr;
403 PFNRESIZEPSEUDOCONSOLE const pfnResizePseudoConsole = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ResizePseudoConsole");
404 if (pfnResizePseudoConsole)
406 COORD size = {cols, rows};
407 pfnResizePseudoConsole(handle->hpc, size);
411 return info.GetReturnValue().SetUndefined();
414 static NAN_METHOD(PtyKill) {
415 Nan::HandleScope scope;
417 if (info.Length() != 1 ||
418 !info[0]->IsNumber()) {
419 Nan::ThrowError("Usage: pty.kill(id)");
423 int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
425 const pty_baton* handle = get_pty_baton(id);
427 HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
428 bool fLoadedDll = hLibrary != nullptr;
431 PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ClosePseudoConsole");
432 if (pfnClosePseudoConsole)
434 pfnClosePseudoConsole(handle->hpc);
438 CloseHandle(handle->hShell);
440 return info.GetReturnValue().SetUndefined();
447 extern "C" void init(v8::Local<v8::Object> target) {
448 Nan::HandleScope scope;
449 Nan::SetMethod(target, "startProcess", PtyStartProcess);
450 Nan::SetMethod(target, "connect", PtyConnect);
451 Nan::SetMethod(target, "resize", PtyResize);
452 Nan::SetMethod(target, "kill", PtyKill);
455 NODE_MODULE(pty, init);