installed pty
[VSoRC/.git] / node_modules / node-pty / src / win / conpty.cc
1 /**
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).
5  *
6  * pty.cc:
7  *   This file is responsible for starting processes
8  *   with pseudo-terminal file descriptors.
9  */
10
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
15 #endif
16
17 #include <iostream>
18 #include <nan.h>
19 #include <Shlwapi.h> // PathCombine, PathIsRelative
20 #include <sstream>
21 #include <string>
22 #include <vector>
23 #include <Windows.h>
24 #include <strsafe.h>
25 #include "path_util.h"
26
27 extern "C" void init(v8::Local<v8::Object>);
28
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)
33
34 typedef VOID* HPCON;
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);
38
39 #endif
40
41 struct pty_baton {
42   int id;
43   HANDLE hIn;
44   HANDLE hOut;
45   HPCON hpc;
46
47   HANDLE hShell;
48   HANDLE hWait;
49   Nan::Callback cb;
50   uv_async_t async;
51   uv_thread_t tid;
52
53   pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {};
54 };
55
56 static std::vector<pty_baton*> ptyHandles;
57 static volatile LONG ptyCounter;
58
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) {
63       return ptyHandle;
64     }
65   }
66   return nullptr;
67 }
68
69 template <typename T>
70 std::vector<T> vectorFromString(const std::basic_string<T> &str) {
71     return std::vector<T>(str.begin(), str.end());
72 }
73
74 void throwNanError(const Nan::FunctionCallbackInfo<v8::Value>* info, const char* text, const bool getLastError) {
75   std::stringstream errorText;
76   errorText << text;
77   if (getLastError) {
78     errorText << ", error code: " << GetLastError();
79   }
80   Nan::ThrowError(errorText.str().c_str());
81   (*info).GetReturnValue().SetUndefined();
82 }
83
84 // Returns a new server named pipe.  It has not yet been connected.
85 bool createDataServerPipe(bool write,
86                           std::wstring kind,
87                           HANDLE* hServer,
88                           std::wstring &name,
89                           const std::wstring &pipeName)
90 {
91   *hServer = INVALID_HANDLE_VALUE;
92
93   name = L"\\\\.\\pipe\\" + pipeName + L"-" + kind;
94
95   const DWORD winOpenMode =  PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE/*  | FILE_FLAG_OVERLAPPED */;
96
97   SECURITY_ATTRIBUTES sa = {};
98   sa.nLength = sizeof(sa);
99
100   *hServer = CreateNamedPipeW(
101       name.c_str(),
102       /*dwOpenMode=*/winOpenMode,
103       /*dwPipeMode=*/PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
104       /*nMaxInstances=*/1,
105       /*nOutBufferSize=*/0,
106       /*nInBufferSize=*/0,
107       /*nDefaultTimeOut=*/30000,
108       &sa);
109
110   return *hServer != INVALID_HANDLE_VALUE;
111 }
112
113 HRESULT CreateNamedPipesAndPseudoConsole(COORD size,
114                                          DWORD dwFlags,
115                                          HANDLE *phInput,
116                                          HANDLE *phOutput,
117                                          HPCON* phPC,
118                                          std::wstring& inName,
119                                          std::wstring& outName,
120                                          const std::wstring& pipeName)
121 {
122   HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
123   bool fLoadedDll = hLibrary != nullptr;
124   if (fLoadedDll)
125   {
126     PFNCREATEPSEUDOCONSOLE const pfnCreate = (PFNCREATEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "CreatePseudoConsole");
127     if (pfnCreate)
128     {
129       if (phPC == NULL || phInput == NULL || phOutput == NULL)
130       {
131         return E_INVALIDARG;
132       }
133
134       bool success = createDataServerPipe(true, L"in", phInput, inName, pipeName);
135       if (!success)
136       {
137         return HRESULT_FROM_WIN32(GetLastError());
138       }
139       success = createDataServerPipe(false, L"out", phOutput, outName, pipeName);
140       if (!success)
141       {
142         return HRESULT_FROM_WIN32(GetLastError());
143       }
144       return pfnCreate(size, *phInput, *phOutput, dwFlags, phPC);
145     }
146     else
147     {
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());
152     }
153   }
154
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());
158 }
159
160 static NAN_METHOD(PtyStartProcess) {
161   Nan::HandleScope scope;
162
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{};
168
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)");
177     return;
178   }
179
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();
186
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());
192   } else {
193     shellpath = filename;
194   }
195
196   std::string shellpath_(shellpath.begin(), shellpath.end());
197
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());
202     return;
203   }
204
205   HANDLE hIn, hOut;
206   HPCON hpc;
207   HRESULT hr = CreateNamedPipesAndPseudoConsole({cols, rows}, inheritCursor ? 1/*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0, &hIn, &hOut, &hpc, inName, outName, pipeName);
208
209   // Restore default handling of ctrl+c
210   SetConsoleCtrlHandler(NULL, FALSE);
211
212   // Set return values
213   marshal = Nan::New<v8::Object>();
214
215   if (SUCCEEDED(hr)) {
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));
220   } else {
221     Nan::ThrowError("Cannot launch conpty");
222     return;
223   }
224
225   Nan::Set(marshal, Nan::New<v8::String>("fd").ToLocalChecked(), Nan::New<v8::Number>(-1));
226   {
227     std::string coninPipeNameStr(inName.begin(), inName.end());
228     Nan::Set(marshal, Nan::New<v8::String>("conin").ToLocalChecked(), Nan::New<v8::String>(coninPipeNameStr).ToLocalChecked());
229
230     std::string conoutPipeNameStr(outName.begin(), outName.end());
231     Nan::Set(marshal, Nan::New<v8::String>("conout").ToLocalChecked(), Nan::New<v8::String>(conoutPipeNameStr).ToLocalChecked());
232   }
233   info.GetReturnValue().Set(marshal);
234 }
235
236 VOID CALLBACK OnProcessExitWinEvent(
237     _In_ PVOID context,
238     _In_ BOOLEAN TimerOrWaitFired) {
239   pty_baton *baton = static_cast<pty_baton*>(context);
240
241   // Fire OnProcessExit
242   uv_async_send(&baton->async);
243 }
244
245 static void OnProcessExit(uv_async_t *async) {
246   Nan::HandleScope scope;
247   pty_baton *baton = static_cast<pty_baton*>(async->data);
248
249   UnregisterWait(baton->hWait);
250
251   // Get exit code
252   DWORD exitCode = 0;
253   GetExitCodeProcess(baton->hShell, &exitCode);
254
255   // Call function
256   v8::Local<v8::Value> args[1] = {
257     Nan::New<v8::Number>(exitCode)
258   };
259
260   Nan::AsyncResource asyncResource("node-pty.callback");
261   baton->cb.Call(1, args, &asyncResource);
262   // Clean up
263   baton->cb.Reset();
264 }
265
266 static NAN_METHOD(PtyConnect) {
267   Nan::HandleScope scope;
268
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.
272
273   std::stringstream errorText;
274   BOOL fSuccess = FALSE;
275
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)");
283     return;
284   }
285
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]);
291
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());
295
296   // Prepare cwd
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());
299
300   // Prepare environment
301   std::wstring env;
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';
307     }
308     envBlock << L'\0';
309     env = envBlock.str();
310   }
311   auto envV = vectorFromString(env);
312   LPWSTR envArg = envV.empty() ? nullptr : envV.data();
313
314   // Fetch pty handle from ID and start process
315   pty_baton* handle = get_pty_baton(id);
316
317   BOOL success = ConnectNamedPipe(handle->hIn, nullptr);
318   success = ConnectNamedPipe(handle->hOut, nullptr);
319
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;
327
328   SIZE_T size = 0;
329   InitializeProcThreadAttributeList(NULL, 1, 0, &size);
330   BYTE *attrList = new BYTE[size];
331   siEx.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList);
332
333   fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size);
334   if (!fSuccess) {
335     return throwNanError(&info, "InitializeProcThreadAttributeList failed", true);
336   }
337   fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList,
338                                        0,
339                                        PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
340                                        handle->hpc,
341                                        sizeof(HPCON),
342                                        NULL,
343                                        NULL);
344   if (!fSuccess) {
345     return throwNanError(&info, "UpdateProcThreadAttribute failed", true);
346   }
347
348   PROCESS_INFORMATION piClient{};
349   fSuccess = !!CreateProcessW(
350       nullptr,
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
360   );
361   if (!fSuccess) {
362     return throwNanError(&info, "Cannot create process", true);
363   }
364
365   // Update handle
366   handle->hShell = piClient.hProcess;
367   handle->cb.Reset(exitCallback);
368   handle->async.data = handle;
369
370   // Setup OnProcessExit callback
371   uv_async_init(uv_default_loop(), &handle->async, OnProcessExit);
372
373   // Setup Windows wait for process exit event
374   RegisterWaitForSingleObject(&handle->hWait, piClient.hProcess, OnProcessExitWinEvent, (PVOID)handle, INFINITE, WT_EXECUTEONLYONCE);
375
376   // Return
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);
380 }
381
382 static NAN_METHOD(PtyResize) {
383   Nan::HandleScope scope;
384
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)");
390     return;
391   }
392
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();
396
397   const pty_baton* handle = get_pty_baton(id);
398
399   HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
400   bool fLoadedDll = hLibrary != nullptr;
401   if (fLoadedDll)
402   {
403     PFNRESIZEPSEUDOCONSOLE const pfnResizePseudoConsole = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ResizePseudoConsole");
404     if (pfnResizePseudoConsole)
405     {
406       COORD size = {cols, rows};
407       pfnResizePseudoConsole(handle->hpc, size);
408     }
409   }
410
411   return info.GetReturnValue().SetUndefined();
412 }
413
414 static NAN_METHOD(PtyKill) {
415   Nan::HandleScope scope;
416
417   if (info.Length() != 1 ||
418       !info[0]->IsNumber()) {
419     Nan::ThrowError("Usage: pty.kill(id)");
420     return;
421   }
422
423   int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
424
425   const pty_baton* handle = get_pty_baton(id);
426
427   HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
428   bool fLoadedDll = hLibrary != nullptr;
429   if (fLoadedDll)
430   {
431     PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ClosePseudoConsole");
432     if (pfnClosePseudoConsole)
433     {
434       pfnClosePseudoConsole(handle->hpc);
435     }
436   }
437
438   CloseHandle(handle->hShell);
439
440   return info.GetReturnValue().SetUndefined();
441 }
442
443 /**
444 * Init
445 */
446
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);
453 };
454
455 NODE_MODULE(pty, init);