From b1fdda458a09072ea0fd53aa4616573dd6b30421 Mon Sep 17 00:00:00 2001 From: kkocdko <31189892+kkocdko@users.noreply.github.com> Date: Fri, 26 Jan 2024 17:17:03 +0800 Subject: [PATCH] Port to NAPI (#644) * Port to NAPI The "5th pty bug" in #432 fixed also. * Fix help message in pty.cc * Move NAPI deps to devDependencies in package.json * Apply most of deepak1556's suggestions * Fix winpty * Fix conpty missing CloseHandle * Use unique_ptr to avoid `goto`s * Why macos failed? * fix: ci and minor cleanups * fix build failed on windows --------- Co-authored-by: deepak1556 --- azure-pipelines.yml | 7 +- binding.gyp | 19 +- package.json | 2 +- src/unix/pty.cc | 511 +++++++++++++++------------------ src/win/conpty.cc | 320 ++++++++++----------- src/win/conpty_console_list.cc | 31 +- src/win/path_util.cc | 28 +- src/win/path_util.h | 7 +- src/win/winpty.cc | 259 +++++++---------- yarn.lock | 10 +- 10 files changed, 543 insertions(+), 651 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b546a698..e07f17ef 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -42,6 +42,9 @@ jobs: inputs: versionSpec: $(node_version) displayName: 'Install Node.js' + - script: | + python3 -m pip install setuptools + displayName: Install setuptools (macOS) - script: | npm i displayName: 'Install dependencies and build' @@ -54,7 +57,7 @@ jobs: - job: Windows pool: - vmImage: 'windows-2019' + vmImage: 'windows-latest' strategy: matrix: node_16_x: @@ -87,7 +90,7 @@ jobs: steps: - task: NodeTool@0 inputs: - versionSpec: '16.x' + versionSpec: '18.x' displayName: 'Install Node.js' - script: | npm i diff --git a/binding.gyp b/binding.gyp index 6a6d2430..f21c7df4 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,5 +1,8 @@ { 'target_defaults': { + 'dependencies': [ + " +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include +#include #include #include #include #include +#include #include #include @@ -99,27 +102,107 @@ int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12)); }) #endif -/** - * Structs - */ - -struct pty_baton { - Nan::Persistent cb; - int exit_code; - int signal_code; - pid_t pid; - uv_async_t async; - uv_thread_t tid; +struct ExitEvent { + int exit_code = 0, signal_code = 0; }; +void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) { + std::thread *th = new std::thread; + // Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE. + auto tsfn = Napi::ThreadSafeFunction::New( + env, + cb, // JavaScript function called asynchronously + "SetupExitCallback_resource", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + [th](Napi::Env) { // Finalizer used to clean threads up + th->join(); + delete th; + }); + *th = std::thread([tsfn = std::move(tsfn), pid] { + auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) { + cb.Call({Napi::Number::New(env, exit_event->exit_code), + Napi::Number::New(env, exit_event->signal_code)}); + delete exit_event; + }; + + int ret; + int stat_loc; +#if defined(__APPLE__) + // Based on + // https://source.chromium.org/chromium/chromium/src/+/main:base/process/kill_mac.cc;l=35-69? + int kq = HANDLE_EINTR(kqueue()); + struct kevent change = {0}; + EV_SET(&change, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL); + ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL)); + if (ret == -1) { + if (errno == ESRCH) { + // At this point, one of the following has occurred: + // 1. The process has died but has not yet been reaped. + // 2. The process has died and has already been reaped. + // 3. The process is in the process of dying. It's no longer + // kqueueable, but it may not be waitable yet either. Mark calls + // this case the "zombie death race". + ret = HANDLE_EINTR(waitpid(pid, &stat_loc, WNOHANG)); + if (ret == 0) { + ret = kill(pid, SIGKILL); + if (ret != -1) { + HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); + } + } + } + } else { + struct kevent event = {0}; + ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL)); + if (ret == 1) { + if ((event.fflags & NOTE_EXIT) && + (event.ident == static_cast(pid))) { + // The process is dead or dying. This won't block for long, if at + // all. + HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); + } + } + } +#else + while (true) { + errno = 0; + if ((ret = waitpid(pid, &stat_loc, 0)) != pid) { + if (ret == -1 && errno == EINTR) { + continue; + } + if (ret == -1 && errno == ECHILD) { + // XXX node v0.8.x seems to have this problem. + // waitpid is already handled elsewhere. + ; + } else { + assert(false); + } + } + break; + } +#endif + ExitEvent *exit_event = new ExitEvent; + if (WIFEXITED(stat_loc)) { + exit_event->exit_code = WEXITSTATUS(stat_loc); // errno? + } + if (WIFSIGNALED(stat_loc)) { + exit_event->signal_code = WTERMSIG(stat_loc); + } + auto status = tsfn.BlockingCall(exit_event, callback); // In main thread + assert(status == napi_ok); + + tsfn.Release(); + }); +} + /** * Methods */ -NAN_METHOD(PtyFork); -NAN_METHOD(PtyOpen); -NAN_METHOD(PtyResize); -NAN_METHOD(PtyGetProc); +Napi::Value PtyFork(const Napi::CallbackInfo& info); +Napi::Value PtyOpen(const Napi::CallbackInfo& info); +Napi::Value PtyResize(const Napi::CallbackInfo& info); +Napi::Value PtyGetProc(const Napi::CallbackInfo& info); /** * Functions @@ -136,15 +219,6 @@ static char * pty_getproc(int, char *); #endif -static void -pty_waitpid(void *); - -static void -pty_after_waitpid(uv_async_t *); - -static void -pty_after_close(uv_handle_t *); - #if defined(__APPLE__) || defined(__OpenBSD__) static void pty_posix_spawn(char** argv, char** env, @@ -155,62 +229,75 @@ pty_posix_spawn(char** argv, char** env, int* err); #endif -NAN_METHOD(PtyFork) { - Nan::HandleScope scope; +struct DelBuf { + int len; + DelBuf(int len) : len(len) {} + void operator()(char **p) { + if (p == nullptr) + return; + for (int i = 0; i < len; i++) + free(p[i]); + delete[] p; + } +}; + +Napi::Value PtyFork(const Napi::CallbackInfo& info) { + Napi::Env napiEnv(info.Env()); + Napi::HandleScope scope(napiEnv); if (info.Length() != 11 || - !info[0]->IsString() || - !info[1]->IsArray() || - !info[2]->IsArray() || - !info[3]->IsString() || - !info[4]->IsNumber() || - !info[5]->IsNumber() || - !info[6]->IsNumber() || - !info[7]->IsNumber() || - !info[8]->IsBoolean() || - !info[9]->IsString() || - !info[10]->IsFunction()) { - return Nan::ThrowError( - "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, utf8, helperPath, onexit)"); + !info[0].IsString() || + !info[1].IsArray() || + !info[2].IsArray() || + !info[3].IsString() || + !info[4].IsNumber() || + !info[5].IsNumber() || + !info[6].IsNumber() || + !info[7].IsNumber() || + !info[8].IsBoolean() || + !info[9].IsString() || + !info[10].IsFunction()) { + throw Napi::Error::New(napiEnv, "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, utf8, helperPath, onexit)"); } // file - Nan::Utf8String file(info[0]); + std::string file = info[0].As(); // args - v8::Local argv_ = v8::Local::Cast(info[1]); + Napi::Array argv_ = info[1].As(); // env - v8::Local env_ = v8::Local::Cast(info[2]); - int envc = env_->Length(); - char **env = new char*[envc+1]; + Napi::Array env_ = info[2].As(); + int envc = env_.Length(); + std::unique_ptr env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1)); + char **env = env_unique_ptr.get(); env[envc] = NULL; for (int i = 0; i < envc; i++) { - Nan::Utf8String pair(Nan::Get(env_, i).ToLocalChecked()); - env[i] = strdup(*pair); + std::string pair = env_.Get(i).As(); + env[i] = strdup(pair.c_str()); } // cwd - Nan::Utf8String cwd_(info[3]); + std::string cwd_ = info[3].As(); // size struct winsize winp; - winp.ws_col = info[4]->IntegerValue(Nan::GetCurrentContext()).FromJust(); - winp.ws_row = info[5]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + winp.ws_col = info[4].As().Int32Value(); + winp.ws_row = info[5].As().Int32Value(); winp.ws_xpixel = 0; winp.ws_ypixel = 0; #if !defined(__APPLE__) // uid / gid - int uid = info[6]->IntegerValue(Nan::GetCurrentContext()).FromJust(); - int gid = info[7]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + int uid = info[6].As().Int32Value(); + int gid = info[7].As().Int32Value(); #endif // termios struct termios t = termios(); struct termios *term = &t; term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; - if (Nan::To(info[8]).FromJust()) { + if (info[8].As().Value()) { #if defined(IUTF8) term->c_iflag |= IUTF8; #endif @@ -245,45 +332,44 @@ NAN_METHOD(PtyFork) { cfsetospeed(term, B38400); // helperPath - Nan::Utf8String helper_path(info[9]); + std::string helper_path = info[9].As(); pid_t pid; int master; #if defined(__APPLE__) - int argc = argv_->Length(); + int argc = argv_.Length(); int argl = argc + 4; - char **argv = new char*[argl]; - argv[0] = strdup(*helper_path); - argv[1] = strdup(*cwd_); - argv[2] = strdup(*file); + std::unique_ptr argv_unique_ptr(new char *[argl], DelBuf(argl)); + char **argv = argv_unique_ptr.get(); + argv[0] = strdup(helper_path.c_str()); + argv[1] = strdup(cwd_.c_str()); + argv[2] = strdup(file.c_str()); argv[argl - 1] = NULL; for (int i = 0; i < argc; i++) { - Nan::Utf8String arg(Nan::Get(argv_, i).ToLocalChecked()); - argv[i + 3] = strdup(*arg); + std::string arg = argv_.Get(i).As(); + argv[i + 3] = strdup(arg.c_str()); } int err = -1; pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err); if (err != 0) { - Nan::ThrowError("posix_spawnp failed."); - goto done; + throw Napi::Error::New(napiEnv, "posix_spawnp failed."); } if (pty_nonblock(master) == -1) { - Nan::ThrowError("Could not set master fd to nonblocking."); - goto done; + throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); } #else - int argc = argv_->Length(); + int argc = argv_.Length(); int argl = argc + 2; - char **argv = new char*[argl]; - argv[0] = strdup(*file); + std::unique_ptr argv_unique_ptr(new char *[argl], DelBuf(argl)); + char** argv = argv_unique_ptr.get(); + argv[0] = strdup(file.c_str()); argv[argl - 1] = NULL; for (int i = 0; i < argc; i++) { - Nan::Utf8String arg(Nan::Get(argv_, i).ToLocalChecked()); - argv[i + 1] = strdup(*arg); + std::string arg = argv_.Get(i).As(); + argv[i + 1] = strdup(arg.c_str()); } - char* cwd = strdup(*cwd_); sigset_t newmask, oldmask; struct sigaction sig_action; // temporarily block all signals @@ -303,14 +389,6 @@ NAN_METHOD(PtyFork) { for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1 sigaction(i, &sig_action, NULL); } - } else { - for (int i = 0; i < argl; i++) free(argv[i]); - delete[] argv; - - for (int i = 0; i < envc; i++) free(env[i]); - delete[] env; - - free(cwd); } // reenable signals @@ -318,11 +396,10 @@ NAN_METHOD(PtyFork) { switch (pid) { case -1: - Nan::ThrowError("forkpty(3) failed."); - goto done; + throw Napi::Error::New(napiEnv, "forkpty(3) failed."); case 0: - if (strlen(cwd)) { - if (chdir(cwd) == -1) { + if (strlen(cwd_.c_str())) { + if (chdir(cwd_.c_str()) == -1) { perror("chdir(2) failed."); _exit(1); } @@ -349,62 +426,36 @@ NAN_METHOD(PtyFork) { } default: if (pty_nonblock(master) == -1) { - Nan::ThrowError("Could not set master fd to nonblocking."); - goto done; + throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); } } #endif - { - v8::Local obj = Nan::New(); - Nan::Set(obj, - Nan::New("fd").ToLocalChecked(), - Nan::New(master)); - Nan::Set(obj, - Nan::New("pid").ToLocalChecked(), - Nan::New(pid)); - Nan::Set(obj, - Nan::New("pty").ToLocalChecked(), - Nan::New(ptsname(master)).ToLocalChecked()); - - pty_baton *baton = new pty_baton(); - baton->exit_code = 0; - baton->signal_code = 0; - baton->cb.Reset(v8::Local::Cast(info[10])); - baton->pid = pid; - baton->async.data = baton; - - uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid); + Napi::Object obj = Napi::Object::New(napiEnv); + obj.Set("fd", Napi::Number::New(napiEnv, master)); + obj.Set("pid", Napi::Number::New(napiEnv, pid)); + obj.Set("pty", Napi::String::New(napiEnv, ptsname(master))); - uv_thread_create(&baton->tid, pty_waitpid, static_cast(baton)); - - return info.GetReturnValue().Set(obj); - } - -done: -#if defined(__APPLE__) - for (int i = 0; i < argl; i++) free(argv[i]); - delete[] argv; - - for (int i = 0; i < envc; i++) free(env[i]); - delete[] env; -#endif - return info.GetReturnValue().SetUndefined(); + // Set up process exit callback. + Napi::Function cb = info[10].As(); + SetupExitCallback(napiEnv, cb, pid); + return obj; } -NAN_METHOD(PtyOpen) { - Nan::HandleScope scope; +Napi::Value PtyOpen(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 2 || - !info[0]->IsNumber() || - !info[1]->IsNumber()) { - return Nan::ThrowError("Usage: pty.open(cols, rows)"); + !info[0].IsNumber() || + !info[1].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.open(cols, rows)"); } // size struct winsize winp; - winp.ws_col = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(); - winp.ws_row = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + winp.ws_col = info[0].As().Int32Value(); + winp.ws_row = info[1].As().Int32Value(); winp.ws_xpixel = 0; winp.ws_ypixel = 0; @@ -413,98 +464,98 @@ NAN_METHOD(PtyOpen) { int ret = openpty(&master, &slave, nullptr, NULL, static_cast(&winp)); if (ret == -1) { - return Nan::ThrowError("openpty(3) failed."); + throw Napi::Error::New(env, "openpty(3) failed."); } if (pty_nonblock(master) == -1) { - return Nan::ThrowError("Could not set master fd to nonblocking."); + throw Napi::Error::New(env, "Could not set master fd to nonblocking."); } if (pty_nonblock(slave) == -1) { - return Nan::ThrowError("Could not set slave fd to nonblocking."); + throw Napi::Error::New(env, "Could not set slave fd to nonblocking."); } - v8::Local obj = Nan::New(); - Nan::Set(obj, - Nan::New("master").ToLocalChecked(), - Nan::New(master)); - Nan::Set(obj, - Nan::New("slave").ToLocalChecked(), - Nan::New(slave)); - Nan::Set(obj, - Nan::New("pty").ToLocalChecked(), - Nan::New(ptsname(master)).ToLocalChecked()); + Napi::Object obj = Napi::Object::New(env); + obj.Set("master", Napi::Number::New(env, master)); + obj.Set("slave", Napi::Number::New(env, slave)); + obj.Set("pty", Napi::String::New(env, ptsname(master))); - return info.GetReturnValue().Set(obj); + return obj; } -NAN_METHOD(PtyResize) { - Nan::HandleScope scope; +Napi::Value PtyResize(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 3 || - !info[0]->IsNumber() || - !info[1]->IsNumber() || - !info[2]->IsNumber()) { - return Nan::ThrowError("Usage: pty.resize(fd, cols, rows)"); + !info[0].IsNumber() || + !info[1].IsNumber() || + !info[2].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.resize(fd, cols, rows)"); } - int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + int fd = info[0].As().Int32Value(); struct winsize winp; - winp.ws_col = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust(); - winp.ws_row = info[2]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + winp.ws_col = info[1].As().Int32Value(); + winp.ws_row = info[2].As().Int32Value(); winp.ws_xpixel = 0; winp.ws_ypixel = 0; if (ioctl(fd, TIOCSWINSZ, &winp) == -1) { switch (errno) { - case EBADF: return Nan::ThrowError("ioctl(2) failed, EBADF"); - case EFAULT: return Nan::ThrowError("ioctl(2) failed, EFAULT"); - case EINVAL: return Nan::ThrowError("ioctl(2) failed, EINVAL"); - case ENOTTY: return Nan::ThrowError("ioctl(2) failed, ENOTTY"); + case EBADF: + throw Napi::Error::New(env, "ioctl(2) failed, EBADF"); + case EFAULT: + throw Napi::Error::New(env, "ioctl(2) failed, EFAULT"); + case EINVAL: + throw Napi::Error::New(env, "ioctl(2) failed, EINVAL"); + case ENOTTY: + throw Napi::Error::New(env, "ioctl(2) failed, ENOTTY"); } - return Nan::ThrowError("ioctl(2) failed"); + throw Napi::Error::New(env, "ioctl(2) failed"); } - return info.GetReturnValue().SetUndefined(); + return env.Undefined(); } /** * Foreground Process Name */ -NAN_METHOD(PtyGetProc) { - Nan::HandleScope scope; +Napi::Value PtyGetProc(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); #if defined(__APPLE__) if (info.Length() != 1 || - !info[0]->IsNumber()) { - return Nan::ThrowError("Usage: pty.process(pid)"); + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.process(pid)"); } - int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + int fd = info[0].As().Int32Value(); char *name = pty_getproc(fd); #else if (info.Length() != 2 || - !info[0]->IsNumber() || - !info[1]->IsString()) { - return Nan::ThrowError("Usage: pty.process(fd, tty)"); + !info[0].IsNumber() || + !info[1].IsString()) { + throw Napi::Error::New(env, "Usage: pty.process(fd, tty)"); } - int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + int fd = info[0].As().Int32Value(); - Nan::Utf8String tty_(info[1]); - char *tty = strdup(*tty_); + std::string tty_ = info[1].As(); + char *tty = strdup(tty_.c_str()); char *name = pty_getproc(fd, tty); free(tty); #endif if (name == NULL) { - return info.GetReturnValue().SetUndefined(); + return env.Undefined(); } - v8::Local name_ = Nan::New(name).ToLocalChecked(); + Napi::String name_ = Napi::String::New(env, name); free(name); - return info.GetReturnValue().Set(name_); + return name_; } /** @@ -518,114 +569,6 @@ pty_nonblock(int fd) { return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } -/** - * pty_waitpid - * Wait for SIGCHLD to read exit status. - */ - -static void -pty_waitpid(void *data) { - int ret; - int stat_loc; - pty_baton *baton = static_cast(data); - errno = 0; -#if defined(__APPLE__) - // Based on - // https://source.chromium.org/chromium/chromium/src/+/main:base/process/kill_mac.cc;l=35-69? - int kq = HANDLE_EINTR(kqueue()); - struct kevent change = {0}; - EV_SET(&change, baton->pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL); - ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL)); - if (ret == -1) { - if (errno == ESRCH) { - // At this point, one of the following has occurred: - // 1. The process has died but has not yet been reaped. - // 2. The process has died and has already been reaped. - // 3. The process is in the process of dying. It's no longer - // kqueueable, but it may not be waitable yet either. Mark calls - // this case the "zombie death race". - ret = HANDLE_EINTR(waitpid(baton->pid, &stat_loc, WNOHANG)); - if (ret == 0) { - ret = kill(baton->pid, SIGKILL); - if (ret != -1) { - HANDLE_EINTR(waitpid(baton->pid, &stat_loc, 0)); - } - } - } - } else { - struct kevent event = {0}; - ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL)); - if (ret == 1) { - if ((event.fflags & NOTE_EXIT) && - (event.ident == static_cast(baton->pid))) { - // The process is dead or dying. This won't block for long, if at - // all. - HANDLE_EINTR(waitpid(baton->pid, &stat_loc, 0)); - } - } - } -#else - if ((ret = waitpid(baton->pid, &stat_loc, 0)) != baton->pid) { - if (ret == -1 && errno == EINTR) { - return pty_waitpid(baton); - } - if (ret == -1 && errno == ECHILD) { - // XXX node v0.8.x seems to have this problem. - // waitpid is already handled elsewhere. - ; - } else { - assert(false); - } - } -#endif - - if (WIFEXITED(stat_loc)) { - baton->exit_code = WEXITSTATUS(stat_loc); // errno? - } - - if (WIFSIGNALED(stat_loc)) { - baton->signal_code = WTERMSIG(stat_loc); - } - - uv_async_send(&baton->async); -} - -/** - * pty_after_waitpid - * Callback after exit status has been read. - */ - -static void -pty_after_waitpid(uv_async_t *async) { - Nan::HandleScope scope; - pty_baton *baton = static_cast(async->data); - - v8::Local argv[] = { - Nan::New(baton->exit_code), - Nan::New(baton->signal_code), - }; - - v8::Local cb = Nan::New(baton->cb); - baton->cb.Reset(); - memset(&baton->cb, -1, sizeof(baton->cb)); - Nan::AsyncResource resource("pty_after_waitpid"); - resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), cb, 2, argv); - - uv_close((uv_handle_t *)async, pty_after_close); -} - -/** - * pty_after_close - * uv_close() callback - free handle data - */ - -static void -pty_after_close(uv_handle_t *handle) { - uv_async_t *async = (uv_async_t *)handle; - pty_baton *baton = static_cast(async->data); - delete baton; -} - /** * pty_getproc * Taken from tmux. @@ -828,12 +771,12 @@ pty_posix_spawn(char** argv, char** env, * Init */ -NAN_MODULE_INIT(init) { - Nan::HandleScope scope; - Nan::Export(target, "fork", PtyFork); - Nan::Export(target, "open", PtyOpen); - Nan::Export(target, "resize", PtyResize); - Nan::Export(target, "process", PtyGetProc); +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("fork", Napi::Function::New(env, PtyFork)); + exports.Set("open", Napi::Function::New(env, PtyOpen)); + exports.Set("resize", Napi::Function::New(env, PtyResize)); + exports.Set("process", Napi::Function::New(env, PtyGetProc)); + return exports; } -NODE_MODULE(pty, init) +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/src/win/conpty.cc b/src/win/conpty.cc index f614cc4a..c41796ca 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -8,24 +8,20 @@ * with pseudo-terminal file descriptors. */ -// node versions lower than 10 define this as 0x502 which disables many of the definitions needed to compile -#include -#if NODE_MODULE_VERSION <= 57 - #define _WIN32_WINNT 0x600 -#endif +#define _WIN32_WINNT 0x600 -#include -#include +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include +#include #include // PathCombine, PathIsRelative #include #include +#include #include #include #include #include "path_util.h" -extern "C" void init(v8::Local); - // 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 \ @@ -46,10 +42,6 @@ struct pty_baton { 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) {}; }; @@ -67,19 +59,65 @@ static pty_baton* get_pty_baton(int id) { return nullptr; } -template -std::vector vectorFromString(const std::basic_string &str) { - return std::vector(str.begin(), str.end()); +static bool remove_pty_baton(int id) { + for (size_t i = 0; i < ptyHandles.size(); ++i) { + pty_baton* ptyHandle = ptyHandles[i]; + if (ptyHandle->id == id) { + ptyHandles.erase(ptyHandles.begin() + i); + ptyHandle = nullptr; + return true; + } + } + return false; +} + +struct ExitEvent { + int exit_code = 0; +}; + +void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { + std::thread *th = new std::thread; + // Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE. + auto tsfn = Napi::ThreadSafeFunction::New( + env, + cb, // JavaScript function called asynchronously + "SetupExitCallback_resource", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + [th](Napi::Env) { // Finalizer used to clean threads up + th->join(); + delete th; + }); + *th = std::thread([tsfn = std::move(tsfn), baton] { + auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) { + cb.Call({Napi::Number::New(env, exit_event->exit_code)}); + delete exit_event; + }; + + ExitEvent *exit_event = new ExitEvent; + // Wait for process to complete. + WaitForSingleObject(baton->hShell, INFINITE); + // Get process exit code. + GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code)); + // Clean up handles + // 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); + + auto status = tsfn.BlockingCall(exit_event, callback); // In main thread + assert(status == napi_ok); + + tsfn.Release(); + }); } -void throwNanError(const Nan::FunctionCallbackInfo* info, const char* text, const bool getLastError) { +Napi::Error errorWithCode(const Napi::CallbackInfo& info, const char* text) { std::stringstream errorText; errorText << text; - if (getLastError) { - errorText << ", error code: " << GetLastError(); - } - Nan::ThrowError(errorText.str().c_str()); - (*info).GetReturnValue().SetUndefined(); + errorText << ", error code: " << GetLastError(); + return Napi::Error::New(info.Env(), errorText.str()); } // Returns a new server named pipe. It has not yet been connected. @@ -158,32 +196,32 @@ HRESULT CreateNamedPipesAndPseudoConsole(COORD size, return HRESULT_FROM_WIN32(GetLastError()); } -static NAN_METHOD(PtyStartProcess) { - Nan::HandleScope scope; +static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); - v8::Local marshal; + Napi::Object marshal; std::wstring inName, outName; BOOL fSuccess = FALSE; std::unique_ptr 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; + !info[0].IsString() || + !info[1].IsNumber() || + !info[2].IsNumber() || + !info[3].IsBoolean() || + !info[4].IsString() || + !info[5].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.startProcess(file, cols, rows, debug, pipeName, inheritCursor)"); } - const std::wstring filename(path_util::to_wstring(Nan::Utf8String(info[0]))); - const SHORT cols = static_cast(info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust()); - const SHORT rows = static_cast(info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust()); - const bool debug = Nan::To(info[3]).FromJust(); - const std::wstring pipeName(path_util::to_wstring(Nan::Utf8String(info[4]))); - const bool inheritCursor = Nan::To(info[5]).FromJust(); + const std::wstring filename(path_util::to_wstring(info[0].As())); + const SHORT cols = static_cast(info[1].As().Uint32Value()); + const SHORT rows = static_cast(info[2].As().Uint32Value()); + const bool debug = info[3].As().Value(); + const std::wstring pipeName(path_util::to_wstring(info[4].As())); + const bool inheritCursor = info[5].As().Value(); // use environment 'Path' variable to determine location of // the relative path that we have recieved (e.g cmd.exe) @@ -195,10 +233,10 @@ static NAN_METHOD(PtyStartProcess) { } if (shellpath.empty() || !path_util::file_exists(shellpath)) { - std::wstringstream why; - why << "File not found: " << shellpath; - Nan::ThrowError(path_util::from_wstring(why.str().c_str())); - return; + std::string why; + why += "File not found: "; + why += path_util::wstring_to_string(shellpath); + throw Napi::Error::New(env, why); } HANDLE hIn, hOut; @@ -209,74 +247,35 @@ static NAN_METHOD(PtyStartProcess) { SetConsoleCtrlHandler(NULL, FALSE); // Set return values - marshal = Nan::New(); + marshal = Napi::Object::New(env); if (SUCCEEDED(hr)) { // We were able to instantiate a conpty const int ptyId = InterlockedIncrement(&ptyCounter); - Nan::Set(marshal, Nan::New("pty").ToLocalChecked(), Nan::New(ptyId)); + marshal.Set("pty", Napi::Number::New(env, ptyId)); ptyHandles.insert(ptyHandles.end(), new pty_baton(ptyId, hIn, hOut, hpc)); } else { - Nan::ThrowError("Cannot launch conpty"); - return; + throw Napi::Error::New(env, "Cannot launch conpty"); } - std::string inNameStr(path_util::from_wstring(inName.c_str())); + std::string inNameStr = path_util::wstring_to_string(inName); if (inNameStr.empty()) { - Nan::ThrowError("Failed to initialize conpty conin"); - return; + throw Napi::Error::New(env, "Failed to initialize conpty conin"); } - std::string outNameStr(path_util::from_wstring(outName.c_str())); + std::string outNameStr = path_util::wstring_to_string(outName); if (outNameStr.empty()) { - Nan::ThrowError("Failed to initialize conpty conout"); - return; + throw Napi::Error::New(env, "Failed to initialize conpty conout"); } - Nan::Set(marshal, Nan::New("fd").ToLocalChecked(), Nan::New(-1)); - Nan::Set(marshal, Nan::New("conin").ToLocalChecked(), Nan::New(inNameStr).ToLocalChecked()); - Nan::Set(marshal, Nan::New("conout").ToLocalChecked(), Nan::New(outNameStr).ToLocalChecked()); - info.GetReturnValue().Set(marshal); + marshal.Set("fd", Napi::Number::New(env, -1)); + marshal.Set("conin", Napi::String::New(env, inNameStr)); + marshal.Set("conout", Napi::String::New(env, outNameStr)); + return marshal; } -VOID CALLBACK OnProcessExitWinEvent( - _In_ PVOID context, - _In_ BOOLEAN TimerOrWaitFired) { - pty_baton *baton = static_cast(context); - - // Fire OnProcessExit - uv_async_send(&baton->async); -} - -static void OnProcessExit(uv_async_t *async) { - Nan::HandleScope scope; - pty_baton *baton = static_cast(async->data); - - UnregisterWait(baton->hWait); - - // Get exit code - DWORD exitCode = 0; - GetExitCodeProcess(baton->hShell, &exitCode); - - // Clean up handles - // 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); - - // Call function - v8::Local args[1] = { - Nan::New(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; +static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); // 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 @@ -286,26 +285,24 @@ static NAN_METHOD(PtyConnect) { 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; + !info[0].IsNumber() || + !info[1].IsString() || + !info[2].IsString() || + !info[3].IsArray() || + !info[4].IsFunction()) { + throw Napi::Error::New(env, "Usage: pty.connect(id, cmdline, cwd, env, exitCallback)"); } - 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 envValues = info[3].As(); - const v8::Local exitCallback = v8::Local::Cast(info[4]); + const int id = info[0].As().Int32Value(); + const std::wstring cmdline(path_util::to_wstring(info[1].As())); + const std::wstring cwd(path_util::to_wstring(info[2].As())); + const Napi::Array envValues = info[3].As(); + Napi::Function exitCallback = info[4].As(); // Fetch pty handle from ID and start process pty_baton* handle = get_pty_baton(id); if (!handle) { - Nan::ThrowError("Invalid pty handle"); - return; + throw Napi::Error::New(env, "Invalid pty handle"); } // Prepare command line @@ -317,17 +314,17 @@ static NAN_METHOD(PtyConnect) { hr = StringCchCopyW(mutableCwd.get(), cwd.length() + 1, cwd.c_str()); // Prepare environment - std::wstring env; + std::wstring envStr; 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'; + std::wstring envBlock; + for(uint32_t i = 0; i < envValues.Length(); i++) { + envBlock += path_util::to_wstring(envValues.Get(i).As()); + envBlock += L'\0'; } - envBlock << L'\0'; - env = envBlock.str(); + envBlock += L'\0'; + envStr = std::move(envBlock); } - auto envV = vectorFromString(env); + std::vector envV(envStr.cbegin(), envStr.cend()); LPWSTR envArg = envV.empty() ? nullptr : envV.data(); ConnectNamedPipe(handle->hIn, nullptr); @@ -348,7 +345,7 @@ static NAN_METHOD(PtyConnect) { fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size); if (!fSuccess) { - return throwNanError(&info, "InitializeProcThreadAttributeList failed", true); + throw errorWithCode(info, "InitializeProcThreadAttributeList failed"); } fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList, 0, @@ -358,7 +355,7 @@ static NAN_METHOD(PtyConnect) { NULL, NULL); if (!fSuccess) { - return throwNanError(&info, "UpdateProcThreadAttribute failed", true); + throw errorWithCode(info, "UpdateProcThreadAttribute failed"); } PROCESS_INFORMATION piClient{}; @@ -375,40 +372,34 @@ static NAN_METHOD(PtyConnect) { &piClient // lpProcessInformation ); if (!fSuccess) { - return throwNanError(&info, "Cannot create process", true); + throw errorWithCode(info, "Cannot create process"); } // 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); + SetupExitCallback(env, exitCallback, handle); // Return - v8::Local marshal = Nan::New(); - Nan::Set(marshal, Nan::New("pid").ToLocalChecked(), Nan::New(piClient.dwProcessId)); - info.GetReturnValue().Set(marshal); + auto marshal = Napi::Object::New(env); + marshal.Set("pid", Napi::Number::New(env, piClient.dwProcessId)); + return marshal; } -static NAN_METHOD(PtyResize) { - Nan::HandleScope scope; +static Napi::Value PtyResize(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 3 || - !info[0]->IsNumber() || - !info[1]->IsNumber() || - !info[2]->IsNumber()) { - Nan::ThrowError("Usage: pty.resize(id, cols, rows)"); - return; + !info[0].IsNumber() || + !info[1].IsNumber() || + !info[2].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.resize(id, cols, rows)"); } - int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(); - SHORT cols = static_cast(info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust()); - SHORT rows = static_cast(info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust()); + int id = info[0].As().Int32Value(); + SHORT cols = static_cast(info[1].As().Uint32Value()); + SHORT rows = static_cast(info[2].As().Uint32Value()); const pty_baton* handle = get_pty_baton(id); @@ -426,19 +417,19 @@ static NAN_METHOD(PtyResize) { } } - return info.GetReturnValue().SetUndefined(); + return env.Undefined(); } -static NAN_METHOD(PtyClear) { - Nan::HandleScope scope; +static Napi::Value PtyClear(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 1 || - !info[0]->IsNumber()) { - Nan::ThrowError("Usage: pty.clear(id)"); - return; + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.clear(id)"); } - int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(); + int id = info[0].As().Int32Value(); const pty_baton* handle = get_pty_baton(id); @@ -455,19 +446,19 @@ static NAN_METHOD(PtyClear) { } } - return info.GetReturnValue().SetUndefined(); + return env.Undefined(); } -static NAN_METHOD(PtyKill) { - Nan::HandleScope scope; +static Napi::Value PtyKill(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 1 || - !info[0]->IsNumber()) { - Nan::ThrowError("Usage: pty.kill(id)"); - return; + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.kill(id)"); } - int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(); + int id = info[0].As().Int32Value(); const pty_baton* handle = get_pty_baton(id); @@ -484,22 +475,23 @@ static NAN_METHOD(PtyKill) { } CloseHandle(handle->hShell); + assert(remove_pty_baton(id)); } - return info.GetReturnValue().SetUndefined(); + return env.Undefined(); } /** * Init */ -extern "C" void init(v8::Local target) { - Nan::HandleScope scope; - Nan::SetMethod(target, "startProcess", PtyStartProcess); - Nan::SetMethod(target, "connect", PtyConnect); - Nan::SetMethod(target, "resize", PtyResize); - Nan::SetMethod(target, "clear", PtyClear); - Nan::SetMethod(target, "kill", PtyKill); +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("startProcess", Napi::Function::New(env, PtyStartProcess)); + exports.Set("connect", Napi::Function::New(env, PtyConnect)); + exports.Set("resize", Napi::Function::New(env, PtyResize)); + exports.Set("clear", Napi::Function::New(env, PtyClear)); + exports.Set("kill", Napi::Function::New(env, PtyKill)); + return exports; }; -NODE_MODULE(pty, init); +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init); diff --git a/src/win/conpty_console_list.cc b/src/win/conpty_console_list.cc index 822b0b2b..4c8ab393 100644 --- a/src/win/conpty_console_list.cc +++ b/src/win/conpty_console_list.cc @@ -2,23 +2,24 @@ * Copyright (c) 2019, Microsoft Corporation (MIT License). */ -#include +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include #include -static NAN_METHOD(ApiConsoleProcessList) { +static Napi::Value ApiConsoleProcessList(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); if (info.Length() != 1 || - !info[0]->IsNumber()) { - Nan::ThrowError("Usage: getConsoleProcessList(shellPid)"); - return; + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: getConsoleProcessList(shellPid)"); } - const DWORD pid = info[0]->Uint32Value(Nan::GetCurrentContext()).FromJust(); + const DWORD pid = info[0].As().Uint32Value(); if (!FreeConsole()) { - Nan::ThrowError("FreeConsole failed"); + throw Napi::Error::New(env, "FreeConsole failed"); } if (!AttachConsole(pid)) { - Nan::ThrowError("AttachConsole failed"); + throw Napi::Error::New(env, "AttachConsole failed"); } auto processList = std::vector(64); auto processCount = GetConsoleProcessList(&processList[0], static_cast(processList.size())); @@ -28,16 +29,16 @@ static NAN_METHOD(ApiConsoleProcessList) { } FreeConsole(); - v8::Local result = Nan::New(); + Napi::Array result = Napi::Array::New(env); for (DWORD i = 0; i < processCount; i++) { - Nan::Set(result, i, Nan::New(processList[i])); + result.Set(i, Napi::Number::New(env, processList[i])); } - info.GetReturnValue().Set(result); + return result; } -extern "C" void init(v8::Local target) { - Nan::HandleScope scope; - Nan::SetMethod(target, "getConsoleProcessList", ApiConsoleProcessList); +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("getConsoleProcessList", Napi::Function::New(env, ApiConsoleProcessList)); + return exports; }; -NODE_MODULE(pty, init); +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init); diff --git a/src/win/path_util.cc b/src/win/path_util.cc index 5ff438ee..9b6b8201 100644 --- a/src/win/path_util.cc +++ b/src/win/path_util.cc @@ -4,25 +4,29 @@ * Copyright (c) 2018, Microsoft Corporation (MIT License). */ -#include +#include #include // PathCombine - +#include #include "path_util.h" namespace path_util { -const wchar_t* to_wstring(const Nan::Utf8String& str) { - const char *bytes = *str; - int sizeOfStr = MultiByteToWideChar(CP_UTF8, 0, bytes, -1, NULL, 0); - if (sizeOfStr <= 0) { - return L""; +std::wstring to_wstring(const Napi::String& str) { + const std::u16string & u16 = str.Utf16Value(); + return std::wstring(u16.begin(), u16.end()); +} + +std::string wstring_to_string(const std::wstring &wide_string) { + if (wide_string.empty()) { + return ""; } - wchar_t *output = new wchar_t[sizeOfStr]; - int status = MultiByteToWideChar(CP_UTF8, 0, bytes, -1, output, sizeOfStr); - if (status == 0) { - return L""; + const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), nullptr, 0, nullptr, nullptr); + if (size_needed <= 0) { + return ""; } - return output; + std::string result(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), &result.at(0), size_needed, nullptr, nullptr); + return result; } const char* from_wstring(const wchar_t* wstr) { diff --git a/src/win/path_util.h b/src/win/path_util.h index 5cb0d873..0be99b6d 100644 --- a/src/win/path_util.h +++ b/src/win/path_util.h @@ -7,13 +7,16 @@ #ifndef NODE_PTY_PATH_UTIL_H_ #define NODE_PTY_PATH_UTIL_H_ -#include +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include +#include #define MAX_ENV 65536 namespace path_util { -const wchar_t* to_wstring(const Nan::Utf8String& str); +std::wstring to_wstring(const Napi::String& str); +std::string wstring_to_string(const std::wstring &wide_string); const char* from_wstring(const wchar_t* wstr); bool file_exists(std::wstring filename); std::wstring get_shell_path(std::wstring filename); diff --git a/src/win/winpty.cc b/src/win/winpty.cc index b8bde75e..3996f8d6 100644 --- a/src/win/winpty.cc +++ b/src/win/winpty.cc @@ -8,9 +8,11 @@ * with pseudo-terminal file descriptors. */ +#define NODE_ADDON_API_DISABLE_DEPRECATED +#include #include +#include #include -#include #include // PathCombine, PathIsRelative #include #include @@ -24,8 +26,6 @@ /** * Misc */ -extern "C" void init(v8::Local); - #define WINPTY_DBG_VARIABLE TEXT("WINPTYDBG") /** @@ -66,28 +66,28 @@ static bool remove_pipe_handle(DWORD pid) { return false; } -void throw_winpty_error(const char *generalMsg, winpty_error_ptr_t error_ptr) { - std::wstringstream why; - std::wstring msg(winpty_error_msg(error_ptr)); - why << generalMsg << ": " << msg; - Nan::ThrowError(path_util::from_wstring(why.str().c_str())); +Napi::Error error_with_winpty_msg(const char *generalMsg, winpty_error_ptr_t error_ptr, Napi::Env env) { + std::string why; + why += generalMsg; + why += ": "; + why += path_util::wstring_to_string(winpty_error_msg(error_ptr)); winpty_error_free(error_ptr); + return Napi::Error::New(env, why); } -static NAN_METHOD(PtyGetExitCode) { - Nan::HandleScope scope; +static Napi::Value PtyGetExitCode(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 1 || - !info[0]->IsNumber()) { - Nan::ThrowError("Usage: pty.getExitCode(pid)"); - return; + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.getExitCode(pid)"); } - DWORD pid = info[0]->Uint32Value(Nan::GetCurrentContext()).FromJust(); + DWORD pid = info[0].As().Uint32Value(); HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); if (handle == NULL) { - info.GetReturnValue().Set(Nan::New(-1)); - return; + return Napi::Number::New(env, -1); } DWORD exitCode = 0; @@ -97,95 +97,86 @@ static NAN_METHOD(PtyGetExitCode) { } CloseHandle(handle); - info.GetReturnValue().Set(Nan::New(exitCode)); + return Napi::Number::New(env, exitCode); } -static NAN_METHOD(PtyGetProcessList) { - Nan::HandleScope scope; +static Napi::Value PtyGetProcessList(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 1 || - !info[0]->IsNumber()) { - Nan::ThrowError("Usage: pty.getProcessList(pid)"); - return; + !info[0].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.getProcessList(pid)"); } - DWORD pid = info[0]->Uint32Value(Nan::GetCurrentContext()).FromJust(); + DWORD pid = info[0].As().Uint32Value(); winpty_t *pc = get_pipe_handle(pid); if (pc == nullptr) { - info.GetReturnValue().Set(Nan::New(0)); - return; + return Napi::Number::New(env, 0); } int processList[64]; const int processCount = 64; int actualCount = winpty_get_console_process_list(pc, processList, processCount, nullptr); if (actualCount <= 0) { - info.GetReturnValue().Set(Nan::New(0)); - return; + return Napi::Number::New(env, 0); } - uint32_t actualCountSize = static_cast(actualCount); - v8::Local result = Nan::New(actualCountSize); - for (uint32_t i = 0; i < actualCountSize; i++) { - Nan::Set(result, i, Nan::New(processList[i])); + Napi::Array result = Napi::Array::New(env, actualCount); + for (int i = 0; i < actualCount; i++) { + result.Set(i, Napi::Number::New(env, processList[i])); } - info.GetReturnValue().Set(result); + return result; } -static NAN_METHOD(PtyStartProcess) { - Nan::HandleScope scope; +static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); 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; + !info[0].IsString() || + !info[1].IsString() || + !info[2].IsArray() || + !info[3].IsString() || + !info[4].IsNumber() || + !info[5].IsNumber() || + !info[6].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.startProcess(file, cmdline, env, cwd, cols, rows, debug)"); } - 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])); + std::wstring filename(path_util::to_wstring(info[0].As())); + std::wstring cmdline(path_util::to_wstring(info[1].As())); + std::wstring cwd(path_util::to_wstring(info[3].As())); // create environment block - std::wstring env; - const v8::Local envValues = v8::Local::Cast(info[2]); + std::wstring envStr; + const Napi::Array envValues = info[2].As(); 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'; + std::wstring envBlock; + for(uint32_t i = 0; i < envValues.Length(); i++) { + envBlock += path_util::to_wstring(envValues.Get(i).As()); + envBlock += L'\0'; } - - env = envBlock.str(); + envStr = std::move(envBlock); } // 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)) { + if (::PathIsRelativeW(filename.c_str())) { shellpath = path_util::get_shell_path(filename); } else { shellpath = filename; } if (shellpath.empty() || !path_util::file_exists(shellpath)) { - std::wstringstream why; - why << "File not found: " << shellpath; - Nan::ThrowError(path_util::from_wstring(why.str().c_str())); - delete filename; - delete cmdline; - delete cwd; - return; + std::string why; + why += "File not found: "; + why += path_util::wstring_to_string(shellpath); + throw Napi::Error::New(env, why); } - int cols = info[4]->Int32Value(Nan::GetCurrentContext()).FromJust(); - int rows = info[5]->Int32Value(Nan::GetCurrentContext()).FromJust(); - bool debug = Nan::To(info[6]).FromJust(); + int cols = info[4].As().Int32Value(); + int rows = info[5].As().Int32Value(); + bool debug = info[6].As().Value(); // Enable/disable debugging SetEnvironmentVariable(WINPTY_DBG_VARIABLE, debug ? "1" : NULL); // NULL = deletes variable @@ -194,11 +185,7 @@ static NAN_METHOD(PtyStartProcess) { 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); - delete filename; - delete cmdline; - delete cwd; - return; + throw error_with_winpty_msg("Error creating WinPTY config", error_ptr, env); } winpty_error_free(error_ptr); @@ -209,23 +196,15 @@ static NAN_METHOD(PtyStartProcess) { 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); - delete filename; - delete cmdline; - delete cwd; - return; + throw error_with_winpty_msg("Error launching WinPTY agent", error_ptr, env); } winpty_error_free(error_ptr); // 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); + winpty_spawn_config_t* config = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, shellpath.c_str(), cmdline.c_str(), cwd.c_str(), envStr.c_str(), &error_ptr); if (config == nullptr) { - throw_winpty_error("Error creating WinPTY spawn config", error_ptr); winpty_free(pc); - delete filename; - delete cmdline; - delete cwd; - return; + throw error_with_winpty_msg("Error creating WinPTY spawn config", error_ptr, env); } winpty_error_free(error_ptr); @@ -234,121 +213,99 @@ static NAN_METHOD(PtyStartProcess) { 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); if (handle) { CloseHandle(handle); } winpty_free(pc); - delete filename; - delete cmdline; - delete cwd; - return; + throw error_with_winpty_msg("Unable to start terminal process", error_ptr, env); } winpty_error_free(error_ptr); LPCWSTR coninPipeName = winpty_conin_name(pc); std::string coninPipeNameStr(path_util::from_wstring(coninPipeName)); if (coninPipeNameStr.empty()) { - Nan::ThrowError("Failed to initialize winpty conin"); CloseHandle(handle); winpty_free(pc); - delete filename; - delete cmdline; - delete cwd; - return; + throw Napi::Error::New(env, "Failed to initialize winpty conin"); } LPCWSTR conoutPipeName = winpty_conout_name(pc); std::string conoutPipeNameStr(path_util::from_wstring(conoutPipeName)); if (conoutPipeNameStr.empty()) { - Nan::ThrowError("Failed to initialize winpty conout"); CloseHandle(handle); winpty_free(pc); - delete filename; - delete cmdline; - delete cwd; - return; + throw Napi::Error::New(env, "Failed to initialize winpty conout"); } DWORD innerPid = GetProcessId(handle); if (createdHandles[innerPid]) { - std::stringstream why; - why << "There is already a process with innerPid " << innerPid; - Nan::ThrowError(why.str().c_str()); CloseHandle(handle); winpty_free(pc); - delete filename; - delete cmdline; - delete cwd; - return; + std::stringstream why; + why << "There is already a process with innerPid " << innerPid; + throw Napi::Error::New(env, why.str()); } createdHandles[innerPid] = handle; // Save pty struct for later use ptyHandles.push_back(pc); - v8::Local marshal = Nan::New(); DWORD pid = GetProcessId(winpty_agent_process(pc)); - Nan::Set(marshal, Nan::New("innerPid").ToLocalChecked(), Nan::New(static_cast(innerPid))); - Nan::Set(marshal, Nan::New("pid").ToLocalChecked(), Nan::New(static_cast(pid))); - Nan::Set(marshal, Nan::New("pty").ToLocalChecked(), Nan::New(InterlockedIncrement(&ptyCounter))); - Nan::Set(marshal, Nan::New("fd").ToLocalChecked(), Nan::New(-1)); - Nan::Set(marshal, Nan::New("conin").ToLocalChecked(), Nan::New(coninPipeNameStr).ToLocalChecked()); - Nan::Set(marshal, Nan::New("conout").ToLocalChecked(), Nan::New(conoutPipeNameStr).ToLocalChecked()); - info.GetReturnValue().Set(marshal); - - delete filename; - delete cmdline; - delete cwd; + Napi::Object marshal = Napi::Object::New(env); + marshal.Set("innerPid", Napi::Number::New(env, (int)innerPid)); + marshal.Set("pid", Napi::Number::New(env, (int)pid)); + marshal.Set("pty", Napi::Number::New(env, InterlockedIncrement(&ptyCounter))); + marshal.Set("fd", Napi::Number::New(env, -1)); + marshal.Set("conin", Napi::String::New(env, coninPipeNameStr)); + marshal.Set("conout", Napi::String::New(env, conoutPipeNameStr)); + + return marshal; } -static NAN_METHOD(PtyResize) { - Nan::HandleScope scope; +static Napi::Value PtyResize(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 3 || - !info[0]->IsNumber() || - !info[1]->IsNumber() || - !info[2]->IsNumber()) { - Nan::ThrowError("Usage: pty.resize(pid, cols, rows)"); - return; + !info[0].IsNumber() || + !info[1].IsNumber() || + !info[2].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.resize(pid, cols, rows)"); } - DWORD pid = info[0]->Uint32Value(Nan::GetCurrentContext()).FromJust(); - int cols = info[1]->Int32Value(Nan::GetCurrentContext()).FromJust(); - int rows = info[2]->Int32Value(Nan::GetCurrentContext()).FromJust(); + DWORD pid = info[0].As().Uint32Value(); + int cols = info[1].As().Int32Value(); + int rows = info[2].As().Int32Value(); winpty_t *pc = get_pipe_handle(pid); if (pc == nullptr) { - Nan::ThrowError("The pty doesn't appear to exist"); - return; + throw Napi::Error::New(env, "The pty doesn't appear to exist"); } BOOL success = winpty_set_size(pc, cols, rows, nullptr); if (!success) { - Nan::ThrowError("The pty could not be resized"); - return; + throw Napi::Error::New(env, "The pty could not be resized"); } - return info.GetReturnValue().SetUndefined(); + return env.Undefined(); } -static NAN_METHOD(PtyKill) { - Nan::HandleScope scope; +static Napi::Value PtyKill(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); if (info.Length() != 2 || - !info[0]->IsNumber() || - !info[1]->IsNumber()) { - Nan::ThrowError("Usage: pty.kill(pid, innerPid)"); - return; + !info[0].IsNumber() || + !info[1].IsNumber()) { + throw Napi::Error::New(env, "Usage: pty.kill(pid, innerPid)"); } - DWORD pid = info[0]->Uint32Value(Nan::GetCurrentContext()).FromJust(); - DWORD innerPid = info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust(); + DWORD pid = info[0].As().Uint32Value(); + DWORD innerPid = info[1].As().Uint32Value(); winpty_t *pc = get_pipe_handle(pid); if (pc == nullptr) { - Nan::ThrowError("Pty seems to have been killed already"); - return; + throw Napi::Error::New(env, "Pty seems to have been killed already"); } assert(remove_pipe_handle(pid)); @@ -357,20 +314,20 @@ static NAN_METHOD(PtyKill) { createdHandles.erase(innerPid); CloseHandle(innerPidHandle); - return info.GetReturnValue().SetUndefined(); + return env.Undefined(); } /** * Init */ -extern "C" void init(v8::Local 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); +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("startProcess", Napi::Function::New(env, PtyStartProcess)); + exports.Set("resize", Napi::Function::New(env, PtyResize)); + exports.Set("kill", Napi::Function::New(env, PtyKill)); + exports.Set("getExitCode", Napi::Function::New(env, PtyGetExitCode)); + exports.Set("getProcessList", Napi::Function::New(env, PtyGetProcessList)); + return exports; }; -NODE_MODULE(pty, init); +NODE_API_MODULE(NODE_GYP_MODULE_NAME, init); diff --git a/yarn.lock b/yarn.lock index 81a8bcd5..d8194385 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1318,11 +1318,6 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.17.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" - integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== - nanoid@3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" @@ -1343,6 +1338,11 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-addon-api@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.0.tgz#71f609369379c08e251c558527a107107b5e0fdb" + integrity sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g== + node-gyp@^9.4.0: version "9.4.0" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.0.tgz#2a7a91c7cba4eccfd95e949369f27c9ba704f369"