Rewrite terminal.cpp

Rewrites the Terminal class as a GDExtension to be used directly in
Godot without a terminal.gd proxy.

Breaks a lot of things in its current state (e.g. signals and other
functions have not be implemented yet), but does add support for
transparent colors and true color inversion. It also seems to
be about 4x faster (FPS-wise) than the old version with some basic
stress testing.

Old source code has been moved to a different directory to be copied
over and/or rewritten piece by piece.
This commit is contained in:
Leroy Hopson 2024-02-07 00:01:14 +13:00
parent 7d2e22530e
commit a849423096
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
29 changed files with 1431 additions and 857 deletions

View file

@ -0,0 +1,675 @@
/**
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
* Copyright (c) 2017, Daniel Imms (MIT License)
* Copyright (c) 2021, Leroy Hopson (MIT License)
*
* pty.cc:
* This file is responsible for starting processes
* with pseudo-terminal file descriptors.
*
* See:
* man pty
* man tty_ioctl
* man termios
* man forkpty
*/
/**
* Includes
*/
#include "pty.h"
#include "libuv_utils.h"
#include <godot_cpp/variant/callable.hpp>
#include <uv.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
/* forkpty */
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
#if defined(__GLIBC__) || defined(__CYGWIN__)
#include <pty.h>
#elif defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__)
#include <util.h>
#elif defined(__FreeBSD__)
#include <libutil.h>
#elif defined(__sun)
#include <stropts.h> /* for I_PUSH */
#else
#include <pty.h>
#endif
#include <termios.h> /* tcgetattr, tty_ioctl */
/* Some platforms name VWERASE and VDISCARD differently */
#if !defined(VWERASE) && defined(VWERSE)
#define VWERASE VWERSE
#endif
#if !defined(VDISCARD) && defined(VDISCRD)
#define VDISCARD VDISCRD
#endif
/* environ for execvpe */
/* node/src/node_child_process.cc */
#if defined(__APPLE__) && !TARGET_OS_IPHONE
#include <crt_externs.h>
#define environ (*_NSGetEnviron())
#else
extern char **environ;
#endif
/* for pty_getproc */
#if defined(__linux__)
#include <stdint.h>
#include <stdio.h>
#elif defined(__APPLE__)
#include <libproc.h>
#include <sys/sysctl.h>
#endif
/* NSIG - macro for highest signal + 1, should be defined */
#ifndef NSIG
#define NSIG 32
#endif
using namespace godot;
/**
* Structs
*/
struct pty_baton {
Callable cb;
int exit_code;
int signal_code;
pid_t pid;
uv_async_t async;
uv_thread_t tid;
};
/**
* Functions
*/
static int pty_execvpe(const char *, char **, char **);
static int pty_nonblock(int);
static char *pty_getproc(int, char *);
static int pty_openpty(int *, int *, char *, const struct termios *,
const struct winsize *);
static pid_t pty_forkpty(int *, char *, const struct termios *,
const struct winsize *);
static void pty_waitpid(void *);
static void pty_after_waitpid(uv_async_t *);
static void pty_after_close(uv_handle_t *);
Array PTYUnix::fork(String p_file, int _ignored, PackedStringArray p_args,
PackedStringArray p_env, String p_cwd, int p_cols,
int p_rows, int p_uid, int p_gid, bool p_utf8,
Callable p_on_exit) {
// file
char *file = strdup(p_file.utf8().get_data());
// args
int i = 0;
int argc = p_args.size();
int argl = argc + 1 + 1;
char **argv = new char *[argl];
argv[0] = strdup(file);
argv[argl - 1] = NULL;
for (; i < argc; i++) {
char *arg = strdup(p_args[i].utf8().get_data());
argv[i + 1] = strdup(arg);
}
// env
i = 0;
int envc = p_env.size();
char **env = new char *[envc + 1];
env[envc] = NULL;
for (; i < envc; i++) {
char *pairs = strdup(p_env[i].utf8().get_data());
env[i] = strdup(pairs);
}
// cwd
char *cwd = strdup(p_cwd.utf8().get_data());
// size
struct winsize winp;
winp.ws_col = p_cols;
winp.ws_row = p_rows;
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
// termios
struct termios t = termios();
struct termios *term = &t;
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
if (p_utf8) {
#if defined(IUTF8)
term->c_iflag |= IUTF8;
#endif
}
term->c_oflag = OPOST | ONLCR;
term->c_cflag = CREAD | CS8 | HUPCL;
term->c_lflag =
ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL;
term->c_cc[VEOF] = 4;
term->c_cc[VEOL] = -1;
term->c_cc[VEOL2] = -1;
term->c_cc[VERASE] = 0x7f;
term->c_cc[VWERASE] = 23;
term->c_cc[VKILL] = 21;
term->c_cc[VREPRINT] = 18;
term->c_cc[VINTR] = 3;
term->c_cc[VQUIT] = 0x1c;
term->c_cc[VSUSP] = 26;
term->c_cc[VSTART] = 17;
term->c_cc[VSTOP] = 19;
term->c_cc[VLNEXT] = 22;
term->c_cc[VDISCARD] = 15;
term->c_cc[VMIN] = 1;
term->c_cc[VTIME] = 0;
#if (__APPLE__)
term->c_cc[VDSUSP] = 25;
term->c_cc[VSTATUS] = 20;
#endif
cfsetispeed(term, B38400);
cfsetospeed(term, B38400);
// uid / gid
int uid = p_uid;
int gid = p_gid;
// fork the pty
int master = -1;
sigset_t newmask, oldmask;
struct sigaction sig_action;
// temporarily block all signals
// this is needed due to a race condition in openpty
// and to avoid running signal handlers in the child
// before exec* happened
sigfillset(&newmask);
pthread_sigmask(SIG_SETMASK, &newmask, &oldmask);
pid_t pid = pty_forkpty(&master, nullptr, term, &winp);
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
sigaction(i, &sig_action, NULL);
}
}
// reenable signals
pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
if (pid) {
for (i = 0; i < argl; i++)
std::free(argv[i]);
delete[] argv;
for (i = 0; i < envc; i++)
std::free(env[i]);
delete[] env;
std::free(cwd);
}
switch (pid) {
case -1:
ERR_PRINT("forkpty(3) failed.");
return Array::make(FAILED);
case 0:
if (strlen(cwd)) {
if (chdir(cwd) == -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);
}
}
pty_execvpe(argv[0], argv, env);
perror("execvp(3) failed.");
_exit(1);
default:
if (pty_nonblock(master) == -1) {
ERR_PRINT("Could not set master fd to nonblocking.");
return Array::make(FAILED);
}
Dictionary result;
result["fd"] = (int)master;
result["pid"] = (int)pid;
result["pty"] = ptsname(master);
pty_baton *baton = new pty_baton();
baton->exit_code = 0;
baton->signal_code = 0;
baton->cb = p_on_exit;
baton->pid = pid;
baton->async.data = baton;
uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid);
uv_thread_create(&baton->tid, pty_waitpid, static_cast<void *>(baton));
return Array::make(OK, result);
}
return Array::make(FAILED);
}
Array PTYUnix::open(int p_cols, int p_rows) {
// size
struct winsize winp;
winp.ws_col = p_cols;
winp.ws_row = p_rows;
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
// pty
int master, slave;
int ret = pty_openpty(&master, &slave, nullptr, NULL, &winp);
if (ret == -1) {
ERR_PRINT("openpty(3) failed.");
return Array::make(FAILED);
}
if (pty_nonblock(master) == -1) {
ERR_PRINT("Could not set master fd to nonblocking.");
return Array::make(FAILED);
}
if (pty_nonblock(slave) == -1) {
ERR_PRINT("Could not set slave fd to nonblocking.");
return Array::make(FAILED);
}
Dictionary dict;
dict["master"] = master;
dict["slave"] = slave;
dict["pty"] = ptsname(master);
return Array::make(OK, dict);
}
Error PTYUnix::resize(int p_fd, int p_cols, int p_rows) {
int fd = p_fd;
struct winsize winp;
winp.ws_col = p_cols;
winp.ws_row = p_rows;
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
switch (errno) {
case EBADF:
RETURN_UV_ERR(UV_EBADF)
case EFAULT:
RETURN_UV_ERR(UV_EFAULT)
case EINVAL:
RETURN_UV_ERR(UV_EINVAL);
case ENOTTY:
RETURN_UV_ERR(UV_ENOTTY);
}
ERR_PRINT("ioctl(2) failed");
return FAILED;
}
return OK;
}
/**
* Foreground Process Name
*/
String PTYUnix::process(int p_fd, String p_tty) {
int fd = p_fd;
char *tty = strdup(p_tty.utf8().get_data());
char *name = pty_getproc(fd, tty);
std::free(tty);
if (name == NULL) {
return "";
}
String name_ = String(name);
std::free(name);
return name_;
}
/**
* execvpe
*/
// execvpe(3) is not portable.
// http://www.gnu.org/software/gnulib/manual/html_node/execvpe.html
static int pty_execvpe(const char *file, char **argv, char **envp) {
char **old = environ;
environ = envp;
int ret = execvp(file, argv);
environ = old;
return ret;
}
/**
* Nonblocking FD
*/
static int pty_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/**
* pty_waitpid
* Wait for SIGCHLD to read exit status.
*/
static void pty_waitpid(void *data) {
int ret;
int stat_loc;
pty_baton *baton = static_cast<pty_baton *>(data);
errno = 0;
if ((ret = waitpid(baton->pid, &stat_loc, 0)) != baton->pid) {
if (ret == -1 && errno == EINTR) {
return pty_waitpid(baton);
}
if (ret == -1 && errno == ECHILD) {
// XXX node v0.8.x seems to have this problem.
// waitpid is already handled elsewhere.
;
} else {
// assert(false);
}
}
if (WIFEXITED(stat_loc)) {
baton->exit_code = WEXITSTATUS(stat_loc); // errno?
}
if (WIFSIGNALED(stat_loc)) {
baton->signal_code = WTERMSIG(stat_loc);
}
uv_async_send(&baton->async);
}
/**
* pty_after_waitpid
* Callback after exit status has been read.
*/
static void pty_after_waitpid(uv_async_t *async) {
pty_baton *baton = static_cast<pty_baton *>(async->data);
Array argv = Array::make(baton->exit_code, baton->signal_code);
if (baton->cb.is_valid()) {
baton->cb.callv(argv);
baton->cb = (Variant) nullptr;
}
uv_close((uv_handle_t *)async, pty_after_close);
}
/**
* pty_after_close
* uv_close() callback - free handle data
*/
static void pty_after_close(uv_handle_t *handle) {
uv_async_t *async = (uv_async_t *)handle;
pty_baton *baton = static_cast<pty_baton *>(async->data);
delete baton;
}
/**
* pty_getproc
* Taken from tmux.
*/
// Taken from: tmux (http://tmux.sourceforge.net/)
// Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
// Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
// Copyright (c) 2009 Todd Carson <toc@daybefore.net>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#if defined(__linux__)
static char *pty_getproc(int fd, char *tty) {
FILE *f;
char *path, *buf;
size_t len;
int ch;
pid_t pgrp;
int r;
if ((pgrp = tcgetpgrp(fd)) == -1) {
return NULL;
}
r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp);
if (r == -1 || path == NULL)
return NULL;
if ((f = fopen(path, "r")) == NULL) {
free(path);
return NULL;
}
free(path);
len = 0;
buf = NULL;
while ((ch = fgetc(f)) != EOF) {
if (ch == '\0')
break;
buf = (char *)realloc(buf, len + 2);
if (buf == NULL)
return NULL;
buf[len++] = ch;
}
if (buf != NULL) {
buf[len] = '\0';
}
fclose(f);
return buf;
}
#elif defined(__APPLE__)
static char *pty_getproc(int fd, char *tty) {
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, 0};
size_t size;
struct kinfo_proc kp;
if ((mib[3] = tcgetpgrp(fd)) == -1) {
return NULL;
}
size = sizeof kp;
if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) {
return NULL;
}
if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') {
return NULL;
}
return strdup(kp.kp_proc.p_comm);
}
#else
static char *pty_getproc(int fd, char *tty) { return NULL; }
#endif
/**
* openpty(3) / forkpty(3)
*/
static int pty_openpty(int *amaster, int *aslave, char *name,
const struct termios *termp,
const struct winsize *winp) {
#if defined(__sun)
char *slave_name;
int slave;
int master = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (master == -1)
return -1;
if (amaster)
*amaster = master;
if (grantpt(master) == -1)
goto err;
if (unlockpt(master) == -1)
goto err;
slave_name = ptsname(master);
if (slave_name == NULL)
goto err;
if (name)
strcpy(name, slave_name);
slave = open(slave_name, O_RDWR | O_NOCTTY);
if (slave == -1)
goto err;
if (aslave)
*aslave = slave;
ioctl(slave, I_PUSH, "ptem");
ioctl(slave, I_PUSH, "ldterm");
ioctl(slave, I_PUSH, "ttcompat");
if (termp)
tcsetattr(slave, TCSAFLUSH, termp);
if (winp)
ioctl(slave, TIOCSWINSZ, winp);
return 0;
err:
close(master);
return -1;
#else
return openpty(amaster, aslave, name, (termios *)termp, (winsize *)winp);
#endif
}
static pid_t pty_forkpty(int *amaster, char *name, const struct termios *termp,
const struct winsize *winp) {
#if defined(__sun)
int master, slave;
int ret = pty_openpty(&master, &slave, name, termp, winp);
if (ret == -1)
return -1;
if (amaster)
*amaster = master;
pid_t pid = fork();
switch (pid) {
case -1: // error in fork, we are still in parent
close(master);
close(slave);
return -1;
case 0: // we are in the child process
close(master);
setsid();
#if defined(TIOCSCTTY)
// glibc does this
if (ioctl(slave, TIOCSCTTY, NULL) == -1) {
_exit(1);
}
#endif
dup2(slave, 0);
dup2(slave, 1);
dup2(slave, 2);
if (slave > 2)
close(slave);
return 0;
default: // we are in the parent process
close(slave);
return pid;
}
return -1;
#else
return forkpty(amaster, name, (termios *)termp, (winsize *)winp);
#endif
}
/**
* Init
*/
void PTYUnix::_bind_methods() {
ClassDB::bind_method(D_METHOD("_init"), &PTYUnix::_init);
ClassDB::bind_method(D_METHOD("fork"), &PTYUnix::fork);
ClassDB::bind_method(D_METHOD("open"), &PTYUnix::open);
ClassDB::bind_method(D_METHOD("resize"), &PTYUnix::resize);
ClassDB::bind_method(D_METHOD("process"), &PTYUnix::process);
}
void PTYUnix::_init() {}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2021-2022 Leroy Hopson <godot-xterm@leroy.geek.nz>
// SPDX-License-Identifier: MIT
#ifndef GODOT_XTERM_PTY_H
#define GODOT_XTERM_PTY_H
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/callable.hpp>
namespace godot {
class PTYUnix : public RefCounted {
GDCLASS(PTYUnix, RefCounted)
public:
Array fork(String file,
int _ignored, /* FIXME: For some reason Pipe throws
ENOTSOCK in read callback if args (or another non-empty,
non-zero) value is in this position. */
PackedStringArray args, PackedStringArray env, String cwd, int cols,
int rows, int uid, int gid, bool utf8, Callable on_exit);
Array open(int cols, int rows);
Error resize(int fd, int cols, int rows);
String process(int fd, String tty);
void _init();
protected:
static void _bind_methods();
};
} // namespace godot
#endif // GODOT_XTERM_PTY_H