feat(rt2): term.raw/restore — no wrecked tty, ever
- two verbs on any tty fd; saved termios in a per-shard 8-entry table; double-raw and restore-without-save refuse by name - restore is a RUNTIME obligation: vm_unwind at depth 0 (uncaught trap, fiber reap) restores the dying fiber's entries newest-first, and wo_vm_destroy sweeps the rest — proven twice in the legs: a DIV0 while raw restores, and even the double-raw REFUSAL (itself a trap) restores the first raw - test_term 39/0 against a real PTY pair made by the test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit b439387dbf4f653e034c721bc3f08b83616c5e24)
This commit is contained in:
parent
c55d6e1d33
commit
22ba51bfd3
5 changed files with 192 additions and 0 deletions
|
|
@ -348,6 +348,10 @@ let stdlib_members : stdlib_member list =
|
|||
Signal {sig} record to the actor. SIGTERM/SIGINT refused (the stop
|
||||
latch). Coalescing disclosed. *)
|
||||
m "signal" "on" 2 102 None (Some signal_record_name);
|
||||
(* runtime-v2 4: raw mode on a tty the process was GIVEN; restore is
|
||||
a runtime obligation (unwind/stop), never only the caller's *)
|
||||
m "term" "raw" 1 103 None None;
|
||||
m "term" "restore" 1 104 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
|
||||
|
|
|
|||
|
|
@ -300,6 +300,26 @@ void wo_vm_signals_drain(wo_vm *vm) {
|
|||
}
|
||||
}
|
||||
|
||||
/* ---- runtime-v2 4: termios adoption -----------------------------------
|
||||
* term.raw saves into the shard table and cfmakeraw's the fd; restore is
|
||||
* a RUNTIME obligation — vm_unwind (full), fib_reap and wo_vm_destroy
|
||||
* all sweep, newest-first. */
|
||||
void wo_term_abandon(wo_vm *vm, wo_fiber *fb) {
|
||||
for (int i = 7; i >= 0; i--)
|
||||
if (vm->ttysave[i].used && vm->ttysave[i].owner == fb) {
|
||||
tcsetattr(vm->ttysave[i].fd, TCSANOW, &vm->ttysave[i].saved);
|
||||
vm->ttysave[i].used = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void wo_term_restore_all(wo_vm *vm) {
|
||||
for (int i = 7; i >= 0; i--)
|
||||
if (vm->ttysave[i].used) {
|
||||
tcsetattr(vm->ttysave[i].fd, TCSANOW, &vm->ttysave[i].saved);
|
||||
vm->ttysave[i].used = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* claim a slot or refuse by name (shared by run/run_dl/spawn forms) */
|
||||
static wo_child *proc_slot_claim(wo_vm *vm, const char **msg) {
|
||||
for (uint32_t i = 0; i < WO_PROC_MAX; i++)
|
||||
|
|
@ -1463,6 +1483,54 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
R[A] = 0;
|
||||
return 0;
|
||||
}
|
||||
/* ---- runtime-v2 4: termios adoption ------------------------------- */
|
||||
case WO_B_TERM_RAW: { /* (fd) -> 0: save, then cfmakeraw */
|
||||
int fd = (int)(int64_t)R[B];
|
||||
int slot = -1;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (vm->ttysave[i].used && vm->ttysave[i].fd == fd) {
|
||||
*msg = "term.raw: fd is already raw";
|
||||
return WO_T_IO;
|
||||
}
|
||||
if (!vm->ttysave[i].used && slot < 0) slot = i;
|
||||
}
|
||||
if (slot < 0) {
|
||||
*msg = "term.raw: saved-termios table full (8)";
|
||||
return WO_T_IO;
|
||||
}
|
||||
struct termios t;
|
||||
if (tcgetattr(fd, &t) != 0) {
|
||||
*msg = strerror(errno);
|
||||
return WO_T_IO;
|
||||
}
|
||||
vm->ttysave[slot].saved = t;
|
||||
vm->ttysave[slot].fd = fd;
|
||||
vm->ttysave[slot].owner = vm->cur;
|
||||
vm->ttysave[slot].used = 1;
|
||||
cfmakeraw(&t);
|
||||
if (tcsetattr(fd, TCSANOW, &t) != 0) {
|
||||
vm->ttysave[slot].used = 0;
|
||||
*msg = strerror(errno);
|
||||
return WO_T_IO;
|
||||
}
|
||||
R[A] = 0;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_TERM_RESTORE: { /* (fd) -> 0 from the saved entry */
|
||||
int fd = (int)(int64_t)R[B];
|
||||
for (int i = 0; i < 8; i++)
|
||||
if (vm->ttysave[i].used && vm->ttysave[i].fd == fd) {
|
||||
if (tcsetattr(fd, TCSANOW, &vm->ttysave[i].saved) != 0) {
|
||||
*msg = strerror(errno);
|
||||
return WO_T_IO;
|
||||
}
|
||||
vm->ttysave[i].used = 0;
|
||||
R[A] = 0;
|
||||
return 0;
|
||||
}
|
||||
*msg = "term.restore: fd was never made raw";
|
||||
return WO_T_IO;
|
||||
}
|
||||
default:
|
||||
*msg = "unknown stdlib builtin";
|
||||
return WO_T_EXPLICIT;
|
||||
|
|
|
|||
|
|
@ -769,6 +769,7 @@ int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap) {
|
|||
|
||||
void wo_vm_destroy(wo_vm *vm) {
|
||||
wo_proc_reap_all(vm); /* iteration 42: no child outlives its shard */
|
||||
wo_term_restore_all(vm); /* runtime-v2 4: no wrecked tty either */
|
||||
/* iteration 35: the fiber pool dies with the vm */
|
||||
while (vm->fib_pool) {
|
||||
wo_fiber *fb = vm->fib_pool;
|
||||
|
|
@ -1458,6 +1459,9 @@ static void vm_gc_safepoint(wo_vm *vm) {
|
|||
* slot sees 0 and skips. stop_depth is 0 for an uncaught trap (the whole
|
||||
* stack dies) and the catching frame's depth for a caught one. */
|
||||
static void vm_unwind(wo_vm *vm, uint32_t stop_depth) {
|
||||
/* runtime-v2 4: a FULL unwind (uncaught trap, fiber reap) restores
|
||||
* the ttys this fiber raw'd — no trap path leaves a wrecked tty */
|
||||
if (stop_depth == 0) wo_term_abandon(vm, vm->cur);
|
||||
for (uint32_t d = vm->cur->depth; d > stop_depth; d--) {
|
||||
const wo_frame *f = &vm->cur->frames[d - 1];
|
||||
vm_release_frame(vm, d, (d == vm->cur->depth) ? f->pc : f->pc - 1, UINT32_MAX);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#ifndef WO_VM_H
|
||||
#define WO_VM_H
|
||||
|
||||
#include <termios.h> /* runtime-v2 4: the saved-termios table */
|
||||
|
||||
#include "loader.h"
|
||||
|
||||
/* structured trap error (spec §6): one shape forever — the CLI prints it,
|
||||
|
|
@ -208,6 +210,11 @@ void wo_vm_signals_drain(struct wo_vm *vm);
|
|||
void wo_actor_notify(struct wo_vm *vm, struct wo_actor *target,
|
||||
uint64_t payload, const char *what);
|
||||
|
||||
/* runtime-v2 4 (sysio.c): restore the ttys a dying fiber raw'd (full
|
||||
* unwind / fib_reap), or every saved tty (wo_vm_destroy). */
|
||||
void wo_term_abandon(struct wo_vm *vm, wo_fiber *fb);
|
||||
void wo_term_restore_all(struct wo_vm *vm);
|
||||
|
||||
/* iteration 24: the one mailbox cap (default 1024, WO_MAILBOX overrides
|
||||
* at boot — soak tests shrink it to force the fail-fast policy). */
|
||||
extern uint32_t wo_mailbox_cap;
|
||||
|
|
@ -265,6 +272,15 @@ typedef struct wo_vm {
|
|||
} sigsubs[8];
|
||||
uint32_t nsigsubs;
|
||||
uint32_t sig_seen;
|
||||
/* runtime-v2 4: ttys this shard put into raw mode. Restore is a
|
||||
* RUNTIME obligation — full unwind, fiber reap and vm destroy all
|
||||
* restore (newest-first) so no trap path leaves a wrecked tty. */
|
||||
struct {
|
||||
int used;
|
||||
int fd;
|
||||
struct termios saved;
|
||||
wo_fiber *owner;
|
||||
} ttysave[8];
|
||||
/* iteration 35, uring backend: the shard's ONE deadline tick — a
|
||||
* TIMEOUT op with a sentinel user_data armed for the nearest fd-park
|
||||
* deadline (fd parks keep exactly one POLL op each; expiry wakes them
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
/* test_term — runtime-v2 3/4/5: signals as events, termios adoption,
|
||||
* fd passing. Named for the terminal-facing half of the track. */
|
||||
#define _XOPEN_SOURCE 700 /* posix_openpt/grantpt/unlockpt + 200809L base */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "cont.h"
|
||||
|
|
@ -136,7 +139,104 @@ static void test_signal_on_delivers_record(void) {
|
|||
signal(SIGUSR1, SIG_DFL);
|
||||
}
|
||||
|
||||
/* ---- runtime-v2 4: termios adoption --------------------------------------
|
||||
* methods on an fd argument: 0 raw(fd), 1 restore(fd), 2 trap_while_raw
|
||||
* (raw then DIV0 — the unwind must restore). */
|
||||
static uint8_t *term_module(size_t *len) {
|
||||
wb_t *b = wb_new();
|
||||
uint32_t kraw = wb_const_text(b, "raw");
|
||||
uint32_t kres = wb_const_text(b, "restore");
|
||||
uint32_t ktrap = wb_const_text(b, "trap_while_raw");
|
||||
uint32_t kone = wb_const_int(b, 1);
|
||||
uint32_t kzero = wb_const_int(b, 0);
|
||||
{ /* raw(fd) */
|
||||
uint32_t code[3];
|
||||
code[0] = wo_ins_abc(WOP_BUILTIN, 1, 0, WO_B_TERM_RAW);
|
||||
code[1] = wo_ins_abc(WOP_RET0, 0, 0, 0);
|
||||
wb_method(b, kraw, WOB_NONE, 1, 2, code, 2, NULL, 0, NULL, 0);
|
||||
}
|
||||
{ /* restore(fd) */
|
||||
uint32_t code[3];
|
||||
code[0] = wo_ins_abc(WOP_BUILTIN, 1, 0, WO_B_TERM_RESTORE);
|
||||
code[1] = wo_ins_abc(WOP_RET0, 0, 0, 0);
|
||||
wb_method(b, kres, WOB_NONE, 1, 2, code, 2, NULL, 0, NULL, 0);
|
||||
}
|
||||
{ /* trap_while_raw(fd): raw, then DIV0 */
|
||||
uint32_t code[6];
|
||||
code[0] = wo_ins_abc(WOP_BUILTIN, 1, 0, WO_B_TERM_RAW);
|
||||
code[1] = wo_ins_abx(WOP_LOADK, 1, (uint16_t)kone);
|
||||
code[2] = wo_ins_abx(WOP_LOADK, 2, (uint16_t)kzero);
|
||||
code[3] = wo_ins_abc(WOP_DIV, 3, 1, 2);
|
||||
code[4] = wo_ins_abc(WOP_RET0, 0, 0, 0);
|
||||
wb_method(b, ktrap, WOB_NONE, 1, 4, code, 5, NULL, 0, NULL, 0);
|
||||
}
|
||||
return wb_finish(b, len);
|
||||
}
|
||||
|
||||
static void test_term_raw_restore(void) {
|
||||
/* a real tty pair, made by the TEST (no builtin involved) */
|
||||
int master = posix_openpt(O_RDWR | O_NOCTTY);
|
||||
T_CHECK(master >= 0);
|
||||
T_EQ(grantpt(master), 0);
|
||||
T_EQ(unlockpt(master), 0);
|
||||
char sname[128];
|
||||
T_EQ(ptsname_r(master, sname, sizeof sname), 0);
|
||||
int slave = open(sname, O_RDWR | O_NOCTTY);
|
||||
T_CHECK(slave >= 0);
|
||||
struct termios t0;
|
||||
T_EQ(tcgetattr(slave, &t0), 0);
|
||||
T_CHECK(t0.c_lflag & ECHO);
|
||||
|
||||
size_t len;
|
||||
uint8_t *img = term_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 args[1] = {(uint64_t)slave};
|
||||
uint64_t ret = 0;
|
||||
wo_err err;
|
||||
|
||||
/* raw clears ECHO/ICANON */
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 0, args, 1, &ret, &err), 0);
|
||||
struct termios tr;
|
||||
T_EQ(tcgetattr(slave, &tr), 0);
|
||||
T_CHECK((tr.c_lflag & (ECHO | ICANON)) == 0);
|
||||
/* restore brings the saved flags back */
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 1, args, 1, &ret, &err), 0);
|
||||
struct termios t1;
|
||||
T_EQ(tcgetattr(slave, &t1), 0);
|
||||
T_EQ((long long)t1.c_lflag, (long long)t0.c_lflag);
|
||||
/* restore with nothing saved refuses by name */
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 1, args, 1, &ret, &err), -1);
|
||||
T_CHECK(strstr(err.msg, "never made raw") != NULL);
|
||||
/* double raw refuses by name — and the refusal is a TRAP, so the
|
||||
* full unwind restores the first raw (the obligation, observed) */
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 0, args, 1, &ret, &err), 0);
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 0, args, 1, &ret, &err), -1);
|
||||
T_CHECK(strstr(err.msg, "already raw") != NULL);
|
||||
T_EQ(tcgetattr(slave, &t1), 0);
|
||||
T_EQ((long long)t1.c_lflag, (long long)t0.c_lflag);
|
||||
/* a trap while raw still restores — the obligation, second proof */
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 2, args, 1, &ret, &err), -1);
|
||||
T_EQ(tcgetattr(slave, &t1), 0);
|
||||
T_EQ((long long)t1.c_lflag, (long long)t0.c_lflag);
|
||||
|
||||
wo_vm_destroy(&VM);
|
||||
wo_module_free(&mod);
|
||||
free(img);
|
||||
close(slave);
|
||||
close(master);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_signal_on_delivers_record();
|
||||
test_term_raw_restore();
|
||||
return t_report("test_term");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue