diff --git a/.agents/docs/2026-09-20-term-pass-control-record.md b/.agents/docs/2026-09-20-term-pass-control-record.md index 4150d28..d34d722 100644 --- a/.agents/docs/2026-09-20-term-pass-control-record.md +++ b/.agents/docs/2026-09-20-term-pass-control-record.md @@ -20,7 +20,7 @@ | openkal-opensbi | [#17](https://github.com/mcpplibs/openkal-opensbi/pull/17) | 0.7.0 → 0.8.0 | 同上 | | openkal-musl | [#38](https://github.com/mcpplibs/openkal-musl/pull/38) | 0.15.0 → 0.16.0 | ioctl 路由到 `openkal.terminal`;信号处置只接受已生效的;`examples/terminal` + `tools/pty-keys.py` | | openkal-llvm-runtime | [#25](https://github.com/mcpplibs/openkal-llvm-runtime/pull/25) | 0.11.0 → 0.12.0 | 跟随 musl 0.16.0(并把停在 main 的 0.11.0 带回) | -| mcpp-index | 待填 | — | 注册以上全部描述符 | +| mcpp-index | [#442](https://github.com/mcpplibs/mcpp-index/pull/442) | — | 一次注册九个描述符;顺带补上 `main` 上缺的两处 `xpm.macosx` | ## 2. 设计落点(与设计稿的差异) @@ -81,12 +81,74 @@ done **比较时去掉回车,排除的是一件已记录的事**:openkal 的模式字管「键入」,不管「写出」,所以输出后处理(`OPOST`/`ONLCR`)在本端口的 `tcsetattr` 下不受影响,而宿主 C 库的 `cfmakeraw` 会清掉它。两份 transcript 因此每行差一个字节,其余完全一致。这条限制写进了 openkal-musl 的 README 限制表,并作为下一波的候选问题记在设计稿 §7 O2。 -### 3.4 生态级 +### 3.4 生态级:一个真实的全屏程序 -(待填:发布、index 注册、沙箱解析、真实 TUI 程序 `re-cloud-code` 的构建与运行。) +`/home/speak/workspace/scode/re-cloud-code` 是报告这个问题的项目——一个 C++ 全屏 TUI,`packages/tui-kit/src/term.cppm` 里正是 `tcgetattr` / `cfmakeraw` / `tcsetattr` 加 `sigaction(SIGINT)` 的写法,源码注释里还留着「openkal-musl 的 tcsetattr 答 ENOTTY」这一句。 + +在本波的链上构建(`openkal-linux@0.14.0` · `openkal-musl@0.16.0` · `openkal-llvm-runtime@0.12.0`,目标 `x86_64-linux-musl`),在带尺寸的伪终端里运行: + +| 观察 | 结果 | +| --- | --- | +| 程序进入全屏并绘制 | 是(边框、颜色、状态行都在) | +| 键入的文本到达程序 | 是——输入框里出现 `❯ hello openkal` | +| 按下 `^C` 之后程序还在 | 是,且它把 `^C` 当作「清空输入」处理,而不是被结束 | + +对照 issue #36:同一类程序在旧链上 `tcsetattr rc=-1 errno=25`,然后 `Pane is dead (signal 2)`。 + +这一项是用本地工作树(path 依赖)测的,因为它跑在 index 注册之前;注册之后的同一条链由 §3.5 的沙箱脚本按发布版本重新验证一次。 ## 4. 本波修掉的、与设计无关但同源的缺陷 - **`KAL_VERSION_MINOR` 停在 11**:整个生态的 `kal_version` 都答 0.11,条款 6.2 给 load 期绑定消费者的比较形同虚设。修正并加了检查。 - **openkal-musl 的三处「报告成功却什么都没做」**:`TCGETS`、`TIOCGWINSZ` 不回写调用者结构,`SIG_IGN` 不安装。前两者由本波的 ioctl 路由一并解决,第三者改为「只接受已经生效的处置」。 - **openkal-emscripten 的 absence 检查依赖 index**:它按本仓库清单里的版本号去 index 取规范,于是在提升版本的分支上必然失败,并把失败报成「链接没有提到 kal_process_spawn」。改为两个依赖都取工作树。 + +## 5. 发布 + +| 包 | 版本 | tag | sha256(GitHub archive 与 GitCode 资产逐字节相同) | +| --- | --- | --- | --- | +| openkal | 0.14.0 | `0.14.0` | `0e0410fbda5f246c79e45ff157c919e23173b3a8a12a90a9538aeeacea1e692c` | +| openkal-linux | 0.14.0 | `0.14.0` | `331a6524a4ff604a208330f45c5b9f0ea17943f4783523a6170444341a27ef45` | +| openkal-macos | 0.11.0 | `0.11.0` | `576356ead9a99bb299d9d8ac6e7d266064112cff1775252935922e67e6a45809` | +| openkal-windows | 0.9.0 | `0.9.0` | `27122bef192b14998ba603ad54031b8c454e0ca372ce907ed5487fe5f7f33d02` | +| openkal-emscripten | 0.3.0 | `0.3.0` | `34067d2ac9011344cd6cdf6f4866d771ba49b5e05d392bb0d51f573f31fe6606` | +| openkal-uefi | 0.8.0 | `0.8.0` | `a1c920dc862a974431b7c8518cdb8085bc6d0ff22bbacbd4d97fe7f8b16720ac` | +| openkal-opensbi | 0.8.0 | `0.8.0` | `557a28f7d775871df7dad68422617d9bbcd844e8bb2be190f38fe87aab05e693` | +| openkal-musl | 0.16.0 | `0.16.0` | `8ffa4a2a79fcc7fe7565c1e69624b9d3b575020ed97d2043e20cdf542d519105` | +| openkal-llvm-runtime | 0.12.0 | `0.12.0` | `e009f6195ef517c40beaf5093cde59764fc870502aab5967f5a2817f8df47cff` | + +mcpp-index PR [#442](https://github.com/mcpplibs/mcpp-index/pull/442) 一次注册以上九个描述符。 + +### 5.1 发布惯例上发现的一处缺陷(上一波留下的,本波未能修好) + +`openkal-musl 0.15.0` 与 `openkal-llvm-runtime 0.11.0` 在 GitHub 上的标签带 `v` 前缀,而描述符的 GLOBAL 地址按惯例写无前缀形式,**这两个版本的 GLOBAL 地址返回 14 字节的 Not Found**。补一个无前缀标签也修不好:GitCode 上的资产不是 GitHub archive 本身(`75803192…` 对 `ee953bd8…`),而一个版本只有一个 sha256。要修得把 GitCode 资产换成 GitHub 的 archive,而同名资产 `gtc` 拒绝覆盖。已在 index PR 里记录,留给仓库所有者。 + +本波九个包都按惯例发布:无前缀标签,GitCode 资产是 GitHub archive 原样上传,两边 sha256 相同(openkal 0.14.0 下载两边比对确认)。 + +## 6. 生态级自我 review + +按本波计划 §1 的八个角度逐条回答,附上没有做到的那几项。 + +| 角度 | 结论与证据 | +| --- | --- | +| 架构 | 净增一个模式位、一条条款 6.2 的规则、两条实现规则、一条 §11 记录。不新增接口、不新增操作、不动结构布局、不动 props 字 | +| 稳定性 | 旧实现读回 0 仍是真话,因此没有「先还债才能诚实」的前置;index 旧描述符原样保留,0.13 消费者不受影响 | +| 优雅简洁 | 与被取代的方案相比,少了一条条款 6.2 的例外与一个强制 props 位;发现机制用的是 terminal.h 已承诺、套件已观察的 get/set 往返 | +| 用户体验 | 消费者零改动:`tcgetattr` / `cfmakeraw` / `tcsetattr` 的既有写法在真实 TUI 上直接正确(§3.4) | +| 兼容性 | 0.14 程序在 0.13 实现上读回 0 并可降级;版本号变真之后,用旧实现配新规范的组合会被套件判为过旧——这正是 version.h 那段话要的行为 | +| 跨平台 | 四个实现各自映射本机机制的**全集**;没有线路规程的环境恒报 1 并忽略置 0 | +| 一致性 | 规范文字不提 termios、不提 console;端口层的映射方向不对称,理由写在端口里 | +| 无感升级 | 九个包按依赖序发布并一次注册;消费者改一行版本号 | +| 测试覆盖 | 见下 | + +**测试覆盖的实情,包括没有覆盖到的部分。** + +- Linux 腿:端到端全覆盖——套件 199 held(带尺寸 pty)、端口探针与宿主 C 库逐行一致(CI 每次运行)、真实 TUI 程序在 pty 中键入与 `^C` 都正确。 +- macOS / Windows / emscripten 三条腿:编译、链接、套件都通过,但**CI 没有伪终端**,所以 `openkal.terminal` 段在这三条腿上报告为「未观察」。新位在它们上面是「按同一份源码形状映射、按同一套声明编译」而不是「被观察到」。 +- 可做而本波没做的下一步:macOS 的 CI 有 `script`,`OPENKAL_CONFORMANCE_RUNNER="script -q /dev/null"` 就能把套件放进伪终端里跑(`tools/run-conformance.sh` 里 `$runner "./$binary"` 是不加引号展开的,本就为此留着)。Windows 需要一个控制台宿主,代价更高。 + + **但现在打开它会立刻红,而红的原因是另一件事。** 没有父终端的 CI 里,`script` 开出来的伪终端窗口尺寸是 0x0;openkal-linux 与 openkal-macos 把这对零原样报成一个尺寸并返回 `kal_ok`,于是套件的「a reported display size is not zero in either dimension」不成立。openkal-emscripten 对同一情况的处理相反,它的注释写着 "A ZERO DIMENSION IS NOT A SIZE",报 `kal_err_not_supported`。**三个实现对「尺寸是零」的答法不一致,而规范没有就此落过字**:本地用 `pty.fork` 不设尺寸跑套件就能重现(本波 §3.2 记的那一次)。 + + 这项缺陷早于本波,影响面小(只在没有尺寸的伪终端上),修法也小(`terminal.h` 一句 + 两个实现各两行),但要动三个包的版本,所以记在这里留给下一波,和「打开 macOS 的伪终端行」一起做。 + +**其余已知且记录在案的限制**:输出后处理(`OPOST`)不在模式字内,raw 模式下写 `\n` 仍会先得到回车;混合保留态恢复为环境惯常的集合;`VMIN`/`VTIME` 不可表达,要「会放弃的读」用 `kal_timeout_read`;openkal-musl 0.15.0 与 openkal-llvm-runtime 0.11.0 的 GLOBAL 镜像地址仍然取不到东西(上一波留下,修法需要所有者在 GitCode 侧删除同名资产)。 diff --git a/.agents/docs/2026-09-20-term-pass-control-verify.sh b/.agents/docs/2026-09-20-term-pass-control-verify.sh new file mode 100755 index 0000000..8301f75 --- /dev/null +++ b/.agents/docs/2026-09-20-term-pass-control-verify.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# Ecosystem verification for the KAL_TERM_PASS_CONTROL wave, resolved from the +# published index only. +# +# B64=$(base64 -w0 ) +# xlings subos new term014 && xlings subos use term014 --sandbox --cmd \ +# "echo $B64 | base64 -d > /tmp/v.sh && MCPP_VERIFY_VERSION= bash /tmp/v.sh" +# +# The subject is what a consumer gets by writing a version number: that openkal +# 0.14.0 and the packages of its wave resolve, that the declarations carry the +# new position in both forms, that an implementation answers `kal_version' with +# the number the package is (it answered 0.11 through two releases), and that a +# program above the C environment enters raw mode and reads the interrupt +# keystroke as data. +# +# A step that cannot run here says so and is counted as not run, never as +# passed. +set -u + +VER="${MCPP_VERIFY_VERSION:?set MCPP_VERIFY_VERSION}" +STORE="${MCPP_VERIFY_BIN:-$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp}" +XL="${XLINGS_BIN:-$(command -v xlings)}" +OPENKAL="${OPENKAL_VERSION:-0.14.0}" +LINUX="${OPENKAL_LINUX:-0.14.0}" +MUSL="${OPENKAL_MUSL:-0.16.0}" + +fails=0; skipped="" +fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); } +ok() { printf 'ok: %s\n' "$1"; } +section() { printf '\n== %s ==\n' "$1"; } +skip() { printf 'NOT RUN: %s\n' "$1"; skipped="$skipped + - $1"; } + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +# -- A. identity and mirror --------------------------------------------------- +section "A. identity and mirror" +got=$("$STORE" --version 2>&1 | head -1) +[ "$got" = "mcpp $VER" ] && ok "$got from $STORE" || fail "version is '$got' at $STORE" +"$STORE" self config --mirror CN >/dev/null 2>&1 || true +"$XL" config --mirror CN >/dev/null 2>&1 || true +xm=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.xlings/.xlings.json'))).get('mirror',''))" 2>/dev/null) +[ "$xm" = "CN" ] && ok "xlings mirror is CN" || fail "xlings mirror is '$xm'" +"$STORE" index update >/dev/null 2>&1 || true + +# -- B. the specification resolves, and says which version it is -------------- +# `kal_version' is the constant the header carries, and every implementation +# answers with it. Two releases went out with 11 written there while the package +# was 0.12 and 0.13, so the comparison clause 6.2 gives a consumer bound at load +# was between two equal numbers. This reads it back through an implementation. +section "B. openkal $OPENKAL resolves and states its version" +mkdir -p "$work/ver/src" +cat > "$work/ver/mcpp.toml" < "$work/ver/src/main.c" <<'C' +#include +#include +#include + +static void say(const char* s) { + kal_uintptr n = 0; while (s[n]) n++; + kal_stream_write(kal_stdout(), s, n); +} +static void number(kal_u64 v) { + char b[24]; int i = 0; + if (v == 0) { say("0"); return; } + while (v) { b[i++] = (char)('0' + (v % 10)); v /= 10; } + char o[25]; int j = 0; + while (i) o[j++] = b[--i]; + o[j] = 0; say(o); +} +int main(void) { + const kal_u64 v = kal_version(); + say("header "); number(KAL_VERSION_MAJOR); say("."); + number(KAL_VERSION_MINOR); say("."); number(KAL_VERSION_PATCH); + say(" implementation "); number((v >> 32) & 0xffff); say("."); + number((v >> 16) & 0xffff); say("."); number(v & 0xffff); + say(" pass_control "); number(KAL_TERM_PASS_CONTROL); + say("\n"); + return (v >= KAL_VERSION) ? 0 : 1; +} +C +out=$(cd "$work/ver" && "$STORE" run 2>&1); rc=$? +line=$(printf '%s\n' "$out" | grep -E '^header ' | head -1) +if [ $rc -ne 0 ] || [ -z "$line" ]; then + fail "the version probe did not build or run"; printf '%s\n' "$out" | tail -8 +else + case "$line" in + "header 0.14.0 implementation 0.14.0 pass_control 4") + ok "$line" ;; + *) fail "the version probe reported: $line" ;; + esac +fi + +# -- C. the position is reachable in both forms ------------------------------- +# A macro does not cross a module boundary, and every position in this +# specification therefore exists twice. A consumer that imports the module is +# the only thing that establishes the second one. +section "C. the position is reachable as a macro and as a module constant" +mkdir -p "$work/mod/src" +sed 's/name = "verprobe"/name = "modprobe"/; s/\[targets.verprobe\]/[targets.modprobe]/; s|main = "src/main.c"|main = "src/main.cpp"|' \ + "$work/ver/mcpp.toml" > "$work/mod/mcpp.toml" +cat > "$work/mod/src/main.cpp" <<'CPP' +import openkal.terminal; +import openkal.macros; +import openkal.stream; + +static_assert(kal::terminal::pass_control.bits == kal::macros::KAL_TERM_PASS_CONTROL_M); +static_assert(kal::terminal::pass_control.bits == 4u); + +int main() { + const char m[] = "module and macro agree\n"; + kal_stream_write(kal_stdout(), m, sizeof m - 1); + return 0; +} +CPP +out=$(cd "$work/mod" && "$STORE" run 2>&1); rc=$? +if [ $rc -eq 0 ] && printf '%s\n' "$out" | grep -q 'module and macro agree'; then + ok "kal::terminal::pass_control and KAL_TERM_PASS_CONTROL_M are the same position" +else + fail "the module form of the position did not compile or run"; printf '%s\n' "$out" | tail -8 +fi + +# -- D. a program above the C environment enters raw mode --------------------- +# The criterion is a relation: the transcript above openkal-musl is compared +# with the transcript of the same source above this machine's own C library, +# upon a pseudo-terminal of the same kind. An assertion written here --- "0x03 +# arrives" --- would pass on a system whose terminal does not deliver it at all. +section "D. the interrupt keystroke arrives as data above openkal-musl $MUSL" +mkdir -p "$work/term/src" +cat > "$work/term/mcpp.toml" < "$work/term/src/main.c" <<'C' +#define _GNU_SOURCE 1 +#include +#include +#include +#include + +static void show(const char* tag, const struct termios* t) { + printf("%s icanon=%d echo=%d isig=%d ixon=%d vmin=%d\r\n", tag, + (t->c_lflag & ICANON) != 0, (t->c_lflag & ECHO) != 0, + (t->c_lflag & ISIG) != 0, (t->c_iflag & IXON) != 0, + (int)t->c_cc[VMIN]); +} + +int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); + printf("isatty %d\r\n", isatty(0)); + struct termios original; + memset(&original, 0x5a, sizeof original); + if (tcgetattr(0, &original) != 0) { printf("no terminal\r\n"); return 1; } + show("before", &original); + struct termios raw = original; + cfmakeraw(&raw); + printf("tcsetattr %d\r\n", tcsetattr(0, TCSANOW, &raw)); + struct termios back; + memset(&back, 0x5a, sizeof back); + tcgetattr(0, &back); + show("readback", &back); + printf("reading\r\n"); + for (char c; read(0, &c, 1) == 1; ) { + printf("byte 0x%02x\r\n", (unsigned char)c); + if (c == 'q') break; + } + tcsetattr(0, TCSANOW, &original); + printf("done\r\n"); + return 0; +} +C +out=$(cd "$work/term" && "$STORE" build 2>&1); rc=$? +bin=$(find "$work/term/target" -type f -name termprobe -perm -u+x 2>/dev/null | head -1) +if [ $rc -ne 0 ] || [ -z "$bin" ]; then + fail "the terminal probe did not build above openkal-musl $MUSL" + printf '%s\n' "$out" | tail -10 +else + cat > "$work/pty.py" <<'PY' +import os, pty, select, sys, time +marker, keys, cmd = sys.argv[1].encode(), bytes.fromhex(sys.argv[2]), sys.argv[3:] +pid, fd = pty.fork() +if pid == 0: + os.execvp(cmd[0], cmd); os._exit(127) +out, typed, deadline = bytearray(), False, time.monotonic() + 30 +while time.monotonic() < deadline: + r, _, _ = select.select([fd], [], [], 0.5) + if r: + try: chunk = os.read(fd, 4096) + except OSError: break + if not chunk: break + out += chunk + if not typed and marker in out: + time.sleep(0.2); os.write(fd, keys); typed = True +os.waitpid(pid, 0) +sys.stdout.buffer.write(bytes(out).replace(b"\r", b"")) +PY + if ! command -v python3 >/dev/null 2>&1; then + skip "D: no python3 here, so nothing can type at a pseudo-terminal" + else + port=$(python3 "$work/pty.py" reading 61620371 "$bin" 2>/dev/null) + cc="$(command -v cc || command -v gcc || true)" + if [ -n "$cc" ] && "$cc" "$work/term/src/main.c" -o "$work/control" 2>/dev/null; then + ctrl=$(python3 "$work/pty.py" reading 61620371 "$work/control" 2>/dev/null) + if printf '%s\n' "$ctrl" | grep -q 'byte 0x03'; then + if [ "$port" = "$ctrl" ]; then + ok "the transcripts agree, keystroke for keystroke" + else + fail "a terminal above openkal-musl does not behave as this machine's C library" + printf -- '--- control\n%s\n--- port\n%s\n' "$ctrl" "$port" + fi + else + skip "D: this machine's own C library did not receive the keystroke as data either" + fi + else + # Without a control the claim is weaker and is stated as such. + if printf '%s\n' "$port" | grep -q 'byte 0x03' && + printf '%s\n' "$port" | grep -q 'readback icanon=0 echo=0 isig=0 ixon=0 vmin=1'; then + ok "raw mode is entered and the interrupt keystroke arrives as 0x03 (no control available)" + else + fail "the terminal probe above openkal-musl did not read the keystroke as data" + printf '%s\n' "$port" + fi + fi + fi +fi + +section "summary" +printf '%d assertion(s) failed\n' "$fails" +[ -n "$skipped" ] && printf 'not run:%s\n' "$skipped" +[ $fails -eq 0 ]