From df32ee3c1825b16fa0d70489b61ae89bc79bcd9d Mon Sep 17 00:00:00 2001 From: Leroy Hopson Date: Wed, 29 Jun 2022 22:58:48 +0700 Subject: [PATCH] Refactor PTY PTY now provides a public interface to an underlying instance of PTYNative. The PTYNative class can be extended as appropriate for each platform and the platform-specific implementation will be selected by PTY at runtime. --- .../terminal/editor_terminal.gd | 4 +- addons/godot_xterm/nodes/pty/pty.gd | 237 ------------------ addons/godot_xterm/nodes/pty/pty_native.gd | 28 +++ addons/godot_xterm/nodes/pty/unix/pty_unix.gd | 230 +++++++++++++++++ addons/godot_xterm/plugin.gd | 2 +- addons/godot_xterm/pty.gd | 213 +++++++++------- test/platform/unix/unix.test.gd | 56 +++++ test/unit/pty.test.gd | 102 ++++++++ 8 files changed, 539 insertions(+), 333 deletions(-) delete mode 100644 addons/godot_xterm/nodes/pty/pty.gd create mode 100644 addons/godot_xterm/nodes/pty/pty_native.gd create mode 100644 addons/godot_xterm/nodes/pty/unix/pty_unix.gd create mode 100644 test/unit/pty.test.gd diff --git a/addons/godot_xterm/editor_plugins/terminal/editor_terminal.gd b/addons/godot_xterm/editor_plugins/terminal/editor_terminal.gd index 7e4daf6..df944bf 100644 --- a/addons/godot_xterm/editor_plugins/terminal/editor_terminal.gd +++ b/addons/godot_xterm/editor_plugins/terminal/editor_terminal.gd @@ -61,8 +61,8 @@ func _ready(): func _poll(): - if pty and pty._pipe: - pty._pipe.poll() + if pty and pty.has_method("get_master"): + pty.get_master().poll() update() diff --git a/addons/godot_xterm/nodes/pty/pty.gd b/addons/godot_xterm/nodes/pty/pty.gd deleted file mode 100644 index 7d95b85..0000000 --- a/addons/godot_xterm/nodes/pty/pty.gd +++ /dev/null @@ -1,237 +0,0 @@ -# Derived from https://github.com/microsoft/node-pty/blob/main/src/terminal.ts -# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) -# Copyright (c) 2016, Daniel Imms (MIT License). -# Copyright (c) 2018, Microsoft Corporation (MIT License). -# Copyright (c) 2021, Leroy Hopson (MIT License). - -tool -extends Node - -const LibuvUtils := preload("./libuv_utils.gd") -const Pipe := preload("./pipe.gdns") -const Terminal := preload("../../terminal.gd") - -const DEFAULT_NAME := "xterm-256color" -const DEFAULT_COLS := 80 -const DEFAULT_ROWS := 24 -const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"} - -## Default messages to indicate PAUSE/RESUME for automatic flow control. -## To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh), -## the sequences can be customized in IPtyForkOptions. -#const FLOW_CONTROL_PAUSE = char(0x13) # defaults to XOFF -#const FLOW_CONTROL_RESUME = char(0x11) # defaults to XON - -# Any signal_number can be sent to the pty's process using the kill() function, -# these are just the signals with numbers specified in the POSIX standard. -enum Signal { - SIGHUP = 1, # Hangup - SIGINT = 2, # Terminal interrupt signal - SIGQUIT = 3, # Terminal quit signal - SIGILL = 4, # Illegal instruction - SIGTRAP = 5, # Trace/breakpoint trap - SIGABRT = 6, # Process abort signal - SIGFPE = 8, # Erroneous arithmetic operation - SIGKILL = 9, # Kill (cannot be caught or ignored) - SIGSEGV = 11, # Invalid memory reference - SIGPIPE = 13, # Write on a pipe with no one to read it - SIGALRM = 14, # Alarm clock - SIGTERM = 15, # Termination signal -} - -signal data_received(data) -signal exited(exit_code, signum) - -export(NodePath) var terminal_path := NodePath() setget set_terminal_path - -var _terminal: Terminal = null setget _set_terminal - -# The name of the process. -#var process: String - -# The process ID. -var _pid: int - -# The column size in characters. -export(int) var cols: int = DEFAULT_COLS setget set_cols - -# The row size in characters. -export(int) var rows: int = DEFAULT_ROWS setget set_rows - -# Environment to be set for the child program. -export(Dictionary) var env := DEFAULT_ENV - -# If true the environment variables in the env Dictionary will be merged with -# the environment variables of the operating system (e.g. printenv), with the -# former taking precedence in the case of conflicts. -export(bool) var use_os_env := true - -# (EXPERIMENTAL) -# If true, PTY node will create a blocking libuv loop in a new thread. -# signals will be emitted using call_deferred. -#export (bool) var use_threads := false - -## String encoding of the underlying pty. -## If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding. -## If unset, incoming data will be delivered as raw bytes (PoolByteArray type). -## By default 'utf8' is assumed, to unset it explicitly set it to `null`. -#var encoding: String = "utf8" - -## (EXPERIMENTAL) -## Whether to enable flow control handling (false by default). If enabled a message of `flow_control_pause` -## will pause the socket and thus blocking the child program execution due to buffer back pressure. -## A message of `flow_control_resume` will resume the socket into flow mode. -## For performance reasons only a single message as a whole will match (no message part matching). -## If flow control is enabled the `flow_control_pause` and `flow_control_resume` messages are not forwarded to -## the underlying pseudoterminal. -#var handle_flow_control: bool = false -# -## (EXPERIMENTAL) -## The string that should pause the pty when `handle_flow_control` is true. Default is XOFF ("\u0013"). -#var flow_control_pause: String = FLOW_CONTROL_PAUSE -# -## (EXPERIMENTAL) -## The string that should resume the pty when `handle_flow_control` is true. Default is XON ("\u0011"). -#var flow_control_resume: String = FLOW_CONTROL_RESUME - -var _pipe: Pipe - - -func set_cols(value: int): - resize(value, rows) - - -func set_rows(value: int): - resize(cols, value) - - -func set_terminal_path(value := NodePath()): - terminal_path = value - _set_terminal(get_node_or_null(terminal_path)) - - -func _set_terminal(value: Terminal): - if _terminal == value: - return - - # Disconect the current terminal, if any. - if _terminal: - disconnect("data_received", _terminal, "write") - _terminal.disconnect("data_sent", self, "write") - _terminal.disconnect("size_changed", self, "resizev") - - _terminal = value - - if not _terminal: - return - - # Connect the new terminal. - # FIXME! resize(terminal.get_cols(), terminal.get_rows()) - if not _terminal.is_connected("size_changed", self, "resizev"): - _terminal.connect("size_changed", self, "resizev") - if not _terminal.is_connected("data_sent", self, "write"): - _terminal.connect("data_sent", self, "write") - if not is_connected("data_received", _terminal, "write"): - connect("data_received", _terminal, "write") - - -# Writes data to the socket. -# data: The data to write. -func write(data) -> void: - assert(data is String or data is PoolByteArray) - - if data is PoolByteArray: - data = data.get_string_from_utf8() - -# if handle_flow_control: -# # PAUSE/RESUME messages are not forwarded to the pty. -# if data == flow_control_pause: -# pause() -# return -# if data == flow_control_resume: -# resume() -# return -# # Everything else goes to the real pty. - _write(data) - - -func _write(data: String) -> void: - if _pipe: - _pipe.write(data) - - -# Resizes the dimensions of the pty. -# cols: The number of columns. -# rows: The number of rows. -# Also accepts a single Vector2 argument where x is the the number of columns -# and y is the number of rows. -func resize(cols, rows = null) -> void: - if cols <= 0 or rows <= 0 or cols == NAN or rows == NAN or cols == INF or rows == INF: - push_error("Resizing must be done using positive cols and rows.") - _resize(cols, rows) - - -# Same as resize() but takes a Vector2. -func resizev(size: Vector2) -> void: - resize(size.x, size.y) - - -func _resize(cols: int, rows: int) -> void: - assert(false, "Not implemented.") - - -# Kill the pty. -# sigint: The signal to send. By default this is SIGHUP. -# This is not supported on Windows. -func kill(signum: int = Signal.SIGHUP) -> void: - if _pipe: - _pipe.close() - if _pid > 0: - LibuvUtils.kill(_pid, signum) - - -func fork( - p_file: String, - p_args = PoolStringArray(), - p_cwd: String = LibuvUtils.get_cwd(), - p_cols: int = DEFAULT_COLS, - p_rows: int = DEFAULT_ROWS -) -> int: - assert(false, "Not implemented.") - return FAILED - - -func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array: - assert(false, "Not implemented.") - return [FAILED] - - -func _parse_env(env: Dictionary = {}) -> PoolStringArray: - var keys := env.keys() - var pairs := PoolStringArray() - - for key in keys: - var value = env[key] - var valid = key is String and value is String - assert(valid, "Env key/value pairs must be of type String/String.") - - if not valid: - push_warning("Skipping invalid env key/value pair.") - continue - - pairs.append("%s=%s" % [key, value]) - - return pairs - - -func _process(_delta): - if _pipe: - _pipe.poll() - - -func _notification(what: int): - match what: - NOTIFICATION_PARENTED: - var parent = get_parent() - if parent is Terminal: - set_terminal_path(get_path_to(parent)) diff --git a/addons/godot_xterm/nodes/pty/pty_native.gd b/addons/godot_xterm/nodes/pty/pty_native.gd new file mode 100644 index 0000000..8c1e27a --- /dev/null +++ b/addons/godot_xterm/nodes/pty/pty_native.gd @@ -0,0 +1,28 @@ +tool +extends Node + +signal data_received(data) +signal exited(exit_code, signum) + + +func open(cols: int, rows: int): + return _not_implemented() + + +func resize(cols: int, rows: int): + return _not_implemented() + + +func get_master(): + return _not_implemented() + + +func _not_implemented() -> int: + var method := "" + + var stack = get_stack() + if stack.size() >= 2 and "function" in stack[1]: + method = "%s()" % stack[1].function + + push_error("Method %s not implemented on the current platform (%s)." % [method, OS.get_name()]) + return ERR_METHOD_NOT_FOUND diff --git a/addons/godot_xterm/nodes/pty/unix/pty_unix.gd b/addons/godot_xterm/nodes/pty/unix/pty_unix.gd new file mode 100644 index 0000000..163c5e4 --- /dev/null +++ b/addons/godot_xterm/nodes/pty/unix/pty_unix.gd @@ -0,0 +1,230 @@ +# Derived from https://github.com/microsoft/node-pty/blob/main/src/unixTerminal.ts +# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License). +# Copyright (c) 2016, Daniel Imms (MIT License). +# Copyright (c) 2018, Microsoft Corporation (MIT License). +# Copyright (c) 2021-2022, Leroy Hopson (MIT License). +tool +extends "../pty_native.gd" + +const LibuvUtils := preload("../libuv_utils.gd") +const Pipe := preload("../pipe.gdns") +const PTYUnix = preload("./pty_unix.gdns") + +const DEFAULT_NAME := "xterm-256color" +const DEFAULT_COLS := 80 +const DEFAULT_ROWS := 24 +const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"} + +const FALLBACK_FILE = "sh" + +## Default messages to indicate PAUSE/RESUME for automatic flow control. +## To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh), +## the sequences can be customized in IPtyForkOptions. +#const FLOW_CONTROL_PAUSE = char(0x13) # defaults to XOFF +#const FLOW_CONTROL_RESUME = char(0x11) # defaults to XON + +# Any signal_number can be sent to the pty's process using the kill() function, +# these are just the signals with numbers specified in the POSIX standard. +enum Signal { + SIGHUP = 1, # Hangup + SIGINT = 2, # Terminal interrupt signal + SIGQUIT = 3, # Terminal quit signal + SIGILL = 4, # Illegal instruction + SIGTRAP = 5, # Trace/breakpoint trap + SIGABRT = 6, # Process abort signal + SIGFPE = 8, # Erroneous arithmetic operation + SIGKILL = 9, # Kill (cannot be caught or ignored) + SIGSEGV = 11, # Invalid memory reference + SIGPIPE = 13, # Write on a pipe with no one to read it + SIGALRM = 14, # Alarm clock + SIGTERM = 15, # Termination signal +} + +# The name of the process. +#var process: String + +# The process ID. +var _pid: int + +# The column size in characters. +var cols: int = DEFAULT_COLS setget set_cols + +# The row size in characters. +var rows: int = DEFAULT_ROWS setget set_rows + +# Environment to be set for the child program. +var env := DEFAULT_ENV + +# If true the environment variables in the env Dictionary will be merged with +# the environment variables of the operating system (e.g. printenv), with the +# former taking precedence in the case of conflicts. +var use_os_env := true + +var _pipe: Pipe + +# Security warning: use this option with great caution, as opened file descriptors +# with higher privileges might leak to the child program. +var uid: int +var gid: int + +var _fd: int = -1 +var _exit_cb: FuncRef + + +func set_cols(value: int): + resize(value, rows) + + +func set_rows(value: int): + resize(cols, value) + + +# Writes data to the socket. +# data: The data to write. +func write(data) -> void: + assert(data is String or data is PoolByteArray) + + if data is PoolByteArray: + data = data.get_string_from_utf8() + + if _pipe: + _pipe.write(data) + + +func resize(cols: int, rows: int) -> void: + if cols <= 0 or rows <= 0 or cols == NAN or rows == NAN or cols == INF or rows == INF: + push_error("Resizing must be done using positive cols and rows.") + + if _fd < 0: + return + + PTYUnix.new().resize(_fd, cols, rows) + + +func kill(signum: int = Signal.SIGHUP) -> void: + if _pipe: + _pipe.close() + if _pid > 0: + LibuvUtils.kill(_pid, signum) + + +func _parse_env(env: Dictionary = {}) -> PoolStringArray: + var keys := env.keys() + var pairs := PoolStringArray() + + for key in keys: + var value = env[key] + var valid = key is String and value is String + assert(valid, "Env key/value pairs must be of type String/String.") + + if not valid: + push_warning("Skipping invalid env key/value pair.") + continue + + pairs.append("%s=%s" % [key, value]) + + return pairs + + +func _process(_delta): + if _pipe: + _pipe.poll() + + +func fork( + file: String = OS.get_environment("SHELL"), + args: PoolStringArray = PoolStringArray(), + cwd = LibuvUtils.get_cwd(), + p_cols: int = DEFAULT_COLS, + p_rows: int = DEFAULT_ROWS, + uid: int = -1, + gid: int = -1, + utf8 = true +) -> int: + # File. + if file.empty(): + file = FALLBACK_FILE + + # Environment variables. + # If we are using OS env vars, sanitize them to remove variables that might confuse our terminal. + var final_env := _sanitize_env(LibuvUtils.get_os_environ()) if use_os_env else {} + for key in env.keys(): + final_env[key] = env[key] + var parsed_env: PoolStringArray = _parse_env(final_env) + + # Exit callback. + _exit_cb = FuncRef.new() + _exit_cb.set_instance(self) + _exit_cb.set_function("_on_exit") + + # Actual fork. + var result = PTYUnix.new().fork( # VERY IMPORTANT: The must be set null or 0, otherwise will get an ENOTSOCK error after connecting our pipe to the fd. + file, null, args, parsed_env, cwd, cols, rows, uid, gid, utf8, _exit_cb + ) + + if result[0] != OK: + push_error("Fork failed.") + return FAILED + + _fd = result[1].fd + if _fd < 0: + push_error("File descriptor must be a non-negative integer value.") + return FAILED + + _pid = result[1].pid + + _pipe = Pipe.new() + _pipe.open(_fd) + + # Must connect to signal AFTER opening, otherwise we will get error ENOTSOCK. + _pipe.connect("data_received", self, "_on_pipe_data_received") + + return OK + + +func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array: + return PTYUnix.new().open(cols, rows) + + +func get_master(): + if _pipe: + return _pipe + return null + + +func _exit_tree(): + _exit_cb = null + if _pid > 1: + LibuvUtils.kill(_pid, Signal.SIGHUP) + if _pipe: + while _pipe.get_status() != 0: + continue + + +func _on_pipe_data_received(data): + emit_signal("data_received", data) + + +func _on_exit(exit_code: int, signum: int) -> void: + if is_instance_valid(self): + _pid = -1 + emit_signal("exited", exit_code, signum) + + +func _sanitize_env(env: Dictionary) -> Dictionary: + # Make sure we didn't start our server from inside tmux. + env.erase("TMUX") + env.erase("TMUX_PANE") + + # Make sure we didn't start our server from inside screen. + # http://web.mit.edu/gnu/doc/html/screen_20.html + env.erase("STY") + env.erase("WINDOW") + + # Delete some variables that might confuse our terminal. + env.erase("WINDOWID") + env.erase("TERMCAP") + env.erase("COLUMNS") + env.erase("LINES") + + return env diff --git a/addons/godot_xterm/plugin.gd b/addons/godot_xterm/plugin.gd index 7b6cd4c..2fa5a93 100644 --- a/addons/godot_xterm/plugin.gd +++ b/addons/godot_xterm/plugin.gd @@ -27,7 +27,7 @@ func _enter_tree(): var pty_script match OS.get_name(): "X11", "Server", "OSX": - pty_script = load("%s/nodes/pty/pty.gd" % base_dir) + pty_script = load("%s/pty.gd" % base_dir) add_custom_type("PTY", "Node", pty_script, pty_icon) terminal_panel = preload("./editor_plugins/terminal/terminal_panel.tscn").instance() terminal_panel.editor_plugin = self diff --git a/addons/godot_xterm/pty.gd b/addons/godot_xterm/pty.gd index 8b3a90f..ae3defe 100644 --- a/addons/godot_xterm/pty.gd +++ b/addons/godot_xterm/pty.gd @@ -1,131 +1,158 @@ -# Derived from https://github.com/microsoft/node-pty/blob/main/src/unixTerminal.ts +# Derived from https://github.com/microsoft/node-pty/blob/main/src/terminal.ts # Copyright (c) 2012-2015, Christopher Jeffrey (MIT License). # Copyright (c) 2016, Daniel Imms (MIT License). # Copyright (c) 2018, Microsoft Corporation (MIT License). -# Copyright (c) 2021, Leroy Hopson (MIT License). - +# Copyright (c) 2021-2022, Leroy Hopson (MIT License). tool -extends "./nodes/pty/pty.gd" +extends Node -const PTYUnix = preload("./nodes/pty/unix/pty_unix.gdns") +const _LibuvUtils := preload("./nodes/pty/libuv_utils.gd") +const _PTYNative := preload("./nodes/pty/pty_native.gd") +const _PTYUnix := preload("./nodes/pty/unix/pty_unix.gd") +const _Terminal := preload("./terminal.gd") -const FALLBACK_FILE = "sh" +const DEFAULT_NAME := "xterm-256color" +const DEFAULT_COLS := 80 +const DEFAULT_ROWS := 24 +const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"} -# Security warning: use this option with great caution, as opened file descriptors -# with higher privileges might leak to the child program. -var uid: int -var gid: int +# Any signal_number can be sent to the pty's process using the kill() function, +# these are just the signals with numbers specified in the POSIX standard. +const Signal = _PTYUnix.Signal -var thread: Thread +signal data_received(data) +signal exited(exit_code, signum) -var _fd: int = -1 -var _exit_cb: FuncRef +export(NodePath) var terminal_path := NodePath() setget set_terminal_path -#static func get_uid() -> int: -# return -1 # Not implemented. +var _terminal: _Terminal = null setget _set_terminal -#static func get_gid() -> int: -# return -1 # Not implemented. +# The column size in characters. +export(int) var cols: int = DEFAULT_COLS setget set_cols + +# The row size in characters. +export(int) var rows: int = DEFAULT_ROWS setget set_rows + +# Environment to be set for the child program. +export(Dictionary) var env := DEFAULT_ENV + +# If true the environment variables in the env Dictionary will be merged with +# the environment variables of the operating system (e.g. printenv), with the +# former taking precedence in the case of conflicts. +export(bool) var use_os_env := true + +var _pty_native: _PTYNative -func _resize(cols: int, rows: int) -> void: - if _fd < 0: +func _init(): + var os_name := OS.get_name() + match os_name: + "X11", "Server", "OSX": + _pty_native = _PTYUnix.new() + _: + push_error("PTY is not support on current platform (%s)." % os_name) + + _pty_native.connect("data_received", self, "_on_pty_native_data_received") + _pty_native.connect("exited", self, "_on_pty_native_exited") + + add_child(_pty_native) + + +func set_cols(value: int): + resize(value, rows) + + +func set_rows(value: int): + resize(cols, value) + + +func set_terminal_path(value := NodePath()): + terminal_path = value + _set_terminal(get_node_or_null(terminal_path)) + + +func _set_terminal(value: _Terminal): + if _terminal == value: return - PTYUnix.new().resize(_fd, cols, rows) + # Disconect the current terminal, if any. + if _terminal: + disconnect("data_received", _terminal, "write") + _terminal.disconnect("data_sent", self, "write") + _terminal.disconnect("size_changed", self, "resizev") + + _terminal = value + + if not _terminal: + return + + # Connect the new terminal. + # FIXME! resize(terminal.get_cols(), terminal.get_rows()) + if not _terminal.is_connected("size_changed", self, "resizev"): + _terminal.connect("size_changed", self, "resizev") + if not _terminal.is_connected("data_sent", self, "write"): + _terminal.connect("data_sent", self, "write") + if not is_connected("data_received", _terminal, "write"): + connect("data_received", _terminal, "write") -func _fork_thread(args): - var result = preload("./nodes/pty/unix/pty_unix.gdns").new().callv("fork", args) - return result +# Writes data to the socket. +# data: The data to write. +func write(data) -> void: + _pty_native.write(data) + + +# Resizes the dimensions of the pty. +# cols: The number of columns. +# rows: The number of rows. +func resize(cols, rows = null) -> void: + _pty_native.resize(cols, rows) + + +# Same as resize() but takes a Vector2. +func resizev(size: Vector2) -> void: + resize(size.x, size.y) + + +# Kill the pty. +# sigint: The signal to send. By default this is SIGHUP. +# This is not supported on Windows. +func kill(signum: int = Signal.SIGHUP) -> void: + _pty_native.kill(signum) + + +func _notification(what: int): + match what: + NOTIFICATION_PARENTED: + var parent = get_parent() + if parent is _Terminal: + set_terminal_path(get_path_to(parent)) func fork( file: String = OS.get_environment("SHELL"), args: PoolStringArray = PoolStringArray(), - cwd = LibuvUtils.get_cwd(), + cwd = _LibuvUtils.get_cwd(), p_cols: int = DEFAULT_COLS, p_rows: int = DEFAULT_ROWS, uid: int = -1, gid: int = -1, utf8 = true ) -> int: - # File. - if file.empty(): - file = FALLBACK_FILE - - # Environment variables. - # If we are using OS env vars, sanitize them to remove variables that might confuse our terminal. - var final_env := _sanitize_env(LibuvUtils.get_os_environ()) if use_os_env else {} - for key in env.keys(): - final_env[key] = env[key] - var parsed_env: PoolStringArray = _parse_env(final_env) - - # Exit callback. - _exit_cb = FuncRef.new() - _exit_cb.set_instance(self) - _exit_cb.set_function("_on_exit") - - # Actual fork. - var result = PTYUnix.new().fork( # VERY IMPORTANT: The must be set null or 0, otherwise will get an ENOTSOCK error after connecting our pipe to the fd. - file, null, args, parsed_env, cwd, cols, rows, uid, gid, utf8, _exit_cb - ) - - if result[0] != OK: - push_error("Fork failed.") - return FAILED - - _fd = result[1].fd - if _fd < 0: - push_error("File descriptor must be a non-negative integer value.") - return FAILED - - _pid = result[1].pid - - _pipe = Pipe.new() - _pipe.open(_fd) - - # Must connect to signal AFTER opening, otherwise we will get error ENOTSOCK. - _pipe.connect("data_received", self, "_on_pipe_data_received") - - return OK + return _pty_native.fork(file, args, cwd, p_cols, p_rows, uid, gid, utf8) func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array: - return PTYUnix.new().open(cols, rows) + return _pty_native.open(cols, rows) -func _exit_tree(): - if _pid > 1: - LibuvUtils.kill(_pid, Signal.SIGHUP) - while _pipe.get_status() != 0: - continue +func get_master(): + return _pty_native.get_master() -func _on_pipe_data_received(data): +func _on_pty_native_data_received(data): emit_signal("data_received", data) -func _on_exit(exit_code: int, signum: int) -> void: - if is_instance_valid(self): - _pid = -1 - emit_signal("exited", exit_code, signum) - - -func _sanitize_env(env: Dictionary) -> Dictionary: - # Make sure we didn't start our server from inside tmux. - env.erase("TMUX") - env.erase("TMUX_PANE") - - # Make sure we didn't start our server from inside screen. - # http://web.mit.edu/gnu/doc/html/screen_20.html - env.erase("STY") - env.erase("WINDOW") - - # Delete some variables that might confuse our terminal. - env.erase("WINDOWID") - env.erase("TERMCAP") - env.erase("COLUMNS") - env.erase("LINES") - - return env +func _on_pty_native_exited(exit_code: int, signum: int) -> void: + emit_signal("exited", exit_code, signum) diff --git a/test/platform/unix/unix.test.gd b/test/platform/unix/unix.test.gd index ac9a7fe..d2d8e4f 100644 --- a/test/platform/unix/unix.test.gd +++ b/test/platform/unix/unix.test.gd @@ -24,6 +24,62 @@ func test_fork_succeeds(): assert_eq(err, OK) +func test_fork_has_output(): + pty.call_deferred("fork", "exit") + yield(yield_to(pty, "data_received", 1), YIELD) + var expected := PoolByteArray( + [ + 101, + 120, + 101, + 99, + 118, + 112, + 40, + 51, + 41, + 32, + 102, + 97, + 105, + 108, + 101, + 100, + 46, + 58, + 32, + 78, + 111, + 32, + 115, + 117, + 99, + 104, + 32, + 102, + 105, + 108, + 101, + 32, + 111, + 114, + 32, + 100, + 105, + 114, + 101, + 99, + 116, + 111, + 114, + 121, + 13, + 10 + ] + ) + assert_signal_emitted_with_parameters(pty, "data_received", [expected]) + + func test_open_succeeds(): var result = pty.open() assert_eq(result[0], OK) diff --git a/test/unit/pty.test.gd b/test/unit/pty.test.gd new file mode 100644 index 0000000..f50be5c --- /dev/null +++ b/test/unit/pty.test.gd @@ -0,0 +1,102 @@ +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" + const PTY := preload("res://addons/godot_xterm/pty.gd") + + var pty: PTY + var mock_pty_native: MockPTY + + func before_each(): + pty = add_child_autofree(PTY.new()) + mock_pty_native = autofree(MockPTY.new()) + pty._pty_native = mock_pty_native + watch_signals(mock_pty_native) + + +class TestPTYInterfaceGodotXterm2_0_0: + extends BaseTest + # Test that PTY class conforms to the GodotXterm 2.0.0 specification published at: + # https://github.com/lihop/godot-xterm/wiki/PTY + + func test_has_property_terminal_path(): + assert_true("terminal_path" in pty, "Expected pty to have property terminal_path") + assert_typeof(pty.terminal_path, typeof(NodePath())) + + func test_has_property_cols(): + assert_true("cols" in pty, "Expected pty to have property cols.") + assert_typeof(pty.cols, typeof(0)) + + func test_has_property_rows(): + assert_true("rows" in pty, "Expected pty to have property rows.") + assert_typeof(pty.rows, typeof(0)) + + func test_has_property_env(): + assert_true("env" in pty, "Expected pty to have property env.") + assert_typeof(pty.env, typeof(Dictionary())) + + func test_has_property_use_os_env(): + assert_true("use_os_env" in pty, "Expected pty to have property use_os_env.") + assert_typeof(pty.use_os_env, typeof(false)) + + func test_has_method_fork(): + assert_has_method(pty, "fork") + + func test_has_method_kill(): + assert_has_method(pty, "kill") + + func test_has_method_open(): + assert_has_method(pty, "open") + + func test_has_method_resize(): + assert_has_method(pty, "resize") + + func test_has_method_resizev(): + assert_has_method(pty, "resizev") + + func test_has_method_write(): + assert_has_method(pty, "write") + + func test_has_signal_data_received(): + assert_has_signal(pty, "data_received") + + func test_has_signal_exited(): + assert_has_signal(pty, "exited") + + func test_has_enum_Signal(): + assert_true("Signal" in pty, "Expected pty to have enum Signal.") + assert_typeof(pty.Signal, 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.Signal.size(), + signals.size() - 1, + "Expected Signal enum to have at least %d members." % signals.size() + ) + for signame in signals.keys(): + assert_has(pty.Signal, signame, "Expected Signal enum to have member %s." % signame) + assert_eq( + pty.Signal[signame], + signals[signame], + "Expected Signal enum member %s to have value %d." % [signame, signals[signame]] + )