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

@ -218,10 +218,13 @@ env.Append(CXXFLAGS=['-std=c++14'])
env.Append(CPPPATH=[
'src/',
'thirdparty/libtsm/build/src/tsm',
'thirdparty/libtsm/build/src/shared',
'thirdparty/godot-cpp/include/',
'thirdparty/godot-cpp/include/core/',
'thirdparty/godot-cpp/include/gen/',
'thirdparty/godot-cpp/godot-headers/'
'thirdparty/godot-cpp/godot-headers/',
'thirdparty/libuv/src',
'thirdparty/libuv/include'
])
env.Append(LIBPATH=[
'thirdparty/godot-cpp/bin/',
@ -247,10 +250,12 @@ sources = []
sources.append('src/libgodotxtermnative.cpp')
sources.append('src/terminal.cpp')
# Psuedoterminal not supported on windows (yet) or HTML5.
if env['platform'] != 'windows' and env['platform'] != 'javascript':
sources.append('src/pseudoterminal.cpp')
env.Append(LIBS=['util'])
# PTY not supported on HTML5 or Windows (yet).
if env['platform'] != 'javascript' and env['platform'] != 'windows':
sources.append('src/pipe.cpp')
sources.append('src/libuv_utils.cpp')
sources.append('src/node_pty/unix/pty.cc')
env.Append(LIBS=['util', env.File('thirdparty/libuv/build/libuv_a.a')])
if env['platform'] == 'linux':
libsuffix = "a"

View file

@ -15,21 +15,32 @@ fi
# Update git submodules.
LIBTSM_DIR=${NATIVE_DIR}/thirdparty/libtsm
if [ -z "$(ls -A -- "$LIBTSM_DIR")" ]; then
cd ${NATIVE_DIR}
git submodule update --init --recursive -- $LIBTSM_DIR
fi
GODOT_CPP_DIR=${NATIVE_DIR}/thirdparty/godot-cpp
if [ -z "$(ls -A -- "$GODOT_CPP_DIR")" ]; then
cd ${NATIVE_DIR}
git submodule update --init --recursive -- $GODOT_CPP_DIR
fi
updateSubmodules() {
eval $1=$2 # E.g LIBUV_DIR=${NATIVE_DIR}/thirdparty/libuv
if [ -z "$(ls -A -- "$2")" ]; then
cd ${NATIVE_DIR}
git submodule update --init --recursive -- $2
fi
}
updateSubmodules LIBUV_DIR ${NATIVE_DIR}/thirdparty/libuv
updateSubmodules LIBTSM_DIR ${NATIVE_DIR}/thirdparty/libtsm
updateSubmodules GODOT_CPP_DIR ${NATIVE_DIR}/thirdparty/godot-cpp
# Build godot-cpp bindings.
cd ${GODOT_CPP_DIR}
scons generate_bindings=yes target=debug -j$(nproc)
# Build libuv as a static library.
cd ${LIBUV_DIR}
mkdir build || true
cd build
cmake .. -DCMAKE_BUILD_TYPE=debug -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE
cd ..
cmake --build build -j$(nproc)
# Build libgodot-xterm.
cd ${NATIVE_DIR}
scons target=debug -j$(nproc)

View file

@ -6,6 +6,9 @@ mkShell {
cacert # Required for git clone on GithHub actions runner.
# Used to build libuv.
cmake
# Used to build for javascript platform.
docker
docker-compose

View file

@ -1,6 +1,9 @@
#include "terminal.h"
#if defined(__unix__) /* Linux and macOS */ && !defined(__EMSCRIPTEN__)
#include "pseudoterminal.h"
#if !defined(__EMSCRIPTEN__) && !defined(__WIN32)
#include "libuv_utils.h"
#include "node_pty/unix/pty.h"
#include "pipe.h"
#endif
extern "C" void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
@ -14,9 +17,10 @@ godot_gdnative_terminate(godot_gdnative_terminate_options *o) {
extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) {
godot::Godot::nativescript_init(handle);
godot::register_tool_class<godot::Terminal>();
#if defined(__unix__) && !defined(__EMSCRIPTEN__)
godot::register_class<godot::Pseudoterminal>();
#if !defined(__EMSCRIPTEN__) && !defined(__WIN32)
godot::register_tool_class<godot::Pipe>();
godot::register_tool_class<godot::LibuvUtils>();
godot::register_tool_class<godot::PTYUnix>();
#endif
}

View file

@ -0,0 +1,190 @@
#include "libuv_utils.h"
#include <uv.h>
using namespace godot;
void LibuvUtils::_register_methods() {
register_method("_init", &LibuvUtils::_init);
register_method("get_os_environ", &LibuvUtils::get_os_environ);
register_method("get_os_release", &LibuvUtils::get_os_release);
register_method("get_cwd", &LibuvUtils::get_cwd);
}
LibuvUtils::LibuvUtils() {}
LibuvUtils::~LibuvUtils() {}
void LibuvUtils::_init() {}
Dictionary LibuvUtils::get_os_environ() {
Dictionary result;
uv_env_item_t *env;
int count;
uv_os_environ(&env, &count);
for (int i = 0; i < count; i++) {
result[String(env[i].name)] = String(env[i].value);
}
uv_os_free_environ(env, count);
return result;
}
String LibuvUtils::get_os_release() { return "TODO"; }
String LibuvUtils::get_cwd() {
#ifndef PATH_MAX
#define PATH_MAX MAX_PATH
#endif
size_t size = PATH_MAX;
char *buffer = (char *)malloc(size * sizeof(char));
int err;
err = uv_cwd(buffer, &size);
if (err == UV_ENOBUFS) {
// Buffer was too small. `size` has been set to the required length, so
// resize buffer and try again.
buffer = (char *)realloc(buffer, size * sizeof(char));
err = uv_cwd(buffer, &size);
}
if (err < 0) {
UV_ERR_PRINT(err);
return "";
}
String result = String(buffer);
std::free(buffer);
return result;
}
godot_error LibuvUtils::translate_uv_errno(int uv_err) {
if (uv_err >= 0)
return GODOT_OK;
// Rough translation of libuv error to godot error.
// Not necessarily accurate.
switch (uv_err) {
case UV_EEXIST: // file already exists
return GODOT_ERR_ALREADY_EXISTS;
case UV_EADDRINUSE: // address already in use
return GODOT_ERR_ALREADY_IN_USE;
case UV_EBUSY: // resource busy or locked
case UV_ETXTBSY: // text file is busy
return GODOT_ERR_BUSY;
case UV_ECONNREFUSED: // connection refused
return GODOT_ERR_CANT_CONNECT;
case UV_ECONNABORTED: // software caused connection abort
case UV_ECONNRESET: // connection reset by peer
case UV_EISCONN: // socket is already connected
case UV_ENOTCONN: // socket is not connected
return GODOT_ERR_CONNECTION_ERROR;
case UV_ENODEV: // no such device
case UV_ENXIO: // no such device or address
case UV_ESRCH: // no such process
return GODOT_ERR_DOES_NOT_EXIST;
case UV_EROFS: // read-only file system
return GODOT_ERR_FILE_CANT_WRITE;
case UV_EOF: // end of file
return GODOT_ERR_FILE_EOF;
case UV_ENOENT: // no such file or directory
return GODOT_ERR_FILE_NOT_FOUND;
case UV_EAI_BADFLAGS: // bad ai_flags value
case UV_EAI_BADHINTS: // invalid value for hints
case UV_EFAULT: // bad address in system call argument
case UV_EFTYPE: // inappropriate file type or format
case UV_EINVAL: // invalid argument
case UV_ENOTTY: // inappropriate ioctl for device
case UV_EPROTOTYPE: // protocol wrong type for socket
return GODOT_ERR_INVALID_PARAMETER; // Parameter passed is invalid
case UV_ENOSYS: // function not implemented
return GODOT_ERR_METHOD_NOT_FOUND;
case UV_EAI_MEMORY: // out of memory
return GODOT_ERR_OUT_OF_MEMORY;
case UV_E2BIG: // argument list too long
case UV_EFBIG: // file too large
case UV_EMSGSIZE: // message too long
case UV_ENAMETOOLONG: // name too long
case UV_EOVERFLOW: // value too large for defined data type
case UV_ERANGE: // result too large
return GODOT_ERR_PARAMETER_RANGE_ERROR; // Parameter given out of range
case UV_ETIMEDOUT:
return GODOT_ERR_TIMEOUT; // connection timed out
case UV_EACCES: // permission denied
case UV_EPERM: // operation not permitted
case UV_EXDEV: // cross-device link not permitted
return GODOT_ERR_UNAUTHORIZED;
case UV_EADDRNOTAVAIL: // address not available
case UV_EAFNOSUPPORT: // address family not supported
case UV_EAGAIN: // resource temporarily unavailable
case UV_EAI_ADDRFAMILY: // address family not supported
case UV_EAI_FAMILY: // ai_family not supported
case UV_EAI_SERVICE: // service not available for socket type
case UV_EAI_SOCKTYPE: // socket type not supported
case UV_ENOPROTOOPT: // protocol not available
case UV_ENOTSUP: // operation not supported on socket
case UV_EPROTONOSUPPORT: // protocol not supported
case UV_ESOCKTNOSUPPORT: // socket type not supported
return GODOT_ERR_UNAVAILABLE; // What is requested is
// unsupported/unavailable
case UV_EAI_NODATA: // no address
case UV_EDESTADDRREQ: // destination address required
return GODOT_ERR_UNCONFIGURED;
case UV_EAI_AGAIN: // temporary failure
case UV_EAI_CANCELED: // request canceled
case UV_EAI_FAIL: // permanent failure
case UV_EAI_NONAME: // unknown node or service
case UV_EAI_OVERFLOW: // argument buffer overflow
case UV_EAI_PROTOCOL: // resolved protocol is unknown
case UV_EALREADY: // connection already in progress
case UV_EBADF: // bad file descriptor
case UV_ECANCELED: // operation canceled
case UV_ECHARSET: // invalid Unicode character
case UV_EHOSTUNREACH: // host is unreachable
case UV_EIO: // i/o error
case UV_EILSEQ: // illegal byte sequence
case UV_EISDIR: // illegal operation on a directory
case UV_ELOOP: // too many symbolic links encountered
case UV_EMFILE: // too many open files
case UV_ENETDOWN: // network is down
case UV_ENETUNREACH: // network is unreachable
case UV_ENFILE: // file table overflow
case UV_ENOBUFS: // no buffer space available
case UV_ENOMEM: // not enough memory
case UV_ESHUTDOWN: // cannot send after transport endpoint shutdown
case UV_EINTR: // interrupted system call
case UV_EMLINK: // too many links
case UV_ENONET: // machine is not on the network
case UV_ENOSPC: // no space left on device
case UV_ENOTDIR: // not a directory
case UV_ENOTEMPTY: // directory not empty
case UV_ENOTSOCK: // socket operation on non-socket
case UV_EPIPE: // broken pipe
case UV_EPROTO: // protocol error
case UV_ESPIPE: // invalid seek
case UV_UNKNOWN: // unknown error
default:
return GODOT_FAILED; // Generic fail error
}
}

View file

@ -0,0 +1,43 @@
#ifndef GODOT_XTERM_UV_UTILS_H
#define GODOT_XTERM_UV_UTILS_H
#include <Godot.hpp>
#include <uv.h>
#define UV_ERR_PRINT(uv_err) \
ERR_PRINT(String(uv_err_name(uv_err)) + String(": ") + \
String(uv_strerror(uv_err)));
#define RETURN_UV_ERR(uv_err) \
UV_ERR_PRINT(uv_err); \
return LibuvUtils::translate_uv_errno(uv_err);
#define RETURN_IF_UV_ERR(uv_err) \
if (uv_err < 0) { \
RETURN_UV_ERR(uv_err); \
}
namespace godot {
class LibuvUtils : public Reference {
GODOT_CLASS(LibuvUtils, Reference)
public:
static void _register_methods();
LibuvUtils();
~LibuvUtils();
void _init();
Dictionary get_os_environ();
String get_os_release();
String get_cwd();
public:
static godot_error translate_uv_errno(int uv_err);
};
} // namespace godot
#endif // GODOT_XTERM_UV_UTILS_H

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

View file

@ -0,0 +1,108 @@
// Copyright (c) 2021, Leroy Hopson (MIT License).
#include "pipe.h"
#include "libuv_utils.h"
#include <Dictionary.hpp>
#include <InputEventKey.hpp>
#include <OS.hpp>
#include <ResourceLoader.hpp>
#include <Theme.hpp>
#include <Timer.hpp>
#include <algorithm>
#include <thread>
#include <vector>
#include <xkbcommon/xkbcommon-keysyms.h>
using namespace godot;
void Pipe::_register_methods() {
register_method("_init", &Pipe::_init);
register_method("poll", &Pipe::_poll_connection);
register_method("open", &Pipe::open);
register_method("write", &Pipe::write);
register_signal<Pipe>("data_received", "data",
GODOT_VARIANT_TYPE_POOL_BYTE_ARRAY);
}
Pipe::Pipe() {}
Pipe::~Pipe() {}
void Pipe::_init() {}
void _poll_connection();
void _read_cb(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf);
void _write_cb(uv_write_t *req, int status);
void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf);
godot_error Pipe::open(int fd, bool ipc = false) {
RETURN_IF_UV_ERR(uv_pipe_init(uv_default_loop(), &handle, ipc));
handle.data = this;
RETURN_IF_UV_ERR(uv_pipe_open(&handle, fd));
RETURN_IF_UV_ERR(uv_stream_set_blocking((uv_stream_t *)&handle, false));
RETURN_IF_UV_ERR(
uv_read_start((uv_stream_t *)&handle, _alloc_buffer, _read_cb));
return GODOT_OK;
}
godot_error Pipe::write(String p_data) {
char *s = p_data.alloc_c_string();
size_t len = strlen(s);
uv_buf_t bufs[] = {{.base = s, .len = len}};
uv_write_t req;
req.data = s;
uv_write(&req, (uv_stream_t *)&handle, bufs, 1, _write_cb);
uv_run(uv_default_loop(), UV_RUN_NOWAIT);
return GODOT_OK;
}
int Pipe::get_status() {
_poll_connection();
return status;
}
void Pipe::_poll_connection() { uv_run(uv_default_loop(), UV_RUN_NOWAIT); }
void _read_cb(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf) {
Pipe *pipe = static_cast<Pipe *>(handle->data);
if (nread < 0) {
switch (nread) {
case UV_EOF:
// Normal after shell exits.
return;
case UV_EIO:
// Can happen when the process exits.
// As long as PTY has caught it, we should be fine.
return;
default:
UV_ERR_PRINT(nread);
}
return;
}
PoolByteArray data;
data.resize(nread);
memcpy(data.write().ptr(), buf->base, nread);
pipe->emit_signal("data_received", data);
}
void _write_cb(uv_write_t *req, int status) {}
void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = (char *)malloc(suggested_size);
buf->len = suggested_size;
}

View file

@ -0,0 +1,64 @@
// Copyright (c) 2021, Leroy Hopson (MIT License).
#ifndef GODOT_XTERM_PIPE_H
#define GODOT_XTERM_PIPE_H
#include <Array.hpp>
#include <FuncRef.hpp>
#include <Godot.hpp>
#include <Reference.hpp>
#include <StreamPeer.hpp>
#include <StreamPeerBuffer.hpp>
#include <StreamPeerGDNative.hpp>
#include <uv.h>
namespace godot {
class Pipe : public Reference {
GODOT_CLASS(Pipe, Reference)
public:
uv_pipe_t handle;
static void _register_methods();
enum Status {
NONE,
CONNECTING,
CONNECTED,
ERROR,
};
int STATUS_NONE = Status::NONE;
int STATUS_CONNECTING = Status::CONNECTING;
int STATUS_CONNECTED = Status::CONNECTING;
int STATUS_ERROR = Status::ERROR;
Pipe();
~Pipe();
void _init();
godot_error open(int fd, bool ipc);
int get_status();
godot_error write(String p_data);
void pause();
void resume();
protected:
const godot_net_stream_peer *interface;
public:
Status status;
private:
void _poll_connection();
static godot_error _translate_error(int err);
};
} // namespace godot
#endif // GODOT_XTERM_PIPE_H

View file

@ -1,155 +0,0 @@
#include "pseudoterminal.h"
#include <libgen.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
// Platform specific includes.
#if defined(__linux__)
#include <pty.h>
#endif
#if defined(__APPLE__)
#include <sys/ioctl.h>
#include <util.h>
#endif
using namespace godot;
void Pseudoterminal::_register_methods() {
register_method("_init", &Pseudoterminal::_init);
register_method("_ready", &Pseudoterminal::_ready);
register_method("write", &Pseudoterminal::write);
register_method("resize", &Pseudoterminal::resize);
register_signal<Pseudoterminal>((char *)"data_sent", "data",
GODOT_VARIANT_TYPE_POOL_BYTE_ARRAY);
register_signal<Pseudoterminal>((char *)"exited", "status",
GODOT_VARIANT_TYPE_INT);
}
Pseudoterminal::Pseudoterminal() {}
Pseudoterminal::~Pseudoterminal() { pty_thread.join(); }
void Pseudoterminal::_init() {
bytes_to_write = 0;
pty_thread = std::thread(&Pseudoterminal::process_pty, this);
}
void Pseudoterminal::process_pty() {
int fd;
char *name;
int status;
should_process_pty = true;
struct termios termios = {};
termios.c_iflag = IGNPAR | ICRNL;
termios.c_oflag = 0;
termios.c_cflag = B38400 | CRTSCTS | CS8 | CLOCAL | CREAD;
termios.c_lflag = ICANON;
termios.c_cc[VMIN] = 1;
termios.c_cc[VTIME] = 0;
pid_t pty_pid = forkpty(&fd, NULL, NULL, NULL);
if (pty_pid == -1) {
ERR_PRINT(
String("Error forking pty: {0}").format(Array::make(strerror(errno))));
should_process_pty = false;
return;
} else if (pty_pid == 0) {
/* Child */
char termenv[11] = {"TERM=xterm"};
putenv(termenv);
char colortermenv[20] = {"COLORTERM=truecolor"};
putenv(colortermenv);
char *shell = getenv("SHELL");
char *argv[] = {basename(shell), NULL};
execvp(shell, argv);
} else {
Vector2 zero = Vector2(0, 0);
/* Parent */
while (1) {
{
std::lock_guard<std::mutex> guard(size_mutex);
if (size != zero) {
struct winsize ws;
memset(&ws, 0, sizeof(ws));
ws.ws_col = size.x;
ws.ws_row = size.y;
ioctl(fd, TIOCSWINSZ, &ws);
}
}
if (waitpid(pty_pid, &status, WNOHANG)) {
emit_signal("exited", status);
return;
}
int ready = -1;
fd_set read_fds;
fd_set write_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
FD_SET(fd, &write_fds);
struct timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 0;
ready = select(fd + 1, &read_fds, &write_fds, NULL, &timeout);
if (ready > 0) {
if (FD_ISSET(fd, &write_fds)) {
std::lock_guard<std::mutex> guard(write_buffer_mutex);
if (bytes_to_write > 0) {
::write(fd, write_buffer, bytes_to_write);
bytes_to_write = 0;
}
}
if (FD_ISSET(fd, &read_fds)) {
std::lock_guard<std::mutex> guard(read_buffer_mutex);
int ret;
int bytes_read = 0;
bytes_read = read(fd, read_buffer, MAX_READ_BUFFER_LENGTH);
// TODO: handle error (-1)
if (bytes_read <= 0)
continue;
PoolByteArray data = PoolByteArray();
data.resize(bytes_read);
memcpy(data.write().ptr(), read_buffer, bytes_read);
emit_signal("data_sent", PoolByteArray(data));
}
}
}
}
}
void Pseudoterminal::_ready() {}
void Pseudoterminal::write(PoolByteArray data) {
std::lock_guard<std::mutex> guard(write_buffer_mutex);
bytes_to_write = data.size();
memcpy(write_buffer, data.read().ptr(), bytes_to_write);
}
void Pseudoterminal::resize(Vector2 new_size) {
std::lock_guard<std::mutex> guard(size_mutex);
size = new_size;
}

View file

@ -1,49 +0,0 @@
#ifndef PSEUDOTERMINAL_H
#define PSEUDOTERMINAL_H
#include <Godot.hpp>
#include <Node.hpp>
#include <mutex>
#include <thread>
namespace godot {
class Pseudoterminal : public Node {
GODOT_CLASS(Pseudoterminal, Node)
public:
static const int MAX_READ_BUFFER_LENGTH = 1024;
static const int MAX_WRITE_BUFFER_LENGTH = 1024;
private:
std::thread pty_thread;
bool should_process_pty;
char write_buffer[MAX_WRITE_BUFFER_LENGTH];
int bytes_to_write;
std::mutex write_buffer_mutex;
char read_buffer[MAX_READ_BUFFER_LENGTH];
int bytes_to_read;
std::mutex read_buffer_mutex;
Vector2 size;
std::mutex size_mutex;
void process_pty();
public:
static void _register_methods();
Pseudoterminal();
~Pseudoterminal();
void _init();
void _ready();
void write(PoolByteArray data);
void resize(Vector2 size);
};
} // namespace godot
#endif // PSEUDOTERMINAL_H

@ -0,0 +1 @@
Subproject commit 9ec6bb914febbd392b10bb9e774e25a7a15737c3