From 0dd2378387abfddbc33d1e88f3645d5d10a2a6a2 Mon Sep 17 00:00:00 2001 From: Leroy Hopson Date: Sat, 3 Jul 2021 00:27:34 +0700 Subject: [PATCH] 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. --- .github/workflows/main.yml | 31 +++ .gitmodules | 3 + .gutconfig.json | 24 +- LICENSE | 2 +- addons/godot_xterm/LICENSE | 2 +- addons/godot_xterm/native/SConstruct | 15 +- addons/godot_xterm/native/build.sh | 31 ++- addons/godot_xterm/native/shell.nix | 3 + .../native/src/libgodotxtermnative.cpp | 14 +- addons/godot_xterm/native/src/libuv_utils.cpp | 190 +++++++++++++ addons/godot_xterm/native/src/libuv_utils.h | 43 +++ .../native/src/node_pty/LICENSE.md | 4 +- .../native/src/node_pty/unix/pty.cc | 208 ++++++-------- .../native/src/node_pty/unix/pty.h | 31 +++ addons/godot_xterm/native/src/pipe.cpp | 108 ++++++++ addons/godot_xterm/native/src/pipe.h | 64 +++++ .../godot_xterm/native/src/pseudoterminal.cpp | 155 ----------- .../godot_xterm/native/src/pseudoterminal.h | 49 ---- addons/godot_xterm/native/thirdparty/libuv | 1 + .../nodes/pseudoterminal/README.md | 51 ---- addons/godot_xterm/nodes/pty/libuv_utils.gd | 29 ++ .../libuv_utils.gdns} | 2 +- addons/godot_xterm/nodes/pty/pipe.gdns | 8 + addons/godot_xterm/nodes/pty/pty.gd | 261 ++++++++++++++++++ .../pty_icon.svg} | 0 .../pty_icon.svg.import} | 6 +- addons/godot_xterm/nodes/pty/unix/pty_unix.gd | 124 +++++++++ .../godot_xterm/nodes/pty/unix/pty_unix.gdns | 7 + addons/godot_xterm/plugin.gd | 15 +- examples/menu/menu.gd | 6 +- examples/terminal/terminal.gd | 7 + examples/terminal/terminal.tscn | 19 +- misc/vscode/launch.json | 33 +++ misc/vscode/settings.json | 17 ++ test/integration/unix/pty_unix.test.gd | 117 ++++++++ test/integration/uv_utils/uv_utils.test.gd | 30 ++ 36 files changed, 1268 insertions(+), 442 deletions(-) create mode 100644 addons/godot_xterm/native/src/libuv_utils.cpp create mode 100644 addons/godot_xterm/native/src/libuv_utils.h create mode 100644 addons/godot_xterm/native/src/node_pty/unix/pty.h create mode 100644 addons/godot_xterm/native/src/pipe.cpp create mode 100644 addons/godot_xterm/native/src/pipe.h delete mode 100644 addons/godot_xterm/native/src/pseudoterminal.cpp delete mode 100644 addons/godot_xterm/native/src/pseudoterminal.h create mode 160000 addons/godot_xterm/native/thirdparty/libuv delete mode 100644 addons/godot_xterm/nodes/pseudoterminal/README.md create mode 100644 addons/godot_xterm/nodes/pty/libuv_utils.gd rename addons/godot_xterm/nodes/{pseudoterminal/pseudoterminal.gdns => pty/libuv_utils.gdns} (88%) create mode 100644 addons/godot_xterm/nodes/pty/pipe.gdns create mode 100644 addons/godot_xterm/nodes/pty/pty.gd rename addons/godot_xterm/nodes/{pseudoterminal/pseudoterminal_icon.svg => pty/pty_icon.svg} (100%) rename addons/godot_xterm/nodes/{pseudoterminal/pseudoterminal_icon.svg.import => pty/pty_icon.svg.import} (64%) create mode 100644 addons/godot_xterm/nodes/pty/unix/pty_unix.gd create mode 100644 addons/godot_xterm/nodes/pty/unix/pty_unix.gdns create mode 100644 examples/terminal/terminal.gd create mode 100644 misc/vscode/launch.json create mode 100644 test/integration/unix/pty_unix.test.gd create mode 100644 test/integration/uv_utils/uv_utils.test.gd diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1fdf4a3..637b6b2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/.gitmodules b/.gitmodules index 18de8ce..5ab6ba0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/.gutconfig.json b/.gutconfig.json index 6740d6d..9473df7 100644 --- a/.gutconfig.json +++ b/.gutconfig.json @@ -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 } diff --git a/LICENSE b/LICENSE index 47743c7..81b2ab5 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/addons/godot_xterm/LICENSE b/addons/godot_xterm/LICENSE index 47743c7..81b2ab5 100644 --- a/addons/godot_xterm/LICENSE +++ b/addons/godot_xterm/LICENSE @@ -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 diff --git a/addons/godot_xterm/native/SConstruct b/addons/godot_xterm/native/SConstruct index c6cbc5b..ef940d9 100644 --- a/addons/godot_xterm/native/SConstruct +++ b/addons/godot_xterm/native/SConstruct @@ -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" diff --git a/addons/godot_xterm/native/build.sh b/addons/godot_xterm/native/build.sh index bf9f153..3cce71e 100755 --- a/addons/godot_xterm/native/build.sh +++ b/addons/godot_xterm/native/build.sh @@ -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) diff --git a/addons/godot_xterm/native/shell.nix b/addons/godot_xterm/native/shell.nix index 21a13b0..91b3d1c 100644 --- a/addons/godot_xterm/native/shell.nix +++ b/addons/godot_xterm/native/shell.nix @@ -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 diff --git a/addons/godot_xterm/native/src/libgodotxtermnative.cpp b/addons/godot_xterm/native/src/libgodotxtermnative.cpp index 0d98612..040c128 100644 --- a/addons/godot_xterm/native/src/libgodotxtermnative.cpp +++ b/addons/godot_xterm/native/src/libgodotxtermnative.cpp @@ -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(); -#if defined(__unix__) && !defined(__EMSCRIPTEN__) - godot::register_class(); +#if !defined(__EMSCRIPTEN__) && !defined(__WIN32) + godot::register_tool_class(); + godot::register_tool_class(); + godot::register_tool_class(); #endif } diff --git a/addons/godot_xterm/native/src/libuv_utils.cpp b/addons/godot_xterm/native/src/libuv_utils.cpp new file mode 100644 index 0000000..3748595 --- /dev/null +++ b/addons/godot_xterm/native/src/libuv_utils.cpp @@ -0,0 +1,190 @@ +#include "libuv_utils.h" +#include + +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 + } +} \ No newline at end of file diff --git a/addons/godot_xterm/native/src/libuv_utils.h b/addons/godot_xterm/native/src/libuv_utils.h new file mode 100644 index 0000000..4f6eb9b --- /dev/null +++ b/addons/godot_xterm/native/src/libuv_utils.h @@ -0,0 +1,43 @@ +#ifndef GODOT_XTERM_UV_UTILS_H +#define GODOT_XTERM_UV_UTILS_H + +#include +#include + +#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 \ No newline at end of file diff --git a/addons/godot_xterm/native/src/node_pty/LICENSE.md b/addons/godot_xterm/native/src/node_pty/LICENSE.md index 49477d1..960ea18 100644 --- a/addons/godot_xterm/native/src/node_pty/LICENSE.md +++ b/addons/godot_xterm/native/src/node_pty/LICENSE.md @@ -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. -``` \ No newline at end of file +``` diff --git a/addons/godot_xterm/native/src/node_pty/unix/pty.cc b/addons/godot_xterm/native/src/node_pty/unix/pty.cc index e776649..560c78b 100644 --- a/addons/godot_xterm/native/src/node_pty/unix/pty.cc +++ b/addons/godot_xterm/native/src/node_pty/unix/pty.cc @@ -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 +#include + #include -#include #include #include #include @@ -77,12 +82,14 @@ extern char **environ; #define NSIG 32 #endif +using namespace godot; + /** * Structs */ struct pty_baton { - Nan::Persistent cb; + Ref 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 p_on_exit) { // file - Nan::Utf8String file(info[0]); + char *file = p_file.alloc_c_string(); // args int i = 0; - v8::Local argv_ = v8::Local::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 env_ = v8::Local::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(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 obj = Nan::New(); - Nan::Set(obj, Nan::New("fd").ToLocalChecked(), - Nan::New(master)); - Nan::Set(obj, Nan::New("pid").ToLocalChecked(), - Nan::New(pid)); - Nan::Set(obj, Nan::New("pty").ToLocalChecked(), - Nan::New(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::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(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 obj = Nan::New(); - Nan::Set(obj, Nan::New("master").ToLocalChecked(), - Nan::New(master)); - Nan::Set(obj, Nan::New("slave").ToLocalChecked(), - Nan::New(slave)); - Nan::Set(obj, Nan::New("pty").ToLocalChecked(), - Nan::New(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 name_ = Nan::New(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(async->data); - v8::Local argv[] = { - Nan::New(baton->exit_code), - Nan::New(baton->signal_code), - }; + Array argv = Array::make(baton->exit_code, baton->signal_code); - v8::Local cb = Nan::New(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() {} diff --git a/addons/godot_xterm/native/src/node_pty/unix/pty.h b/addons/godot_xterm/native/src/node_pty/unix/pty.h new file mode 100644 index 0000000..f9fe3fe --- /dev/null +++ b/addons/godot_xterm/native/src/node_pty/unix/pty.h @@ -0,0 +1,31 @@ +// Copyright (c) 2021, Leroy Hopson (MIT License). + +#ifndef GODOT_XTERM_PTY_H +#define GODOT_XTERM_PTY_H + +#include +#include + +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 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 diff --git a/addons/godot_xterm/native/src/pipe.cpp b/addons/godot_xterm/native/src/pipe.cpp new file mode 100644 index 0000000..7bb2241 --- /dev/null +++ b/addons/godot_xterm/native/src/pipe.cpp @@ -0,0 +1,108 @@ +// Copyright (c) 2021, Leroy Hopson (MIT License). + +#include "pipe.h" +#include "libuv_utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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("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(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; +} diff --git a/addons/godot_xterm/native/src/pipe.h b/addons/godot_xterm/native/src/pipe.h new file mode 100644 index 0000000..395b063 --- /dev/null +++ b/addons/godot_xterm/native/src/pipe.h @@ -0,0 +1,64 @@ +// Copyright (c) 2021, Leroy Hopson (MIT License). + +#ifndef GODOT_XTERM_PIPE_H +#define GODOT_XTERM_PIPE_H + +#include +#include +#include +#include +#include +#include +#include +#include + +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 diff --git a/addons/godot_xterm/native/src/pseudoterminal.cpp b/addons/godot_xterm/native/src/pseudoterminal.cpp deleted file mode 100644 index 09666cc..0000000 --- a/addons/godot_xterm/native/src/pseudoterminal.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "pseudoterminal.h" -#include -#include -#include -#include - -// Platform specific includes. -#if defined(__linux__) -#include -#endif -#if defined(__APPLE__) -#include -#include -#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((char *)"data_sent", "data", - GODOT_VARIANT_TYPE_POOL_BYTE_ARRAY); - register_signal((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 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 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 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 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 guard(size_mutex); - size = new_size; -} diff --git a/addons/godot_xterm/native/src/pseudoterminal.h b/addons/godot_xterm/native/src/pseudoterminal.h deleted file mode 100644 index 8526c33..0000000 --- a/addons/godot_xterm/native/src/pseudoterminal.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef PSEUDOTERMINAL_H -#define PSEUDOTERMINAL_H - -#include -#include -#include -#include - -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 \ No newline at end of file diff --git a/addons/godot_xterm/native/thirdparty/libuv b/addons/godot_xterm/native/thirdparty/libuv new file mode 160000 index 0000000..9ec6bb9 --- /dev/null +++ b/addons/godot_xterm/native/thirdparty/libuv @@ -0,0 +1 @@ +Subproject commit 9ec6bb914febbd392b10bb9e774e25a7a15737c3 diff --git a/addons/godot_xterm/nodes/pseudoterminal/README.md b/addons/godot_xterm/nodes/pseudoterminal/README.md deleted file mode 100644 index 08d130f..0000000 --- a/addons/godot_xterm/nodes/pseudoterminal/README.md +++ /dev/null @@ -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 diff --git a/addons/godot_xterm/nodes/pty/libuv_utils.gd b/addons/godot_xterm/nodes/pty/libuv_utils.gd new file mode 100644 index 0000000..0ab58fd --- /dev/null +++ b/addons/godot_xterm/nodes/pty/libuv_utils.gd @@ -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.") diff --git a/addons/godot_xterm/nodes/pseudoterminal/pseudoterminal.gdns b/addons/godot_xterm/nodes/pty/libuv_utils.gdns similarity index 88% rename from addons/godot_xterm/nodes/pseudoterminal/pseudoterminal.gdns rename to addons/godot_xterm/nodes/pty/libuv_utils.gdns index a56d402..8c03188 100644 --- a/addons/godot_xterm/nodes/pseudoterminal/pseudoterminal.gdns +++ b/addons/godot_xterm/nodes/pty/libuv_utils.gdns @@ -4,5 +4,5 @@ [resource] resource_name = "Terminal" -class_name = "Pseudoterminal" +class_name = "LibuvUtils" library = ExtResource( 1 ) diff --git a/addons/godot_xterm/nodes/pty/pipe.gdns b/addons/godot_xterm/nodes/pty/pipe.gdns new file mode 100644 index 0000000..8560b5c --- /dev/null +++ b/addons/godot_xterm/nodes/pty/pipe.gdns @@ -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 ) diff --git a/addons/godot_xterm/nodes/pty/pty.gd b/addons/godot_xterm/nodes/pty/pty.gd new file mode 100644 index 0000000..3e524e0 --- /dev/null +++ b/addons/godot_xterm/nodes/pty/pty.gd @@ -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)) diff --git a/addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg b/addons/godot_xterm/nodes/pty/pty_icon.svg similarity index 100% rename from addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg rename to addons/godot_xterm/nodes/pty/pty_icon.svg diff --git a/addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg.import b/addons/godot_xterm/nodes/pty/pty_icon.svg.import similarity index 64% rename from addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg.import rename to addons/godot_xterm/nodes/pty/pty_icon.svg.import index 7634b78..c9a8f4d 100644 --- a/addons/godot_xterm/nodes/pseudoterminal/pseudoterminal_icon.svg.import +++ b/addons/godot_xterm/nodes/pty/pty_icon.svg.import @@ -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] diff --git a/addons/godot_xterm/nodes/pty/unix/pty_unix.gd b/addons/godot_xterm/nodes/pty/unix/pty_unix.gd new file mode 100644 index 0000000..f52262b --- /dev/null +++ b/addons/godot_xterm/nodes/pty/unix/pty_unix.gd @@ -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 diff --git a/addons/godot_xterm/nodes/pty/unix/pty_unix.gdns b/addons/godot_xterm/nodes/pty/unix/pty_unix.gdns new file mode 100644 index 0000000..f8b0eb1 --- /dev/null +++ b/addons/godot_xterm/nodes/pty/unix/pty_unix.gdns @@ -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 ) diff --git a/addons/godot_xterm/plugin.gd b/addons/godot_xterm/plugin.gd index 9a0732e..2a9c86d 100644 --- a/addons/godot_xterm/plugin.gd +++ b/addons/godot_xterm/plugin.gd @@ -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") diff --git a/examples/menu/menu.gd b/examples/menu/menu.gd index 258566e..f3d9c94 100644 --- a/examples/menu/menu.gd +++ b/examples/menu/menu.gd @@ -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() diff --git a/examples/terminal/terminal.gd b/examples/terminal/terminal.gd new file mode 100644 index 0000000..901567a --- /dev/null +++ b/examples/terminal/terminal.gd @@ -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")) diff --git a/examples/terminal/terminal.tscn b/examples/terminal/terminal.tscn index df98ed1..0302b84 100644 --- a/examples/terminal/terminal.tscn +++ b/examples/terminal/terminal.tscn @@ -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" +} diff --git a/misc/vscode/launch.json b/misc/vscode/launch.json new file mode 100644 index 0000000..7461f66 --- /dev/null +++ b/misc/vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/misc/vscode/settings.json b/misc/vscode/settings.json index a8ee331..0adf680 100644 --- a/misc/vscode/settings.json +++ b/misc/vscode/settings.json @@ -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" + } } diff --git a/test/integration/unix/pty_unix.test.gd b/test/integration/unix/pty_unix.test.gd new file mode 100644 index 0000000..4429774 --- /dev/null +++ b/test/integration/unix/pty_unix.test.gd @@ -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)} diff --git a/test/integration/uv_utils/uv_utils.test.gd b/test/integration/uv_utils/uv_utils.test.gd new file mode 100644 index 0000000..d41d75c --- /dev/null +++ b/test/integration/uv_utils/uv_utils.test.gd @@ -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)