feat(rt2): spawn_pty + resize — a child that believes it owns a terminal
- posix_openpt/grantpt/unlockpt/ptsname_r (plain libc, no -lutil); child setsid + opens the slave as its controlling terminal, initial TIOCSWINSZ from the call - Child.stdin == Child.stdout = the master (caller's copy); the slot keeps a private dup so resize survives the caller closing theirs - proc.resize -> TIOCSWINSZ; refuses by name on a pipe child - legs: test -t proves a real tty; stty size reads "24 80" then "40 120" after a mid-sleep resize; refusal asserted; test_proc 193/0 ASan clean Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 9836c9cd5197153517c054b243cab3b453d130a0)
This commit is contained in:
parent
803ff0b790
commit
0c7d0530e9
3 changed files with 270 additions and 0 deletions
|
|
@ -340,6 +340,10 @@ let stdlib_members : stdlib_member list =
|
|||
m "proc" "spawn" 2 97 (Some (TNullable (TScalar child_record_name))) (Some child_record_name);
|
||||
m "proc" "wait_dl" 2 98 (Some (TNullable (TScalar "Int"))) None;
|
||||
m "proc" "signal" 2 99 None None;
|
||||
(* runtime-v2 2: the PTY child — stdin==stdout=master, stderr -1;
|
||||
resize refuses by name on a pipe child *)
|
||||
m "proc" "spawn_pty" 4 100 (Some (TNullable (TScalar child_record_name))) (Some child_record_name);
|
||||
m "proc" "resize" 3 101 None None;
|
||||
(* json — both members are lowered specially (emit.ml): encode needs its
|
||||
argument's static kind, and decode has no type until an `as` names one,
|
||||
so neither goes through the generic builtin path. They are listed here
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@
|
|||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <termios.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <signal.h>
|
||||
|
|
@ -1275,6 +1277,107 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
fb->park_done = 0;
|
||||
return WO_SYS_PARKED;
|
||||
}
|
||||
/* ---- runtime-v2 2: the PTY child ---------------------------------- */
|
||||
case WO_B_PROC_SPAWN_PTY: { /* Child: stdin==stdout=master (raw side —
|
||||
* the line discipline lives on the slave),
|
||||
* stderr -1. The slot keeps a private dup
|
||||
* of the master so resize survives the
|
||||
* caller closing its copy. */
|
||||
char argbuf[62][512];
|
||||
char *argv[64];
|
||||
if (proc_argv(R[B], R[B + 1], path, sizeof path, argbuf, argv, msg))
|
||||
return WO_T_BOUNDS;
|
||||
int cols = (int)(int64_t)R[B + 2], rows = (int)(int64_t)R[B + 3];
|
||||
if (cols <= 0) cols = 80;
|
||||
if (rows <= 0) rows = 24;
|
||||
wo_child *ch = proc_slot_claim(vm, msg);
|
||||
if (!ch) return WO_T_IO;
|
||||
int master = posix_openpt(O_RDWR | O_NOCTTY);
|
||||
char sname[128];
|
||||
if (master < 0 || grantpt(master) != 0 || unlockpt(master) != 0 ||
|
||||
ptsname_r(master, sname, sizeof sname) != 0) {
|
||||
if (master >= 0) close(master);
|
||||
*msg = strerror(errno);
|
||||
return WO_T_IO;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
close(master);
|
||||
*msg = strerror(errno);
|
||||
return WO_T_IO;
|
||||
}
|
||||
if (pid == 0) {
|
||||
setsid(); /* the slave becomes the CONTROLLING terminal */
|
||||
int slave = open(sname, O_RDWR);
|
||||
if (slave < 0) _exit(127);
|
||||
struct winsize ws;
|
||||
memset(&ws, 0, sizeof ws);
|
||||
ws.ws_col = (unsigned short)cols;
|
||||
ws.ws_row = (unsigned short)rows;
|
||||
ioctl(slave, TIOCSWINSZ, &ws);
|
||||
dup2(slave, STDIN_FILENO);
|
||||
dup2(slave, STDOUT_FILENO);
|
||||
dup2(slave, STDERR_FILENO);
|
||||
close(slave);
|
||||
close(master);
|
||||
execvp(path, argv);
|
||||
_exit(127);
|
||||
}
|
||||
fcntl(master, F_SETFL, fcntl(master, F_GETFL, 0) | O_NONBLOCK);
|
||||
int pidfd = (int)syscall(SYS_pidfd_open, pid, 0);
|
||||
if (pidfd < 0) {
|
||||
int e = errno;
|
||||
kill(pid, SIGKILL);
|
||||
int st;
|
||||
while (waitpid(pid, &st, 0) < 0 && errno == EINTR) {}
|
||||
close(master);
|
||||
*msg = strerror(e);
|
||||
return WO_T_IO;
|
||||
}
|
||||
memset(ch, 0, sizeof *ch);
|
||||
ch->used = 1;
|
||||
ch->streaming = 1;
|
||||
ch->pid = (int)pid;
|
||||
ch->pidfd = pidfd;
|
||||
ch->epfd = -1;
|
||||
ch->ofd = ch->efd = -1;
|
||||
ch->master_dup = dup(master);
|
||||
ch->gen = ++vm->proc_gen;
|
||||
ch->owner_actor = vm->cur->actor;
|
||||
vm->nchildren++;
|
||||
wo_hdr *o = record_of(vm, R[B + 4], 4, msg);
|
||||
if (!o) {
|
||||
close(master);
|
||||
proc_slot_kill(vm, ch);
|
||||
return R[B + 4] >= vm->mod->class_cnt ? WO_T_BOUNDS : WO_T_OOM;
|
||||
}
|
||||
uint64_t *fp = wo_fields(o);
|
||||
fp[0] = ((uint64_t)ch->gen << 6) | (uint64_t)(ch - vm->children);
|
||||
fp[1] = (uint64_t)master;
|
||||
fp[2] = (uint64_t)master;
|
||||
fp[3] = (uint64_t)(int64_t)-1;
|
||||
R[A] = (uint64_t)(uintptr_t)o;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_PROC_RESIZE: { /* (id, cols, rows) -> 0: TIOCSWINSZ through
|
||||
* the slot's own master dup */
|
||||
wo_child *ch = proc_slot_by_id(vm, R[B], msg);
|
||||
if (!ch) return WO_T_IO;
|
||||
if (ch->master_dup < 0) {
|
||||
*msg = "child has no terminal to resize";
|
||||
return WO_T_IO;
|
||||
}
|
||||
struct winsize ws;
|
||||
memset(&ws, 0, sizeof ws);
|
||||
ws.ws_col = (unsigned short)(int64_t)R[B + 1];
|
||||
ws.ws_row = (unsigned short)(int64_t)R[B + 2];
|
||||
if (ioctl(ch->master_dup, TIOCSWINSZ, &ws) != 0) {
|
||||
*msg = strerror(errno);
|
||||
return WO_T_IO;
|
||||
}
|
||||
R[A] = 0;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_PROC_SIGNAL: { /* (id, sig) -> 0 through the pidfd */
|
||||
wo_child *ch = proc_slot_by_id(vm, R[B], msg);
|
||||
if (!ch) return WO_T_IO;
|
||||
|
|
|
|||
|
|
@ -881,6 +881,166 @@ static void test_stream_churn(void) {
|
|||
free(img);
|
||||
}
|
||||
|
||||
/* ---- runtime-v2 2: the PTY child ----------------------------------------
|
||||
* pty module: spawn_pty `sh -c <script>` at cols x rows; optionally sleep
|
||||
* then resize; read once from the master; close; return the text. */
|
||||
static uint8_t *pty_module(const char *script, int64_t cols, int64_t rows,
|
||||
int do_resize, int64_t rcols, int64_t rrows,
|
||||
size_t *len) {
|
||||
wb_t *b = wb_new();
|
||||
uint32_t kchild = wb_const_text(b, "Child");
|
||||
uint32_t km = wb_const_text(b, "main");
|
||||
uint32_t kcmd = wb_const_text(b, "sh");
|
||||
uint32_t kdc = wb_const_text(b, "-c");
|
||||
uint32_t kscript = wb_const_text(b, script);
|
||||
uint8_t kinds[4] = {WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR};
|
||||
uint32_t cls = wb_class(b, kchild, 0, kinds, 4);
|
||||
uint32_t kcls = wb_const_int(b, (int64_t)cls);
|
||||
uint32_t kcols = wb_const_int(b, cols);
|
||||
uint32_t krows = wb_const_int(b, rows);
|
||||
uint32_t krc = wb_const_int(b, rcols);
|
||||
uint32_t krr = wb_const_int(b, rrows);
|
||||
uint32_t k100 = wb_const_int(b, 100);
|
||||
uint32_t kms = wb_const_int(b, 4000);
|
||||
uint32_t kmax = wb_const_int(b, 64);
|
||||
uint32_t code[40];
|
||||
uint32_t n = 0;
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 1, (uint16_t)kcmd);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 2, WO_K_TEXT, WO_B_MULTI_NEW);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kdc);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 6, 2, WO_B_MULTI_PUSH);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kscript);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 6, 2, WO_B_MULTI_PUSH);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kcols);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 4, (uint16_t)krows);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)kcls);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PROC_SPAWN_PTY);
|
||||
code[n++] = wo_ins_abc(WOP_DROP, 2, 0, 0);
|
||||
code[n++] = wo_ins_abc(WOP_GETF, 9, 0, 1); /* the master */
|
||||
code[n++] = wo_ins_abc(WOP_GETF, 4, 0, 0); /* the id */
|
||||
if (do_resize) { /* let the child start, then resize */
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 1, (uint16_t)k100);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_TIME_SLEEP);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)krc);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 6, (uint16_t)krr);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 4, WO_B_PROC_RESIZE);
|
||||
}
|
||||
code[n++] = wo_ins_abc(WOP_MOVE, 4, 9, 0);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)kmax);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 6, (uint16_t)kms);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 7, 4, WO_B_NET_READ_DL);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 9, WO_B_NET_CLOSE);
|
||||
code[n++] = wo_ins_abc(WOP_DROP, 0, 0, 0);
|
||||
code[n++] = wo_ins_abc(WOP_RET, 7, 0, 0);
|
||||
wb_method(b, km, WOB_NONE, 0, 10, code, n, NULL, 0, NULL, 0);
|
||||
return wb_finish(b, len);
|
||||
}
|
||||
|
||||
/* run a pty module, copy the returned text into out */
|
||||
static void run_pty(const char *script, int64_t cols, int64_t rows,
|
||||
int do_resize, int64_t rcols, int64_t rrows, char *out,
|
||||
size_t outcap) {
|
||||
size_t len;
|
||||
uint8_t *img = pty_module(script, cols, rows, do_resize, rcols, rrows, &len);
|
||||
wo_module mod;
|
||||
char lerr[256];
|
||||
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
|
||||
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
|
||||
uint64_t ret = 0;
|
||||
wo_err err;
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 0, NULL, 0, &ret, &err), 0);
|
||||
out[0] = '\0';
|
||||
const wo_str *s = (const wo_str *)(uintptr_t)ret;
|
||||
if (ret != 0 && ret != WO_NIL_SCALAR && s) {
|
||||
size_t c = s->len < outcap - 1 ? s->len : outcap - 1;
|
||||
memcpy(out, s->data, c);
|
||||
out[c] = '\0';
|
||||
wo_str_free(&VM.rt, (wo_str *)s);
|
||||
}
|
||||
wo_vm_destroy(&VM);
|
||||
int st;
|
||||
T_EQ(waitpid(-1, &st, WNOHANG), -1);
|
||||
T_EQ(errno, ECHILD);
|
||||
wo_module_free(&mod);
|
||||
free(img);
|
||||
}
|
||||
|
||||
static void test_pty_is_a_tty(void) {
|
||||
char out[128];
|
||||
run_pty("test -t 0 && test -t 1 && echo yes-tty", 24, 80, 0, 0, 0, out,
|
||||
sizeof out);
|
||||
T_CHECK(strncmp(out, "yes-tty", 7) == 0); /* discipline adds \r\n */
|
||||
}
|
||||
|
||||
static void test_pty_size_and_resize(void) {
|
||||
char out[128];
|
||||
/* the initial size is what spawn_pty stated */
|
||||
run_pty("stty size", 80, 24, 0, 0, 0, out, sizeof out);
|
||||
T_CHECK(strncmp(out, "24 80", 5) == 0);
|
||||
/* a resize during the child's sleep is what stty then reports */
|
||||
run_pty("sleep 0.3; stty size", 80, 24, 1, 120, 40, out, sizeof out);
|
||||
T_CHECK(strncmp(out, "40 120", 6) == 0);
|
||||
}
|
||||
|
||||
/* resize on a pipe child refuses by name */
|
||||
static uint8_t *pipe_resize_module(size_t *len) {
|
||||
wb_t *b = wb_new();
|
||||
uint32_t kchild = wb_const_text(b, "Child");
|
||||
uint32_t km = wb_const_text(b, "main");
|
||||
uint32_t kcmd = wb_const_text(b, "sleep");
|
||||
uint32_t karg = wb_const_text(b, "1");
|
||||
uint8_t kinds[4] = {WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR};
|
||||
uint32_t cls = wb_class(b, kchild, 0, kinds, 4);
|
||||
uint32_t kcls = wb_const_int(b, (int64_t)cls);
|
||||
uint32_t k80 = wb_const_int(b, 80);
|
||||
uint32_t k24 = wb_const_int(b, 24);
|
||||
uint32_t code[24];
|
||||
uint32_t n = 0;
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 1, (uint16_t)kcmd);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 2, WO_K_TEXT, WO_B_MULTI_NEW);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)karg);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 6, 2, WO_B_MULTI_PUSH);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kcls);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PROC_SPAWN);
|
||||
code[n++] = wo_ins_abc(WOP_DROP, 2, 0, 0);
|
||||
code[n++] = wo_ins_abc(WOP_GETF, 1, 0, 1);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_NET_CLOSE);
|
||||
code[n++] = wo_ins_abc(WOP_GETF, 1, 0, 2);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_NET_CLOSE);
|
||||
code[n++] = wo_ins_abc(WOP_GETF, 1, 0, 3);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_NET_CLOSE);
|
||||
code[n++] = wo_ins_abc(WOP_GETF, 4, 0, 0);
|
||||
code[n++] = wo_ins_abc(WOP_DROP, 0, 0, 0);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)k80);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 6, (uint16_t)k24);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 4, WO_B_PROC_RESIZE);
|
||||
code[n++] = wo_ins_abc(WOP_RET0, 0, 0, 0);
|
||||
wb_method(b, km, WOB_NONE, 0, 9, code, n, NULL, 0, NULL, 0);
|
||||
return wb_finish(b, len);
|
||||
}
|
||||
|
||||
static void test_resize_refuses_on_pipe_child(void) {
|
||||
size_t len;
|
||||
uint8_t *img = pipe_resize_module(&len);
|
||||
wo_module mod;
|
||||
char lerr[256];
|
||||
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
|
||||
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
|
||||
uint64_t ret = 0;
|
||||
wo_err err;
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 0, NULL, 0, &ret, &err), -1);
|
||||
T_EQ(err.code, WO_T_IO);
|
||||
T_CHECK(strstr(err.msg, "terminal") != NULL);
|
||||
wo_vm_destroy(&VM);
|
||||
int st;
|
||||
T_EQ(waitpid(-1, &st, WNOHANG), -1);
|
||||
T_EQ(errno, ECHILD);
|
||||
wo_module_free(&mod);
|
||||
free(img);
|
||||
}
|
||||
|
||||
static void on_alarm(int sig) { (void)sig; } /* interrupt, don't die */
|
||||
|
||||
static int64_t mono_ms(void) {
|
||||
|
|
@ -931,6 +1091,9 @@ int main(void) {
|
|||
test_stream_wait();
|
||||
test_stream_one_waiter();
|
||||
test_stream_churn();
|
||||
test_pty_is_a_tty();
|
||||
test_pty_size_and_resize();
|
||||
test_resize_refuses_on_pipe_child();
|
||||
test_stop_kills_child(); /* last: it latches the stop flag */
|
||||
return t_report("test_proc");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue