mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-22 09:40:25 +01:00
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.
This commit is contained in:
parent
d11853e9f6
commit
df32ee3c18
8 changed files with 539 additions and 333 deletions
|
@ -61,8 +61,8 @@ func _ready():
|
||||||
|
|
||||||
|
|
||||||
func _poll():
|
func _poll():
|
||||||
if pty and pty._pipe:
|
if pty and pty.has_method("get_master"):
|
||||||
pty._pipe.poll()
|
pty.get_master().poll()
|
||||||
update()
|
update()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
|
28
addons/godot_xterm/nodes/pty/pty_native.gd
Normal file
28
addons/godot_xterm/nodes/pty/pty_native.gd
Normal file
|
@ -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
|
230
addons/godot_xterm/nodes/pty/unix/pty_unix.gd
Normal file
230
addons/godot_xterm/nodes/pty/unix/pty_unix.gd
Normal file
|
@ -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
|
|
@ -27,7 +27,7 @@ func _enter_tree():
|
||||||
var pty_script
|
var pty_script
|
||||||
match OS.get_name():
|
match OS.get_name():
|
||||||
"X11", "Server", "OSX":
|
"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)
|
add_custom_type("PTY", "Node", pty_script, pty_icon)
|
||||||
terminal_panel = preload("./editor_plugins/terminal/terminal_panel.tscn").instance()
|
terminal_panel = preload("./editor_plugins/terminal/terminal_panel.tscn").instance()
|
||||||
terminal_panel.editor_plugin = self
|
terminal_panel.editor_plugin = self
|
||||||
|
|
|
@ -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) 2012-2015, Christopher Jeffrey (MIT License).
|
||||||
# Copyright (c) 2016, Daniel Imms (MIT License).
|
# Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
# Copyright (c) 2018, Microsoft Corporation (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
|
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
|
# Any signal_number can be sent to the pty's process using the kill() function,
|
||||||
# with higher privileges might leak to the child program.
|
# these are just the signals with numbers specified in the POSIX standard.
|
||||||
var uid: int
|
const Signal = _PTYUnix.Signal
|
||||||
var gid: int
|
|
||||||
|
|
||||||
var thread: Thread
|
signal data_received(data)
|
||||||
|
signal exited(exit_code, signum)
|
||||||
|
|
||||||
var _fd: int = -1
|
export(NodePath) var terminal_path := NodePath() setget set_terminal_path
|
||||||
var _exit_cb: FuncRef
|
|
||||||
|
|
||||||
#static func get_uid() -> int:
|
var _terminal: _Terminal = null setget _set_terminal
|
||||||
# return -1 # Not implemented.
|
|
||||||
|
|
||||||
#static func get_gid() -> int:
|
# The column size in characters.
|
||||||
# return -1 # Not implemented.
|
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:
|
func _init():
|
||||||
if _fd < 0:
|
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
|
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):
|
# Writes data to the socket.
|
||||||
var result = preload("./nodes/pty/unix/pty_unix.gdns").new().callv("fork", args)
|
# data: The data to write.
|
||||||
return result
|
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(
|
func fork(
|
||||||
file: String = OS.get_environment("SHELL"),
|
file: String = OS.get_environment("SHELL"),
|
||||||
args: PoolStringArray = PoolStringArray(),
|
args: PoolStringArray = PoolStringArray(),
|
||||||
cwd = LibuvUtils.get_cwd(),
|
cwd = _LibuvUtils.get_cwd(),
|
||||||
p_cols: int = DEFAULT_COLS,
|
p_cols: int = DEFAULT_COLS,
|
||||||
p_rows: int = DEFAULT_ROWS,
|
p_rows: int = DEFAULT_ROWS,
|
||||||
uid: int = -1,
|
uid: int = -1,
|
||||||
gid: int = -1,
|
gid: int = -1,
|
||||||
utf8 = true
|
utf8 = true
|
||||||
) -> int:
|
) -> int:
|
||||||
# File.
|
return _pty_native.fork(file, args, cwd, p_cols, p_rows, uid, gid, utf8)
|
||||||
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:
|
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():
|
func get_master():
|
||||||
if _pid > 1:
|
return _pty_native.get_master()
|
||||||
LibuvUtils.kill(_pid, Signal.SIGHUP)
|
|
||||||
while _pipe.get_status() != 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
func _on_pipe_data_received(data):
|
func _on_pty_native_data_received(data):
|
||||||
emit_signal("data_received", data)
|
emit_signal("data_received", data)
|
||||||
|
|
||||||
|
|
||||||
func _on_exit(exit_code: int, signum: int) -> void:
|
func _on_pty_native_exited(exit_code: int, signum: int) -> void:
|
||||||
if is_instance_valid(self):
|
emit_signal("exited", exit_code, signum)
|
||||||
_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
|
|
||||||
|
|
|
@ -24,6 +24,62 @@ func test_fork_succeeds():
|
||||||
assert_eq(err, OK)
|
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():
|
func test_open_succeeds():
|
||||||
var result = pty.open()
|
var result = pty.open()
|
||||||
assert_eq(result[0], OK)
|
assert_eq(result[0], OK)
|
||||||
|
|
102
test/unit/pty.test.gd
Normal file
102
test/unit/pty.test.gd
Normal file
|
@ -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]]
|
||||||
|
)
|
Loading…
Reference in a new issue