diff --git a/addons/godot_xterm/native/src/node_pty/LICENSE.md b/addons/godot_xterm/native/src/node_pty/LICENSE.md new file mode 100644 index 0000000..49477d1 --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/LICENSE.md @@ -0,0 +1,95 @@ +The code under the `node_pty` directory is taken from the [node-pty project](https://github.com/microsoft/node-pty), which in turn contains code from the [Tmux project](http://tmux.sourceforge.net/). + +### Node-pty License + +``` +Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +The MIT License (MIT) + +Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +MIT License + +Copyright (c) 2018 - present Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### Tmux License + +``` +Copyright (c) 2009 Nicholas Marriott +Copyright (c) 2009 Joshua Elsasser +Copyright (c) 2009 Todd Carson + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING +OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +``` \ No newline at end of file diff --git a/addons/godot_xterm/native/src/node_pty/unix/pty.cc b/addons/godot_xterm/native/src/node_pty/unix/pty.cc new file mode 100644 index 0000000..ce44a3e --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/unix/pty.cc @@ -0,0 +1,734 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2017, Daniel Imms (MIT License) + * + * pty.cc: + * This file is responsible for starting processes + * with pseudo-terminal file descriptors. + * + * See: + * man pty + * man tty_ioctl + * man termios + * man forkpty + */ + +/** + * Includes + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* forkpty */ +/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */ +#if defined(__GLIBC__) || defined(__CYGWIN__) +#include +#elif defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) +#include +#elif defined(__FreeBSD__) +#include +#elif defined(__sun) +#include /* for I_PUSH */ +#else +#include +#endif + +#include /* tcgetattr, tty_ioctl */ + +/* Some platforms name VWERASE and VDISCARD differently */ +#if !defined(VWERASE) && defined(VWERSE) +#define VWERASE VWERSE +#endif +#if !defined(VDISCARD) && defined(VDISCRD) +#define VDISCARD VDISCRD +#endif + +/* environ for execvpe */ +/* node/src/node_child_process.cc */ +#if defined(__APPLE__) && !TARGET_OS_IPHONE +#include +#define environ (*_NSGetEnviron()) +#else +extern char **environ; +#endif + +/* for pty_getproc */ +#if defined(__linux__) +#include +#include +#elif defined(__APPLE__) +#include +#include +#endif + +/* NSIG - macro for highest signal + 1, should be defined */ +#ifndef NSIG +#define NSIG 32 +#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; +}; + +/** + * Methods + */ + +NAN_METHOD(PtyFork); +NAN_METHOD(PtyOpen); +NAN_METHOD(PtyResize); +NAN_METHOD(PtyGetProc); + +/** + * Functions + */ + +static int +pty_execvpe(const char *, char **, char **); + +static int +pty_nonblock(int); + +static char * +pty_getproc(int, char *); + +static int +pty_openpty(int *, int *, char *, + const struct termios *, + const struct winsize *); + +static pid_t +pty_forkpty(int *, char *, + const struct termios *, + const struct winsize *); + +static void +pty_waitpid(void *); + +static void +pty_after_waitpid(uv_async_t *); + +static void +pty_after_close(uv_handle_t *); + +NAN_METHOD(PtyFork) { + Nan::HandleScope scope; + + if (info.Length() != 10 || + !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]->IsFunction()) { + return Nan::ThrowError( + "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, utf8, onexit)"); + } + + // file + Nan::Utf8String file(info[0]); + + // args + int i = 0; + v8::Local argv_ = v8::Local::Cast(info[1]); + int argc = argv_->Length(); + int argl = argc + 1 + 1; + char **argv = new char*[argl]; + argv[0] = strdup(*file); + argv[argl-1] = NULL; + for (; i < argc; i++) { + Nan::Utf8String arg(Nan::Get(argv_, i).ToLocalChecked()); + argv[i+1] = strdup(*arg); + } + + // env + i = 0; + v8::Local env_ = v8::Local::Cast(info[2]); + int envc = env_->Length(); + char **env = new char*[envc+1]; + env[envc] = NULL; + for (; i < envc; i++) { + Nan::Utf8String pair(Nan::Get(env_, i).ToLocalChecked()); + env[i] = strdup(*pair); + } + + // cwd + Nan::Utf8String cwd_(info[3]); + char *cwd = strdup(*cwd_); + + // size + struct winsize winp; + winp.ws_col = info[4]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + winp.ws_row = info[5]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + + // termios + struct termios t = termios(); + struct termios *term = &t; + term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; + if (Nan::To(info[8]).FromJust()) { +#if defined(IUTF8) + term->c_iflag |= IUTF8; +#endif + } + term->c_oflag = OPOST | ONLCR; + term->c_cflag = CREAD | CS8 | HUPCL; + term->c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL; + + term->c_cc[VEOF] = 4; + term->c_cc[VEOL] = -1; + term->c_cc[VEOL2] = -1; + term->c_cc[VERASE] = 0x7f; + term->c_cc[VWERASE] = 23; + term->c_cc[VKILL] = 21; + term->c_cc[VREPRINT] = 18; + term->c_cc[VINTR] = 3; + term->c_cc[VQUIT] = 0x1c; + term->c_cc[VSUSP] = 26; + term->c_cc[VSTART] = 17; + term->c_cc[VSTOP] = 19; + term->c_cc[VLNEXT] = 22; + term->c_cc[VDISCARD] = 15; + term->c_cc[VMIN] = 1; + term->c_cc[VTIME] = 0; + + #if (__APPLE__) + term->c_cc[VDSUSP] = 25; + term->c_cc[VSTATUS] = 20; + #endif + + cfsetispeed(term, B38400); + cfsetospeed(term, B38400); + + // uid / gid + int uid = info[6]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + int gid = info[7]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + + // fork the pty + int master = -1; + + sigset_t newmask, oldmask; + struct sigaction sig_action; + + // temporarily block all signals + // this is needed due to a race condition in openpty + // and to avoid running signal handlers in the child + // before exec* happened + sigfillset(&newmask); + pthread_sigmask(SIG_SETMASK, &newmask, &oldmask); + + pid_t pid = pty_forkpty(&master, nullptr, term, &winp); + + if (!pid) { + // remove all signal handler from child + sig_action.sa_handler = SIG_DFL; + sig_action.sa_flags = 0; + sigemptyset(&sig_action.sa_mask); + for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1 + sigaction(i, &sig_action, NULL); + } + } + // reenable signals + pthread_sigmask(SIG_SETMASK, &oldmask, NULL); + + if (pid) { + for (i = 0; i < argl; i++) free(argv[i]); + delete[] argv; + for (i = 0; i < envc; i++) free(env[i]); + delete[] env; + free(cwd); + } + + switch (pid) { + case -1: + return Nan::ThrowError("forkpty(3) failed."); + case 0: + if (strlen(cwd)) { + if (chdir(cwd) == -1) { + perror("chdir(2) failed."); + _exit(1); + } + } + + if (uid != -1 && gid != -1) { + if (setgid(gid) == -1) { + perror("setgid(2) failed."); + _exit(1); + } + if (setuid(uid) == -1) { + perror("setuid(2) failed."); + _exit(1); + } + } + + pty_execvpe(argv[0], argv, env); + + perror("execvp(3) failed."); + _exit(1); + default: + if (pty_nonblock(master) == -1) { + return Nan::ThrowError("Could not set master fd to nonblocking."); + } + + 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[9])); + baton->pid = pid; + baton->async.data = baton; + + uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid); + + uv_thread_create(&baton->tid, pty_waitpid, static_cast(baton)); + + return info.GetReturnValue().Set(obj); + } + + return info.GetReturnValue().SetUndefined(); +} + +NAN_METHOD(PtyOpen) { + Nan::HandleScope scope; + + if (info.Length() != 2 || + !info[0]->IsNumber() || + !info[1]->IsNumber()) { + return Nan::ThrowError("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_xpixel = 0; + winp.ws_ypixel = 0; + + // pty + int master, slave; + int ret = pty_openpty(&master, &slave, nullptr, NULL, &winp); + + if (ret == -1) { + return Nan::ThrowError("openpty(3) failed."); + } + + if (pty_nonblock(master) == -1) { + return Nan::ThrowError("Could not set master fd to nonblocking."); + } + + if (pty_nonblock(slave) == -1) { + return Nan::ThrowError("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()); + + return info.GetReturnValue().Set(obj); +} + +NAN_METHOD(PtyResize) { + Nan::HandleScope scope; + + if (info.Length() != 3 || + !info[0]->IsNumber() || + !info[1]->IsNumber() || + !info[2]->IsNumber()) { + return Nan::ThrowError("Usage: pty.resize(fd, cols, rows)"); + } + + int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + + struct winsize winp; + winp.ws_col = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + winp.ws_row = info[2]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + 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"); + } + return Nan::ThrowError("ioctl(2) failed"); + } + + return info.GetReturnValue().SetUndefined(); +} + +/** + * Foreground Process Name + */ +NAN_METHOD(PtyGetProc) { + Nan::HandleScope scope; + + if (info.Length() != 2 || + !info[0]->IsNumber() || + !info[1]->IsString()) { + return Nan::ThrowError("Usage: pty.process(fd, tty)"); + } + + int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(); + + Nan::Utf8String tty_(info[1]); + char *tty = strdup(*tty_); + char *name = pty_getproc(fd, tty); + free(tty); + + if (name == NULL) { + return info.GetReturnValue().SetUndefined(); + } + + v8::Local name_ = Nan::New(name).ToLocalChecked(); + free(name); + return info.GetReturnValue().Set(name_); +} + +/** + * execvpe + */ + +// execvpe(3) is not portable. +// http://www.gnu.org/software/gnulib/manual/html_node/execvpe.html +static int +pty_execvpe(const char *file, char **argv, char **envp) { + char **old = environ; + environ = envp; + int ret = execvp(file, argv); + environ = old; + return ret; +} + +/** + * Nonblocking FD + */ + +static int +pty_nonblock(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags == -1) return -1; + 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 ((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); + } + } + + 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. + */ + +// Taken from: tmux (http://tmux.sourceforge.net/) +// Copyright (c) 2009 Nicholas Marriott +// Copyright (c) 2009 Joshua Elsasser +// Copyright (c) 2009 Todd Carson +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER +// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING +// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#if defined(__linux__) + +static char * +pty_getproc(int fd, char *tty) { + FILE *f; + char *path, *buf; + size_t len; + int ch; + pid_t pgrp; + int r; + + if ((pgrp = tcgetpgrp(fd)) == -1) { + return NULL; + } + + r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp); + if (r == -1 || path == NULL) return NULL; + + if ((f = fopen(path, "r")) == NULL) { + free(path); + return NULL; + } + + free(path); + + len = 0; + buf = NULL; + while ((ch = fgetc(f)) != EOF) { + if (ch == '\0') break; + buf = (char *)realloc(buf, len + 2); + if (buf == NULL) return NULL; + buf[len++] = ch; + } + + if (buf != NULL) { + buf[len] = '\0'; + } + + fclose(f); + return buf; +} + +#elif defined(__APPLE__) + +static char * +pty_getproc(int fd, char *tty) { + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 }; + size_t size; + struct kinfo_proc kp; + + if ((mib[3] = tcgetpgrp(fd)) == -1) { + return NULL; + } + + size = sizeof kp; + if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) { + return NULL; + } + + if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') { + return NULL; + } + + return strdup(kp.kp_proc.p_comm); +} + +#else + +static char * +pty_getproc(int fd, char *tty) { + return NULL; +} + +#endif + +/** + * openpty(3) / forkpty(3) + */ + +static int +pty_openpty(int *amaster, + int *aslave, + char *name, + const struct termios *termp, + const struct winsize *winp) { +#if defined(__sun) + char *slave_name; + int slave; + int master = open("/dev/ptmx", O_RDWR | O_NOCTTY); + if (master == -1) return -1; + if (amaster) *amaster = master; + + if (grantpt(master) == -1) goto err; + if (unlockpt(master) == -1) goto err; + + slave_name = ptsname(master); + if (slave_name == NULL) goto err; + if (name) strcpy(name, slave_name); + + slave = open(slave_name, O_RDWR | O_NOCTTY); + if (slave == -1) goto err; + if (aslave) *aslave = slave; + + ioctl(slave, I_PUSH, "ptem"); + ioctl(slave, I_PUSH, "ldterm"); + ioctl(slave, I_PUSH, "ttcompat"); + + if (termp) tcsetattr(slave, TCSAFLUSH, termp); + if (winp) ioctl(slave, TIOCSWINSZ, winp); + + return 0; + +err: + close(master); + return -1; +#else + return openpty(amaster, aslave, name, (termios *)termp, (winsize *)winp); +#endif +} + +static pid_t +pty_forkpty(int *amaster, + char *name, + const struct termios *termp, + const struct winsize *winp) { +#if defined(__sun) + int master, slave; + + int ret = pty_openpty(&master, &slave, name, termp, winp); + if (ret == -1) return -1; + if (amaster) *amaster = master; + + pid_t pid = fork(); + + switch (pid) { + case -1: // error in fork, we are still in parent + close(master); + close(slave); + return -1; + case 0: // we are in the child process + close(master); + setsid(); + +#if defined(TIOCSCTTY) + // glibc does this + if (ioctl(slave, TIOCSCTTY, NULL) == -1) { + _exit(1); + } +#endif + + dup2(slave, 0); + dup2(slave, 1); + dup2(slave, 2); + + if (slave > 2) close(slave); + + return 0; + default: // we are in the parent process + close(slave); + return pid; + } + + return -1; +#else + return forkpty(amaster, name, (termios *)termp, (winsize *)winp); +#endif +} + + +/** + * 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); +} + +NODE_MODULE(pty, init) diff --git a/addons/godot_xterm/native/src/node_pty/win/conpty.cc b/addons/godot_xterm/native/src/node_pty/win/conpty.cc new file mode 100644 index 0000000..4500f6e --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/win/conpty.cc @@ -0,0 +1,455 @@ +/** + * 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. + */ + +// 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 + +#include +#include +#include // PathCombine, PathIsRelative +#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 \ + 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 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 +std::vector vectorFromString(const std::basic_string &str) { + return std::vector(str.begin(), str.end()); +} + +void throwNanError(const Nan::FunctionCallbackInfo* 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 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; + } + + 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(info[3]).FromJust(); + const std::wstring pipeName(path_util::to_wstring(Nan::Utf8String(info[4]))); + const bool inheritCursor = Nan::To(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(); + + 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)); + ptyHandles.insert(ptyHandles.end(), new pty_baton(ptyId, hIn, hOut, hpc)); + } else { + Nan::ThrowError("Cannot launch conpty"); + return; + } + + Nan::Set(marshal, Nan::New("fd").ToLocalChecked(), Nan::New(-1)); + { + std::string coninPipeNameStr(inName.begin(), inName.end()); + Nan::Set(marshal, Nan::New("conin").ToLocalChecked(), Nan::New(coninPipeNameStr).ToLocalChecked()); + + std::string conoutPipeNameStr(outName.begin(), outName.end()); + Nan::Set(marshal, Nan::New("conout").ToLocalChecked(), Nan::New(conoutPipeNameStr).ToLocalChecked()); + } + info.GetReturnValue().Set(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); + + // 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; + + // 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 envValues = info[3].As(); + const v8::Local exitCallback = v8::Local::Cast(info[4]); + + // Prepare command line + std::unique_ptr mutableCommandline = std::make_unique(cmdline.length() + 1); + HRESULT hr = StringCchCopyW(mutableCommandline.get(), cmdline.length() + 1, cmdline.c_str()); + + // Prepare cwd + std::unique_ptr mutableCwd = std::make_unique(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(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 marshal = Nan::New(); + Nan::Set(marshal, Nan::New("pid").ToLocalChecked(), Nan::New(piClient.dwProcessId)); + info.GetReturnValue().Set(marshal); +} + +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(id, cols, rows)"); + return; + } + + int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(); + 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); + } + } + + return info.GetReturnValue().SetUndefined(); +} + +static NAN_METHOD(PtyKill) { + Nan::HandleScope scope; + + if (info.Length() != 1 || + !info[0]->IsNumber()) { + Nan::ThrowError("Usage: pty.kill(id)"); + return; + } + + int id = info[0]->Int32Value(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) + { + PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ClosePseudoConsole"); + if (pfnClosePseudoConsole) + { + pfnClosePseudoConsole(handle->hpc); + } + } + + CloseHandle(handle->hShell); + + return info.GetReturnValue().SetUndefined(); +} + +/** +* 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, "kill", PtyKill); +}; + +NODE_MODULE(pty, init); diff --git a/addons/godot_xterm/native/src/node_pty/win/conpty_console_list.cc b/addons/godot_xterm/native/src/node_pty/win/conpty_console_list.cc new file mode 100644 index 0000000..be135ba --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/win/conpty_console_list.cc @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2019, Microsoft Corporation (MIT License). + */ + +#include +#include + +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(64); + auto processCount = GetConsoleProcessList(&processList[0], processList.size()); + if (processList.size() < processCount) { + processList.resize(processCount); + processCount = GetConsoleProcessList(&processList[0], processList.size()); + } + FreeConsole(); + + v8::Local result = Nan::New(); + for (DWORD i = 0; i < processCount; i++) { + Nan::Set(result, i, Nan::New(processList[i])); + } + info.GetReturnValue().Set(result); +} + +extern "C" void init(v8::Local target) { + Nan::HandleScope scope; + Nan::SetMethod(target, "getConsoleProcessList", ApiConsoleProcessList); +}; + +NODE_MODULE(pty, init); diff --git a/addons/godot_xterm/native/src/node_pty/win/path_util.cc b/addons/godot_xterm/native/src/node_pty/win/path_util.cc new file mode 100644 index 0000000..4e69f30 --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/win/path_util.cc @@ -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 +#include // PathCombine + +#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 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(path.c_str()), filename_); + + if (searchPath == NULL) { + continue; + } + + if (file_exists(searchPath)) { + shellpath = searchPath; + break; + } + } + + return shellpath; +} + +} // namespace path_util diff --git a/addons/godot_xterm/native/src/node_pty/win/path_util.h b/addons/godot_xterm/native/src/node_pty/win/path_util.h new file mode 100644 index 0000000..8dd58d2 --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/win/path_util.h @@ -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 + +#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_ diff --git a/addons/godot_xterm/native/src/node_pty/win/winpty.cc b/addons/godot_xterm/native/src/node_pty/win/winpty.cc new file mode 100644 index 0000000..b054dee --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/win/winpty.cc @@ -0,0 +1,312 @@ +/** + * 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 +#include +#include // PathCombine, PathIsRelative +#include +#include +#include +#include +#include +#include + +#include "path_util.h" + +/** +* Misc +*/ +extern "C" void init(v8::Local); + +#define WINPTY_DBG_VARIABLE TEXT("WINPTYDBG") + +/** +* winpty +*/ +static std::vector 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(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(0)); + return; + } + int processList[64]; + const int processCount = 64; + int actualCount = winpty_get_console_process_list(pc, processList, processCount, nullptr); + + v8::Local result = Nan::New(actualCount); + for (uint32_t i = 0; i < actualCount; i++) { + Nan::Set(result, i, Nan::New(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 envValues = v8::Local::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(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 marshal = Nan::New(); + Nan::Set(marshal, Nan::New("innerPid").ToLocalChecked(), Nan::New((int)GetProcessId(handle))); + Nan::Set(marshal, Nan::New("innerPidHandle").ToLocalChecked(), Nan::New((int)handle)); + Nan::Set(marshal, Nan::New("pid").ToLocalChecked(), Nan::New((int)winpty_agent_process(pc))); + Nan::Set(marshal, Nan::New("pty").ToLocalChecked(), Nan::New(InterlockedIncrement(&ptyCounter))); + Nan::Set(marshal, Nan::New("fd").ToLocalChecked(), Nan::New(-1)); + { + LPCWSTR coninPipeName = winpty_conin_name(pc); + std::wstring coninPipeNameWStr(coninPipeName); + std::string coninPipeNameStr(coninPipeNameWStr.begin(), coninPipeNameWStr.end()); + Nan::Set(marshal, Nan::New("conin").ToLocalChecked(), Nan::New(coninPipeNameStr).ToLocalChecked()); + LPCWSTR conoutPipeName = winpty_conout_name(pc); + std::wstring conoutPipeNameWStr(conoutPipeName); + std::string conoutPipeNameStr(conoutPipeNameWStr.begin(), conoutPipeNameWStr.end()); + Nan::Set(marshal, Nan::New("conout").ToLocalChecked(), Nan::New(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 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);