Add new PTY node (replaces Pseudoterminal node)

Uses fork of node-pty native code for forking pseudoterminals.
Uses libuv pipe handle to communicate with the child process.

- Paves the way for cross-platform (Linux, macOS and Windows) support.
- Renames Pseudoterminal to PTY (which is much easier to type and spell :D).
- Better performance than the old Pseudoterminal node. Especially when
  streaming large amounts of data such as running the `yes` command.
- Allows setting custom file, args, initial window size, cwd, env vars
  (including important ones such as TERM and COLORTERM) and uid/gid
  on Linux and macOS.
- Returns process exit code and terminating signal.
This commit is contained in:
Leroy Hopson 2021-07-03 00:27:34 +07:00 committed by Leroy Hopson
parent bfa561357e
commit 0dd2378387
36 changed files with 1268 additions and 442 deletions

View file

@ -1,5 +1,7 @@
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.
### Node-pty License
```
@ -92,4 +94,4 @@ 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

@ -1,6 +1,7 @@
/**
* 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
@ -17,8 +18,12 @@
* Includes
*/
#include "pty.h"
#include "libuv_utils.h"
#include <FuncRef.hpp>
#include <uv.h>
#include <errno.h>
#include <nan.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@ -77,12 +82,14 @@ extern char **environ;
#define NSIG 32
#endif
using namespace godot;
/**
* Structs
*/
struct pty_baton {
Nan::Persistent<v8::Function> cb;
Ref<FuncRef> cb;
int exit_code;
int signal_code;
pid_t pid;
@ -90,15 +97,6 @@ struct pty_baton {
uv_thread_t tid;
};
/**
* Methods
*/
NAN_METHOD(PtyFork);
NAN_METHOD(PtyOpen);
NAN_METHOD(PtyResize);
NAN_METHOD(PtyGetProc);
/**
* Functions
*/
@ -121,52 +119,41 @@ 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)");
}
Array PTYUnix::fork(String p_file, int _ignored, PoolStringArray p_args,
PoolStringArray p_env, String p_cwd, int p_cols, int p_rows,
int p_uid, int p_gid, bool p_utf8, Ref<FuncRef> p_on_exit) {
// file
Nan::Utf8String file(info[0]);
char *file = p_file.alloc_c_string();
// args
int i = 0;
v8::Local<v8::Array> argv_ = v8::Local<v8::Array>::Cast(info[1]);
int argc = argv_->Length();
int argc = p_args.size();
int argl = argc + 1 + 1;
char **argv = new char *[argl];
argv[0] = strdup(*file);
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);
char *arg = p_args[i].alloc_c_string();
argv[i + 1] = strdup(arg);
}
// env
i = 0;
v8::Local<v8::Array> env_ = v8::Local<v8::Array>::Cast(info[2]);
int envc = env_->Length();
int envc = p_env.size();
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);
char *pairs = p_env[i].alloc_c_string();
env[i] = strdup(pairs);
}
// cwd
Nan::Utf8String cwd_(info[3]);
char *cwd = strdup(*cwd_);
char *cwd = strdup(p_cwd.alloc_c_string());
// size
struct winsize winp;
winp.ws_col = info[4]->IntegerValue(Nan::GetCurrentContext()).FromJust();
winp.ws_row = info[5]->IntegerValue(Nan::GetCurrentContext()).FromJust();
winp.ws_col = p_cols;
winp.ws_row = p_rows;
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
@ -174,7 +161,7 @@ NAN_METHOD(PtyFork) {
struct termios t = termios();
struct termios *term = &t;
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
if (Nan::To<bool>(info[8]).FromJust()) {
if (p_utf8) {
#if defined(IUTF8)
term->c_iflag |= IUTF8;
#endif
@ -210,8 +197,8 @@ NAN_METHOD(PtyFork) {
cfsetospeed(term, B38400);
// uid / gid
int uid = info[6]->IntegerValue(Nan::GetCurrentContext()).FromJust();
int gid = info[7]->IntegerValue(Nan::GetCurrentContext()).FromJust();
int uid = p_uid;
int gid = p_gid;
// fork the pty
int master = -1;
@ -242,17 +229,18 @@ NAN_METHOD(PtyFork) {
if (pid) {
for (i = 0; i < argl; i++)
free(argv[i]);
std::free(argv[i]);
delete[] argv;
for (i = 0; i < envc; i++)
free(env[i]);
std::free(env[i]);
delete[] env;
free(cwd);
std::free(cwd);
}
switch (pid) {
case -1:
return Nan::ThrowError("forkpty(3) failed.");
ERR_PRINT("forkpty(3) failed.");
return Array::make(GODOT_FAILED);
case 0:
if (strlen(cwd)) {
if (chdir(cwd) == -1) {
@ -278,21 +266,19 @@ NAN_METHOD(PtyFork) {
_exit(1);
default:
if (pty_nonblock(master) == -1) {
return Nan::ThrowError("Could not set master fd to nonblocking.");
ERR_PRINT("Could not set master fd to nonblocking.");
return Array::make(GODOT_FAILED);
}
v8::Local<v8::Object> obj = Nan::New<v8::Object>();
Nan::Set(obj, Nan::New<v8::String>("fd").ToLocalChecked(),
Nan::New<v8::Number>(master));
Nan::Set(obj, Nan::New<v8::String>("pid").ToLocalChecked(),
Nan::New<v8::Number>(pid));
Nan::Set(obj, Nan::New<v8::String>("pty").ToLocalChecked(),
Nan::New<v8::String>(ptsname(master)).ToLocalChecked());
Dictionary result = Dictionary::make();
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.Reset(v8::Local<v8::Function>::Cast(info[9]));
baton->cb = p_on_exit;
baton->pid = pid;
baton->async.data = baton;
@ -300,23 +286,17 @@ NAN_METHOD(PtyFork) {
uv_thread_create(&baton->tid, pty_waitpid, static_cast<void *>(baton));
return info.GetReturnValue().Set(obj);
return Array::make(GODOT_OK, result);
}
return info.GetReturnValue().SetUndefined();
return Array::make(GODOT_FAILED);
}
NAN_METHOD(PtyOpen) {
Nan::HandleScope scope;
if (info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsNumber()) {
return Nan::ThrowError("Usage: pty.open(cols, rows)");
}
Array PTYUnix::open(int p_cols, int p_rows) {
// size
struct winsize winp;
winp.ws_col = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
winp.ws_row = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust();
winp.ws_col = p_cols;
winp.ws_row = p_rows;
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
@ -325,85 +305,72 @@ NAN_METHOD(PtyOpen) {
int ret = pty_openpty(&master, &slave, nullptr, NULL, &winp);
if (ret == -1) {
return Nan::ThrowError("openpty(3) failed.");
ERR_PRINT("openpty(3) failed.");
return Array::make(GODOT_FAILED);
}
if (pty_nonblock(master) == -1) {
return Nan::ThrowError("Could not set master fd to nonblocking.");
ERR_PRINT("Could not set master fd to nonblocking.");
return Array::make(GODOT_FAILED);
}
if (pty_nonblock(slave) == -1) {
return Nan::ThrowError("Could not set slave fd to nonblocking.");
ERR_PRINT("Could not set slave fd to nonblocking.");
return Array::make(GODOT_FAILED);
}
v8::Local<v8::Object> obj = Nan::New<v8::Object>();
Nan::Set(obj, Nan::New<v8::String>("master").ToLocalChecked(),
Nan::New<v8::Number>(master));
Nan::Set(obj, Nan::New<v8::String>("slave").ToLocalChecked(),
Nan::New<v8::Number>(slave));
Nan::Set(obj, Nan::New<v8::String>("pty").ToLocalChecked(),
Nan::New<v8::String>(ptsname(master)).ToLocalChecked());
Dictionary dict = Dictionary::make();
dict["master"] = master;
dict["slave"] = slave;
dict["pty"] = ptsname(master);
return info.GetReturnValue().Set(obj);
return Array::make(GODOT_OK, dict);
}
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();
godot_error PTYUnix::resize(int p_fd, int p_cols, int p_rows) {
int fd = p_fd;
struct winsize winp;
winp.ws_col = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust();
winp.ws_row = info[2]->IntegerValue(Nan::GetCurrentContext()).FromJust();
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 Nan::ThrowError("ioctl(2) failed, EBADF");
RETURN_UV_ERR(UV_EBADF)
case EFAULT:
return Nan::ThrowError("ioctl(2) failed, EFAULT");
RETURN_UV_ERR(UV_EFAULT)
case EINVAL:
return Nan::ThrowError("ioctl(2) failed, EINVAL");
RETURN_UV_ERR(UV_EINVAL);
case ENOTTY:
return Nan::ThrowError("ioctl(2) failed, ENOTTY");
RETURN_UV_ERR(UV_ENOTTY);
}
return Nan::ThrowError("ioctl(2) failed");
ERR_PRINT("ioctl(2) failed");
return GODOT_FAILED;
}
return info.GetReturnValue().SetUndefined();
return GODOT_OK;
}
/**
* Foreground Process Name
*/
NAN_METHOD(PtyGetProc) {
Nan::HandleScope scope;
String PTYUnix::process(int p_fd, String p_tty) {
int fd = p_fd;
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 *tty = p_tty.alloc_c_string();
char *name = pty_getproc(fd, tty);
free(tty);
std::free(tty);
if (name == NULL) {
return info.GetReturnValue().SetUndefined();
return "";
}
v8::Local<v8::String> name_ = Nan::New<v8::String>(name).ToLocalChecked();
free(name);
return info.GetReturnValue().Set(name_);
String name_ = String(name);
std::free(name);
return name_;
}
/**
@ -453,7 +420,7 @@ static void pty_waitpid(void *data) {
// waitpid is already handled elsewhere.
;
} else {
assert(false);
// assert(false);
}
}
@ -474,19 +441,12 @@ static void pty_waitpid(void *data) {
*/
static void pty_after_waitpid(uv_async_t *async) {
Nan::HandleScope scope;
pty_baton *baton = static_cast<pty_baton *>(async->data);
v8::Local<v8::Value> argv[] = {
Nan::New<v8::Integer>(baton->exit_code),
Nan::New<v8::Integer>(baton->signal_code),
};
Array argv = Array::make(baton->exit_code, baton->signal_code);
v8::Local<v8::Function> cb = Nan::New<v8::Function>(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);
ERR_FAIL_COND(baton->cb == nullptr);
baton->cb->call_funcv(argv);
uv_close((uv_handle_t *)async, pty_after_close);
}
@ -701,12 +661,12 @@ static pid_t pty_forkpty(int *amaster, char *name, const struct termios *termp,
* 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);
void PTYUnix::_register_methods() {
register_method("_init", &PTYUnix::_init);
register_method("fork", &PTYUnix::fork);
register_method("open", &PTYUnix::open);
register_method("resize", &PTYUnix::resize);
register_method("process", &PTYUnix::process);
}
NODE_MODULE(pty, init)
void PTYUnix::_init() {}

View file

@ -0,0 +1,31 @@
// Copyright (c) 2021, Leroy Hopson (MIT License).
#ifndef GODOT_XTERM_PTY_H
#define GODOT_XTERM_PTY_H
#include <FuncRef.hpp>
#include <Godot.hpp>
namespace godot {
class PTYUnix : public Reference {
GODOT_CLASS(PTYUnix, Reference)
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);
godot_error resize(int fd, int cols, int rows);
String process(int fd, String tty);
void _init();
static void _register_methods();
};
} // namespace godot
#endif // GODOT_XTERM_PTY_H