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

@ -46,6 +46,18 @@ jobs:
with:
path: addons/godot_xterm/native/thirdparty/godot-cpp
key: godot-cpp-${{ matrix.platform }}-${{ matrix.target }}-${{ matrix.bits }}-${{ env.GODOT_CPP_COMMIT_HASH }}
- name: Get libuv submodule commit hash
shell: bash
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
run: |
echo ::set-env name=LIBUV_COMMIT_HASH::$(git ls-tree HEAD addons/godot_xterm/native/thirdparty/libuv -l | cut -d\ -f3)
- name: Cache libuv
uses: actions/cache@v2
id: cache-libuv
with:
path: addons/godot_xterm/native/thirdparty/libuv
key: libuv-cache-${{ matrix.platform }}-${{ matrix.target }}-${{ matrix.bits }}-${{ env.LIBUV_COMMIT_HASH }}
- name: Cache emscripten
if: ${{ matrix.platform == 'javascript' }}
uses: actions/cache@v2
@ -86,6 +98,25 @@ jobs:
cd addons/godot_xterm/native/thirdparty/godot-cpp
scons platform=${{ matrix.platform }} target=${{ matrix.target }} bits=${{ matrix.bits }} generate_bindings=yes -j2
- name: Build libuv
if: ${{ matrix.bits == 64 && steps.cache-libuv.outputs.cache-hit != 'true' }}
uses: lukka/run-cmake@v3
with:
cmakeListsOrSettingsJson: CMakeListsTxtAdvanced
cmakeListsTxtPath: '${{ github.workspace }}/addons/godot_xterm/native/thirdparty/libuv/CMakeLists.txt'
useVcpkgToolchainFile: true
cmakeAppendedArgs: '-DCMAKE_BUILD_TYPE=${{ matrix.target }} -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE'
buildDirectory: '${{ github.workspace }}/addons/godot_xterm/native/thirdparty/libuv/build'
- name: Build libuv 32 bit
if: ${{ matrix.bits == 32 && steps.cache-libuv.outputs.cache-hit != 'true' }}
uses: lukka/run-cmake@v3
with:
cmakeListsOrSettingsJson: CMakeListsTxtAdvanced
cmakeListsTxtPath: '${{ github.workspace }}/addons/godot_xterm/native/thirdparty/libuv/CMakeLists.txt'
useVcpkgToolchainFile: true
cmakeAppendedArgs: '-DCMAKE_BUILD_TYPE=${{ matrix.target }} -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -DCMAKE_SYSTEM_PROCESSOR=i686 -DCMAKE_C_FLAGS=-m32'
buildDirectory: '${{ github.workspace }}/addons/godot_xterm/native/thirdparty/libuv/build'
- name: Build libgodot-xterm
run: |
cd addons/godot_xterm/native

3
.gitmodules vendored
View file

@ -7,3 +7,6 @@
[submodule "misc/export_templates/godot"]
path = misc/export_templates/godot
url = https://github.com/godotengine/godot
[submodule "addons/godot_xterm/native/thirdparty/libuv"]
path = addons/godot_xterm/native/thirdparty/libuv
url = https://github.com/libuv/libuv

View file

@ -1,20 +1,8 @@
{
"dirs": [ "res://test" ],
"disable_colors": false,
"double_strategy": "partial",
"ignore_pause": false,
"include_subdirs": true,
"inner_class": "",
"log_level": 1,
"opacity": 100,
"post_run_script": "",
"pre_run_script": "",
"prefix": "test_",
"selected": "",
"should_exit": true,
"should_exit_on_success": false,
"should_maximize": false,
"suffix": ".gd",
"tests": [],
"unit_test_name": ""
"dirs": [
"res://"
],
"prefix": "",
"suffix": ".test.gd",
"include_subdirs": true
}

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Leroy Hopson and [contributors](https://github.com/lihop/godot-xterm/contributors)
Copyright (c) 2020-2021, Leroy Hopson and [contributors](https://github.com/lihop/godot-xterm/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Leroy Hopson and [contributors](https://github.com/lihop/godot-xterm/contributors)
Copyright (c) 2020-2021, Leroy Hopson and [contributors](https://github.com/lihop/godot-xterm/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

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

View file

@ -1,51 +0,0 @@
# Pseudoterminal
**Inherits:** [Node] < [Object]
Can be used with the [Terminal] node to get an actual shell. Currently only tested/working on Linux and MacOS.
## Methods
| Returns | Signature |
|---------|----------------------------------------|
| void | write **(** [PoolByteArray] data **)** |
| void | resize **(** [Vector2] size **)** |
## Signals
- **data_sent** **(** [PoolByteArray] data **)**
Emitted when some data comes out of the pseudoterminal.
In a typical setup this signal would be connected to the [Terminal]'s `write()` method.
---
- **exited** **(** [int] status **)**
Emitted when the pseudoterminal's process (typically a shell like `bash` or `sh`) has exited. `status` is the exit code.
For example, if you are using the terminal with a `bash` shell, then issuing the `exit` command would cause this signal to be emitted.
```bash
> exit
exit
```
## Method Descriptions
- void **write** **(** [PoolByteArray] data **)**
Writes data to the pseudoterminal. In a typical setup this would be connected to the [Terminal]'s `data_sent()` signal.
- void **resize** **(** [Vector2] size **)**
Used to notify the pseudoterminal about window size changes. In a typical setup it would be connected to the [Terminal]'s `size_changed()` signal.
[Node]: https://docs.godotengine.org/en/stable/classes/class_node.html
[int]: https://docs.godotengine.org/en/stable/classes/class_int.html
[Object]: https://docs.godotengine.org/en/stable/classes/class_object.html
[PoolByteArray]: https://docs.godotengine.org/en/stable/classes/class_poolbytearray.html
[Terminal]: ../terminal/README.md
[Vector2]: https://docs.godotengine.org/en/stable/classes/class_vector2.html

View file

@ -0,0 +1,29 @@
# Copyright (c) 2021, Leroy Hopson (MIT License)
tool
extends Object
# Wrapper around libuv utility functions.
# GDNative does not currently support registering static functions so we fake it.
# Only the static functions of this class should be called.
const LibuvUtils := preload("./libuv_utils.gdns")
static func get_os_environ() -> Dictionary:
# While Godot has OS.get_environment(), I could see a way to get all environent
# variables, other than by OS.execute() which would require to much platform
# specific code. Easier to use libuv's utility function.
return LibuvUtils.new().get_os_environ()
static func get_cwd() -> String:
# Use uv_cwd() rather than Directory.get_current_dir() because the latter
# defaults to res:// even if starting godot from a different directory.
return LibuvUtils.new().get_cwd()
static func get_windows_build_number() -> int:
assert(OS.get_name() == "Windows", "This function is only supported on Windows.")
var release: String = LibuvUtils.new().get_os_release()
assert(false, "Not implemented.")
return 0
static func new():
assert(false, "Abstract sealed (i.e. static) class should not be instantiated.")

View file

@ -4,5 +4,5 @@
[resource]
resource_name = "Terminal"
class_name = "Pseudoterminal"
class_name = "LibuvUtils"
library = ExtResource( 1 )

View file

@ -0,0 +1,8 @@
[gd_resource type="NativeScript" load_steps=2 format=2]
[ext_resource path="res://addons/godot_xterm/native/godotxtermnative.gdnlib" type="GDNativeLibrary" id=1]
[resource]
resource_name = "Terminal"
class_name = "Pipe"
library = ExtResource( 1 )

View file

@ -0,0 +1,261 @@
# Derived from https://github.com/microsoft/node-pty/blob/main/src/terminal.ts
# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
# Copyright (c) 2016, Daniel Imms (MIT License).
# Copyright (c) 2018, Microsoft Corporation (MIT License).
# Copyright (c) 2021, Leroy Hopson (MIT License).
tool
extends Node
const LibuvUtils := preload("./libuv_utils.gd")
const Pipe := preload("./pipe.gdns")
const Terminal := preload("../terminal/terminal.gd")
const DEFAULT_NAME := "xterm-256color"
const DEFAULT_COLS := 80
const DEFAULT_ROWS := 24
const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"}
## Default messages to indicate PAUSE/RESUME for automatic flow control.
## To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh),
## the sequences can be customized in IPtyForkOptions.
#const FLOW_CONTROL_PAUSE = char(0x13) # defaults to XOFF
#const FLOW_CONTROL_RESUME = char(0x11) # defaults to XON
enum Status { NONE, OPEN, EXITED, ERROR }
const STATUS_NONE = Status.NONE
const STATUS_OPEN = Status.OPEN
const STATUS_EXITED = Status.EXITED
const STATUS_ERROR = Status.ERROR
# Any signal_number can be sent to the pty's process using the kill() function,
# these are just the signals with numbers specified in the POSIX standard.
enum Signal {
SIGHUP = 1, # Hangup
SIGINT = 2, # Terminal interrupt signal
SIGQUIT = 3, # Terminal quit signal
SIGILL = 4, # Illegal instruction
SIGTRAP = 5, # Trace/breakpoint trap
SIGABRT = 6, # Process abort signal
SIGFPE = 8, # Erroneous arithmetic operation
SIGKILL = 9, # Kill (cannot be caught or ignored)
SIGSEGV = 11, # Invalid memory reference
SIGPIPE = 13, # Write on a pipe with no one to read it
SIGALRM = 14, # Alarm clock
SIGTERM = 15, # Termination signal
}
signal data_received(data)
signal exited(exit_code, signum)
signal errored(message)
export (NodePath) var terminal_path := NodePath() setget set_terminal_path
var status := STATUS_NONE
var error_string := ""
var terminal: Terminal = null setget set_terminal
## Name of the terminal to be set in environment ($TERM variable).
#export (String) var term_name: String
# The name of the process.
var process: String
# The process ID.
var pid: int
# The column size in characters.
export (int) var cols: int = DEFAULT_COLS setget set_cols
# The row size in characters.
export (int) var rows: int = DEFAULT_ROWS setget set_rows
# Working directory to be set for the child program.
#export (String) var cwd := LibuvUtils.get_cwd()
# Environment to be set for the child program.
export (Dictionary) var env := DEFAULT_ENV
# If true the environment variables in the env Dictionary will be merged with
# the environment variables of the operating system (e.g. printenv), with the
# former taking precedence in the case of conflicts.
export (bool) var use_os_env := true
# If true, pty will call fork() in in _ready().
export (bool) var autostart := false
# (EXPERIMENTAL)
# If true, PTY node will create a blocking libuv loop in a new thread.
# signals will be emitted using call_deferred.
#export (bool) var use_threads := false
## String encoding of the underlying pty.
## If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding.
## If unset, incoming data will be delivered as raw bytes (PoolByteArray type).
## By default 'utf8' is assumed, to unset it explicitly set it to `null`.
#var encoding: String = "utf8"
## (EXPERIMENTAL)
## Whether to enable flow control handling (false by default). If enabled a message of `flow_control_pause`
## will pause the socket and thus blocking the child program execution due to buffer back pressure.
## A message of `flow_control_resume` will resume the socket into flow mode.
## For performance reasons only a single message as a whole will match (no message part matching).
## If flow control is enabled the `flow_control_pause` and `flow_control_resume` messages are not forwarded to
## the underlying pseudoterminal.
#var handle_flow_control: bool = false
#
## (EXPERIMENTAL)
## The string that should pause the pty when `handle_flow_control` is true. Default is XOFF ("\u0013").
#var flow_control_pause: String = FLOW_CONTROL_PAUSE
#
## (EXPERIMENTAL)
## The string that should resume the pty when `handle_flow_control` is true. Default is XON ("\u0011").
#var flow_control_resume: String = FLOW_CONTROL_RESUME
#var _native_term # Platform appropriate instance of this class.
var _pipe: Pipe
func set_cols(value: int):
resize(value, rows)
func set_rows(value: int):
resize(cols, value)
func set_terminal_path(value := NodePath()):
terminal_path = value
set_terminal(get_node_or_null(terminal_path))
func set_terminal(value: Terminal):
if terminal == value:
return
# Disconect the current terminal, if any.
if terminal:
disconnect("data_received", terminal, "write")
terminal.disconnect("data_sent", self, "write")
terminal.disconnect("size_changed", self, "resize")
terminal = value
if not terminal:
return
# Connect the new terminal.
# FIXME! resize(terminal.get_cols(), terminal.get_rows())
if not terminal.is_connected("size_changed", self, "resize"):
terminal.connect("size_changed", self, "resize")
if not terminal.is_connected("data_sent", self, "write"):
terminal.connect("data_sent", self, "write")
if not is_connected("data_received", terminal, "write"):
connect("data_received", terminal, "write")
# Writes data to the socket.
# data: The data to write.
func write(data) -> void:
assert(data is String or data is PoolByteArray)
if data is PoolByteArray:
data = data.get_string_from_utf8()
# if handle_flow_control:
# # PAUSE/RESUME messages are not forwarded to the pty.
# if data == flow_control_pause:
# pause()
# return
# if data == flow_control_resume:
# resume()
# return
# # Everything else goes to the real pty.
_write(data)
func _write(data: String) -> void:
if _pipe:
_pipe.write(data)
# Resizes the dimensions of the pty.
# cols: The number of columns.
# rows: The number of rows.
# Also accepts a single Vector2 argument where x is the the number of columns
# and y is the number of rows.
func resize(cols, rows = null) -> void:
assert(
(cols is Vector2 and rows == null) or (cols is int and rows is int),
"Usage: resize(size: Vector2) or resize(cols: int, rows: int)"
)
if cols is Vector2:
rows = cols.y # Must get rows before reassigning cols!
cols = cols.x
if cols <= 0 or rows <= 0 or cols == NAN or rows == NAN or cols == INF or rows == INF:
push_error("Resizing must be done using positive cols and rows.")
_resize(cols, rows)
func _resize(cols: int, rows: int) -> void:
assert(false, "Not implemented.")
# Close, kill and destroy the pipe.
func destroy() -> void:
pass
# Kill the pty.
# sigint: The signal to send. By default this is SIGHUP.
# This is not supported on Windows.
func kill(sigint: int = Signal.SIGHUP) -> void:
pass
func fork(
p_file: String,
p_args = PoolStringArray(),
p_cwd: String = LibuvUtils.get_cwd(),
p_cols: int = DEFAULT_COLS,
p_rows: int = DEFAULT_ROWS
):
push_error("Not implemented.")
func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> void:
pass
func _parse_env(env: Dictionary = {}) -> PoolStringArray:
var keys := env.keys()
var pairs := PoolStringArray()
for key in keys:
var value = env[key]
var valid = key is String and value is String
assert(valid, "Env key/value pairs must be of type String/String.")
if not valid:
push_warning("Skipping invalid env key/value pair.")
continue
pairs.append("%s=%s" % [key, value])
return pairs
func _process(_delta):
if _pipe:
_pipe.poll()
func _notification(what: int):
match what:
NOTIFICATION_PARENTED:
var parent = get_parent()
if parent is Terminal:
set_terminal_path(get_path_to(parent))

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -2,15 +2,15 @@
importer="texture"
type="StreamTexture"
path="res://.import/pseudoterminal_icon.svg-0b26aed87c28626d61aa92bd9e34d5a9.stex"
path="res://.import/pty_icon.svg-e9e42570b4744b3370a02d174395c793.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg"
dest_files=[ "res://.import/pseudoterminal_icon.svg-0b26aed87c28626d61aa92bd9e34d5a9.stex" ]
source_file="res://addons/godot_xterm/nodes/pty/pty_icon.svg"
dest_files=[ "res://.import/pty_icon.svg-e9e42570b4744b3370a02d174395c793.stex" ]
[params]

View file

@ -0,0 +1,124 @@
# Derived from https://github.com/microsoft/node-pty/blob/main/src/unixTerminal.ts
# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License).
# Copyright (c) 2016, Daniel Imms (MIT License).
# Copyright (c) 2018, Microsoft Corporation (MIT License).
# Copyright (c) 2021, Leroy Hopson (MIT License).
tool
extends "../pty.gd"
const PTYUnix = preload("./pty_unix.gdns")
const FALLBACK_FILE = "sh"
# Security warning: use this option with great caution, as opened file descriptors
# with higher privileges might leak to the child program.
var uid: int
var gid: int
var thread: Thread
var _fd: int = -1
var _exit_cb: FuncRef
static func get_uid() -> int:
return -1 # Not implemented.
static func get_gid() -> int:
return -1 # Not implemented.
func _ready():
if autostart:
fork()
func _resize(cols: int, rows: int) -> void:
if _fd < 0:
return
PTYUnix.new().resize(_fd, cols, rows)
func _fork_thread(args):
var result = preload("./pty_unix.gdns").new().callv("fork", args)
print(result)
return result
func fork(
file: String = OS.get_environment("SHELL"),
args: PoolStringArray = PoolStringArray(),
cwd = LibuvUtils.get_cwd(),
p_cols: int = DEFAULT_COLS,
p_rows: int = DEFAULT_ROWS,
uid: int = -1,
gid: int = -1,
utf8 = true
):
# File.
if file.empty():
file = FALLBACK_FILE
# Environment variables.
# If we are using OS env vars, sanitize them to remove variables that might confuse our terminal.
var final_env := _sanitize_env(LibuvUtils.get_os_environ()) if use_os_env else {}
for key in env.keys():
final_env[key] = env[key]
var parsed_env: PoolStringArray = _parse_env(final_env)
# Exit callback.
_exit_cb = FuncRef.new()
_exit_cb.set_instance(self)
_exit_cb.function = "_on_exit"
# Actual fork.
var result = PTYUnix.new().fork( # VERY IMPORTANT: The must be set null or 0, otherwise will get an ENOTSOCK error after connecting our pipe to the fd.
file, null, args, parsed_env, cwd, cols, rows, uid, gid, utf8, _exit_cb
)
if result[0] != OK:
push_error("Fork failed.")
status = STATUS_ERROR
return FAILED
_fd = result[1].fd
if _fd < 0:
push_error("File descriptor must be a non-negative integer value.")
status = STATUS_ERROR
return FAILED
_pipe = Pipe.new()
_pipe.open(_fd)
# Must connect to signal AFTER opening, otherwise we will get error ENOTSOCK.
_pipe.connect("data_received", self, "_on_pipe_data_received")
return OK
func _on_pipe_data_received(data):
emit_signal("data_received", data)
func _on_exit(exit_code: int, signum: int) -> void:
emit_signal("exited", exit_code, signum)
func _sanitize_env(env: Dictionary) -> Dictionary:
# Make sure we didn't start our server from inside tmux.
env.erase("TMUX")
env.erase("TMUX_PANE")
# Make sure we didn't start our server from inside screen.
# http://web.mit.edu/gnu/doc/html/screen_20.html
env.erase("STY")
env.erase("WINDOW")
# Delete some variables that might confuse our terminal.
env.erase("WINDOWID")
env.erase("TERMCAP")
env.erase("COLUMNS")
env.erase("LINES")
return env

View file

@ -0,0 +1,7 @@
[gd_resource type="NativeScript" load_steps=2 format=2]
[ext_resource path="res://addons/godot_xterm/native/godotxtermnative.gdnlib" type="GDNativeLibrary" id=1]
[resource]
class_name = "PTYUnix"
library = ExtResource( 1 )

View file

@ -1,6 +1,7 @@
tool
extends EditorPlugin
var pty_supported := OS.get_name() in ["X11", "Server", "OSX"]
var asciicast_import_plugin
@ -15,9 +16,13 @@ func _enter_tree():
var terminal_icon = preload("res://addons/godot_xterm/nodes/terminal/terminal_icon.svg")
add_custom_type("Terminal", "Control", terminal_script, terminal_icon)
var pseudoterminal_script = preload("res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal.gdns")
var pseudoterminal_icon = preload("res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg")
add_custom_type("Pseudoterminal", "Node", pseudoterminal_script, pseudoterminal_icon)
if pty_supported:
var pty_icon = load("res://addons/godot_xterm/nodes/pty/pty_icon.svg")
var pty_script
match OS.get_name():
"X11", "Server", "OSX":
pty_script = load("res://addons/godot_xterm/nodes/pty/unix/pty_unix.gd")
add_custom_type("PTY", "Node", pty_script, pty_icon)
func _exit_tree():
@ -26,4 +31,6 @@ func _exit_tree():
remove_custom_type("Asciicast")
remove_custom_type("Terminal")
remove_custom_type("Psuedoterminal")
if pty_supported:
remove_custom_type("PTY")

View file

@ -150,11 +150,7 @@ func _on_Terminal_key_pressed(data: String, event: InputEventKey) -> void:
"Terminal not Supported on Windows"
)
var scene = item.scene.instance()
var pty = (
scene
if OS.has_feature("JavaScript")
else scene.get_node("Pseudoterminal")
)
var pty = scene if OS.has_feature("JavaScript") else scene.get_node("PTY")
get_tree().get_root().add_child(scene)
visible = false
scene.grab_focus()

View file

@ -0,0 +1,7 @@
extends "res://addons/godot_xterm/nodes/terminal/terminal.gd"
onready var pty = $PTY
func _ready():
pty.fork(OS.get_environment("SHELL"))

View file

@ -1,20 +1,21 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/godot_xterm/nodes/terminal/terminal.gd" type="Script" id=1]
[ext_resource path="res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal.gdns" type="Script" id=2]
[ext_resource path="res://addons/godot_xterm/nodes/pty/unix/pty_unix.gd" type="Script" id=2]
[ext_resource path="res://examples/terminal/terminal.gd" type="Script" id=3]
[node name="Terminal" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 1
script = ExtResource( 1 )
focus_mode = 2
script = ExtResource( 3 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Pseudoterminal" type="Node" parent="."]
[node name="PTY" type="Node" parent="."]
script = ExtResource( 2 )
[connection signal="data_sent" from="." to="Pseudoterminal" method="write"]
[connection signal="size_changed" from="." to="Pseudoterminal" method="resize"]
[connection signal="data_sent" from="Pseudoterminal" to="." method="write"]
terminal_path = NodePath("..")
env = {
"COLORTERM": "truecolor",
"TERM": "xterm-256color"
}

33
misc/vscode/launch.json Normal file
View file

@ -0,0 +1,33 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "cpp - Build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "/run/current-system/sw/bin/godot",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
//{
// "description": "Debug forked child process",
// "text": "-gdb-set follow-fork-mode child",
//}
],
"preLaunchTask": "build",
"miDebuggerPath": "gdb"
}
]
}

View file

@ -3,4 +3,21 @@
"clang-format.executable": "/run/current-system/sw/bin/clang-format",
"gut-extension.additionalOptions": "-d --no-window",
"nixEnvSelector.nixShellConfig": "NOT_MODIFIED_ENV",
"files.associations": {
"array": "cpp",
"istream": "cpp",
"limits": "cpp",
"numeric": "cpp",
"sstream": "cpp",
"streambuf": "cpp",
"utility": "cpp",
"optional": "cpp",
"ostream": "cpp",
"ratio": "cpp",
"system_error": "cpp",
"functional": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"*.inc": "cpp"
}
}

View file

@ -0,0 +1,117 @@
extends "res://addons/gut/test.gd"
# WARNING: These test can only be run on the "Unix" platforms (X11, Server, and OSX).
# Some of the tests also rely on listing the files in /dev/pts. If you open or close
# terminals while these tests are running, it may cause inaccurate results.
const PTYUnixNative := preload("res://addons/godot_xterm/nodes/pty/unix/pty_unix.gdns")
const PTYUnix := preload("res://addons/godot_xterm/nodes/pty/unix/pty_unix.gd")
func before_all():
assert(
OS.get_name() in ["X11", "Server", "OSX"], "Unix only tests cannot be run on this platform."
)
class TestFork:
extends "res://addons/gut/test.gd"
const PTYUnix := preload("res://addons/godot_xterm/nodes/pty/unix/pty_unix.gd")
var sh_path: String
func before_all():
var output = []
var exit_code = OS.execute("command", PoolStringArray(["-v", "sh"]), true, output)
assert(exit_code == 0, "sh is required for these tests.")
sh_path = output[0].strip_edges()
func test_fork_creates_new_pts():
var num_pts = Helper._get_pts().size()
var pty = PTYUnix.new()
add_child_autofree(pty)
var err = pty.fork(sh_path)
assert_eq(err, OK)
var new_num_pts = Helper._get_pts().size()
assert_eq(new_num_pts, num_pts + 1)
class TestOpen:
extends "res://addons/gut/test.gd"
func test_open_creates_new_pts():
var num_pts = Helper._get_pts().size()
var result = PTYUnixNative.new().open(0, 0)
assert_eq(result[0], OK)
var new_num_pts = Helper._get_pts().size()
assert_eq(new_num_pts, num_pts + 1)
func test_pty_has_correct_name():
var original_pts = Helper._get_pts()
var result = PTYUnixNative.new().open(0, 0)
assert_eq(result[0], OK)
var new_pts = Helper._get_pts()
for pt in original_pts:
new_pts.erase(pt)
assert_true(result[1].pty in new_pts)
func test_pty_has_correct_win_size():
var cols = 7684
var rows = 9314
var result = PTYUnixNative.new().open(cols, rows)
assert_eq(result[0], OK)
var winsize = Helper._get_winsize(result[1].master)
assert_eq(winsize.cols, cols)
assert_eq(winsize.rows, rows)
class Helper:
static func _get_pts() -> Array:
var dir := Directory.new()
if dir.open("/dev/pts") != OK or dir.list_dir_begin(true, true) != OK:
assert(false, "Could not open /dev/pts.")
var pts := []
var file_name := dir.get_next()
while file_name != "":
if file_name.is_valid_integer():
pts.append("/dev/pts/%s" % file_name)
file_name = dir.get_next()
return pts
static func _get_winsize(fd: int) -> Dictionary:
var output = []
assert(
OS.execute("command", ["-v", "python"], true, output) == 0,
"Python must be installed to run this test."
)
var python_path = output[0].strip_edges()
var exit_code = OS.execute(
python_path,
[
"-c",
(
"import struct, fcntl, termios; print(struct.unpack('hh', fcntl.ioctl(%d, termios.TIOCGWINSZ, '1234')))"
% fd
)
],
true,
output
)
assert(exit_code == 0, "Failed to run python command for this test.")
var size = str2var("Vector2" + output[0].strip_edges())
return {rows = int(size.x), cols = int(size.y)}

View file

@ -0,0 +1,30 @@
extends "res://addons/gut/test.gd"
const LibuvUtils := preload("res://addons/godot_xterm/nodes/pty/libuv_utils.gd")
class TestGetOSEnviron:
extends "res://addons/gut/test.gd"
const EMPTY_VAR = "GODOT_XTERM_TEST_EMPTY_ENV_VAR"
const TEST_VAR = "GODOT_XTERM_TEST_ENV_VAR"
const TEST_VAL = "TEST123"
var env: Dictionary
func before_each():
assert(OS.set_environment(EMPTY_VAR, ""))
assert(OS.set_environment(TEST_VAR, TEST_VAL))
env = LibuvUtils.get_os_environ()
func test_has_empty_var():
assert_has(env, EMPTY_VAR)
func test_empty_var_has_empty_val():
assert_eq(env[EMPTY_VAR], "")
func test_has_test_var():
assert_has(env, TEST_VAR)
func test_test_var_has_correct_val():
assert_eq(env[TEST_VAR], TEST_VAL)