Rewrite terminal.cpp

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

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

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

View file

@ -0,0 +1,6 @@
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/).
The code has been modified to remove references to node/V8 and make it compatible with GDNative. Any copyrightable modifications are released under the [same license](/addons/godot_xterm/LICENSE) as the rest of the GodotXterm project.
The text of the node-pty license can be found in [LICENSE_node-pty](./LICENSE_node-pty)
The text of the tmux license can be found in [LICENSE_tmux](./LICENSE_tmux)

View file

@ -0,0 +1,69 @@
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.

View file

@ -0,0 +1,15 @@
Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
Copyright (c) 2009 Todd Carson <toc@daybefore.net>
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.

View file

@ -0,0 +1,675 @@
/**
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
* Copyright (c) 2017, Daniel Imms (MIT License)
* Copyright (c) 2021, Leroy Hopson (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 "pty.h"
#include "libuv_utils.h"
#include <godot_cpp/variant/callable.hpp>
#include <uv.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
/* forkpty */
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
#if defined(__GLIBC__) || defined(__CYGWIN__)
#include <pty.h>
#elif defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__)
#include <util.h>
#elif defined(__FreeBSD__)
#include <libutil.h>
#elif defined(__sun)
#include <stropts.h> /* for I_PUSH */
#else
#include <pty.h>
#endif
#include <termios.h> /* 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 <crt_externs.h>
#define environ (*_NSGetEnviron())
#else
extern char **environ;
#endif
/* for pty_getproc */
#if defined(__linux__)
#include <stdint.h>
#include <stdio.h>
#elif defined(__APPLE__)
#include <libproc.h>
#include <sys/sysctl.h>
#endif
/* NSIG - macro for highest signal + 1, should be defined */
#ifndef NSIG
#define NSIG 32
#endif
using namespace godot;
/**
* Structs
*/
struct pty_baton {
Callable cb;
int exit_code;
int signal_code;
pid_t pid;
uv_async_t async;
uv_thread_t tid;
};
/**
* 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 *);
Array PTYUnix::fork(String p_file, int _ignored, PackedStringArray p_args,
PackedStringArray p_env, String p_cwd, int p_cols,
int p_rows, int p_uid, int p_gid, bool p_utf8,
Callable p_on_exit) {
// file
char *file = strdup(p_file.utf8().get_data());
// args
int i = 0;
int argc = p_args.size();
int argl = argc + 1 + 1;
char **argv = new char *[argl];
argv[0] = strdup(file);
argv[argl - 1] = NULL;
for (; i < argc; i++) {
char *arg = strdup(p_args[i].utf8().get_data());
argv[i + 1] = strdup(arg);
}
// env
i = 0;
int envc = p_env.size();
char **env = new char *[envc + 1];
env[envc] = NULL;
for (; i < envc; i++) {
char *pairs = strdup(p_env[i].utf8().get_data());
env[i] = strdup(pairs);
}
// cwd
char *cwd = strdup(p_cwd.utf8().get_data());
// size
struct winsize winp;
winp.ws_col = p_cols;
winp.ws_row = p_rows;
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 (p_utf8) {
#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 = p_uid;
int gid = p_gid;
// 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++)
std::free(argv[i]);
delete[] argv;
for (i = 0; i < envc; i++)
std::free(env[i]);
delete[] env;
std::free(cwd);
}
switch (pid) {
case -1:
ERR_PRINT("forkpty(3) failed.");
return Array::make(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) {
ERR_PRINT("Could not set master fd to nonblocking.");
return Array::make(FAILED);
}
Dictionary result;
result["fd"] = (int)master;
result["pid"] = (int)pid;
result["pty"] = ptsname(master);
pty_baton *baton = new pty_baton();
baton->exit_code = 0;
baton->signal_code = 0;
baton->cb = p_on_exit;
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<void *>(baton));
return Array::make(OK, result);
}
return Array::make(FAILED);
}
Array PTYUnix::open(int p_cols, int p_rows) {
// size
struct winsize winp;
winp.ws_col = p_cols;
winp.ws_row = p_rows;
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
// pty
int master, slave;
int ret = pty_openpty(&master, &slave, nullptr, NULL, &winp);
if (ret == -1) {
ERR_PRINT("openpty(3) failed.");
return Array::make(FAILED);
}
if (pty_nonblock(master) == -1) {
ERR_PRINT("Could not set master fd to nonblocking.");
return Array::make(FAILED);
}
if (pty_nonblock(slave) == -1) {
ERR_PRINT("Could not set slave fd to nonblocking.");
return Array::make(FAILED);
}
Dictionary dict;
dict["master"] = master;
dict["slave"] = slave;
dict["pty"] = ptsname(master);
return Array::make(OK, dict);
}
Error PTYUnix::resize(int p_fd, int p_cols, int p_rows) {
int fd = p_fd;
struct winsize winp;
winp.ws_col = p_cols;
winp.ws_row = p_rows;
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
switch (errno) {
case EBADF:
RETURN_UV_ERR(UV_EBADF)
case EFAULT:
RETURN_UV_ERR(UV_EFAULT)
case EINVAL:
RETURN_UV_ERR(UV_EINVAL);
case ENOTTY:
RETURN_UV_ERR(UV_ENOTTY);
}
ERR_PRINT("ioctl(2) failed");
return FAILED;
}
return OK;
}
/**
* Foreground Process Name
*/
String PTYUnix::process(int p_fd, String p_tty) {
int fd = p_fd;
char *tty = strdup(p_tty.utf8().get_data());
char *name = pty_getproc(fd, tty);
std::free(tty);
if (name == NULL) {
return "";
}
String name_ = String(name);
std::free(name);
return 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<pty_baton *>(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) {
pty_baton *baton = static_cast<pty_baton *>(async->data);
Array argv = Array::make(baton->exit_code, baton->signal_code);
if (baton->cb.is_valid()) {
baton->cb.callv(argv);
baton->cb = (Variant) nullptr;
}
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<pty_baton *>(async->data);
delete baton;
}
/**
* pty_getproc
* Taken from tmux.
*/
// Taken from: tmux (http://tmux.sourceforge.net/)
// Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
// Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
// Copyright (c) 2009 Todd Carson <toc@daybefore.net>
//
// 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
*/
void PTYUnix::_bind_methods() {
ClassDB::bind_method(D_METHOD("_init"), &PTYUnix::_init);
ClassDB::bind_method(D_METHOD("fork"), &PTYUnix::fork);
ClassDB::bind_method(D_METHOD("open"), &PTYUnix::open);
ClassDB::bind_method(D_METHOD("resize"), &PTYUnix::resize);
ClassDB::bind_method(D_METHOD("process"), &PTYUnix::process);
}
void PTYUnix::_init() {}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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