mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-22 09:40:25 +01:00
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:
parent
bfa561357e
commit
0dd2378387
36 changed files with 1268 additions and 442 deletions
31
.github/workflows/main.yml
vendored
31
.github/workflows/main.yml
vendored
|
@ -46,6 +46,18 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: addons/godot_xterm/native/thirdparty/godot-cpp
|
path: addons/godot_xterm/native/thirdparty/godot-cpp
|
||||||
key: godot-cpp-${{ matrix.platform }}-${{ matrix.target }}-${{ matrix.bits }}-${{ env.GODOT_CPP_COMMIT_HASH }}
|
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
|
- name: Cache emscripten
|
||||||
if: ${{ matrix.platform == 'javascript' }}
|
if: ${{ matrix.platform == 'javascript' }}
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
|
@ -86,6 +98,25 @@ jobs:
|
||||||
cd addons/godot_xterm/native/thirdparty/godot-cpp
|
cd addons/godot_xterm/native/thirdparty/godot-cpp
|
||||||
scons platform=${{ matrix.platform }} target=${{ matrix.target }} bits=${{ matrix.bits }} generate_bindings=yes -j2
|
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
|
- name: Build libgodot-xterm
|
||||||
run: |
|
run: |
|
||||||
cd addons/godot_xterm/native
|
cd addons/godot_xterm/native
|
||||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -7,3 +7,6 @@
|
||||||
[submodule "misc/export_templates/godot"]
|
[submodule "misc/export_templates/godot"]
|
||||||
path = misc/export_templates/godot
|
path = misc/export_templates/godot
|
||||||
url = https://github.com/godotengine/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
|
||||||
|
|
|
@ -1,20 +1,8 @@
|
||||||
{
|
{
|
||||||
"dirs": [ "res://test" ],
|
"dirs": [
|
||||||
"disable_colors": false,
|
"res://"
|
||||||
"double_strategy": "partial",
|
],
|
||||||
"ignore_pause": false,
|
"prefix": "",
|
||||||
"include_subdirs": true,
|
"suffix": ".test.gd",
|
||||||
"inner_class": "",
|
"include_subdirs": true
|
||||||
"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": ""
|
|
||||||
}
|
}
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -218,10 +218,13 @@ env.Append(CXXFLAGS=['-std=c++14'])
|
||||||
env.Append(CPPPATH=[
|
env.Append(CPPPATH=[
|
||||||
'src/',
|
'src/',
|
||||||
'thirdparty/libtsm/build/src/tsm',
|
'thirdparty/libtsm/build/src/tsm',
|
||||||
|
'thirdparty/libtsm/build/src/shared',
|
||||||
'thirdparty/godot-cpp/include/',
|
'thirdparty/godot-cpp/include/',
|
||||||
'thirdparty/godot-cpp/include/core/',
|
'thirdparty/godot-cpp/include/core/',
|
||||||
'thirdparty/godot-cpp/include/gen/',
|
'thirdparty/godot-cpp/include/gen/',
|
||||||
'thirdparty/godot-cpp/godot-headers/'
|
'thirdparty/godot-cpp/godot-headers/',
|
||||||
|
'thirdparty/libuv/src',
|
||||||
|
'thirdparty/libuv/include'
|
||||||
])
|
])
|
||||||
env.Append(LIBPATH=[
|
env.Append(LIBPATH=[
|
||||||
'thirdparty/godot-cpp/bin/',
|
'thirdparty/godot-cpp/bin/',
|
||||||
|
@ -247,10 +250,12 @@ sources = []
|
||||||
sources.append('src/libgodotxtermnative.cpp')
|
sources.append('src/libgodotxtermnative.cpp')
|
||||||
sources.append('src/terminal.cpp')
|
sources.append('src/terminal.cpp')
|
||||||
|
|
||||||
# Psuedoterminal not supported on windows (yet) or HTML5.
|
# PTY not supported on HTML5 or Windows (yet).
|
||||||
if env['platform'] != 'windows' and env['platform'] != 'javascript':
|
if env['platform'] != 'javascript' and env['platform'] != 'windows':
|
||||||
sources.append('src/pseudoterminal.cpp')
|
sources.append('src/pipe.cpp')
|
||||||
env.Append(LIBS=['util'])
|
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':
|
if env['platform'] == 'linux':
|
||||||
libsuffix = "a"
|
libsuffix = "a"
|
||||||
|
|
|
@ -15,21 +15,32 @@ fi
|
||||||
|
|
||||||
|
|
||||||
# Update git submodules.
|
# Update git submodules.
|
||||||
LIBTSM_DIR=${NATIVE_DIR}/thirdparty/libtsm
|
updateSubmodules() {
|
||||||
if [ -z "$(ls -A -- "$LIBTSM_DIR")" ]; then
|
eval $1=$2 # E.g LIBUV_DIR=${NATIVE_DIR}/thirdparty/libuv
|
||||||
|
|
||||||
|
if [ -z "$(ls -A -- "$2")" ]; then
|
||||||
cd ${NATIVE_DIR}
|
cd ${NATIVE_DIR}
|
||||||
git submodule update --init --recursive -- $LIBTSM_DIR
|
git submodule update --init --recursive -- $2
|
||||||
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
|
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.
|
# Build godot-cpp bindings.
|
||||||
cd ${GODOT_CPP_DIR}
|
cd ${GODOT_CPP_DIR}
|
||||||
scons generate_bindings=yes target=debug -j$(nproc)
|
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.
|
# Build libgodot-xterm.
|
||||||
cd ${NATIVE_DIR}
|
cd ${NATIVE_DIR}
|
||||||
scons target=debug -j$(nproc)
|
scons target=debug -j$(nproc)
|
||||||
|
|
|
@ -6,6 +6,9 @@ mkShell {
|
||||||
|
|
||||||
cacert # Required for git clone on GithHub actions runner.
|
cacert # Required for git clone on GithHub actions runner.
|
||||||
|
|
||||||
|
# Used to build libuv.
|
||||||
|
cmake
|
||||||
|
|
||||||
# Used to build for javascript platform.
|
# Used to build for javascript platform.
|
||||||
docker
|
docker
|
||||||
docker-compose
|
docker-compose
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
#include "terminal.h"
|
#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
|
#endif
|
||||||
|
|
||||||
extern "C" void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
|
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) {
|
extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) {
|
||||||
godot::Godot::nativescript_init(handle);
|
godot::Godot::nativescript_init(handle);
|
||||||
|
|
||||||
godot::register_tool_class<godot::Terminal>();
|
godot::register_tool_class<godot::Terminal>();
|
||||||
#if defined(__unix__) && !defined(__EMSCRIPTEN__)
|
#if !defined(__EMSCRIPTEN__) && !defined(__WIN32)
|
||||||
godot::register_class<godot::Pseudoterminal>();
|
godot::register_tool_class<godot::Pipe>();
|
||||||
|
godot::register_tool_class<godot::LibuvUtils>();
|
||||||
|
godot::register_tool_class<godot::PTYUnix>();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
190
addons/godot_xterm/native/src/libuv_utils.cpp
Normal file
190
addons/godot_xterm/native/src/libuv_utils.cpp
Normal 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
|
||||||
|
}
|
||||||
|
}
|
43
addons/godot_xterm/native/src/libuv_utils.h
Normal file
43
addons/godot_xterm/native/src/libuv_utils.h
Normal 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
|
|
@ -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 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
|
### Node-pty License
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||||
* Copyright (c) 2017, Daniel Imms (MIT License)
|
* Copyright (c) 2017, Daniel Imms (MIT License)
|
||||||
|
* Copyright (c) 2021, Leroy Hopson (MIT License)
|
||||||
*
|
*
|
||||||
* pty.cc:
|
* pty.cc:
|
||||||
* This file is responsible for starting processes
|
* This file is responsible for starting processes
|
||||||
|
@ -17,8 +18,12 @@
|
||||||
* Includes
|
* Includes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "pty.h"
|
||||||
|
#include "libuv_utils.h"
|
||||||
|
#include <FuncRef.hpp>
|
||||||
|
#include <uv.h>
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <nan.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
@ -77,12 +82,14 @@ extern char **environ;
|
||||||
#define NSIG 32
|
#define NSIG 32
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
using namespace godot;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Structs
|
* Structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
struct pty_baton {
|
struct pty_baton {
|
||||||
Nan::Persistent<v8::Function> cb;
|
Ref<FuncRef> cb;
|
||||||
int exit_code;
|
int exit_code;
|
||||||
int signal_code;
|
int signal_code;
|
||||||
pid_t pid;
|
pid_t pid;
|
||||||
|
@ -90,15 +97,6 @@ struct pty_baton {
|
||||||
uv_thread_t tid;
|
uv_thread_t tid;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
NAN_METHOD(PtyFork);
|
|
||||||
NAN_METHOD(PtyOpen);
|
|
||||||
NAN_METHOD(PtyResize);
|
|
||||||
NAN_METHOD(PtyGetProc);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functions
|
* Functions
|
||||||
*/
|
*/
|
||||||
|
@ -121,52 +119,41 @@ static void pty_after_waitpid(uv_async_t *);
|
||||||
|
|
||||||
static void pty_after_close(uv_handle_t *);
|
static void pty_after_close(uv_handle_t *);
|
||||||
|
|
||||||
NAN_METHOD(PtyFork) {
|
Array PTYUnix::fork(String p_file, int _ignored, PoolStringArray p_args,
|
||||||
Nan::HandleScope scope;
|
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) {
|
||||||
if (info.Length() != 10 || !info[0]->IsString() || !info[1]->IsArray() ||
|
|
||||||
!info[2]->IsArray() || !info[3]->IsString() || !info[4]->IsNumber() ||
|
|
||||||
!info[5]->IsNumber() || !info[6]->IsNumber() || !info[7]->IsNumber() ||
|
|
||||||
!info[8]->IsBoolean() || !info[9]->IsFunction()) {
|
|
||||||
return Nan::ThrowError("Usage: pty.fork(file, args, env, cwd, cols, rows, "
|
|
||||||
"uid, gid, utf8, onexit)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// file
|
// file
|
||||||
Nan::Utf8String file(info[0]);
|
char *file = p_file.alloc_c_string();
|
||||||
|
|
||||||
// args
|
// args
|
||||||
int i = 0;
|
int i = 0;
|
||||||
v8::Local<v8::Array> argv_ = v8::Local<v8::Array>::Cast(info[1]);
|
int argc = p_args.size();
|
||||||
int argc = argv_->Length();
|
|
||||||
int argl = argc + 1 + 1;
|
int argl = argc + 1 + 1;
|
||||||
char **argv = new char *[argl];
|
char **argv = new char *[argl];
|
||||||
argv[0] = strdup(*file);
|
argv[0] = strdup(file);
|
||||||
argv[argl - 1] = NULL;
|
argv[argl - 1] = NULL;
|
||||||
for (; i < argc; i++) {
|
for (; i < argc; i++) {
|
||||||
Nan::Utf8String arg(Nan::Get(argv_, i).ToLocalChecked());
|
char *arg = p_args[i].alloc_c_string();
|
||||||
argv[i + 1] = strdup(*arg);
|
argv[i + 1] = strdup(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// env
|
// env
|
||||||
i = 0;
|
i = 0;
|
||||||
v8::Local<v8::Array> env_ = v8::Local<v8::Array>::Cast(info[2]);
|
int envc = p_env.size();
|
||||||
int envc = env_->Length();
|
|
||||||
char **env = new char *[envc + 1];
|
char **env = new char *[envc + 1];
|
||||||
env[envc] = NULL;
|
env[envc] = NULL;
|
||||||
for (; i < envc; i++) {
|
for (; i < envc; i++) {
|
||||||
Nan::Utf8String pair(Nan::Get(env_, i).ToLocalChecked());
|
char *pairs = p_env[i].alloc_c_string();
|
||||||
env[i] = strdup(*pair);
|
env[i] = strdup(pairs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// cwd
|
// cwd
|
||||||
Nan::Utf8String cwd_(info[3]);
|
char *cwd = strdup(p_cwd.alloc_c_string());
|
||||||
char *cwd = strdup(*cwd_);
|
|
||||||
|
|
||||||
// size
|
// size
|
||||||
struct winsize winp;
|
struct winsize winp;
|
||||||
winp.ws_col = info[4]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
winp.ws_col = p_cols;
|
||||||
winp.ws_row = info[5]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
winp.ws_row = p_rows;
|
||||||
winp.ws_xpixel = 0;
|
winp.ws_xpixel = 0;
|
||||||
winp.ws_ypixel = 0;
|
winp.ws_ypixel = 0;
|
||||||
|
|
||||||
|
@ -174,7 +161,7 @@ NAN_METHOD(PtyFork) {
|
||||||
struct termios t = termios();
|
struct termios t = termios();
|
||||||
struct termios *term = &t;
|
struct termios *term = &t;
|
||||||
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
|
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
|
||||||
if (Nan::To<bool>(info[8]).FromJust()) {
|
if (p_utf8) {
|
||||||
#if defined(IUTF8)
|
#if defined(IUTF8)
|
||||||
term->c_iflag |= IUTF8;
|
term->c_iflag |= IUTF8;
|
||||||
#endif
|
#endif
|
||||||
|
@ -210,8 +197,8 @@ NAN_METHOD(PtyFork) {
|
||||||
cfsetospeed(term, B38400);
|
cfsetospeed(term, B38400);
|
||||||
|
|
||||||
// uid / gid
|
// uid / gid
|
||||||
int uid = info[6]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
int uid = p_uid;
|
||||||
int gid = info[7]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
int gid = p_gid;
|
||||||
|
|
||||||
// fork the pty
|
// fork the pty
|
||||||
int master = -1;
|
int master = -1;
|
||||||
|
@ -242,17 +229,18 @@ NAN_METHOD(PtyFork) {
|
||||||
|
|
||||||
if (pid) {
|
if (pid) {
|
||||||
for (i = 0; i < argl; i++)
|
for (i = 0; i < argl; i++)
|
||||||
free(argv[i]);
|
std::free(argv[i]);
|
||||||
delete[] argv;
|
delete[] argv;
|
||||||
for (i = 0; i < envc; i++)
|
for (i = 0; i < envc; i++)
|
||||||
free(env[i]);
|
std::free(env[i]);
|
||||||
delete[] env;
|
delete[] env;
|
||||||
free(cwd);
|
std::free(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (pid) {
|
switch (pid) {
|
||||||
case -1:
|
case -1:
|
||||||
return Nan::ThrowError("forkpty(3) failed.");
|
ERR_PRINT("forkpty(3) failed.");
|
||||||
|
return Array::make(GODOT_FAILED);
|
||||||
case 0:
|
case 0:
|
||||||
if (strlen(cwd)) {
|
if (strlen(cwd)) {
|
||||||
if (chdir(cwd) == -1) {
|
if (chdir(cwd) == -1) {
|
||||||
|
@ -278,21 +266,19 @@ NAN_METHOD(PtyFork) {
|
||||||
_exit(1);
|
_exit(1);
|
||||||
default:
|
default:
|
||||||
if (pty_nonblock(master) == -1) {
|
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>();
|
Dictionary result = Dictionary::make();
|
||||||
Nan::Set(obj, Nan::New<v8::String>("fd").ToLocalChecked(),
|
result["fd"] = (int)master;
|
||||||
Nan::New<v8::Number>(master));
|
result["pid"] = (int)pid;
|
||||||
Nan::Set(obj, Nan::New<v8::String>("pid").ToLocalChecked(),
|
result["pty"] = ptsname(master);
|
||||||
Nan::New<v8::Number>(pid));
|
|
||||||
Nan::Set(obj, Nan::New<v8::String>("pty").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(ptsname(master)).ToLocalChecked());
|
|
||||||
|
|
||||||
pty_baton *baton = new pty_baton();
|
pty_baton *baton = new pty_baton();
|
||||||
baton->exit_code = 0;
|
baton->exit_code = 0;
|
||||||
baton->signal_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->pid = pid;
|
||||||
baton->async.data = baton;
|
baton->async.data = baton;
|
||||||
|
|
||||||
|
@ -300,23 +286,17 @@ NAN_METHOD(PtyFork) {
|
||||||
|
|
||||||
uv_thread_create(&baton->tid, pty_waitpid, static_cast<void *>(baton));
|
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
|
// size
|
||||||
struct winsize winp;
|
struct winsize winp;
|
||||||
winp.ws_col = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
winp.ws_col = p_cols;
|
||||||
winp.ws_row = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
winp.ws_row = p_rows;
|
||||||
winp.ws_xpixel = 0;
|
winp.ws_xpixel = 0;
|
||||||
winp.ws_ypixel = 0;
|
winp.ws_ypixel = 0;
|
||||||
|
|
||||||
|
@ -325,85 +305,72 @@ NAN_METHOD(PtyOpen) {
|
||||||
int ret = pty_openpty(&master, &slave, nullptr, NULL, &winp);
|
int ret = pty_openpty(&master, &slave, nullptr, NULL, &winp);
|
||||||
|
|
||||||
if (ret == -1) {
|
if (ret == -1) {
|
||||||
return Nan::ThrowError("openpty(3) failed.");
|
ERR_PRINT("openpty(3) failed.");
|
||||||
|
return Array::make(GODOT_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pty_nonblock(master) == -1) {
|
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) {
|
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>();
|
Dictionary dict = Dictionary::make();
|
||||||
Nan::Set(obj, Nan::New<v8::String>("master").ToLocalChecked(),
|
dict["master"] = master;
|
||||||
Nan::New<v8::Number>(master));
|
dict["slave"] = slave;
|
||||||
Nan::Set(obj, Nan::New<v8::String>("slave").ToLocalChecked(),
|
dict["pty"] = ptsname(master);
|
||||||
Nan::New<v8::Number>(slave));
|
|
||||||
Nan::Set(obj, Nan::New<v8::String>("pty").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(ptsname(master)).ToLocalChecked());
|
|
||||||
|
|
||||||
return info.GetReturnValue().Set(obj);
|
return Array::make(GODOT_OK, dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
NAN_METHOD(PtyResize) {
|
godot_error PTYUnix::resize(int p_fd, int p_cols, int p_rows) {
|
||||||
Nan::HandleScope scope;
|
int fd = p_fd;
|
||||||
|
|
||||||
if (info.Length() != 3 || !info[0]->IsNumber() || !info[1]->IsNumber() ||
|
|
||||||
!info[2]->IsNumber()) {
|
|
||||||
return Nan::ThrowError("Usage: pty.resize(fd, cols, rows)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
struct winsize winp;
|
struct winsize winp;
|
||||||
winp.ws_col = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
winp.ws_col = p_cols;
|
||||||
winp.ws_row = info[2]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
winp.ws_row = p_rows;
|
||||||
winp.ws_xpixel = 0;
|
winp.ws_xpixel = 0;
|
||||||
winp.ws_ypixel = 0;
|
winp.ws_ypixel = 0;
|
||||||
|
|
||||||
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
|
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
|
||||||
switch (errno) {
|
switch (errno) {
|
||||||
case EBADF:
|
case EBADF:
|
||||||
return Nan::ThrowError("ioctl(2) failed, EBADF");
|
RETURN_UV_ERR(UV_EBADF)
|
||||||
case EFAULT:
|
case EFAULT:
|
||||||
return Nan::ThrowError("ioctl(2) failed, EFAULT");
|
RETURN_UV_ERR(UV_EFAULT)
|
||||||
case EINVAL:
|
case EINVAL:
|
||||||
return Nan::ThrowError("ioctl(2) failed, EINVAL");
|
RETURN_UV_ERR(UV_EINVAL);
|
||||||
case ENOTTY:
|
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
|
* Foreground Process Name
|
||||||
*/
|
*/
|
||||||
NAN_METHOD(PtyGetProc) {
|
String PTYUnix::process(int p_fd, String p_tty) {
|
||||||
Nan::HandleScope scope;
|
int fd = p_fd;
|
||||||
|
|
||||||
if (info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsString()) {
|
char *tty = p_tty.alloc_c_string();
|
||||||
return Nan::ThrowError("Usage: pty.process(fd, tty)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
Nan::Utf8String tty_(info[1]);
|
|
||||||
char *tty = strdup(*tty_);
|
|
||||||
char *name = pty_getproc(fd, tty);
|
char *name = pty_getproc(fd, tty);
|
||||||
free(tty);
|
std::free(tty);
|
||||||
|
|
||||||
if (name == NULL) {
|
if (name == NULL) {
|
||||||
return info.GetReturnValue().SetUndefined();
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
v8::Local<v8::String> name_ = Nan::New<v8::String>(name).ToLocalChecked();
|
String name_ = String(name);
|
||||||
free(name);
|
std::free(name);
|
||||||
return info.GetReturnValue().Set(name_);
|
return name_;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -453,7 +420,7 @@ static void pty_waitpid(void *data) {
|
||||||
// waitpid is already handled elsewhere.
|
// waitpid is already handled elsewhere.
|
||||||
;
|
;
|
||||||
} else {
|
} else {
|
||||||
assert(false);
|
// assert(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,19 +441,12 @@ static void pty_waitpid(void *data) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static void pty_after_waitpid(uv_async_t *async) {
|
static void pty_after_waitpid(uv_async_t *async) {
|
||||||
Nan::HandleScope scope;
|
|
||||||
pty_baton *baton = static_cast<pty_baton *>(async->data);
|
pty_baton *baton = static_cast<pty_baton *>(async->data);
|
||||||
|
|
||||||
v8::Local<v8::Value> argv[] = {
|
Array argv = Array::make(baton->exit_code, baton->signal_code);
|
||||||
Nan::New<v8::Integer>(baton->exit_code),
|
|
||||||
Nan::New<v8::Integer>(baton->signal_code),
|
|
||||||
};
|
|
||||||
|
|
||||||
v8::Local<v8::Function> cb = Nan::New<v8::Function>(baton->cb);
|
ERR_FAIL_COND(baton->cb == nullptr);
|
||||||
baton->cb.Reset();
|
baton->cb->call_funcv(argv);
|
||||||
memset(&baton->cb, -1, sizeof(baton->cb));
|
|
||||||
Nan::AsyncResource resource("pty_after_waitpid");
|
|
||||||
resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), cb, 2, argv);
|
|
||||||
|
|
||||||
uv_close((uv_handle_t *)async, pty_after_close);
|
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
|
* Init
|
||||||
*/
|
*/
|
||||||
|
|
||||||
NAN_MODULE_INIT(init) {
|
void PTYUnix::_register_methods() {
|
||||||
Nan::HandleScope scope;
|
register_method("_init", &PTYUnix::_init);
|
||||||
Nan::Export(target, "fork", PtyFork);
|
register_method("fork", &PTYUnix::fork);
|
||||||
Nan::Export(target, "open", PtyOpen);
|
register_method("open", &PTYUnix::open);
|
||||||
Nan::Export(target, "resize", PtyResize);
|
register_method("resize", &PTYUnix::resize);
|
||||||
Nan::Export(target, "process", PtyGetProc);
|
register_method("process", &PTYUnix::process);
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_MODULE(pty, init)
|
void PTYUnix::_init() {}
|
||||||
|
|
31
addons/godot_xterm/native/src/node_pty/unix/pty.h
Normal file
31
addons/godot_xterm/native/src/node_pty/unix/pty.h
Normal 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
|
108
addons/godot_xterm/native/src/pipe.cpp
Normal file
108
addons/godot_xterm/native/src/pipe.cpp
Normal 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;
|
||||||
|
}
|
64
addons/godot_xterm/native/src/pipe.h
Normal file
64
addons/godot_xterm/native/src/pipe.h
Normal 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
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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
|
|
1
addons/godot_xterm/native/thirdparty/libuv
vendored
Submodule
1
addons/godot_xterm/native/thirdparty/libuv
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 9ec6bb914febbd392b10bb9e774e25a7a15737c3
|
|
@ -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
|
|
29
addons/godot_xterm/nodes/pty/libuv_utils.gd
Normal file
29
addons/godot_xterm/nodes/pty/libuv_utils.gd
Normal 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.")
|
|
@ -4,5 +4,5 @@
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
resource_name = "Terminal"
|
resource_name = "Terminal"
|
||||||
class_name = "Pseudoterminal"
|
class_name = "LibuvUtils"
|
||||||
library = ExtResource( 1 )
|
library = ExtResource( 1 )
|
8
addons/godot_xterm/nodes/pty/pipe.gdns
Normal file
8
addons/godot_xterm/nodes/pty/pipe.gdns
Normal 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 )
|
261
addons/godot_xterm/nodes/pty/pty.gd
Normal file
261
addons/godot_xterm/nodes/pty/pty.gd
Normal 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))
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="StreamTexture"
|
type="StreamTexture"
|
||||||
path="res://.import/pseudoterminal_icon.svg-0b26aed87c28626d61aa92bd9e34d5a9.stex"
|
path="res://.import/pty_icon.svg-e9e42570b4744b3370a02d174395c793.stex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg"
|
source_file="res://addons/godot_xterm/nodes/pty/pty_icon.svg"
|
||||||
dest_files=[ "res://.import/pseudoterminal_icon.svg-0b26aed87c28626d61aa92bd9e34d5a9.stex" ]
|
dest_files=[ "res://.import/pty_icon.svg-e9e42570b4744b3370a02d174395c793.stex" ]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
124
addons/godot_xterm/nodes/pty/unix/pty_unix.gd
Normal file
124
addons/godot_xterm/nodes/pty/unix/pty_unix.gd
Normal 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
|
7
addons/godot_xterm/nodes/pty/unix/pty_unix.gdns
Normal file
7
addons/godot_xterm/nodes/pty/unix/pty_unix.gdns
Normal 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 )
|
|
@ -1,6 +1,7 @@
|
||||||
tool
|
tool
|
||||||
extends EditorPlugin
|
extends EditorPlugin
|
||||||
|
|
||||||
|
var pty_supported := OS.get_name() in ["X11", "Server", "OSX"]
|
||||||
var asciicast_import_plugin
|
var asciicast_import_plugin
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,9 +16,13 @@ func _enter_tree():
|
||||||
var terminal_icon = preload("res://addons/godot_xterm/nodes/terminal/terminal_icon.svg")
|
var terminal_icon = preload("res://addons/godot_xterm/nodes/terminal/terminal_icon.svg")
|
||||||
add_custom_type("Terminal", "Control", terminal_script, terminal_icon)
|
add_custom_type("Terminal", "Control", terminal_script, terminal_icon)
|
||||||
|
|
||||||
var pseudoterminal_script = preload("res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal.gdns")
|
if pty_supported:
|
||||||
var pseudoterminal_icon = preload("res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg")
|
var pty_icon = load("res://addons/godot_xterm/nodes/pty/pty_icon.svg")
|
||||||
add_custom_type("Pseudoterminal", "Node", pseudoterminal_script, pseudoterminal_icon)
|
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():
|
func _exit_tree():
|
||||||
|
@ -26,4 +31,6 @@ func _exit_tree():
|
||||||
|
|
||||||
remove_custom_type("Asciicast")
|
remove_custom_type("Asciicast")
|
||||||
remove_custom_type("Terminal")
|
remove_custom_type("Terminal")
|
||||||
remove_custom_type("Psuedoterminal")
|
|
||||||
|
if pty_supported:
|
||||||
|
remove_custom_type("PTY")
|
||||||
|
|
|
@ -150,11 +150,7 @@ func _on_Terminal_key_pressed(data: String, event: InputEventKey) -> void:
|
||||||
"Terminal not Supported on Windows"
|
"Terminal not Supported on Windows"
|
||||||
)
|
)
|
||||||
var scene = item.scene.instance()
|
var scene = item.scene.instance()
|
||||||
var pty = (
|
var pty = scene if OS.has_feature("JavaScript") else scene.get_node("PTY")
|
||||||
scene
|
|
||||||
if OS.has_feature("JavaScript")
|
|
||||||
else scene.get_node("Pseudoterminal")
|
|
||||||
)
|
|
||||||
get_tree().get_root().add_child(scene)
|
get_tree().get_root().add_child(scene)
|
||||||
visible = false
|
visible = false
|
||||||
scene.grab_focus()
|
scene.grab_focus()
|
||||||
|
|
7
examples/terminal/terminal.gd
Normal file
7
examples/terminal/terminal.gd
Normal 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"))
|
|
@ -1,20 +1,21 @@
|
||||||
[gd_scene load_steps=3 format=2]
|
[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/pty/unix/pty_unix.gd" type="Script" id=2]
|
||||||
[ext_resource path="res://addons/godot_xterm/nodes/pseudoterminal/pseudoterminal.gdns" type="Script" id=2]
|
[ext_resource path="res://examples/terminal/terminal.gd" type="Script" id=3]
|
||||||
|
|
||||||
[node name="Terminal" type="Control"]
|
[node name="Terminal" type="Control"]
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
focus_mode = 1
|
focus_mode = 2
|
||||||
script = ExtResource( 1 )
|
script = ExtResource( 3 )
|
||||||
__meta__ = {
|
__meta__ = {
|
||||||
"_edit_use_anchors_": false
|
"_edit_use_anchors_": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[node name="Pseudoterminal" type="Node" parent="."]
|
[node name="PTY" type="Node" parent="."]
|
||||||
script = ExtResource( 2 )
|
script = ExtResource( 2 )
|
||||||
|
terminal_path = NodePath("..")
|
||||||
[connection signal="data_sent" from="." to="Pseudoterminal" method="write"]
|
env = {
|
||||||
[connection signal="size_changed" from="." to="Pseudoterminal" method="resize"]
|
"COLORTERM": "truecolor",
|
||||||
[connection signal="data_sent" from="Pseudoterminal" to="." method="write"]
|
"TERM": "xterm-256color"
|
||||||
|
}
|
||||||
|
|
33
misc/vscode/launch.json
Normal file
33
misc/vscode/launch.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -3,4 +3,21 @@
|
||||||
"clang-format.executable": "/run/current-system/sw/bin/clang-format",
|
"clang-format.executable": "/run/current-system/sw/bin/clang-format",
|
||||||
"gut-extension.additionalOptions": "-d --no-window",
|
"gut-extension.additionalOptions": "-d --no-window",
|
||||||
"nixEnvSelector.nixShellConfig": "NOT_MODIFIED_ENV",
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
117
test/integration/unix/pty_unix.test.gd
Normal file
117
test/integration/unix/pty_unix.test.gd
Normal 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)}
|
30
test/integration/uv_utils/uv_utils.test.gd
Normal file
30
test/integration/uv_utils/uv_utils.test.gd
Normal 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)
|
Loading…
Reference in a new issue