added windows pty support

This commit is contained in:
Alexander Treml 2025-06-26 15:54:12 +02:00 committed by Leroy Hopson
parent bd26137e78
commit f3820365c6
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
23 changed files with 1478 additions and 695 deletions

View file

@ -2,7 +2,7 @@
[ext_resource type="Script" path="res://addons/godot_xterm/editor_plugins/terminal/terminal_panel.gd" id="1"]
[sub_resource type="Image" id="Image_84jui"]
[sub_resource type="Image" id="Image_law8x"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
@ -12,7 +12,7 @@ data = {
}
[sub_resource type="ImageTexture" id="ImageTexture_q1uu0"]
image = SubResource("Image_84jui")
image = SubResource("Image_law8x")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_osmrc"]
bg_color = Color(0.113329, 0.129458, 0.156802, 1)

View file

@ -26,9 +26,17 @@ sources.append([
])
if env['platform'] == 'linux' or env['platform'] == 'macos':
env.Append(LIBS=['util', env.File('thirdparty/libuv/build/libuv_a.a')])
env.Append(LIBS=['util', env.File('thirdparty/libuv/build/libuv_a.a')])
else:
env.Append(CPPDEFINES=['_PTY_DISABLED'])
env.Append(LIBS=['ws2_32.lib', 'Advapi32', 'User32', 'Userenv', 'iphlpapi'])
if env["target"] == "template_release":
env.Append(LIBS=[env.File('thirdparty/libuv/build/Release/uv_a.lib')])
else:
env.Append(LIBS=[env.File('thirdparty/libuv/build/Debug/uv_a.lib')])
# TODO(ast) this is a bandaid fix (see https://stackoverflow.com/questions/3007312/resolving-lnk4098-defaultlib-msvcrt-conflicts-with)
# TODO(ast) a release build needs to use msvcrt instead of msvcrtd
env.Append(LINKFLAGS=['/VERBOSE:LIB', '/NODEFAULTLIB:libcmtd.lib', '/NODEFAULTLIB:libcmt.lib', '/NODEFAULTLIB:msvcrt.lib'])
if env["platform"] == "macos":
library = env.SharedLibrary(

View file

@ -1,8 +1,16 @@
#!/bin/sh
#!/bin/bash
# SPDX-FileCopyrightText: 2020-2023 Leroy Hopson <godot-xterm@leroy.geek.nz>
# SPDX-License-Identifier: MIT
# Convenience function to keep the terminal open on failure in some terminals
function fail () {
echo "Failure!"
read -p "Press any key to continue" x
exit 1
}
set -x
set -e
# Parse args.
@ -27,8 +35,10 @@ done
target=${target:-template_debug}
if [ "$target" == "template_debug" ]; then
debug_symbols="yes"
uv_target="debug"
else
debug_symbols="no"
uv_target="release"
fi
nproc=$(nproc || sysctl -n hw.ncpu)
@ -52,6 +62,7 @@ updateSubmodules() {
fi
}
# TODO libtsm causes warnings due to usage of nonstandard \e escape sequence. could be replaced with standard \033 or \x1b if this causes issues
updateSubmodules LIBUV_DIR ${NATIVE_DIR}/thirdparty/libuv
updateSubmodules LIBTSM_DIR ${NATIVE_DIR}/thirdparty/libtsm
updateSubmodules GODOT_CPP_DIR ${NATIVE_DIR}/thirdparty/godot-cpp
@ -60,23 +71,26 @@ updateSubmodules GODOT_CPP_DIR ${NATIVE_DIR}/thirdparty/godot-cpp
cd ${LIBUV_DIR}
mkdir build || true
cd build
args="-DCMAKE_BUILD_TYPE=$target -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE \
args="-DCMAKE_BUILD_TYPE=$uv_target -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE \
-DCMAKE_OSX_ARCHITECTURES=$(uname -m)"
if [ "$target" == "template_release" ]; then
args="$args -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL"
else
args="$args -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL"
fi
cmake .. $args
cmake .. $args || fail
cd ..
cmake --build build --config $target -j$nproc
cmake --build build --config $uv_target -j$nproc || fail
# Build libgodot-xterm.
cd ${NATIVE_DIR}
scons target=$target arch=$(uname -m) debug_symbols=$debug_symbols
scons target=$target arch=$(uname -m) debug_symbols=$debug_symbols || fail
# Use Docker to build libgodot-xterm javascript.
#if [ -x "$(command -v docker-compose)" ]; then
# UID_GID="0:0" TARGET=$target docker-compose build javascript
# UID_GID="$(id -u):$(id -g)" TARGET=$target docker-compose run --rm javascript
#fi
echo "Done!"
read -p "Press any key to exit" x

View file

@ -117,6 +117,35 @@ const Terminal::KeyMap Terminal::KEY_MAP = {
{{KEY_Z, 'z'}, XKB_KEY_z},
{{KEY_Z, 'Z'}, XKB_KEY_Z},
// When CTRL is pressed, the unicode is always zero.
// But to handle CTRL+<XY> in TSM, we still need to know the keysym
{{KEY_A, '\0'}, XKB_KEY_a},
{{KEY_B, '\0'}, XKB_KEY_b},
{{KEY_C, '\0'}, XKB_KEY_c},
{{KEY_D, '\0'}, XKB_KEY_d},
{{KEY_E, '\0'}, XKB_KEY_e},
{{KEY_F, '\0'}, XKB_KEY_f},
{{KEY_G, '\0'}, XKB_KEY_g},
{{KEY_H, '\0'}, XKB_KEY_h},
{{KEY_I, '\0'}, XKB_KEY_i},
{{KEY_J, '\0'}, XKB_KEY_j},
{{KEY_K, '\0'}, XKB_KEY_k},
{{KEY_L, '\0'}, XKB_KEY_l},
{{KEY_M, '\0'}, XKB_KEY_m},
{{KEY_N, '\0'}, XKB_KEY_n},
{{KEY_O, '\0'}, XKB_KEY_o},
{{KEY_P, '\0'}, XKB_KEY_p},
{{KEY_Q, '\0'}, XKB_KEY_q},
{{KEY_R, '\0'}, XKB_KEY_r},
{{KEY_S, '\0'}, XKB_KEY_s},
{{KEY_T, '\0'}, XKB_KEY_t},
{{KEY_U, '\0'}, XKB_KEY_u},
{{KEY_V, '\0'}, XKB_KEY_v},
{{KEY_W, '\0'}, XKB_KEY_w},
{{KEY_X, '\0'}, XKB_KEY_x},
{{KEY_Y, '\0'}, XKB_KEY_y},
{{KEY_Z, '\0'}, XKB_KEY_z},
{{KEY_0, '0'}, XKB_KEY_0},
{{KEY_1, '1'}, XKB_KEY_1},
{{KEY_2, '2'}, XKB_KEY_2},

View file

@ -12,17 +12,20 @@
#if (defined(__linux__) || defined(__APPLE__)) && !defined(_PTY_DISABLED)
#include "pty_unix.h"
#include <unistd.h>
#elif defined(_WIN32) && !defined(_PTY_DISABLED)
#include "pty_win.h"
#include <io.h>
#endif
// Require buffer to be flushed after reaching this size.
#define BUFFER_LIMIT 1048576 // 1MB
#define UV_ERR_MSG(uv_err) \
#define UV_ERR_MSG(uv_err) \
String(uv_err_name(uv_err)) + String(": ") + String(uv_strerror(uv_err))
#define ERR_FAIL_UV_ERR(uv_err) \
ERR_FAIL_COND_V_MSG(uv_err < 0, PTY::uv_err_to_godot_err(uv_err), \
UV_ERR_MSG(uv_err))
#define ERR_FAIL_UV_ERR(uv_err) \
ERR_FAIL_COND_V_MSG(uv_err < 0, PTY::uv_err_to_godot_err(uv_err), \
UV_ERR_MSG(uv_err))
using namespace godot;
@ -30,60 +33,62 @@ void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf);
void _write_cb(uv_write_t *req, int status) { std::free(req); }
void _close_cb(uv_handle_t *handle) { /* no-op */ };
void PTY::_bind_methods() {
BIND_ENUM_CONSTANT(SIGNAL_SIGHUP);
BIND_ENUM_CONSTANT(SIGNAL_SIGINT);
BIND_ENUM_CONSTANT(SIGNAL_SIGQUIT);
BIND_ENUM_CONSTANT(SIGNAL_SIGILL);
BIND_ENUM_CONSTANT(SIGNAL_SIGTRAP);
BIND_ENUM_CONSTANT(SIGNAL_SIGABRT);
BIND_ENUM_CONSTANT(SIGNAL_SIGFPE);
BIND_ENUM_CONSTANT(SIGNAL_SIGKILL);
BIND_ENUM_CONSTANT(SIGNAL_SIGSEGV);
BIND_ENUM_CONSTANT(SIGNAL_SIGPIPE);
BIND_ENUM_CONSTANT(SIGNAL_SIGALRM);
BIND_ENUM_CONSTANT(SIGNAL_SIGTERM);
void PTY::_bind_methods()
{
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGHUP);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGINT);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGQUIT);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGILL);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGTRAP);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGABRT);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGFPE);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGKILL);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGSEGV);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGPIPE);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGALRM);
BIND_ENUM_CONSTANT(IPCSIGNAL_SIGTERM);
ADD_SIGNAL(MethodInfo("data_received", PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("exited", PropertyInfo(Variant::INT, "exit_code"), PropertyInfo(Variant::INT, "signal_code")));
ClassDB::bind_method(D_METHOD("set_cols", "num_cols"), &PTY::set_cols);
ClassDB::bind_method(D_METHOD("get_cols"), &PTY::get_cols);
ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "cols"), "set_cols", "get_cols");
ClassDB::bind_method(D_METHOD("set_cols", "num_cols"), &PTY::set_cols);
ClassDB::bind_method(D_METHOD("get_cols"), &PTY::get_cols);
ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "cols"), "set_cols", "get_cols");
ClassDB::bind_method(D_METHOD("set_rows", "num_rows"), &PTY::set_rows);
ClassDB::bind_method(D_METHOD("get_rows"), &PTY::get_rows);
ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "rows"), "set_rows", "get_rows");
ClassDB::bind_method(D_METHOD("set_rows", "num_rows"), &PTY::set_rows);
ClassDB::bind_method(D_METHOD("get_rows"), &PTY::get_rows);
ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "rows"), "set_rows", "get_rows");
ClassDB::bind_method(D_METHOD("get_env"), &PTY::get_env);
ClassDB::bind_method(D_METHOD("set_env", "env"), &PTY::set_env);
ClassDB::add_property("PTY", PropertyInfo(Variant::DICTIONARY, "env"), "set_env", "get_env");
ClassDB::bind_method(D_METHOD("get_env"), &PTY::get_env);
ClassDB::bind_method(D_METHOD("set_env", "env"), &PTY::set_env);
ClassDB::add_property("PTY", PropertyInfo(Variant::DICTIONARY, "env"), "set_env", "get_env");
ClassDB::bind_method(D_METHOD("get_use_os_env"), &PTY::get_use_os_env);
ClassDB::bind_method(D_METHOD("set_use_os_env", "use_os_env"), &PTY::set_use_os_env);
ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_os_env"), "set_use_os_env", "get_use_os_env");
ClassDB::bind_method(D_METHOD("get_use_os_env"), &PTY::get_use_os_env);
ClassDB::bind_method(D_METHOD("set_use_os_env", "use_os_env"), &PTY::set_use_os_env);
ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_os_env"), "set_use_os_env", "get_use_os_env");
ClassDB::bind_method(D_METHOD("set_use_threads", "enabled"), &PTY::set_use_threads);
ClassDB::bind_method(D_METHOD("is_using_threads"), &PTY::is_using_threads);
ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_threads"), "set_use_threads", "is_using_threads");
ClassDB::bind_method(D_METHOD("set_use_threads", "enabled"), &PTY::set_use_threads);
ClassDB::bind_method(D_METHOD("is_using_threads"), &PTY::is_using_threads);
ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_threads"), "set_use_threads", "is_using_threads");
ClassDB::bind_method(D_METHOD("set_terminal_path", "path"), &PTY::set_terminal_path);
ClassDB::bind_method(D_METHOD("get_terminal_path"), &PTY::get_terminal_path);
ClassDB::add_property("PTY", PropertyInfo(Variant::NODE_PATH, "terminal_path", PropertyHint::PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Terminal"), "set_terminal_path", "get_terminal_path");
ClassDB::bind_method(D_METHOD("set_terminal_path", "path"), &PTY::set_terminal_path);
ClassDB::bind_method(D_METHOD("get_terminal_path"), &PTY::get_terminal_path);
ClassDB::add_property("PTY", PropertyInfo(Variant::NODE_PATH, "terminal_path", PropertyHint::PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Terminal"), "set_terminal_path", "get_terminal_path");
ClassDB::bind_method(D_METHOD("get_pts_name"), &PTY::get_pts_name);
ClassDB::bind_method(D_METHOD("get_pts_name"), &PTY::get_pts_name);
ClassDB::bind_method(D_METHOD("fork", "file", "args", "cwd", "cols", "rows"), &PTY::fork, DEFVAL(""), DEFVAL(PackedStringArray()), DEFVAL("."), DEFVAL(80), DEFVAL(24));
ClassDB::bind_method(D_METHOD("open", "cols", "rows"), &PTY::open, DEFVAL(80), DEFVAL(24));
ClassDB::bind_method(D_METHOD("write", "data"), &PTY::write);
ClassDB::bind_method(D_METHOD("resize", "cols", "rows"), &PTY::resize);
ClassDB::bind_method(D_METHOD("resizev", "size"), &PTY::resizev);
ClassDB::bind_method(D_METHOD("kill", "signal"), &PTY::kill);
ClassDB::bind_method(D_METHOD("fork", "file", "args", "cwd", "cols", "rows"), &PTY::fork, DEFVAL(""), DEFVAL(PackedStringArray()), DEFVAL("."), DEFVAL(80), DEFVAL(24));
ClassDB::bind_method(D_METHOD("open", "cols", "rows"), &PTY::open, DEFVAL(80), DEFVAL(24));
ClassDB::bind_method(D_METHOD("write", "data"), &PTY::write);
ClassDB::bind_method(D_METHOD("resize", "cols", "rows"), &PTY::resize);
ClassDB::bind_method(D_METHOD("resizev", "size"), &PTY::resizev);
ClassDB::bind_method(D_METHOD("kill", "signal"), &PTY::kill);
ClassDB::bind_method(D_METHOD("_on_exit", "exit_code", "signal_code"), &PTY::_on_exit);
ClassDB::bind_method(D_METHOD("_on_exit", "exit_code", "signal_code"), &PTY::_on_exit);
}
PTY::PTY() {
PTY::PTY()
{
use_threads = true;
set_process_internal(false);
@ -94,76 +99,92 @@ PTY::PTY() {
env["TERM"] = "xterm-256color";
env["COLORTERM"] = "truecolor";
#if defined(__linux__) || defined(__APPLE__)
uv_loop_init(&loop);
uv_async_init(&loop, &async_handle, [](uv_async_t *handle) {});
uv_pipe_init(&loop, &pipe, false);
#ifdef _WIN32
uv_pipe_init(&loop, &pipe_out, false);
#endif
pipe.data = this;
#endif
}
void PTY::set_cols(const int num_cols) {
if (cols != num_cols) {
void PTY::set_cols(const int num_cols)
{
if (cols != num_cols)
{
cols = num_cols;
resize(cols, rows);
}
}
int PTY::get_cols() const {
return cols;
int PTY::get_cols() const
{
return cols;
}
void PTY::set_rows(const int num_rows) {
if (rows != num_rows) {
void PTY::set_rows(const int num_rows)
{
if (rows != num_rows)
{
rows = num_rows;
resize(cols, rows);
}
}
int PTY::get_rows() const {
return rows;
int PTY::get_rows() const
{
return rows;
}
Dictionary PTY::get_env() const {
return env;
Dictionary PTY::get_env() const
{
return env;
}
void PTY::set_env(const Dictionary &value) {
env = value;
void PTY::set_env(const Dictionary &value)
{
env = value;
}
bool PTY::get_use_os_env() const {
return use_os_env;
bool PTY::get_use_os_env() const
{
return use_os_env;
}
void PTY::set_use_os_env(const bool value) {
use_os_env = value;
void PTY::set_use_os_env(const bool value)
{
use_os_env = value;
}
void PTY::set_use_threads(bool p_use) {
void PTY::set_use_threads(bool p_use)
{
ERR_FAIL_COND(status != STATUS_CLOSED);
use_threads = p_use;
}
bool PTY::is_using_threads() const {
bool PTY::is_using_threads() const
{
return use_threads;
}
void PTY::set_terminal_path(NodePath p_terminal_path) {
void PTY::set_terminal_path(NodePath p_terminal_path)
{
terminal_path = p_terminal_path;
Callable write = Callable(this, "write");
Callable resizev = Callable(this, "resizev");
// Disconnect the current terminal, if any.
if (terminal != nullptr) {
if (terminal != nullptr)
{
disconnect("data_received", Callable(terminal, "write"));
terminal->disconnect("data_sent", write);
terminal->disconnect("size_changed", resizev);
}
terminal = Object::cast_to<Terminal>(get_node_or_null(terminal_path));
if (terminal == nullptr) return;
if (terminal == nullptr)
return;
// Connect the new terminal.
resize(terminal->get_cols(), terminal->get_rows());
@ -175,23 +196,31 @@ void PTY::set_terminal_path(NodePath p_terminal_path) {
connect("data_received", Callable(terminal, "write"), CONNECT_PERSIST);
}
NodePath PTY::get_terminal_path() const {
NodePath PTY::get_terminal_path() const
{
return terminal_path;
}
String PTY::get_pts_name() const {
String PTY::get_pts_name() const
{
return pts_name;
}
Error PTY::fork(const String &file, const PackedStringArray &args, const String &cwd, const int p_cols, const int p_rows) {
Error PTY::fork(const String &file, const PackedStringArray &args, const String &cwd, const int p_cols, const int p_rows)
{
String fork_file = _get_fork_file(file);
Dictionary fork_env = _get_fork_env();
Dictionary result;
#if defined(__linux__) || defined(__APPLE__)
#if defined(__linux__) || defined(__APPLE__)
String helper_path = ProjectSettings::get_singleton()->globalize_path("res://addons/godot_xterm/native/bin/spawn-helper");
result = PTYUnix::fork(fork_file, args, _parse_env(fork_env), cwd, p_cols, p_rows, -1, -1, true, helper_path, Callable(this, "_on_exit"));
#endif
#endif
#if defined(_WIN32)
String helper_path = ProjectSettings::get_singleton()->globalize_path("res://addons/godot_xterm/native/bin/spawn-helper");
result = PTYWin::fork(fork_file, args, _parse_env(fork_env), cwd, p_cols, p_rows, -1, -1, true, helper_path, Callable(this, "_on_exit"));
#endif
Error err = static_cast<Error>((int)result["error"]);
ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to fork.");
@ -199,15 +228,22 @@ Error PTY::fork(const String &file, const PackedStringArray &args, const String
fd = result["fd"];
pid = result["pid"];
pts_name = result["pty"];
#ifdef _WIN32
fd_out = result["fd_out"];
hpc = result["hpc"];
#endif
status = STATUS_OPEN;
#if defined(__linux__) || defined(__APPLE__)
_pipe_open(fd);
uv_read_start((uv_stream_t *)&pipe, _alloc_buffer, _read_cb);
#endif
_pipe_open(fd, &pipe);
#ifdef _WIN32
_pipe_open(fd_out, &pipe_out);
#endif
if (use_threads) {
uv_read_start((uv_stream_t *)&pipe, _alloc_buffer, _read_cb);
if (use_threads)
{
stop_thread.clear();
thread->start(callable_mp(this, &PTY::_thread_func));
}
@ -216,80 +252,97 @@ Error PTY::fork(const String &file, const PackedStringArray &args, const String
return OK;
}
void PTY::kill(const int signal) {
#if (defined(__linux__) || defined(__APPLE__)) && !defined(_PTY_DISABLED)
if (pid > 0) {
void PTY::kill(const int signal)
{
#if !defined(_PTY_DISABLED)
if (pid > 0)
{
uv_kill(pid, signal);
}
#endif
#endif
}
Error PTY::open(const int cols, const int rows) {
Dictionary result;
Error PTY::open(const int cols, const int rows)
{
Dictionary result;
#if defined(__linux__) || defined(__APPLE__)
result = PTYUnix::open(cols, rows);
#endif
#if defined(__linux__) || defined(__APPLE__)
result = PTYUnix::open(cols, rows);
#endif
Error err = static_cast<Error>((int)result["error"]);
ERR_FAIL_COND_V(err != OK, err);
#if defined(_WIN32)
result = PTYWin::open(cols, rows);
#endif
fd = result["master"];
pts_name = result["pty"];
Error err = static_cast<Error>((int)result["error"]);
ERR_FAIL_COND_V(err != OK, err);
return OK;
fd = result["master"];
pts_name = result["pty"];
return OK;
}
void PTY::resize(const int p_cols, const int p_rows) {
void PTY::resize(const int p_cols, const int p_rows)
{
cols = p_cols;
rows = p_rows;
#if defined(__linux__) || defined(__APPLE__)
if (fd > -1) {
#if defined(__linux__) || defined(__APPLE__)
if (fd > -1)
{
PTYUnix::resize(fd, cols, rows);
}
#endif
#endif
#if defined(_WIN32)
if (fd > -1)
{
PTYWin::resize(hpc, cols, rows);
}
#endif
}
void PTY::write(const Variant &data) const {
void PTY::write(const Variant &data) const
{
PackedByteArray bytes;
switch (data.get_type())
{
case Variant::STRING:
bytes = ((String)data).to_utf8_buffer();
break;
case Variant::PACKED_BYTE_ARRAY:
bytes = data;
break;
default:
ERR_FAIL_MSG("Data must be a String or PackedByteArray.");
}
switch (data.get_type())
{
case Variant::STRING:
bytes = ((String)data).to_utf8_buffer();
break;
case Variant::PACKED_BYTE_ARRAY:
bytes = data;
break;
default:
ERR_FAIL_MSG("Data must be a String or PackedByteArray.");
}
if (status == STATUS_OPEN) {
#if defined(__linux__) || defined(__APPLE__)
if (status == STATUS_OPEN)
{
uv_buf_t buf;
buf.base = (char *)bytes.ptr();
buf.len = bytes.size();
uv_write_t *req = (uv_write_t *)malloc(sizeof(uv_write_t));
req->data = (void *)buf.base;
uv_write(req, (uv_stream_t *)&pipe, &buf, 1, _write_cb);
uv_run((uv_loop_t*)&loop, UV_RUN_NOWAIT);
#endif
uv_write(req, (uv_stream_t *)&pipe_out, &buf, 1, _write_cb);
uv_run((uv_loop_t *)&loop, UV_RUN_NOWAIT);
}
}
void PTY::_notification(int p_what) {
void PTY::_notification(int p_what)
{
switch (p_what)
{
case NOTIFICATION_INTERNAL_PROCESS:
case NOTIFICATION_INTERNAL_PROCESS:
{
#if defined(__linux__) || defined(__APPLE__)
if (!use_threads) uv_run(&loop, UV_RUN_NOWAIT);
#endif
if (!use_threads)
uv_run(&loop, UV_RUN_NOWAIT);
buffer_write_mutex->lock();
if (buffer.size() > 0) {
if (buffer.size() > 0)
{
emit_signal("data_received", buffer);
buffer.clear();
buffer_cleared->post();
@ -304,46 +357,59 @@ void PTY::_notification(int p_what) {
}
}
void PTY::_thread_func() {
while (!stop_thread.is_set()) {
if (buffer.size() < BUFFER_LIMIT) {
#if defined(__linux__) || defined(__APPLE__)
void PTY::_thread_func()
{
while (!stop_thread.is_set())
{
if (buffer.size() < BUFFER_LIMIT)
{
uv_run(&loop, UV_RUN_ONCE);
#endif
} else {
}
else
{
buffer_cleared->wait();
}
}
}
void PTY::_close() {
if (use_threads) {
if (thread->is_started()) {
void PTY::_close()
{
if (use_threads)
{
if (thread->is_started())
{
stop_thread.set();
#if defined(__linux__) || defined(__APPLE__)
uv_async_send(&async_handle);
#endif
thread->wait_to_finish();
}
}
#if defined(__linux__) || defined(__APPLE__)
if (!uv_is_closing((uv_handle_t *)&pipe)) {
if (!uv_is_closing((uv_handle_t *)&pipe))
{
uv_close((uv_handle_t *)&pipe, _close_cb);
}
if (!uv_is_closing((uv_handle_t *)&async_handle)) {
if (!uv_is_closing((uv_handle_t *)&async_handle))
{
uv_close((uv_handle_t *)&async_handle, _close_cb);
}
if (status == STATUS_OPEN) {
if (status == STATUS_OPEN)
{
uv_run(&loop, UV_RUN_NOWAIT);
uv_loop_close(&loop);
}
if (fd > 0) close(fd);
if (pid > 0) kill(SIGNAL_SIGHUP);
#endif
#ifdef _WIN32
PTYWin::close(hpc, fd, fd_out);
fd_out = -1;
hpc = -1;
#else
if (fd > 0)
close(fd);
if (pid > 0)
kill(IPCSIGNAL_SIGHUP);
#endif
fd = -1;
pid = -1;
@ -352,38 +418,46 @@ void PTY::_close() {
status = STATUS_CLOSED;
}
String PTY::_get_fork_file(const String &file) const {
if (!file.is_empty()) return file;
String PTY::_get_fork_file(const String &file) const
{
if (!file.is_empty())
return file;
String shell_env = OS::get_singleton()->get_environment("SHELL");
if (!shell_env.is_empty()) {
if (!shell_env.is_empty())
{
return shell_env;
}
#if defined(__linux__)
#if defined(__linux__)
return "sh";
#elif defined(__APPLE__)
#elif defined(__APPLE__)
return "zsh";
#elif defined(_WIN32)
#elif defined(_WIN32)
return "cmd.exe";
#else
#else
return "";
#endif
#endif
}
Dictionary PTY::_get_fork_env() const {
if (!use_os_env) return env;
Dictionary PTY::_get_fork_env() const
{
if (!use_os_env)
return env;
#if defined(_PTY_DISABLED)
#if defined(_PTY_DISABLED)
return env;
#endif
#endif
// TODO This might need windows specific adjustment
return env;
Dictionary os_env;
uv_env_item_t *uv_env;
int count;
uv_os_environ(&uv_env, &count);
for (int i = 0; i < count; i++) {
for (int i = 0; i < count; i++)
{
os_env[uv_env[i].name] = uv_env[i].value;
}
uv_os_free_environ(uv_env, count);
@ -394,18 +468,19 @@ Dictionary PTY::_get_fork_env() const {
// Make sure we didn't start our server from inside screen.
// http://web.mit.edu/gnu/doc/html/screen_20.html
os_env.erase("STY");
os_env.erase("WINDOW");
os_env.erase("STY");
os_env.erase("WINDOW");
// Delete some variables that might confuse our terminal.
os_env.erase("WINDOWID");
os_env.erase("TERMCAP");
os_env.erase("COLUMNS");
os_env.erase("LINES");
os_env.erase("WINDOWID");
os_env.erase("TERMCAP");
os_env.erase("COLUMNS");
os_env.erase("LINES");
// Merge in our custom environment.
PackedStringArray keys = PackedStringArray(env.keys());
for (int i = 0; i < keys.size(); i++) {
for (int i = 0; i < keys.size(); i++)
{
String key = keys[i];
os_env[key] = env[key];
}
@ -413,11 +488,13 @@ Dictionary PTY::_get_fork_env() const {
return os_env;
}
PackedStringArray PTY::_parse_env(const Dictionary &env) const {
PackedStringArray PTY::_parse_env(const Dictionary &env) const
{
PackedStringArray parsed_env;
PackedStringArray keys = PackedStringArray(env.keys());
for (int i = 0; i < keys.size(); i++) {
for (int i = 0; i < keys.size(); i++)
{
String key = keys[i];
parsed_env.push_back(key + "=" + String(env[key]));
}
@ -425,37 +502,41 @@ PackedStringArray PTY::_parse_env(const Dictionary &env) const {
return parsed_env;
}
void PTY::_on_exit(int exit_code, int exit_signal) {
void PTY::_on_exit(int exit_code, int exit_signal)
{
call_deferred("emit_signal", "exited", exit_code, exit_signal);
}
#if defined(__linux__) || defined(__APPLE__)
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;
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;
}
void PTY::_read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf) {
void PTY::_read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf)
{
PTY *pty = static_cast<PTY *>(pipe->data);
if (nread < 0) {
switch (nread) {
case UV_EOF:
// Normal after shell exits.
case UV_EIO:
// Can happen when the process exits.
// As long as PTY has caught it, we should be fine.
uv_read_stop(pipe);
pty->status = PTY::Status::STATUS_CLOSED;
if (nread < 0)
{
switch (nread)
{
case UV_EOF:
// Normal after shell exits.
case UV_EIO:
// Can happen when the process exits.
// As long as PTY has caught it, we should be fine.
uv_read_stop(pipe);
pty->status = PTY::Status::STATUS_CLOSED;
return;
default:
pty->status = PTY::Status::STATUS_ERROR;
}
return;
default:
pty->status = PTY::Status::STATUS_ERROR;
}
return;
}
if (nread > 0) {
if (nread > 0)
{
MutexLock lock(*pty->buffer_write_mutex.ptr());
int old_size = pty->buffer.size();
@ -468,14 +549,11 @@ void PTY::_read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf) {
}
}
Error PTY::_pipe_open(const int fd) {
Error PTY::_pipe_open(const int fd, uv_pipe_t *pipe)
{
ERR_FAIL_COND_V_MSG(fd < 0, FAILED, "File descriptor must be a non-negative integer value.");
ERR_FAIL_UV_ERR(uv_pipe_open(&pipe, fd));
ERR_FAIL_UV_ERR(uv_stream_set_blocking((uv_stream_t *)&pipe, false));
ERR_FAIL_UV_ERR(uv_read_start((uv_stream_t *)&pipe, _alloc_buffer, _read_cb));
ERR_FAIL_UV_ERR(uv_pipe_open(pipe, fd));
ERR_FAIL_UV_ERR(uv_stream_set_blocking((uv_stream_t *)pipe, false));
return OK;
}
#endif
}

View file

@ -19,22 +19,24 @@ namespace godot
GDCLASS(PTY, Node)
public:
enum Signal {
SIGNAL_SIGHUP = 1,
SIGNAL_SIGINT = 2,
SIGNAL_SIGQUIT = 3,
SIGNAL_SIGILL = 4,
SIGNAL_SIGTRAP = 5,
SIGNAL_SIGABRT = 6,
SIGNAL_SIGFPE = 8,
SIGNAL_SIGKILL = 9,
SIGNAL_SIGSEGV = 11,
SIGNAL_SIGPIPE = 13,
SIGNAL_SIGALRM = 14,
SIGNAL_SIGTERM = 15,
enum IPCSignal
{
IPCSIGNAL_SIGHUP = 1,
IPCSIGNAL_SIGINT = 2,
IPCSIGNAL_SIGQUIT = 3,
IPCSIGNAL_SIGILL = 4,
IPCSIGNAL_SIGTRAP = 5,
IPCSIGNAL_SIGABRT = 6,
IPCSIGNAL_SIGFPE = 8,
IPCSIGNAL_SIGKILL = 9,
IPCSIGNAL_SIGSEGV = 11,
IPCSIGNAL_SIGPIPE = 13,
IPCSIGNAL_SIGALRM = 14,
IPCSIGNAL_SIGTERM = 15,
};
enum Status {
enum Status
{
STATUS_CLOSED,
STATUS_OPEN,
STATUS_PAUSED,
@ -65,7 +67,7 @@ namespace godot
String get_pts_name() const;
Error fork(const String &file = "", const PackedStringArray &args = PackedStringArray(), const String &cwd = ".", const int cols = 80, const int rows = 24);
void kill(const int signum = Signal::SIGNAL_SIGHUP);
void kill(const int signum = IPCSignal::IPCSIGNAL_SIGHUP);
Error open(const int cols = 80, const int rows = 24);
void resize(const int cols, const int rows);
void resizev(const Vector2i &size) { resize(size.x, size.y); };
@ -79,6 +81,10 @@ namespace godot
private:
int pid = -1;
int fd = -1;
#ifdef _WIN32
int fd_out = -1;
int64_t hpc = -1; // pseudoconsole handle
#endif
unsigned int cols = 80;
unsigned int rows = 24;
@ -106,15 +112,17 @@ namespace godot
bool use_threads;
void _thread_func();
#if defined(__linux__) || defined(__APPLE__)
uv_loop_t loop;
uv_pipe_t pipe;
Error _pipe_open(const int fd);
#endif
#ifdef _WIN32
uv_pipe_t pipe_out;
#endif
Error _pipe_open(const int fd, uv_pipe_t *pipe);
static void _read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf);
static Error uv_err_to_godot_err(const int uv_err);
};
} // namespace godot
VARIANT_ENUM_CAST(PTY::Signal);
VARIANT_ENUM_CAST(PTY::IPCSignal);

View file

@ -55,10 +55,10 @@
/* Some platforms name VWERASE and VDISCARD differently */
#if !defined(VWERASE) && defined(VWERSE)
#define VWERASE VWERSE
#define VWERASE VWERSE
#endif
#if !defined(VDISCARD) && defined(VDISCRD)
#define VDISCARD VDISCRD
#define VDISCARD VDISCRD
#endif
/* for pty_getproc */
@ -82,7 +82,7 @@
/* macOS 10.14 back does not define this constant */
#ifndef POSIX_SPAWN_SETSID
#define POSIX_SPAWN_SETSID 1024
#define POSIX_SPAWN_SETSID 1024
#endif
/* environ for execvpe */
@ -91,29 +91,32 @@ extern char **environ;
#endif
#if defined(__APPLE__)
extern "C" {
// Changes the current thread's directory to a path or directory file
// descriptor. libpthread only exposes a syscall wrapper starting in
// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes,
// the syscall is issued directly.
int pthread_chdir_np(const char* dir) API_AVAILABLE(macosx(10.12));
int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12));
extern "C"
{
// Changes the current thread's directory to a path or directory file
// descriptor. libpthread only exposes a syscall wrapper starting in
// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes,
// the syscall is issued directly.
int pthread_chdir_np(const char *dir) API_AVAILABLE(macosx(10.12));
int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12));
}
#define HANDLE_EINTR(x) ({ \
int eintr_wrapper_counter = 0; \
decltype(x) eintr_wrapper_result; \
do { \
eintr_wrapper_result = (x); \
#define HANDLE_EINTR(x) ({ \
int eintr_wrapper_counter = 0; \
decltype(x) eintr_wrapper_result; \
do \
{ \
eintr_wrapper_result = (x); \
} while (eintr_wrapper_result == -1 && errno == EINTR && \
eintr_wrapper_counter++ < 100); \
eintr_wrapper_result; \
eintr_wrapper_counter++ < 100); \
eintr_wrapper_result; \
})
#endif
using namespace godot;
static void await_exit(Callable cb, pid_t pid) {
static void await_exit(Callable cb, pid_t pid)
{
int ret;
int stat_loc;
#if defined(__APPLE__)
@ -123,8 +126,10 @@ static void await_exit(Callable cb, pid_t pid) {
struct kevent change = {0};
EV_SET(&change, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL);
ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL));
if (ret == -1) {
if (errno == ESRCH) {
if (ret == -1)
{
if (errno == ESRCH)
{
// At this point, one of the following has occurred:
// 1. The process has died but has not yet been reaped.
// 2. The process has died and has already been reaped.
@ -132,19 +137,25 @@ static void await_exit(Callable cb, pid_t pid) {
// kqueueable, but it may not be waitable yet either. Mark calls
// this case the "zombie death race".
ret = HANDLE_EINTR(waitpid(pid, &stat_loc, WNOHANG));
if (ret == 0) {
if (ret == 0)
{
ret = kill(pid, SIGKILL);
if (ret != -1) {
if (ret != -1)
{
HANDLE_EINTR(waitpid(pid, &stat_loc, 0));
}
}
}
} else {
}
else
{
struct kevent event = {0};
ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL));
if (ret == 1) {
if (ret == 1)
{
if ((event.fflags & NOTE_EXIT) &&
(event.ident == static_cast<uintptr_t>(pid))) {
(event.ident == static_cast<uintptr_t>(pid)))
{
// The process is dead or dying. This won't block for long, if at
// all.
HANDLE_EINTR(waitpid(pid, &stat_loc, 0));
@ -152,16 +163,22 @@ static void await_exit(Callable cb, pid_t pid) {
}
}
#else
while (true) {
while (true)
{
errno = 0;
if ((ret = waitpid(pid, &stat_loc, 0)) != pid) {
if (ret == -1 && errno == EINTR) {
if ((ret = waitpid(pid, &stat_loc, 0)) != pid)
{
if (ret == -1 && errno == EINTR)
{
continue;
}
if (ret == -1 && errno == ECHILD) {
if (ret == -1 && errno == ECHILD)
{
// waitpid is already handled elsewhere.
;
} else {
}
else
{
assert(false);
}
}
@ -169,22 +186,26 @@ static void await_exit(Callable cb, pid_t pid) {
}
#endif
int exit_code = 0, signal_code = 0;
if (WIFEXITED(stat_loc)) {
if (WIFEXITED(stat_loc))
{
exit_code = WEXITSTATUS(stat_loc); // errno?
}
if (WIFSIGNALED(stat_loc)) {
if (WIFSIGNALED(stat_loc))
{
signal_code = WTERMSIG(stat_loc);
}
cb.call_deferred(exit_code, signal_code);
}
static void on_exit(int exit_code, int signal_code, Callable cb, Thread *thread) {
static void on_exit(int exit_code, int signal_code, Callable cb, Thread *thread)
{
cb.call(exit_code, signal_code);
thread->wait_to_finish();
}
void setup_exit_callback(Callable cb, pid_t pid) {
void setup_exit_callback(Callable cb, pid_t pid)
{
Thread *thread = memnew(Thread);
Callable exit_func = create_custom_callable_static_function_pointer(&on_exit).bind(cb, thread);
@ -210,18 +231,20 @@ pty_getproc(int, char *);
#if defined(__APPLE__) || defined(__OpenBSD__)
static void
pty_posix_spawn(char** argv, char** env,
pty_posix_spawn(char **argv, char **env,
const struct termios *termp,
const struct winsize *winp,
int* master,
pid_t* pid,
int* err);
int *master,
pid_t *pid,
int *err);
#endif
struct DelBuf {
struct DelBuf
{
int len;
DelBuf(int len) : len(len) {}
void operator()(char **p) {
void operator()(char **p)
{
if (p == nullptr)
return;
for (int i = 0; i < len; i++)
@ -231,18 +254,18 @@ struct DelBuf {
};
Dictionary PTYUnix::fork(
const String &p_file,
const PackedStringArray &p_args,
const PackedStringArray &p_env,
const String &p_cwd,
const int &p_cols,
const int &p_rows,
const int &p_uid,
const int &p_gid,
const bool &p_utf8,
const String &p_helper_path,
const Callable &p_on_exit
) {
const String &p_file,
const PackedStringArray &p_args,
const PackedStringArray &p_env,
const String &p_cwd,
const int &p_cols,
const int &p_rows,
const int &p_uid,
const int &p_gid,
const bool &p_utf8,
const String &p_helper_path,
const Callable &p_on_exit)
{
Dictionary result;
result["error"] = FAILED;
@ -258,7 +281,8 @@ Dictionary PTYUnix::fork(
std::unique_ptr<char *, DelBuf> env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1));
char **env = env_unique_ptr.get();
env[envc] = NULL;
for (int i = 0; i < envc; i++) {
for (int i = 0; i < envc; i++)
{
std::string pair = env_[i].utf8().get_data();
env[i] = strdup(pair.c_str());
}
@ -276,14 +300,15 @@ Dictionary PTYUnix::fork(
#if !defined(__APPLE__)
// uid / gid
int uid = p_uid;
int gid = p_gid;
int gid = p_gid;
#endif
// termios
struct termios t = termios();
struct termios *term = &t;
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
if (p_utf8) {
if (p_utf8)
{
#if defined(IUTF8)
term->c_iflag |= IUTF8;
#endif
@ -309,10 +334,10 @@ Dictionary PTYUnix::fork(
term->c_cc[VMIN] = 1;
term->c_cc[VTIME] = 0;
#if (__APPLE__)
#if (__APPLE__)
term->c_cc[VDSUSP] = 25;
term->c_cc[VSTATUS] = 20;
#endif
#endif
cfsetispeed(term, B38400);
cfsetospeed(term, B38400);
@ -331,27 +356,31 @@ Dictionary PTYUnix::fork(
argv[1] = strdup(cwd_.c_str());
argv[2] = strdup(file.c_str());
argv[argl - 1] = NULL;
for (int i = 0; i < argc; i++) {
for (int i = 0; i < argc; i++)
{
std::string arg = argv_[i].utf8().get_data();
argv[i + 3] = strdup(arg.c_str());
}
int err = -1;
pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err);
if (err != 0) {
if (err != 0)
{
ERR_FAIL_V_MSG(result, "posix_spawnp failed with error: " + String(strerror(err)));
}
if (pty_nonblock(master) == -1) {
if (pty_nonblock(master) == -1)
{
ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking.");
}
#else
int argc = argv_.size();
int argl = argc + 2;
std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl));
char** argv = argv_unique_ptr.get();
char **argv = argv_unique_ptr.get();
argv[0] = strdup(file.c_str());
argv[argl - 1] = NULL;
for (int i = 0; i < argc; i++) {
for (int i = 0; i < argc; i++)
{
std::string arg = argv_[i].utf8().get_data();
argv[i + 1] = strdup(arg.c_str());
}
@ -365,14 +394,16 @@ Dictionary PTYUnix::fork(
sigfillset(&newmask);
pthread_sigmask(SIG_SETMASK, &newmask, &oldmask);
pid = forkpty(&master, nullptr, static_cast<termios*>(term), static_cast<winsize*>(&winp));
pid = forkpty(&master, nullptr, static_cast<termios *>(term), static_cast<winsize *>(&winp));
if (!pid) {
if (!pid)
{
// remove all signal handler from child
sig_action.sa_handler = SIG_DFL;
sig_action.sa_flags = 0;
sigemptyset(&sig_action.sa_mask);
for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1
for (int i = 0; i < NSIG; i++)
{ // NSIG is a macro for all signals + 1
sigaction(i, &sig_action, NULL);
}
}
@ -380,44 +411,51 @@ Dictionary PTYUnix::fork(
// re-enable signals
pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
switch (pid) {
case -1:
ERR_FAIL_V_MSG(result, "forkpty(3) failed.");
case 0:
if (strlen(cwd_.c_str())) {
if (chdir(cwd_.c_str()) == -1) {
perror("chdir(2) failed.");
_exit(1);
}
}
if (uid != -1 && gid != -1) {
if (setgid(gid) == -1) {
perror("setgid(2) failed.");
_exit(1);
}
if (setuid(uid) == -1) {
perror("setuid(2) failed.");
_exit(1);
}
}
switch (pid)
{
case -1:
ERR_FAIL_V_MSG(result, "forkpty(3) failed.");
case 0:
if (strlen(cwd_.c_str()))
{
if (chdir(cwd_.c_str()) == -1)
{
char **old = environ;
environ = env;
execvp(argv[0], argv);
environ = old;
perror("execvp(3) failed.");
perror("chdir(2) failed.");
_exit(1);
}
default:
if (pty_nonblock(master) == -1) {
ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking.");
}
if (uid != -1 && gid != -1)
{
if (setgid(gid) == -1)
{
perror("setgid(2) failed.");
_exit(1);
}
if (setuid(uid) == -1)
{
perror("setuid(2) failed.");
_exit(1);
}
}
{
char **old = environ;
environ = env;
execvp(argv[0], argv);
environ = old;
perror("execvp(3) failed.");
_exit(1);
}
default:
if (pty_nonblock(master) == -1)
{
ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking.");
}
}
#endif
result["fd"] = master;
result["fd"] = master;
result["pid"] = pid;
result["pty"] = ptsname(master);
@ -430,9 +468,9 @@ Dictionary PTYUnix::fork(
}
Dictionary PTYUnix::open(
const int &p_cols,
const int &p_rows
) {
const int &p_cols,
const int &p_rows)
{
Dictionary result;
result["error"] = FAILED;
@ -445,51 +483,56 @@ Dictionary PTYUnix::open(
// pty
int master, slave;
int ret = openpty(&master, &slave, nullptr, NULL, static_cast<winsize*>(&winp));
int ret = openpty(&master, &slave, nullptr, NULL, static_cast<winsize *>(&winp));
if (ret == -1) {
if (ret == -1)
{
ERR_FAIL_V_MSG(result, "openpty(3) failed.");
}
if (pty_nonblock(master) == -1) {
if (pty_nonblock(master) == -1)
{
ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking.");
}
if (pty_nonblock(slave) == -1) {
if (pty_nonblock(slave) == -1)
{
ERR_FAIL_V_MSG(result, "Could not set slave fd to nonblocking.");
}
result["master"] = master;
result["slave"] = slave;
result["pty"] = ptsname(master);
result["pty"] = ptsname(master);
result["error"] = OK;
return result;
}
void PTYUnix::resize(
const int &p_fd,
const int &p_cols,
const int &p_rows
) {
const int &p_fd,
const int &p_cols,
const int &p_rows)
{
int fd = p_fd;
struct winsize winp;
winp.ws_col = p_cols;
winp.ws_row = p_rows;
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:
ERR_FAIL_MSG("ioctl(2) failed, EBADF");
case EFAULT:
ERR_FAIL_MSG("ioctl(2) failed, EFAULT");
case EINVAL:
ERR_FAIL_MSG("ioctl(2) failed, EINVAL");
case ENOTTY:
ERR_FAIL_MSG("ioctl(2) failed, ENOTTY");
if (ioctl(fd, TIOCSWINSZ, &winp) == -1)
{
switch (errno)
{
case EBADF:
ERR_FAIL_MSG("ioctl(2) failed, EBADF");
case EFAULT:
ERR_FAIL_MSG("ioctl(2) failed, EFAULT");
case EINVAL:
ERR_FAIL_MSG("ioctl(2) failed, EINVAL");
case ENOTTY:
ERR_FAIL_MSG("ioctl(2) failed, ENOTTY");
}
ERR_FAIL_MSG("ioctl(2) failed");
}
@ -501,14 +544,14 @@ void PTYUnix::resize(
* Foreground Process Name
*/
String process(
const int &p_fd,
const String &p_tty
) {
const int &p_fd,
const String &p_tty)
{
#if defined(__APPLE__)
int fd = p_fd;
char *name = pty_getproc(fd);
#else
int fd = p_fd;
int fd = p_fd;
std::string tty_ = p_tty.utf8().get_data();
char *tty = strdup(tty_.c_str());
@ -516,7 +559,8 @@ String process(
free(tty);
#endif
if (name == NULL) {
if (name == NULL)
{
return "";
}
@ -530,9 +574,11 @@ String process(
*/
static int
pty_nonblock(int fd) {
pty_nonblock(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
if (flags == -1)
return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
@ -561,7 +607,8 @@ pty_nonblock(int fd) {
#if defined(__linux__)
static char *
pty_getproc(int fd, char *tty) {
pty_getproc(int fd, char *tty)
{
FILE *f;
char *path, *buf;
size_t len;
@ -569,14 +616,17 @@ pty_getproc(int fd, char *tty) {
pid_t pgrp;
int r;
if ((pgrp = tcgetpgrp(fd)) == -1) {
if ((pgrp = tcgetpgrp(fd)) == -1)
{
return NULL;
}
r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp);
if (r == -1 || path == NULL) return NULL;
if (r == -1 || path == NULL)
return NULL;
if ((f = fopen(path, "r")) == NULL) {
if ((f = fopen(path, "r")) == NULL)
{
free(path);
return NULL;
}
@ -585,14 +635,18 @@ pty_getproc(int fd, char *tty) {
len = 0;
buf = NULL;
while ((ch = fgetc(f)) != EOF) {
if (ch == '\0') break;
while ((ch = fgetc(f)) != EOF)
{
if (ch == '\0')
break;
buf = (char *)realloc(buf, len + 2);
if (buf == NULL) return NULL;
if (buf == NULL)
return NULL;
buf[len++] = ch;
}
if (buf != NULL) {
if (buf != NULL)
{
buf[len] = '\0';
}
@ -603,21 +657,25 @@ pty_getproc(int fd, char *tty) {
#elif defined(__APPLE__)
static char *
pty_getproc(int fd) {
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 };
pty_getproc(int fd)
{
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, 0};
size_t size;
struct kinfo_proc kp;
if ((mib[3] = tcgetpgrp(fd)) == -1) {
if ((mib[3] = tcgetpgrp(fd)) == -1)
{
return NULL;
}
size = sizeof kp;
if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) {
if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1)
{
return NULL;
}
if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') {
if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0')
{
return NULL;
}
@ -627,7 +685,8 @@ pty_getproc(int fd) {
#else
static char *
pty_getproc(int fd, char *tty) {
pty_getproc(int fd, char *tty)
{
return NULL;
}
@ -635,16 +694,18 @@ pty_getproc(int fd, char *tty) {
#if defined(__APPLE__)
static void
pty_posix_spawn(char** argv, char** env,
pty_posix_spawn(char **argv, char **env,
const struct termios *termp,
const struct winsize *winp,
int* master,
pid_t* pid,
int* err) {
int *master,
pid_t *pid,
int *err)
{
int low_fds[3];
size_t count = 0;
for (; count < 3; count++) {
for (; count < 3; count++)
{
low_fds[count] = posix_openpt(O_RDWR);
if (low_fds[count] >= STDERR_FILENO)
break;
@ -655,12 +716,14 @@ pty_posix_spawn(char** argv, char** env,
POSIX_SPAWN_SETSIGMASK |
POSIX_SPAWN_SETSID;
*master = posix_openpt(O_RDWR);
if (*master == -1) {
if (*master == -1)
{
return;
}
int res = grantpt(*master) || unlockpt(*master);
if (res == -1) {
if (res == -1)
{
return;
}
@ -668,25 +731,31 @@ pty_posix_spawn(char** argv, char** env,
int slave;
char slave_pty_name[128];
res = ioctl(*master, TIOCPTYGNAME, slave_pty_name);
if (res == -1) {
if (res == -1)
{
return;
}
slave = open(slave_pty_name, O_RDWR | O_NOCTTY);
if (slave == -1) {
if (slave == -1)
{
return;
}
if (termp) {
if (termp)
{
res = tcsetattr(slave, TCSANOW, termp);
if (res == -1) {
if (res == -1)
{
return;
};
}
if (winp) {
if (winp)
{
res = ioctl(slave, TIOCSWINSZ, winp);
if (res == -1) {
if (res == -1)
{
return;
}
}
@ -702,7 +771,8 @@ pty_posix_spawn(char** argv, char** env,
posix_spawnattr_t attrs;
posix_spawnattr_init(&attrs);
*err = posix_spawnattr_setflags(&attrs, flags);
if (*err != 0) {
if (*err != 0)
{
goto done;
}
@ -710,25 +780,29 @@ pty_posix_spawn(char** argv, char** env,
/* Reset all signal the child to their default behavior */
sigfillset(&signal_set);
*err = posix_spawnattr_setsigdefault(&attrs, &signal_set);
if (*err != 0) {
if (*err != 0)
{
goto done;
}
/* Reset the signal mask for all signals */
sigemptyset(&signal_set);
*err = posix_spawnattr_setsigmask(&attrs, &signal_set);
if (*err != 0) {
if (*err != 0)
{
goto done;
}
do {
do
{
*err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
} while (*err == EINTR);
done:
posix_spawn_file_actions_destroy(&acts);
posix_spawnattr_destroy(&attrs);
for (; count > 0; count--) {
for (; count > 0; count--)
{
close(low_fds[count]);
}
}

View file

@ -12,28 +12,25 @@ namespace godot
{
public:
static Dictionary fork(
const String &p_file,
const PackedStringArray &p_args,
const PackedStringArray &p_env,
const String &p_cwd,
const int &p_cols,
const int &p_rows,
const int &p_uid,
const int &p_gid,
const bool &p_utf8,
const String &p_helper_path,
const Callable &p_on_exit
);
const String &p_file,
const PackedStringArray &p_args,
const PackedStringArray &p_env,
const String &p_cwd,
const int &p_cols,
const int &p_rows,
const int &p_uid,
const int &p_gid,
const bool &p_utf8,
const String &p_helper_path,
const Callable &p_on_exit);
static Dictionary open(
const int &p_cols,
const int &p_rows
);
const int &p_cols,
const int &p_rows);
static void resize(
const int &p_fd,
const int &p_cols,
const int &p_rows
);
const int &p_fd,
const int &p_cols,
const int &p_rows);
};
} // namespace godot

View file

@ -0,0 +1,443 @@
/**
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
* Copyright (c) 2017, Daniel Imms (MIT License)
* Copyright (c) 2024, Leroy Hopson (MIT License)
* Copyright (c) 2024, Alexander Treml (MIT License)
*
* SPDX-License-Identifier: MIT
*
* pty.cc:
* This file is responsible for starting processes
* with pseudo-terminal file descriptors.
*
* See:
* conpty (https://github.com/microsoft/terminal/blob/main/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp)
*/
/**
* Includes
*/
#if defined(_WIN32) && !defined(_PTY_DISABLED)
#include "pty_win.h"
#include <godot_cpp/classes/thread.hpp>
#include <godot_cpp/variant/callable_method_pointer.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <thread>
#include <Windows.h>
#include <io.h>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
HRESULT CreatePseudoConsoleAndPipes(COORD, HPCON *, int &, int &);
HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX *, HPCON);
void setup_exit_callback(Callable, int64_t);
struct DelBuf
{
int len;
DelBuf(int len) : len(len) {}
void operator()(char **p)
{
if (p == nullptr)
return;
for (int i = 0; i < len; i++)
free(p[i]);
delete[] p;
}
};
Dictionary PTYWin::fork(
const String &p_file,
const PackedStringArray &p_args,
const PackedStringArray &p_env,
const String &p_cwd,
const int &p_cols,
const int &p_rows,
const int &p_uid,
const int &p_gid,
const bool &p_utf8,
const String &p_helper_path,
const Callable &p_on_exit)
{
Dictionary result;
// file
std::string file = p_file.utf8().get_data();
// args
PackedStringArray argv_ = p_args;
// env
PackedStringArray env_ = p_env;
int envc = env_.size();
std::unique_ptr<char *, DelBuf> env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1));
char **env = env_unique_ptr.get();
env[envc] = NULL;
for (int i = 0; i < envc; i++)
{
std::string pair = env_[i].utf8().get_data();
env[i] = strdup(pair.c_str());
}
std::string cwd_ = p_cwd.utf8().get_data();
// Determine required size of Pseudo Console
COORD winp{};
winp.X = p_cols;
winp.Y = p_rows;
int uid = p_uid;
int gid = p_gid;
std::string helper_path = p_helper_path.utf8().get_data();
// Build argv
int argc = argv_.size();
int argl = argc + 2;
std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl));
char **argv = argv_unique_ptr.get();
argv[0] = strdup(file.c_str());
argv[argl - 1] = NULL;
for (int i = 0; i < argc; i++)
{
std::string arg = argv_[i].utf8().get_data();
argv[i + 1] = strdup(arg.c_str());
}
// Aggregate argv into command string
std::string cmd;
for (int i = 0; i < argl - 1; i++)
{
if (i > 0)
cmd += " ";
cmd += argv[i];
}
LPSTR lpcmd = const_cast<char *>(cmd.c_str());
HRESULT ret{E_UNEXPECTED};
HPCON hPC{INVALID_HANDLE_VALUE};
// Create the Pseudo Console and pipes to it
int fd{-1};
int fd_out{-1};
ret = CreatePseudoConsoleAndPipes(winp, &hPC, fd, fd_out);
if (S_OK != ret)
{
result["error"] = ERR_CANT_FORK;
return result;
}
// Initialize the necessary startup info struct
STARTUPINFOEX startupInfo{};
if (S_OK != InitializeStartupInfoAttachedToPseudoConsole(&startupInfo, hPC))
{
result["error"] = ERR_UNCONFIGURED;
return result;
}
PROCESS_INFORMATION pi{};
ret = CreateProcess(
NULL, // No module name - use Command Line
lpcmd, // Command Line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Inherit handles
EXTENDED_STARTUPINFO_PRESENT, // Creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&startupInfo.StartupInfo, // Pointer to STARTUPINFO
&pi) // Pointer to PROCESS_INFORMATION
? S_OK
: GetLastError();
result["fd"] = fd;
result["fd_out"] = fd_out;
result["pid"] = static_cast<int64_t>(pi.dwProcessId);
// TODO there is no equivalent to ptsname under windows. If this value is needed to support certain use cases, a workaround needs to be found
result["pty"] = "ConPTY";
result["hpc"] = reinterpret_cast<int64_t>(hPC);
// Set up process exit callback.
Callable cb = p_on_exit;
setup_exit_callback(cb, pi.dwProcessId);
result["error"] = OK;
return result;
}
Dictionary PTYWin::open(
const int &p_cols,
const int &p_rows)
{
HRESULT ret{E_UNEXPECTED};
Dictionary result;
result["error"] = FAILED;
// TODO since the ConPTY API differs in many aspects from the unix PTY, this would need to look somewhat different
// Not currently supported because unsure about how the interface should look, and what the use cases are.
godot::UtilityFunctions::printerr("Not supported under windows!");
return result;
}
void PTYWin::close(uint64_t hpc, int fd, int fd_out)
{
ClosePseudoConsole(reinterpret_cast<HPCON>(hpc));
// TODO(ast) this causes assertion errors. Might be related to this: https://stackoverflow.com/questions/5984144/assertion-error-in-crt-calling-osfile-in-vs-2008
// TODO(ast) leave for now and return when the linking issues are figured out (see SConstruct file)
// // Drain remaining data
// char drain_buf[4096];
// int bytes_read = 0;
// // Check if fd is valid before reading
// if (fd >= 0) {
// do {
// bytes_read = read(fd, drain_buf, sizeof(drain_buf));
// } while (bytes_read > 0);
// godot::UtilityFunctions::print("Draining!");
// } else {
// godot::UtilityFunctions::print("fd is not valid, skipping drain.");
// }
// // Optionally close file descriptors if they are valid
// if (fd >= 0) _close(fd);
// if (fd_out >= 0) _close(fd_out);
}
// TODO(ast) repeatedly resizing sometimes crashes the terminal
void PTYWin::resize(
int64_t p_hpc,
const int &p_cols,
const int &p_rows)
{
COORD winp{};
winp.X = p_cols;
winp.Y = p_rows;
HPCON hpc = reinterpret_cast<HPCON>(p_hpc);
HRESULT hr = ResizePseudoConsole(hpc, winp);
if (FAILED(hr))
{
DWORD err = GetLastError();
godot::UtilityFunctions::printerr("ResizePseudoConsole failed. HRESULT: ", String::num_int64(hr), ", GetLastError: ", String::num_int64(err));
}
}
/**
* Derived from https://github.com/microsoft/terminal/blob/main/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp
* Copyright (c) Microsoft Corporation (MIT License)
*
* SPDX-License-Identifier: MIT
* **/
HRESULT CreatePseudoConsoleAndPipes(COORD size, HPCON *phPC, int &pFd, int &pFd_out)
{
HRESULT hr{E_UNEXPECTED};
// Generate unique pipe names using process id and tick count
DWORD pid = GetCurrentProcessId();
DWORD ticks = GetTickCount();
wchar_t pipe_name_in[128];
wchar_t pipe_name_out[128];
swprintf(pipe_name_in, 128, L"\\\\.\\pipe\\godot_conpty_pipe_%lu_%lu_in", (unsigned long)pid, (unsigned long)ticks);
swprintf(pipe_name_out, 128, L"\\\\.\\pipe\\godot_conpty_pipe_%lu_%lu_out", (unsigned long)pid, (unsigned long)ticks);
// Create bidirectional named pipes. Only one direction will be used on each.
// This is done to achieve compatibility with pty.cpp since uv_pipe_open only works on named pipes
HANDLE hPipeIn = INVALID_HANDLE_VALUE;
HANDLE hPipeOut = INVALID_HANDLE_VALUE;
HANDLE hPipeInFile = INVALID_HANDLE_VALUE;
HANDLE hPipeOutFile = INVALID_HANDLE_VALUE;
bool hPC_created = false;
hPipeIn = CreateNamedPipeW(
pipe_name_in,
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
1, // Max instances
4096, 4096, // Out/in buffer size
0, // Default timeout
NULL // Default security
);
if (hPipeIn == INVALID_HANDLE_VALUE)
{
hr = HRESULT_FROM_WIN32(GetLastError());
goto cleanup;
}
hPipeOut = CreateNamedPipeW(
pipe_name_out,
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
1, 4096, 4096, 0, NULL);
if (hPipeOut == INVALID_HANDLE_VALUE)
{
hr = HRESULT_FROM_WIN32(GetLastError());
goto cleanup;
}
// Connect to the named pipes
hPipeInFile = CreateFileW(
pipe_name_in,
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
if (hPipeInFile == INVALID_HANDLE_VALUE)
{
hr = HRESULT_FROM_WIN32(GetLastError());
goto cleanup;
}
hPipeOutFile = CreateFileW(
pipe_name_out,
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
if (hPipeOutFile == INVALID_HANDLE_VALUE)
{
hr = HRESULT_FROM_WIN32(GetLastError());
goto cleanup;
}
// Create the Pseudo Console
hr = CreatePseudoConsole(size, hPipeOut, hPipeIn, 0, phPC);
if (FAILED(hr))
{
goto cleanup;
}
hPC_created = true;
// Convert HANDLE to C file descriptor
int fd = _open_osfhandle((intptr_t)hPipeInFile, 0);
if (fd == -1)
{
hr = HRESULT_FROM_WIN32(GetLastError());
goto cleanup;
}
pFd = fd;
hPipeInFile = INVALID_HANDLE_VALUE; // Ownership transferred to fd
fd = _open_osfhandle((intptr_t)hPipeOutFile, 0);
if (fd == -1)
{
hr = HRESULT_FROM_WIN32(GetLastError());
goto cleanup;
}
pFd_out = fd;
hPipeOutFile = INVALID_HANDLE_VALUE; // Ownership transferred
hr = S_OK;
cleanup:
// Only close handles that are still valid and not transferred
if (hPipeIn != INVALID_HANDLE_VALUE)
CloseHandle(hPipeIn);
if (hPipeOut != INVALID_HANDLE_VALUE)
CloseHandle(hPipeOut);
if (hPipeInFile != INVALID_HANDLE_VALUE)
CloseHandle(hPipeInFile);
if (hPipeOutFile != INVALID_HANDLE_VALUE)
CloseHandle(hPipeOutFile);
if (FAILED(hr) && hPC_created && phPC)
ClosePseudoConsole(*phPC);
return hr;
}
/**
* Copied from https://github.com/microsoft/terminal/blob/main/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp
* Copyright (c) Microsoft Corporation (MIT License)
*
* SPDX-License-Identifier: MIT
* **/
// Initializes the specified startup info struct with the required properties and
// updates its thread attribute list with the specified ConPTY handle
HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX *pStartupInfo, HPCON hPC)
{
HRESULT hr{E_UNEXPECTED};
if (pStartupInfo)
{
size_t attrListSize{};
pStartupInfo->StartupInfo.cb = sizeof(STARTUPINFOEX);
// Get the size of the thread attribute list.
InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize);
// Allocate a thread attribute list of the correct size
pStartupInfo->lpAttributeList =
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(malloc(attrListSize));
// Initialize thread attribute list
if (pStartupInfo->lpAttributeList && InitializeProcThreadAttributeList(pStartupInfo->lpAttributeList, 1, 0, &attrListSize))
{
// Set Pseudo Console attribute
hr = UpdateProcThreadAttribute(
pStartupInfo->lpAttributeList,
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
hPC,
sizeof(HPCON),
NULL,
NULL)
? S_OK
: HRESULT_FROM_WIN32(GetLastError());
}
else
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
}
return hr;
}
static void await_exit(Callable cb, int64_t pid)
{
DWORD exit_code = 0;
int signal_code = 0;
HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, static_cast<DWORD>(pid));
if (hProcess != NULL)
{
WaitForSingleObject(hProcess, INFINITE);
GetExitCodeProcess(hProcess, &exit_code);
CloseHandle(hProcess);
cb.call_deferred(static_cast<int>(exit_code), signal_code);
}
else
{
godot::UtilityFunctions::printerr("Could not open process!");
}
}
static void on_exit(int exit_code, int signal_code, Callable cb, Thread *thread)
{
cb.call(exit_code, signal_code);
thread->wait_to_finish();
}
void setup_exit_callback(Callable cb, int64_t pid)
{
Thread *thread = memnew(Thread);
Callable exit_func = create_custom_callable_static_function_pointer(&on_exit).bind(cb, thread);
Callable thread_func = create_custom_callable_static_function_pointer(&await_exit).bind(exit_func, pid);
thread->start(thread_func);
}
#endif

View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
// SPDX-License-Identifier: MIT
#pragma once
#include <godot_cpp/variant/callable.hpp>
#include <godot_cpp/variant/dictionary.hpp>
namespace godot
{
class PTYWin
{
public:
static Dictionary fork(
const String &p_file,
const PackedStringArray &p_args,
const PackedStringArray &p_env,
const String &p_cwd,
const int &p_cols,
const int &p_rows,
const int &p_uid,
const int &p_gid,
const bool &p_utf8,
const String &p_helper_path,
const Callable &p_on_exit);
static Dictionary open(
const int &p_cols,
const int &p_rows);
static void resize(
int64_t p_hpc,
const int &p_cols,
const int &p_rows);
static void close(
uint64_t hpc,
int fd,
int fd_out);
};
} // namespace godot

View file

@ -34,9 +34,9 @@ using namespace godot;
void Terminal::_bind_methods()
{
ADD_SIGNAL(MethodInfo("data_sent", PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("key_pressed", PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"), PropertyInfo(Variant::OBJECT, "event")));
ADD_SIGNAL(MethodInfo("size_changed", PropertyInfo(Variant::VECTOR2I, "new_size")));
ADD_SIGNAL(MethodInfo("data_sent", PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("key_pressed", PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"), PropertyInfo(Variant::OBJECT, "event")));
ADD_SIGNAL(MethodInfo("size_changed", PropertyInfo(Variant::VECTOR2I, "new_size")));
ClassDB::bind_method(D_METHOD("get_cols"), &Terminal::get_cols);
ClassDB::bind_method(D_METHOD("get_rows"), &Terminal::get_rows);
@ -146,11 +146,13 @@ int Terminal::get_rows() const
return rows;
}
Vector2i Terminal::get_cursor_pos() const {
Vector2i Terminal::get_cursor_pos() const
{
return Vector2i(tsm_screen_get_cursor_x(screen), tsm_screen_get_cursor_y(screen));
}
Vector2 Terminal::get_cell_size() const {
Vector2 Terminal::get_cell_size() const
{
return cell_size;
}
@ -181,7 +183,8 @@ String Terminal::write(const Variant data)
ERR_FAIL_V_MSG("", "Data must be a String or PackedByteArray.");
}
if (bytes.is_empty()) return "";
if (bytes.is_empty())
return "";
response.clear();
tsm_vte_input(vte, (char *)bytes.ptr(), bytes.size());
@ -190,7 +193,8 @@ String Terminal::write(const Variant data)
return response.get_string_from_utf8();
}
void Terminal::_gui_input(const Ref<InputEvent> &event) {
void Terminal::_gui_input(const Ref<InputEvent> &event)
{
_handle_key_input(event);
_handle_selection(event);
_handle_mouse_wheel(event);
@ -235,13 +239,15 @@ void Terminal::_write_cb(tsm_vte *vte, const char *u8, size_t len, void *data)
{
Terminal *term = static_cast<Terminal *>(data);
if (len > 0) {
if (len > 0)
{
PackedByteArray data;
data.resize(len);
memcpy(data.ptrw(), u8, len);
term->response.append_array(data);
if (term->last_input_event_key.is_valid()) {
if (term->last_input_event_key.is_valid())
{
term->emit_signal("key_pressed", data.get_string_from_utf8(), term->last_input_event_key);
term->last_input_event_key.unref();
}
@ -263,10 +269,11 @@ int Terminal::_draw_cb(struct tsm_screen *con,
{
Terminal *term = static_cast<Terminal *>(data);
if (age != 0 && age <= term->framebuffer_age) return OK;
if (age != 0 && age <= term->framebuffer_age)
return OK;
if (width < 1)
{ // No foreground or background to draw.
{ // No foreground or background to draw.
return OK;
}
@ -276,20 +283,22 @@ int Terminal::_draw_cb(struct tsm_screen *con,
attr_flags |= AttrFlag::INVERSE;
if (attr->blink)
attr_flags |= AttrFlag::BLINK;
if (term->cursor_position.x == posx && term->cursor_position.y == posy) {
if (term->cursor_position.x == posx && term->cursor_position.y == posy)
{
attr_flags |= AttrFlag::CURSOR;
}
// Collect colors.
Color fgcol = std::min(attr->fccode, (int8_t)TSM_COLOR_FOREGROUND) >= 0
? term->palette[attr->fccode]
: Color(attr->fr / 255.0f, attr->fg / 255.0f, attr->fb / 255.0f);
? term->palette[attr->fccode]
: Color(attr->fr / 255.0f, attr->fg / 255.0f, attr->fb / 255.0f);
Color bgcol = std::min(attr->bccode, (int8_t)TSM_COLOR_BACKGROUND) >= 0
? term->palette[attr->bccode]
: Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f);
? term->palette[attr->bccode]
: Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f);
if (attr->inverse && term->inverse_mode == InverseMode::INVERSE_MODE_SWAP) {
if (attr->inverse && term->inverse_mode == InverseMode::INVERSE_MODE_SWAP)
{
std::swap(fgcol.r, bgcol.r);
std::swap(fgcol.g, bgcol.g);
std::swap(fgcol.b, bgcol.b);
@ -300,7 +309,8 @@ int Terminal::_draw_cb(struct tsm_screen *con,
bgcol.a = 0;
// Update images (accounting for ultra-wide characters).
for (int i = 0; i < width && (posx + i) < term->cols; i++) {
for (int i = 0; i < width && (posx + i) < term->cols; i++)
{
term->back_image->set_pixel(posx + i, posy, bgcol);
term->attr_image->set_pixel(posx + i, posy, Color(attr_flags / 255.0f, 0, 0, 0));
}
@ -312,7 +322,7 @@ int Terminal::_draw_cb(struct tsm_screen *con,
term->rs->canvas_item_add_rect(term->char_canvas_item, cell_rect, Color(1, 1, 1, 0));
if (len < 1)
{ // No foreground to draw.
{ // No foreground to draw.
return OK;
}
@ -323,8 +333,7 @@ int Terminal::_draw_cb(struct tsm_screen *con,
Vector2i(cell_position.x, cell_position.y + term->font_offset),
static_cast<uint64_t>(*ch),
term->font_size,
fgcol
);
fgcol);
return OK;
}
@ -333,7 +342,8 @@ void Terminal::_bell_cb(struct tsm_vte *vte, void *data)
{
Terminal *term = static_cast<Terminal *>(data);
if (!term->bell_muted && term->bell_timer->is_stopped()) {
if (!term->bell_muted && term->bell_timer->is_stopped())
{
term->emit_signal("bell");
if (term->bell_cooldown > 0)
@ -393,12 +403,15 @@ bool Terminal::_set(const StringName &p_name, const Variant &p_value)
return false;
}
bool Terminal::_get(const StringName &p_name, Variant &r_value) {
if (p_name == String("cols")) {
bool Terminal::_get(const StringName &p_name, Variant &r_value)
{
if (p_name == String("cols"))
{
r_value = cols;
return true;
}
if (p_name == String("rows")) {
if (p_name == String("rows"))
{
r_value = rows;
return true;
}
@ -485,20 +498,24 @@ void Terminal::update_sizes(bool force)
void Terminal::set_shader_parameters(const String &param, const Variant &value)
{
if (param.is_empty()) {
if (param.is_empty())
{
set_shader_parameters("cols", cols);
set_shader_parameters("rows", rows);
set_shader_parameters("size", size);
set_shader_parameters("cell_size", cell_size);
set_shader_parameters("grid_size", Vector2(cols * cell_size.x, rows * cell_size.y));
} else {
}
else
{
back_material->set_shader_parameter(param, value);
fore_material->set_shader_parameter(param, value);
}
}
void Terminal::initialize_rendering() {
ResourceLoader* rl = ResourceLoader::get_singleton();
void Terminal::initialize_rendering()
{
ResourceLoader *rl = ResourceLoader::get_singleton();
rs = RenderingServer::get_singleton();
attr_texture.instantiate();
@ -542,7 +559,7 @@ void Terminal::initialize_rendering() {
canvas = rs->canvas_create();
rs->canvas_item_set_parent(char_canvas_item, canvas);
viewport = rs-> viewport_create();
viewport = rs->viewport_create();
rs->viewport_attach_canvas(viewport, canvas);
rs->viewport_set_disable_3d(viewport, true);
rs->viewport_set_transparent_background(viewport, true);
@ -563,16 +580,19 @@ void Terminal::initialize_rendering() {
rs->connect("frame_post_draw", Callable(this, "_on_frame_post_draw"));
}
void Terminal::update_theme() {
void Terminal::update_theme()
{
// Update colors.
palette.resize(TSM_COLOR_NUM);
for (int i = 0; i < TSM_COLOR_NUM; i++) {
for (int i = 0; i < TSM_COLOR_NUM; i++)
{
tsm_vte_color color = static_cast<tsm_vte_color>(i);
palette[color] = get_theme_color(String(COLOR_NAMES[i]));
}
// Update fonts.
for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++) {
for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++)
{
FontType type = static_cast<FontType>(i);
fonts[type] = has_theme_font(FONT_TYPES[type]) ? get_theme_font(FONT_TYPES[type]) : get_theme_font(FONT_TYPES[FontType::NORMAL]);
}
@ -581,25 +601,30 @@ void Terminal::update_theme() {
style_normal = get_theme_stylebox("normal");
style_focus = get_theme_stylebox("focus");
if (dynamic_cast<StyleBoxFlat*>(style_normal.ptr()) != nullptr) {
if (dynamic_cast<StyleBoxFlat *>(style_normal.ptr()) != nullptr)
{
// Blend the background color with the style box's background color to get the "true" background color.
Color style_background_color = style_normal->get("bg_color");
palette[TSM_COLOR_BACKGROUND] = style_background_color.blend(palette[TSM_COLOR_BACKGROUND]);
palette[TSM_COLOR_BACKGROUND] = style_background_color.blend(palette[TSM_COLOR_BACKGROUND]);
}
back_material->set_shader_parameter("background_color", palette[TSM_COLOR_BACKGROUND]);
refresh();
}
void Terminal::_on_frame_post_draw() {
if (redraw_requested) {
void Terminal::_on_frame_post_draw()
{
if (redraw_requested)
{
queue_redraw();
redraw_requested = false;
}
}
void Terminal::draw_screen() {
if (framebuffer_age == 0) {
void Terminal::draw_screen()
{
if (framebuffer_age == 0)
{
Rect2 rect = Rect2(Vector2(), size);
rs->viewport_set_clear_mode(viewport, RenderingServer::ViewportClearMode::VIEWPORT_CLEAR_ONLY_NEXT_FRAME);
@ -628,12 +653,14 @@ void Terminal::draw_screen() {
back_texture->update(back_image);
}
void Terminal::refresh() {
void Terminal::refresh()
{
framebuffer_age = 0;
queue_redraw();
}
void Terminal::cleanup_rendering() {
void Terminal::cleanup_rendering()
{
// StyleBox.
rs->free_rid(style_canvas_item);
@ -649,18 +676,22 @@ void Terminal::cleanup_rendering() {
rs->free_rid(char_shader);
}
void Terminal::set_bell_muted(const bool muted) {
void Terminal::set_bell_muted(const bool muted)
{
bell_muted = muted;
}
bool Terminal::get_bell_muted() const {
bool Terminal::get_bell_muted() const
{
return bell_muted;
}
void Terminal::set_bell_cooldown(const double time) {
void Terminal::set_bell_cooldown(const double time)
{
bell_cooldown = time;
if (!bell_timer->is_stopped()) {
if (!bell_timer->is_stopped())
{
bell_timer->stop();
double remaining_time = std::max(0.0, bell_cooldown - bell_timer->get_time_left());
@ -669,7 +700,8 @@ void Terminal::set_bell_cooldown(const double time) {
}
}
double Terminal::get_bell_cooldown() const {
double Terminal::get_bell_cooldown() const
{
return bell_cooldown;
}
@ -695,25 +727,28 @@ double Terminal::get_blink_off_time() const
return blink_off_time;
}
void Terminal::clear() {
// Resize the terminal to a single row, forcing content above in to the scrollback buffer.
tsm_screen_resize(screen, cols, 1);
void Terminal::clear()
{
// Resize the terminal to a single row, forcing content above in to the scrollback buffer.
tsm_screen_resize(screen, cols, 1);
// Clear the scrollback buffer (hence clearing the content that was above).
tsm_screen_clear_sb(screen);
// Clear the scrollback buffer (hence clearing the content that was above).
tsm_screen_clear_sb(screen);
// Resize the screen to its original size.
tsm_screen_resize(screen, cols, rows);
// Resize the screen to its original size.
tsm_screen_resize(screen, cols, rows);
refresh();
refresh();
}
String Terminal::_copy_screen(ScreenCopyFunction func) {
String Terminal::_copy_screen(ScreenCopyFunction func)
{
char *out;
PackedByteArray data;
data.resize(std::max(func(screen, &out), 0));
if (data.size() > 0) {
if (data.size() > 0)
{
memcpy(data.ptrw(), out, data.size());
std::free(out);
}
@ -721,16 +756,20 @@ String Terminal::_copy_screen(ScreenCopyFunction func) {
return data.get_string_from_utf8();
}
void Terminal::select(const int p_from_line, const int p_from_column, const int p_to_line, const int p_to_column) {
void Terminal::select(const int p_from_line, const int p_from_column, const int p_to_line, const int p_to_column)
{
int from_line = std::clamp((int)p_from_line, 0, (int)rows);
int from_column = std::clamp((int)p_from_column, 0, (int)cols);
int to_line = std::clamp((int)p_to_line, 0, (int)rows);
int to_column = std::clamp((int)p_to_column, 0, (int)cols);
if (from_line > to_line) {
if (from_line > to_line)
{
std::swap(to_line, from_line);
std::swap(to_column, from_column);
} else if ((from_line == to_line) && (from_column > to_column)) {
}
else if ((from_line == to_line) && (from_column > to_column))
{
std::swap(to_column, from_column);
}
@ -742,34 +781,40 @@ void Terminal::select(const int p_from_line, const int p_from_column, const int
String selection = copy_selection();
#if defined(__linux__)
#if defined(__linux__)
if (copy_on_selection)
DisplayServer::get_singleton()->clipboard_set_primary(selection);
#endif
#endif
if (selection.length() > 0) {
if (selection.length() > 0)
{
selecting = true;
queue_redraw();
}
}
String Terminal::copy_all() {
String Terminal::copy_all()
{
return _copy_screen(&tsm_screen_copy_all);
}
String Terminal::copy_selection() {
String Terminal::copy_selection()
{
return _copy_screen(&tsm_screen_selection_copy);
}
void Terminal::set_copy_on_selection(const bool p_enabled) {
void Terminal::set_copy_on_selection(const bool p_enabled)
{
copy_on_selection = p_enabled;
}
bool Terminal::get_copy_on_selection() const {
bool Terminal::get_copy_on_selection() const
{
return copy_on_selection;
}
void Terminal::set_inverse_mode(const int mode) {
void Terminal::set_inverse_mode(const int mode)
{
inverse_mode = static_cast<InverseMode>(mode);
bool inverse_enabled = inverse_mode == InverseMode::INVERSE_MODE_INVERT;
@ -778,11 +823,13 @@ void Terminal::set_inverse_mode(const int mode) {
refresh();
}
int Terminal::get_inverse_mode() const {
int Terminal::get_inverse_mode() const
{
return static_cast<int>(inverse_mode);
}
void Terminal::initialize_input() {
void Terminal::initialize_input()
{
selecting = false;
selection_mode = SelectionMode::NONE;
selection_timer = memnew(Timer);
@ -791,129 +838,144 @@ void Terminal::initialize_input() {
add_child(selection_timer, false, INTERNAL_MODE_FRONT);
}
void Terminal::_handle_key_input(Ref<InputEventKey> event) {
if (!event.is_valid() || !event->is_pressed())
return;
void Terminal::_handle_key_input(Ref<InputEventKey> event)
{
if (!event.is_valid() || !event->is_pressed())
return;
const Key keycode = event->get_keycode();
char32_t unicode = event->get_unicode();
uint32_t ascii = unicode <= 127 ? unicode : 0;
const Key keycode = event->get_keycode();
char32_t unicode = event->get_unicode();
uint32_t ascii = unicode <= 127 ? unicode : 0;
unsigned int mods = 0;
if (event->is_alt_pressed())
mods |= TSM_SHIFT_MASK;
if (event->is_ctrl_pressed())
mods |= TSM_CONTROL_MASK;
if (event->is_shift_pressed())
mods |= TSM_SHIFT_MASK;
unsigned int mods = 0;
if (event->is_alt_pressed())
mods |= TSM_ALT_MASK;
if (event->is_ctrl_pressed())
mods |= TSM_CONTROL_MASK;
if (event->is_shift_pressed())
mods |= TSM_SHIFT_MASK;
std::pair<Key, char32_t> key = {keycode, unicode};
uint32_t keysym = (KEY_MAP.count(key) > 0) ? KEY_MAP.at(key) : XKB_KEY_NoSymbol;
std::pair<Key, char32_t> key = {keycode, unicode};
uint32_t keysym = (KEY_MAP.count(key) > 0) ? KEY_MAP.at(key) : XKB_KEY_NoSymbol;
last_input_event_key = event;
tsm_vte_handle_keyboard(vte, keysym, ascii, mods, unicode ? unicode : TSM_VTE_INVALID);
last_input_event_key = event;
tsm_vte_handle_keyboard(vte, keysym, ascii, mods, unicode ? unicode : TSM_VTE_INVALID);
// Return to the bottom of the scrollback buffer if we scrolled up. Ignore
// modifier keys pressed in isolation or if Ctrl+Shift modifier keys are
// pressed.
std::set<Key> mod_keys = {KEY_ALT, KEY_SHIFT, KEY_CTRL, KEY_META};
if (mod_keys.find(keycode) == mod_keys.end() &&
!(event->is_ctrl_pressed() && event->is_shift_pressed())) {
tsm_screen_sb_reset(screen);
queue_redraw();
}
// Prevent focus changing to other inputs when pressing Tab or Arrow keys.
std::set<Key> tab_arrow_keys = {KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN, KEY_TAB};
if (tab_arrow_keys.find(keycode) != tab_arrow_keys.end())
accept_event();
}
void Terminal::_handle_mouse_wheel(Ref<InputEventMouseButton> event) {
if (!event.is_valid() || !event->is_pressed())
return;
void (*scroll_func)(tsm_screen *, unsigned int) = nullptr;
switch (event->get_button_index()) {
case MOUSE_BUTTON_WHEEL_UP:
scroll_func = &tsm_screen_sb_up;
break;
case MOUSE_BUTTON_WHEEL_DOWN:
scroll_func = &tsm_screen_sb_down;
break;
default:
break;
};
if (scroll_func != nullptr) {
// Scroll 5 times as fast as normal if alt is pressed (like TextEdit).
// Otherwise, just scroll 3 lines.
int speed = event->is_alt_pressed() ? 15 : 3;
double factor = event->get_factor();
(*scroll_func)(screen, speed * factor);
queue_redraw();
}
}
void Terminal::_handle_selection(Ref<InputEventMouse> event) {
if (!event.is_valid())
return;
Ref<InputEventMouseButton> mb = event;
if (mb.is_valid()) {
if (!mb->is_pressed() || mb->get_button_index() != MOUSE_BUTTON_LEFT)
return;
if (selecting) {
selecting = false;
selection_mode = SelectionMode::NONE;
tsm_screen_selection_reset(screen);
queue_redraw();
}
selecting = false;
selection_mode = SelectionMode::POINTER;
return;
}
Ref<InputEventMouseMotion> mm = event;
if (mm.is_valid()) {
if ((mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) && selection_mode != SelectionMode::NONE && !selecting) {
selecting = true;
Vector2 start = event->get_position() / cell_size;
tsm_screen_selection_start(screen, start.x, start.y);
queue_redraw();
selection_timer->start();
}
return;
}
}
void Terminal::_on_selection_held() {
if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) || selection_mode == SelectionMode::NONE) {
#if defined(__linux__)
if (copy_on_selection) {
DisplayServer::get_singleton()->clipboard_set_primary(copy_selection());
// Return to the bottom of the scrollback buffer if we scrolled up. Ignore
// modifier keys pressed in isolation or if Ctrl+Shift modifier keys are
// pressed.
std::set<Key> mod_keys = {KEY_ALT, KEY_SHIFT, KEY_CTRL, KEY_META};
if (mod_keys.find(keycode) == mod_keys.end() &&
!(event->is_ctrl_pressed() && event->is_shift_pressed()))
{
tsm_screen_sb_reset(screen);
queue_redraw();
}
#endif
selection_timer->stop();
return;
}
// Prevent focus changing to other inputs when pressing Tab or Arrow keys.
std::set<Key> tab_arrow_keys = {KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN, KEY_TAB};
if (tab_arrow_keys.find(keycode) != tab_arrow_keys.end())
accept_event();
}
Vector2 target = get_local_mouse_position() / cell_size;
tsm_screen_selection_target(screen, target.x, target.y);
queue_redraw();
selection_timer->start();
void Terminal::_handle_mouse_wheel(Ref<InputEventMouseButton> event)
{
if (!event.is_valid() || !event->is_pressed())
return;
void (*scroll_func)(tsm_screen *, unsigned int) = nullptr;
switch (event->get_button_index())
{
case MOUSE_BUTTON_WHEEL_UP:
scroll_func = &tsm_screen_sb_up;
break;
case MOUSE_BUTTON_WHEEL_DOWN:
scroll_func = &tsm_screen_sb_down;
break;
default:
break;
};
if (scroll_func != nullptr)
{
// Scroll 5 times as fast as normal if alt is pressed (like TextEdit).
// Otherwise, just scroll 3 lines.
int speed = event->is_alt_pressed() ? 15 : 3;
double factor = event->get_factor();
(*scroll_func)(screen, speed *factor);
queue_redraw();
}
}
void Terminal::_handle_selection(Ref<InputEventMouse> event)
{
if (!event.is_valid())
return;
Ref<InputEventMouseButton> mb = event;
if (mb.is_valid())
{
if (!mb->is_pressed() || mb->get_button_index() != MOUSE_BUTTON_LEFT)
return;
if (selecting)
{
selecting = false;
selection_mode = SelectionMode::NONE;
tsm_screen_selection_reset(screen);
queue_redraw();
}
selecting = false;
selection_mode = SelectionMode::POINTER;
return;
}
Ref<InputEventMouseMotion> mm = event;
if (mm.is_valid())
{
if ((mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) && selection_mode != SelectionMode::NONE && !selecting)
{
selecting = true;
Vector2 start = event->get_position() / cell_size;
tsm_screen_selection_start(screen, start.x, start.y);
queue_redraw();
selection_timer->start();
}
return;
}
}
void Terminal::_on_selection_held()
{
if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) || selection_mode == SelectionMode::NONE)
{
#if defined(__linux__)
if (copy_on_selection)
{
DisplayServer::get_singleton()->clipboard_set_primary(copy_selection());
}
#endif
selection_timer->stop();
return;
}
Vector2 target = get_local_mouse_position() / cell_size;
tsm_screen_selection_target(screen, target.x, target.y);
queue_redraw();
selection_timer->start();
}
// Add default theme items for the "Terminal" theme type if they don't exist.
// These defaults match Godot's built-in default theme (note: this is different from the default editor theme).
void Terminal::set_default_theme_items() {
void Terminal::set_default_theme_items()
{
Ref<Theme> default_theme = ThemeDB::get_singleton()->get_default_theme();
if (default_theme->get_type_list().has("Terminal")) return;
if (default_theme->get_type_list().has("Terminal"))
return;
// As a workaround, create a new theme and then merge it with the default theme at the end.
// See: https://github.com/godotengine/godot-cpp/issues/1332#issuecomment-2041060614.
@ -927,16 +989,16 @@ void Terminal::set_default_theme_items() {
// Default ANSI colors based on xterm defaults: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors.
// Some discussion about the slight difference of the blue colors: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=241717.
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_0_color" , "Terminal", Color::hex(0x000000FF)); // Black
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_1_color" , "Terminal", Color::hex(0xCD0000FF)); // Red
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_2_color" , "Terminal", Color::hex(0x00CD00FF)); // Green
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_3_color" , "Terminal", Color::hex(0xCDCD00FF)); // Yellow
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_4_color" , "Terminal", Color::hex(0x0000EEFF)); // Blue
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_5_color" , "Terminal", Color::hex(0xCD00CDFF)); // Magenta
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_6_color" , "Terminal", Color::hex(0x00CDCDFF)); // Cyan
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_7_color" , "Terminal", Color::hex(0xE5E5E5FF)); // White
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_8_color" , "Terminal", Color::hex(0x7F7F7FFF)); // Bright Black (Gray)
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_9_color" , "Terminal", Color::hex(0xFF0000FF)); // Bright Red
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_0_color", "Terminal", Color::hex(0x000000FF)); // Black
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_1_color", "Terminal", Color::hex(0xCD0000FF)); // Red
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_2_color", "Terminal", Color::hex(0x00CD00FF)); // Green
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_3_color", "Terminal", Color::hex(0xCDCD00FF)); // Yellow
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_4_color", "Terminal", Color::hex(0x0000EEFF)); // Blue
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_5_color", "Terminal", Color::hex(0xCD00CDFF)); // Magenta
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_6_color", "Terminal", Color::hex(0x00CDCDFF)); // Cyan
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_7_color", "Terminal", Color::hex(0xE5E5E5FF)); // White
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_8_color", "Terminal", Color::hex(0x7F7F7FFF)); // Bright Black (Gray)
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_9_color", "Terminal", Color::hex(0xFF0000FF)); // Bright Red
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_10_color", "Terminal", Color::hex(0x00FF00FF)); // Bright Green
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_11_color", "Terminal", Color::hex(0xFFFF00FF)); // Bright Yellow
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_12_color", "Terminal", Color::hex(0x5C5CFFFF)); // Bright Blue
@ -945,11 +1007,13 @@ void Terminal::set_default_theme_items() {
custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_15_color", "Terminal", Color::hex(0xFFFFFFFF)); // Bright White
// No monospaced font in the default theme, so try to import our own. Will default to a non-monospace font otherwise.
ResourceLoader* rl = ResourceLoader::get_singleton();
ResourceLoader *rl = ResourceLoader::get_singleton();
String const font_path = "res://addons/godot_xterm/themes/fonts/regular.tres";
if (rl->exists(font_path)) {
if (rl->exists(font_path))
{
Ref<Font> default_font = rl->load(font_path);
for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++) {
for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++)
{
FontType type = static_cast<FontType>(i);
custom_theme->set_theme_item(Theme::DATA_TYPE_FONT, FONT_TYPES[type], "Terminal", default_font);
}

View file

@ -27,7 +27,8 @@ namespace godot
private:
typedef std::map<std::pair<Key, char32_t>, uint32_t> KeyMap;
enum FontType {
enum FontType
{
NORMAL,
BOLD,
ITALICS,
@ -37,6 +38,7 @@ namespace godot
static const char *COLOR_NAMES[18];
static const char *FONT_TYPES[4];
static const KeyMap KEY_MAP;
public:
enum AttrFlag
{
@ -45,7 +47,8 @@ namespace godot
CURSOR = 1 << 2,
};
enum InverseMode {
enum InverseMode
{
INVERSE_MODE_INVERT,
INVERSE_MODE_SWAP,
};
@ -89,6 +92,7 @@ namespace godot
String write(const Variant data);
void _gui_input(const Ref<InputEvent> &event) override;
protected:
static void _bind_methods();
@ -123,7 +127,7 @@ namespace godot
// This can be useful in cases where the bell character is being written too
// frequently such as `while true; do echo -e "\a"; done`.
double bell_cooldown;
Timer* bell_timer;
Timer *bell_timer;
static void _bell_cb(struct tsm_vte *vte, void *data);
static int _draw_cb(struct tsm_screen *con, uint64_t id, const uint32_t *ch,
@ -184,14 +188,18 @@ namespace godot
void _handle_mouse_wheel(Ref<InputEventMouseButton> event);
enum SelectionMode { NONE, POINTER };
enum SelectionMode
{
NONE,
POINTER
};
bool selecting = false;
SelectionMode selection_mode = SelectionMode::NONE;
Timer *selection_timer;
void _handle_selection(Ref<InputEventMouse> event);
void _on_selection_held();
typedef std::function<int(struct tsm_screen*, char**)> ScreenCopyFunction;
typedef std::function<int(struct tsm_screen *, char **)> ScreenCopyFunction;
String _copy_screen(ScreenCopyFunction func);
void set_default_theme_items();

View file

@ -0,0 +1,58 @@
[gd_resource type="Theme" load_steps=4 format=3 uid="uid://dtpro3m7sdgvg"]
[ext_resource type="Script" path="res://addons/godot_xterm/resources/xrdb_theme.gd" id="1"]
[ext_resource type="FontFile" uid="uid://c51gnbjamppg" path="res://addons/godot_xterm/themes/fonts/jet_brains_mono/jet_brains_mono_nl-regular-2.304.ttf" id="2"]
[sub_resource type="FontFile" id="FontFile_tdf0u"]
fallbacks = Array[Font]([ExtResource("2")])
cache/0/16/0/ascent = 0.0
cache/0/16/0/descent = 0.0
cache/0/16/0/underline_position = 0.0
cache/0/16/0/underline_thickness = 0.0
cache/0/16/0/scale = 1.0
cache/0/16/0/kerning_overrides/16/0 = Vector2(0, 0)
cache/0/16/0/kerning_overrides/11/0 = Vector2(0, 0)
cache/0/16/0/kerning_overrides/14/0 = Vector2(0, 0)
cache/0/11/0/ascent = 0.0
cache/0/11/0/descent = 0.0
cache/0/11/0/underline_position = 0.0
cache/0/11/0/underline_thickness = 0.0
cache/0/11/0/scale = 1.0
cache/0/11/0/kerning_overrides/16/0 = Vector2(0, 0)
cache/0/11/0/kerning_overrides/11/0 = Vector2(0, 0)
cache/0/11/0/kerning_overrides/14/0 = Vector2(0, 0)
cache/0/14/0/ascent = 0.0
cache/0/14/0/descent = 0.0
cache/0/14/0/underline_position = 0.0
cache/0/14/0/underline_thickness = 0.0
cache/0/14/0/scale = 1.0
cache/0/14/0/kerning_overrides/16/0 = Vector2(0, 0)
cache/0/14/0/kerning_overrides/11/0 = Vector2(0, 0)
cache/0/14/0/kerning_overrides/14/0 = Vector2(0, 0)
[resource]
Terminal/colors/ansi_0_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_10_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_11_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_12_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_13_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_14_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_15_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_1_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_2_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_3_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_4_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_5_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_6_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_7_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_8_color = Color(0, 1, 0.4, 1)
Terminal/colors/ansi_9_color = Color(0, 1, 0.4, 1)
Terminal/colors/background_color = Color(0.156863, 0.156863, 0.156863, 1)
Terminal/colors/cursor = Color(0, 1, 0.4, 1)
Terminal/colors/foreground_color = Color(0, 1, 0.4, 1)
Terminal/font_sizes/font_size = 14
Terminal/fonts/bold_font = null
Terminal/fonts/bold_italics_font = null
Terminal/fonts/italics_font = null
Terminal/fonts/normal_font = SubResource("FontFile_tdf0u")
script = ExtResource("1")

View file

@ -0,0 +1,26 @@
[gd_resource type="Theme" load_steps=3 format=3 uid="uid://0gk8swmcldbg"]
[ext_resource type="FontVariation" uid="uid://vmgmcu8gc6nt" path="res://addons/godot_xterm/themes/fonts/regular.tres" id="1_aigbn"]
[ext_resource type="StyleBox" uid="uid://cxaclm5pavuv6" path="res://themes/normal.stylebox" id="1_bj7pu"]
[resource]
default_font = ExtResource("1_aigbn")
Terminal/colors/ansi_0_color = Color(0, 0, 0, 1)
Terminal/colors/ansi_10_color = Color(0.258824, 1, 0.760784, 1)
Terminal/colors/ansi_11_color = Color(1, 0.929412, 0.631373, 1)
Terminal/colors/ansi_12_color = Color(0.341176, 0.701961, 1, 1)
Terminal/colors/ansi_13_color = Color(0.639216, 0.639216, 0.960784, 1)
Terminal/colors/ansi_14_color = Color(0.4, 0.901961, 1, 1)
Terminal/colors/ansi_15_color = Color(1, 1, 1, 1)
Terminal/colors/ansi_1_color = Color(1, 0.470588, 0.419608, 1)
Terminal/colors/ansi_2_color = Color(0.388235, 0.760784, 0.34902, 1)
Terminal/colors/ansi_3_color = Color(0.980392, 0.890196, 0.270588, 1)
Terminal/colors/ansi_4_color = Color(0.0784314, 0.490196, 0.980392, 1)
Terminal/colors/ansi_5_color = Color(1, 0.54902, 0.8, 1)
Terminal/colors/ansi_6_color = Color(0.560784, 1, 0.858824, 1)
Terminal/colors/ansi_7_color = Color(0.803922, 0.811765, 0.823529, 0.501961)
Terminal/colors/ansi_8_color = Color(0.211765, 0.239216, 0.290196, 1)
Terminal/colors/ansi_9_color = Color(1, 0.439216, 0.521569, 1)
Terminal/colors/foreground_color = Color(0.803922, 0.811765, 0.823529, 1)
Terminal/font_sizes/font_size = 14
Terminal/styles/normal = ExtResource("1_bj7pu")

View file

@ -145,17 +145,6 @@ func _on_Terminal_key_pressed(data: String, event: InputEventKey) -> void:
$Terminal.grab_focus()
scene.queue_free()
"Terminal":
if OS.get_name() == "Windows":
OS.call_deferred(
"alert",
(
"Psuedoterminal node currently"
+ " uses pty.h but needs to use either winpty or conpty"
+ " to work on Windows."
),
"Terminal not Supported on Windows"
)
return
var scene = item.scene.instantiate()
var pty = scene if OS.has_feature("web") else scene.get_node("PTY")
add_child(scene)

View file

@ -1,7 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://brjrtf5fpptw8"]
[ext_resource type="Script" path="res://examples/menu/menu.gd" id="2"]
[ext_resource type="Theme" uid="uid://0gk8swmcldbg" path="res://themes/demo.tres" id="2_7f2wl"]
[ext_resource type="Theme" uid="uid://0gk8swmcldbg" path="res://themes/demo.tres" id="2_pr2sv"]
[node name="Menu" type="Control"]
layout_mode = 3
@ -19,4 +19,4 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2_7f2wl")
theme = ExtResource("2_pr2sv")

View file

@ -2,6 +2,5 @@ extends Terminal
@onready var pty = $PTY
func _ready():
pty.fork(OS.get_environment("SHELL"))
pty.fork()

View file

@ -21,7 +21,7 @@ window/vsync/use_vsync=false
[editor_plugins]
enabled=PackedStringArray("res://addons/godot_xterm/plugin.cfg", "res://addons/zylann.editor_debugger/plugin.cfg")
enabled=PackedStringArray("res://addons/godot_xterm/plugin.cfg")
[rendering]

View file

@ -10,6 +10,7 @@ func before_each():
func test_bell() -> void:
watch_signals(terminal)
terminal.bell_cooldown = 0
terminal.write(char(7))
terminal.write(char(0x07))
@ -42,8 +43,8 @@ class TestTheme:
const TestScene := preload("../scenes/theme.tscn")
const default_theme := preload("res://addons/godot_xterm/themes/default.tres")
const alt_theme := preload("res://addons/godot_xterm/themes/default_light.tres")
const default_theme := preload("res://addons/godot_xterm/themes/default_green.tres")
const alt_theme := preload("res://addons/godot_xterm/themes/default_white.tres")
const COLORS := [
"ansi_0_color",

View file

@ -1,29 +0,0 @@
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():
OS.set_environment(EMPTY_VAR, "")
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)

View file

@ -96,7 +96,7 @@ func test_emits_exit_code_on_failure():
func test_emits_exited_on_kill():
subject.call("fork", "yes")
await wait_frames(1)
subject.call_deferred("kill", PTY.SIGNAL_SIGKILL)
subject.call_deferred("kill", PTY.IPCSIGNAL_SIGKILL)
await wait_for_signal(subject.exited, 1)
assert_signal_emitted(subject, "exited")
@ -104,9 +104,9 @@ func test_emits_exited_on_kill():
func test_emits_exited_with_signal():
subject.call("fork", "yes")
await wait_frames(1)
subject.call_deferred("kill", PTY.SIGNAL_SIGSEGV)
subject.call_deferred("kill", PTY.IPCSIGNAL_SIGSEGV)
await wait_for_signal(subject.exited, 1)
assert_signal_emitted_with_parameters(subject, "exited", [0, PTY.SIGNAL_SIGSEGV])
assert_signal_emitted_with_parameters(subject, "exited", [0, PTY.IPCSIGNAL_SIGSEGV])
# Run the same tests, but with use_threads = false.
@ -189,7 +189,7 @@ class TestPTYSize:
await wait_for_signal(subject.data_received, 1)
func after_each():
subject.call_deferred("kill", PTY.SIGNAL_SIGHUP)
subject.call_deferred("kill", PTY.IPCSIGNAL_SIGHUP)
await wait_for_signal(subject.exited, 1)
func test_pty_default_size():

View file

@ -64,18 +64,18 @@ class TestInterface:
# Enums.
func test_has_enum_signal():
assert_eq(described_class.SIGNAL_SIGHUP, 1)
assert_eq(described_class.SIGNAL_SIGINT, 2)
assert_eq(described_class.SIGNAL_SIGQUIT, 3)
assert_eq(described_class.SIGNAL_SIGILL, 4)
assert_eq(described_class.SIGNAL_SIGTRAP, 5)
assert_eq(described_class.SIGNAL_SIGABRT, 6)
assert_eq(described_class.SIGNAL_SIGFPE, 8)
assert_eq(described_class.SIGNAL_SIGKILL, 9)
assert_eq(described_class.SIGNAL_SIGSEGV, 11)
assert_eq(described_class.SIGNAL_SIGPIPE, 13)
assert_eq(described_class.SIGNAL_SIGALRM, 14)
assert_eq(described_class.SIGNAL_SIGTERM, 15)
assert_eq(described_class.IPCSIGNAL_SIGHUP, 1)
assert_eq(described_class.IPCSIGNAL_SIGINT, 2)
assert_eq(described_class.IPCSIGNAL_SIGQUIT, 3)
assert_eq(described_class.IPCSIGNAL_SIGILL, 4)
assert_eq(described_class.IPCSIGNAL_SIGTRAP, 5)
assert_eq(described_class.IPCSIGNAL_SIGABRT, 6)
assert_eq(described_class.IPCSIGNAL_SIGFPE, 8)
assert_eq(described_class.IPCSIGNAL_SIGKILL, 9)
assert_eq(described_class.IPCSIGNAL_SIGSEGV, 11)
assert_eq(described_class.IPCSIGNAL_SIGPIPE, 13)
assert_eq(described_class.IPCSIGNAL_SIGALRM, 14)
assert_eq(described_class.IPCSIGNAL_SIGTERM, 15)
## Other tests.

View file

@ -1,23 +1,14 @@
extends "res://addons/gut/test.gd"
class MockPTY:
extends "res://addons/godot_xterm/nodes/pty/pty_native.gd"
func write(data):
emit_signal("data_received", data)
class BaseTest:
extends "res://addons/gut/test.gd"
var pty
var mock_pty_native: MockPTY
var mock_pty_native: PTY
func before_each():
pty = add_child_autofree(PTY.new())
mock_pty_native = autofree(MockPTY.new())
pty._pty_native = mock_pty_native
mock_pty_native = autofree(PTY.new())
watch_signals(mock_pty_native)
@ -70,33 +61,17 @@ class TestPTYInterfaceGodotXterm2_0_0:
func test_has_signal_exited():
assert_has_signal(pty, "exited")
# NOTE: This differs from the GodotXterm 2.x API which uses Signal rather than IPCSignal.
# In Godot 4.x, enums are no longer dictionaries and thus need to be inspected individually.
func test_has_enum_Signal():
assert_true("IPCSignal" in pty, "Expected pty to have enum IPCSignal.")
assert_typeof(pty.IPCSignal, typeof(Dictionary()))
var signals = {
SIGHUP = 1,
SIGINT = 2,
SIGQUIT = 3,
SIGILL = 4,
SIGTRAP = 5,
SIGABRT = 6,
SIGFPE = 8,
SIGKILL = 9,
SIGSEGV = 11,
SIGPIPE = 13,
SIGALRM = 14,
SIGTERM = 15,
}
assert_gt(
pty.IPCSignal.size(),
signals.size() - 1,
"Expected Signal enum to have at least %d members." % signals.size()
)
for signame in signals.keys():
assert_has(pty.IPCSignal, signame, "Expected Signal enum to have member %s." % signame)
assert_eq(
pty.IPCSignal[signame],
signals[signame],
"Expected Signal enum member %s to have value %d." % [signame, signals[signame]]
)
assert_eq(pty.IPCSIGNAL_SIGHUP, 1, "Expected pty to have IPCSIGNAL_SIGHUP.")
assert_eq(pty.IPCSIGNAL_SIGINT, 2, "Expected pty to have IPCSIGNAL_SIGINT.")
assert_eq(pty.IPCSIGNAL_SIGQUIT, 3, "Expected pty to have IPCSIGNAL_SIGQUIT.")
assert_eq(pty.IPCSIGNAL_SIGILL, 4, "Expected pty to have IPCSIGNAL_SIGILL.")
assert_eq(pty.IPCSIGNAL_SIGTRAP, 5, "Expected pty to have IPCSIGNAL_SIGTRAP.")
assert_eq(pty.IPCSIGNAL_SIGABRT, 6, "Expected pty to have IPCSIGNAL_SIGABRT.")
assert_eq(pty.IPCSIGNAL_SIGFPE, 8, "Expected pty to have IPCSIGNAL_SIGFPE.")
assert_eq(pty.IPCSIGNAL_SIGKILL, 9, "Expected pty to have IPCSIGNAL_SIGKILL.")
assert_eq(pty.IPCSIGNAL_SIGSEGV, 11, "Expected pty to have IPCSIGNAL_SIGSEGV.")
assert_eq(pty.IPCSIGNAL_SIGPIPE, 13, "Expected pty to have IPCSIGNAL_SIGPIPE.")
assert_eq(pty.IPCSIGNAL_SIGALRM, 14, "Expected pty to have IPCSIGNAL_SIGALRM.")
assert_eq(pty.IPCSIGNAL_SIGTERM, 15, "Expected pty to have IPCSIGNAL_SIGTERM.")