godot-xterm/addons/godot_xterm/nodes/pty/unix/pty_unix.gd

221 lines
5.7 KiB
GDScript

# 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 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 IPCSignal {
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
}
var _LibuvUtils = (
ClassDB.instantiate("LibuvUtils").get_class() if ClassDB.class_exists("LibuvUtils") else null
)
var _Pipe = ClassDB.instantiate("Pipe").get_class() if ClassDB.class_exists("Pipe") else null
var _PTYUnix = (
ClassDB.instantiate("PTYUnix").get_class() if ClassDB.class_exists("PTYUnix") else null
)
# The name of the process.
#var process: String
# The process ID.
var _pid: int
# 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: Callable
# Writes data to the socket.
# data: The data to write.
func write(data) -> void:
var correct_type: bool = data is PackedByteArray or data is String
var err_message := "Invalid type for argument 'data'. Should be of type PackedByteArray or String"
assert(correct_type, err_message)
if _pipe:
_pipe.write(data if data is PackedByteArray else data.to_utf8_buffer())
func resize(cols: int, rows: int) -> void:
if _fd >= 0:
_PTYUnix.new().resize(_fd, cols, rows)
func kill(signum: int = IPCSignal.SIGHUP) -> void:
if _pipe:
_pipe.close()
if _pid > 0:
_LibuvUtils.kill(_pid, signum)
func _parse_env(env: Dictionary = {}) -> PackedStringArray:
var keys := env.keys()
var pairs := PackedStringArray()
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: PackedStringArray = PackedStringArray(),
cwd = _LibuvUtils.get_cwd(),
cols: int = DEFAULT_COLS,
rows: int = DEFAULT_ROWS,
uid: int = -1,
gid: int = -1,
utf8 = true
) -> int:
# File.
if file.is_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: PackedStringArray = _parse_env(final_env)
# Exit callback.
_exit_cb = Callable(self, "on_exit")
# Actual fork.
var result = (
_PTYUnix
. new()
. fork(
# VERY IMPORTANT: The second argument must be 0, otherwise will get an ENOTSOCK error after connecting our pipe to the fd.
file,
0,
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, false)
# Must connect to signal AFTER opening, otherwise we will get error ENOTSOCK.
_pipe.connect("data_received", Callable(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 _exit_tree():
_exit_cb = Callable()
if _pid > 1:
_LibuvUtils.kill(_pid, IPCSignal.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