diff --git a/compiler/src/types.ml b/compiler/src/types.ml index bb6a52a..0502a30 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -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 diff --git a/runtime/src/sysio.c b/runtime/src/sysio.c index 7cf4184..79831b2 100644 --- a/runtime/src/sysio.c +++ b/runtime/src/sysio.c @@ -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; diff --git a/runtime/src/vm.c b/runtime/src/vm.c index 013a55d..6d56620 100644 --- a/runtime/src/vm.c +++ b/runtime/src/vm.c @@ -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); diff --git a/runtime/src/vm.h b/runtime/src/vm.h index 05e885c..cc2c09e 100644 --- a/runtime/src/vm.h +++ b/runtime/src/vm.h @@ -4,6 +4,8 @@ #ifndef WO_VM_H #define WO_VM_H +#include /* 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 diff --git a/runtime/test/test_term.c b/runtime/test/test_term.c index c3a9040..28ce2ab 100644 --- a/runtime/test/test_term.c +++ b/runtime/test/test_term.c @@ -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 +#include #include #include #include #include #include +#include #include #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"); }