summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-27 16:58:31 +0200
committersalmonik <salmonik@tuta.io>2026-08-27 16:58:31 +0200
commit300c0acbd7ab3fae8596ff839c39d9a17c70c68d (patch)
tree5a600b24c037dc798766e3ce3fc004956e517abb
parentexec: PE32+ loader, versioned ABI, first external binaries (v0.6.0) (diff)
downloadrs-dos-300c0acbd7ab3fae8596ff839c39d9a17c70c68d.tar.gz
rs-dos-300c0acbd7ab3fae8596ff839c39d9a17c70c68d.zip
streams: per-invocation channel, pumping mode, filter pipeline wiring
exec: LineChannel bounded ring (cap=16) with abort/eof/marker fields. Shell 'pumping mode' assembles canonical lines from its queue into the channel while the command runs; ^C marks the channel aborted. Shell-side grammar: USE/TIL pre-drain into the channel before spawn; <cmd> <path> TIL <marker> sugar drains paths until the marker line. Interactive TIL: pump_key checks each submitted line against the marker and closes the channel when it matches. Exports callbacks run in the command task but touch shell-owned state: this is safe by construction — the shell blocks in exec_external until the command completes. put_line feeds Pipeline::line directly; get_line pops from the channel (yield-poll when empty). load: drop NX from image mapping (single-privilege kernel, NX bit 63 reserved when EFER.NXE is set by OVMF); compute image span from section RVAs not file size. bins/count: gate binary — reads lines via get_line, reports count via put_line. Verified: count USE file → 6; count <path> TIL END → 2; interactive count TIL DONE → 3; -h filter across task boundary; ^C abort. v1 stream exports: put_line → Pipeline, get_line ← channel, log_line → klog append, exit → abort_current. Accounting: print_one_line → pipe.line() returns false (head kill / pager ^C) → the binary should wrap up promptly; accepted: binary does not check the return value in v1 (phase 3 simplification).
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml2
-rw-r--r--bins/count/Cargo.toml10
-rw-r--r--bins/count/src/main.rs64
-rw-r--r--bins/hello/src/main.rs12
-rw-r--r--rsapi/src/lib.rs3
-rw-r--r--rsnix/src/lib.rs2
-rw-r--r--rsnix/src/load.rs68
-rw-r--r--shell/src/lib.rs342
9 files changed, 459 insertions, 51 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ed37b5f..0ca76eb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10,6 +10,13 @@ dependencies = [
10] 10]
11 11
12[[package]] 12[[package]]
13name = "count"
14version = "0.6.0"
15dependencies = [
16 "rsapi",
17]
18
19[[package]]
13name = "hello" 20name = "hello"
14version = "0.6.0" 21version = "0.6.0"
15dependencies = [ 22dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
index e8e0827..feca451 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
1[workspace] 1[workspace]
2resolver = "2" 2resolver = "2"
3members = ["rsnix", "shell", "rsdos", "rsdosedit", "rspaint", "rsapi", "bins/hello", "bins/boom"] 3members = ["rsnix", "shell", "rsdos", "rsdosedit", "rspaint", "rsapi", "bins/hello", "bins/boom", "bins/count"]
4 4
5[workspace.package] 5[workspace.package]
6version = "0.6.0" 6version = "0.6.0"
diff --git a/bins/count/Cargo.toml b/bins/count/Cargo.toml
new file mode 100644
index 0000000..5ab50cf
--- /dev/null
+++ b/bins/count/Cargo.toml
@@ -0,0 +1,10 @@
1[package]
2name = "count"
3version.workspace = true
4edition.workspace = true
5
6[lints]
7workspace = true
8
9[dependencies]
10rsapi = { path = "../../rsapi" }
diff --git a/bins/count/src/main.rs b/bins/count/src/main.rs
new file mode 100644
index 0000000..6fd6f17
--- /dev/null
+++ b/bins/count/src/main.rs
@@ -0,0 +1,64 @@
1//! count — stream-input consumer (ADR 0006 Phase 3 gate).
2//!
3//! Reads lines through `get_line` until EOF or Abort, then reports how
4//! many it saw via `put_line`. Proves: USE pre-drain, TIL truncation,
5//! interactive pumping, and ^C abort across the task boundary.
6
7#![no_std]
8#![no_main]
9
10use rsapi::{Exports, StreamResult};
11
12#[panic_handler]
13fn panic(_info: &core::panic::PanicInfo) -> ! {
14 loop {
15 core::hint::spin_loop();
16 }
17}
18
19fn put_u32(exports: &Exports, mut v: u32) {
20 // Small fixed buffer; u32 max is 10 digits.
21 let mut buf = [0u8; 10];
22 let mut i = buf.len();
23 if v == 0 {
24 i -= 1;
25 buf[i] = b'0';
26 }
27 while v > 0 {
28 i -= 1;
29 buf[i] = b'0' + (v % 10) as u8;
30 v /= 10;
31 }
32 let suffix = b" line(s)";
33 let mut msg = [0u8; 18];
34 let n = buf.len() - i;
35 msg[..n].copy_from_slice(&buf[i..]);
36 msg[n..n + suffix.len()].copy_from_slice(suffix);
37 (exports.put_line)(rsapi::Line {
38 ptr: msg.as_ptr(),
39 len: n + suffix.len(),
40 });
41}
42
43/// The image entry point — the loader jumps here with the ABI arguments.
44#[unsafe(no_mangle)]
45pub extern "C" fn efi_main(
46 exports: &'static Exports,
47 _argc: usize,
48 _argv: *const rsapi::Arg,
49) -> i32 {
50 let mut count: u32 = 0;
51 loop {
52 let mut line = rsapi::Line {
53 ptr: core::ptr::null(),
54 len: 0,
55 };
56 match (exports.get_line)(&mut line) {
57 StreamResult::Line => count += 1,
58 StreamResult::Eof => break,
59 StreamResult::Abort => return 130, // 128+SIGINT convention
60 }
61 }
62 put_u32(exports, count);
63 0
64}
diff --git a/bins/hello/src/main.rs b/bins/hello/src/main.rs
index d1d58df..2904351 100644
--- a/bins/hello/src/main.rs
+++ b/bins/hello/src/main.rs
@@ -19,15 +19,15 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {
19/// (exports table, argc, argv). The conventional UEFI name is kept so 19/// (exports table, argc, argv). The conventional UEFI name is kept so
20/// the PE looks normal; the signature is the rs-dos ABI. 20/// the PE looks normal; the signature is the rs-dos ABI.
21#[unsafe(no_mangle)] 21#[unsafe(no_mangle)]
22pub extern "C" fn efi_main(exports: &'static Exports, _argc: usize, _argv: *const rsapi::Arg) -> i32 { 22pub extern "C" fn efi_main(
23 exports: &'static Exports,
24 _argc: usize,
25 _argv: *const rsapi::Arg,
26) -> i32 {
23 let msg = b"hello, world - from an external binary"; 27 let msg = b"hello, world - from an external binary";
24 let line = rsapi::Line { 28 let line = rsapi::Line {
25 ptr: msg.as_ptr(), 29 ptr: msg.as_ptr(),
26 len: msg.len(), 30 len: msg.len(),
27 }; 31 };
28 if (exports.put_line)(line) { 32 if (exports.put_line)(line) { 0 } else { 1 }
29 0
30 } else {
31 1
32 }
33} 33}
diff --git a/rsapi/src/lib.rs b/rsapi/src/lib.rs
index f71e69f..d7b69a2 100644
--- a/rsapi/src/lib.rs
+++ b/rsapi/src/lib.rs
@@ -20,7 +20,8 @@ pub const ABI_VERSION: u32 = 1;
20/// Entry-point signature. `extern "C"` on the x86_64-unknown-uefi 20/// Entry-point signature. `extern "C"` on the x86_64-unknown-uefi
21/// target is the Win64 convention: the three arguments arrive in 21/// target is the Win64 convention: the three arguments arrive in
22/// RCX/RDX/R8 on both sides of the table. 22/// RCX/RDX/R8 on both sides of the table.
23pub type EntryPoint = extern "C" fn(exports: &'static Exports, argc: usize, argv: *const Arg) -> i32; 23pub type EntryPoint =
24 extern "C" fn(exports: &'static Exports, argc: usize, argv: *const Arg) -> i32;
24 25
25/// One C-style argv element: pointer + length (rs-dos strings are not 26/// One C-style argv element: pointer + length (rs-dos strings are not
26/// NUL-terminated in memory). 27/// NUL-terminated in memory).
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs
index 376d3b5..325932a 100644
--- a/rsnix/src/lib.rs
+++ b/rsnix/src/lib.rs
@@ -5,6 +5,7 @@ extern crate alloc;
5pub mod ahci; 5pub mod ahci;
6pub mod cursor; 6pub mod cursor;
7pub mod ehci; 7pub mod ehci;
8pub mod exec;
8pub mod fat; 9pub mod fat;
9pub mod font; 10pub mod font;
10pub mod fs; 11pub mod fs;
@@ -12,7 +13,6 @@ pub mod gdt;
12pub mod gop; 13pub mod gop;
13pub mod interrupts; 14pub mod interrupts;
14pub mod klog; 15pub mod klog;
15pub mod exec;
16pub mod load; 16pub mod load;
17pub mod memory; 17pub mod memory;
18pub mod paging; 18pub mod paging;
diff --git a/rsnix/src/load.rs b/rsnix/src/load.rs
index cebca89..488c703 100644
--- a/rsnix/src/load.rs
+++ b/rsnix/src/load.rs
@@ -50,7 +50,8 @@ pub enum LoadError {
50// --------------------------------------------------------------------------- 50// ---------------------------------------------------------------------------
51 51
52fn u16_at(b: &[u8], off: usize) -> Option<u16> { 52fn u16_at(b: &[u8], off: usize) -> Option<u16> {
53 b.get(off..off + 2).map(|s| u16::from_le_bytes([s[0], s[1]])) 53 b.get(off..off + 2)
54 .map(|s| u16::from_le_bytes([s[0], s[1]]))
54} 55}
55fn u32_at(b: &[u8], off: usize) -> Option<u32> { 56fn u32_at(b: &[u8], off: usize) -> Option<u32> {
56 b.get(off..off + 4) 57 b.get(off..off + 4)
@@ -87,7 +88,23 @@ struct Section {
87 raw_size: usize, 88 raw_size: usize,
88} 89}
89 90
90/// Parse headers, apply relocations into `mem`, return the entry RVA. 91/// Total virtual span of a PE image: the highest section RVA+virt_size
92/// (never smaller than one page for the headers).
93fn image_span_bytes(img: &[u8]) -> usize {
94 let e_lfanew = u32_at(img, 0x3C).unwrap_or(0) as usize;
95 let coff = e_lfanew + 4;
96 let num_sections = u16_at(img, coff + 2).unwrap_or(0) as usize;
97 let opt_size = u16_at(img, coff + 16).unwrap_or(0) as usize;
98 let sect_tab = coff + 20 + opt_size;
99 let mut end = PAGE_SIZE; // at least one page for headers
100 for i in 0..num_sections {
101 let s = sect_tab + i * 40;
102 let vsz = u32_at(img, s + 8).unwrap_or(0) as usize;
103 let va = u32_at(img, s + 12).unwrap_or(0) as usize;
104 end = end.max(va + vsz);
105 }
106 end
107}
91fn parse_and_relocate(img: &[u8], mem: usize) -> Result<usize, LoadError> { 108fn parse_and_relocate(img: &[u8], mem: usize) -> Result<usize, LoadError> {
92 // ---- DOS header ---- 109 // ---- DOS header ----
93 if u16_at(img, 0) != Some(DOS_MAGIC) { 110 if u16_at(img, 0) != Some(DOS_MAGIC) {
@@ -151,7 +168,9 @@ fn parse_and_relocate(img: &[u8], mem: usize) -> Result<usize, LoadError> {
151 if n == 0 { 168 if n == 0 {
152 continue; 169 continue;
153 } 170 }
154 let src = img.get(s.raw_ptr..s.raw_ptr + n).ok_or(LoadError::BadSectionTable)?; 171 let src = img
172 .get(s.raw_ptr..s.raw_ptr + n)
173 .ok_or(LoadError::BadSectionTable)?;
155 unsafe { 174 unsafe {
156 let dst = (mem + s.virt_addr) as *mut u8; 175 let dst = (mem + s.virt_addr) as *mut u8;
157 core::ptr::copy_nonoverlapping(src.as_ptr(), dst, n); 176 core::ptr::copy_nonoverlapping(src.as_ptr(), dst, n);
@@ -185,7 +204,9 @@ fn parse_and_relocate(img: &[u8], mem: usize) -> Result<usize, LoadError> {
185 unsafe { 204 unsafe {
186 let p = target as *mut u64; 205 let p = target as *mut u64;
187 let v = p.read_unaligned(); 206 let v = p.read_unaligned();
188 p.write_unaligned(v.wrapping_add((mem as u64).wrapping_sub(image_base as u64))); 207 p.write_unaligned(
208 v.wrapping_add((mem as u64).wrapping_sub(image_base as u64)),
209 );
189 } 210 }
190 } 211 }
191 _ => return Err(LoadError::BadReloc), 212 _ => return Err(LoadError::BadReloc),
@@ -213,17 +234,22 @@ pub fn load_image(bytes: &[u8]) -> Result<Image, LoadError> {
213 return Err(LoadError::NotPe); 234 return Err(LoadError::NotPe);
214 } 235 }
215 236
216 let page_count = bytes.len().div_ceil(PAGE_SIZE); 237 // The memory span is the VIRTUAL layout: the highest section RVA +
238 // virt_size (headers live at rva 0). Sections are scattered across
239 // RVAs, so this can exceed the file size.
240 let image_span = image_span_bytes(bytes);
241 let page_count = image_span.div_ceil(PAGE_SIZE);
217 // Allocate physical pages at a fixed window (identity mapping makes 242 // Allocate physical pages at a fixed window (identity mapping makes
218 // the link address = runtime address after relocation). 243 // the link address = runtime address after relocation).
219 let base = crate::memory::alloc_pages_above(page_count, LOAD_BASE).ok_or(LoadError::AllocFailed)?; 244 let base =
245 crate::memory::alloc_pages_above(page_count, LOAD_BASE).ok_or(LoadError::AllocFailed)?;
220 if base >= LOAD_LIMIT { 246 if base >= LOAD_LIMIT {
221 crate::memory::free_pages(base, page_count); 247 crate::memory::free_pages(base, page_count);
222 return Err(LoadError::TooBig); 248 return Err(LoadError::TooBig);
223 } 249 }
224 // Identity-map them user+writable+nx-default (relocations need write; 250 // Identity-map user+writable: single-privilege-level kernel, no W^X —
225 // we do not enforce W^X here — single-privilege-level kernel). 251 // the image contains code that must execute and data that relocates.
226 if !paging::map_range(base, page_count, paging::PAGE_USER | paging::PAGE_WRITABLE | paging::PAGE_NX) { 252 if !paging::map_range(base, page_count, paging::PAGE_USER | paging::PAGE_WRITABLE) {
227 crate::memory::free_pages(base, page_count); 253 crate::memory::free_pages(base, page_count);
228 return Err(LoadError::MapFailed); 254 return Err(LoadError::MapFailed);
229 } 255 }
@@ -250,15 +276,27 @@ pub fn load_image(bytes: &[u8]) -> Result<Image, LoadError> {
250/// Zero the part of each section between raw_size and virt_size (BSS). 276/// Zero the part of each section between raw_size and virt_size (BSS).
251fn zero_bss_tails(img: &[u8], mem: usize) { 277fn zero_bss_tails(img: &[u8], mem: usize) {
252 // Re-walk the section table minimally (headers are small). 278 // Re-walk the section table minimally (headers are small).
253 let Some(e_lfanew) = u32_at(img, 0x3C) else { return }; 279 let Some(e_lfanew) = u32_at(img, 0x3C) else {
280 return;
281 };
254 let coff = e_lfanew as usize + 4; 282 let coff = e_lfanew as usize + 4;
255 let Some(num_sections) = u16_at(img, coff + 2) else { return }; 283 let Some(num_sections) = u16_at(img, coff + 2) else {
256 let Some(opt_size) = u16_at(img, coff + 16) else { return }; 284 return;
285 };
286 let Some(opt_size) = u16_at(img, coff + 16) else {
287 return;
288 };
257 let sect_tab = coff + 20 + opt_size as usize; 289 let sect_tab = coff + 20 + opt_size as usize;
258 for i in 0..num_sections as usize { 290 for i in 0..num_sections as usize {
259 let Some(virt_size) = u32_at(img, sect_tab + i * 40 + 8) else { return }; 291 let Some(virt_size) = u32_at(img, sect_tab + i * 40 + 8) else {
260 let Some(raw_size) = u32_at(img, sect_tab + i * 40 + 16) else { return }; 292 return;
261 let Some(virt_addr) = u32_at(img, sect_tab + i * 40 + 12) else { return }; 293 };
294 let Some(raw_size) = u32_at(img, sect_tab + i * 40 + 16) else {
295 return;
296 };
297 let Some(virt_addr) = u32_at(img, sect_tab + i * 40 + 12) else {
298 return;
299 };
262 if virt_size > raw_size { 300 if virt_size > raw_size {
263 unsafe { 301 unsafe {
264 core::ptr::write_bytes( 302 core::ptr::write_bytes(
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index 9ca999e..5248ea9 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -711,7 +711,13 @@ fn emit_string(pipe: &mut Pipeline, s: &str) {
711 711
712/// Commands that may consume an input stream (`USE`/`TIL`). 712/// Commands that may consume an input stream (`USE`/`TIL`).
713fn accepts_input(cmd: &str) -> bool { 713fn accepts_input(cmd: &str) -> bool {
714 matches!(cmd, "cat" | "hexdump") 714 // Built-in stream consumers, plus any external binary in /bin/cli —
715 // externals get injected streams by construction (ADR 0006), so the
716 // keyword is never ambiguous for them.
717 if matches!(cmd, "cat" | "hexdump") {
718 return true;
719 }
720 rsnix::exec::resolve("cli", cmd).is_some()
715} 721}
716 722
717fn execute(s: &str) { 723fn execute(s: &str) {
@@ -748,8 +754,10 @@ write `{} <path> TIL <marker>`",
748 // Resolution rule (ADR 0006): \bin\<kind>\<name> first, else the 754 // Resolution rule (ADR 0006): \bin\<kind>\<name> first, else the
749 // built-in. The frozen core (help/clear/echo/poweroff/exit/halt/ 755 // built-in. The frozen core (help/clear/echo/poweroff/exit/halt/
750 // rescan) is exempt — it exists to fix the system and never migrates. 756 // rescan) is exempt — it exists to fix the system and never migrates.
751 if !matches!(cmd, "help" | "clear" | "echo" | "halt" | "poweroff" | "exit" | "rescan") 757 if !matches!(
752 && exec_external(&parsed) 758 cmd,
759 "help" | "clear" | "echo" | "halt" | "poweroff" | "exit" | "rescan"
760 ) && exec_external(&parsed)
753 { 761 {
754 return; 762 return;
755 } 763 }
@@ -801,30 +809,133 @@ write `{} <path> TIL <marker>`",
801} 809}
802 810
803// --------------------------------------------------------------------------- 811// ---------------------------------------------------------------------------
804// External binaries (ADR 0006 Phase 2) 812// External binaries (ADR 0006 Phase 3)
813//
814// Resolution: \bin\cli\<name> on each mounted volume; else built-in.
815//
816// Stream injection: the Exports callbacks run in the COMMAND task but
817// touch shell-owned state. This is safe by construction — the invoking
818// shell blocks in exec_external until the command completes, so nothing
819// else touches the Pipeline or the channel. One invocation at a time.
805// 820//
806// Resolution: \bin\cli\<name> on each mounted volume; else "unknown 821// put_line → Pipeline::line directly (output side needs no queue).
807// command". The Exports table is constructed per invocation: v1 wires 822// get_line ← a bounded per-invocation ring; while the command runs,
808// put_line to the active terminal and get_line to EOF (stream injection 823// the blocked shell "pumps": drains its key queue, assembles
809// through the filter pipeline lands in Phase 3). 824// canonical lines, pushes them into the ring. ^C marks the
825// channel aborted instead of killing a stream consumer.
810// --------------------------------------------------------------------------- 826// ---------------------------------------------------------------------------
811 827
812static mut LAST_EXIT_CODE: i32 = 0; 828static mut LAST_EXIT_CODE: i32 = 0;
813 829
830/// The pipeline of the running external command (None when idle).
831static mut ACTIVE_PIPELINE: Option<Pipeline> = None;
832
814/// Last exit code of an external command ($? equivalent for scripts). 833/// Last exit code of an external command ($? equivalent for scripts).
815pub fn last_exit_code() -> i32 { 834pub fn last_exit_code() -> i32 {
816 unsafe { core::ptr::addr_of!(LAST_EXIT_CODE).read() } 835 unsafe { core::ptr::addr_of!(LAST_EXIT_CODE).read() }
817} 836}
818 837
819/// ABI callback: print one line to the invoking terminal. 838/// Bounded line channel between the pumping shell and the command task.
839struct LineChannel {
840 lines: Vec<String>,
841 len: usize,
842 cap: usize,
843 /// ^C seen: get_line returns Abort once drained.
844 aborted: bool,
845 /// Source exhausted (EOF / marker hit).
846 eof: bool,
847 /// Interactive TIL: exact line that closes the input (pump checks it).
848 marker: Option<String>,
849}
850
851impl LineChannel {
852 const CAP: usize = 16;
853 fn new() -> Self {
854 LineChannel {
855 lines: Vec::with_capacity(Self::CAP),
856 len: 0,
857 cap: Self::CAP,
858 aborted: false,
859 eof: false,
860 marker: None,
861 }
862 }
863 /// Shell side: push one assembled line. Returns false when full
864 /// (the pump waits and retries).
865 fn push(&mut self, s: &str) -> bool {
866 if self.len >= self.cap {
867 return false;
868 }
869 self.lines.push(String::from(s));
870 self.len += 1;
871 true
872 }
873 /// Command side: pop one line.
874 fn pop(&mut self) -> Option<String> {
875 if self.len == 0 {
876 return None;
877 }
878 let s = self.lines.remove(0);
879 self.len -= 1;
880 Some(s)
881 }
882}
883
884/// The current invocation. Only touched by the shell (pump + setup) and
885/// by the ABI callbacks (command task) — never concurrently, see above.
886static mut INVOCATION: Option<LineChannel> = None;
887
888/// Set when the input source is interactive (tty): the pump assembles
889/// canonical lines from keys. File/marker sources are pre-drained into
890/// the channel before spawn instead.
891fn with_invocation<R>(f: impl FnOnce(&mut LineChannel) -> R) -> R {
892 let inv = unsafe {
893 match &mut *core::ptr::addr_of_mut!(INVOCATION) {
894 Some(ch) => ch,
895 None => panic!("exec: no active invocation"),
896 }
897 };
898 f(inv)
899}
900
901/// ABI callback: print one line through the filter pipeline / sink.
820fn abi_put_line(line: rsapi::Line) -> bool { 902fn abi_put_line(line: rsapi::Line) -> bool {
821 term_println(line.as_str()); 903 let pipe = unsafe {
822 true 904 match &mut *core::ptr::addr_of_mut!(ACTIVE_PIPELINE) {
905 Some(p) => p,
906 None => panic!("exec: no active pipeline"),
907 }
908 };
909 pipe.line(line.as_str())
823} 910}
824 911
825/// ABI callback: read one input line — no stream source wired yet (Phase 3). 912/// ABI callback: read one input line from the invocation channel.
826fn abi_get_line(_out: &mut rsapi::Line) -> rsapi::StreamResult { 913fn abi_get_line(out: &mut rsapi::Line) -> rsapi::StreamResult {
827 rsapi::StreamResult::Eof 914 loop {
915 let (line, status) = with_invocation(|ch| match ch.pop() {
916 Some(l) => (Some(l), rsapi::StreamResult::Line),
917 None if ch.aborted => (None, rsapi::StreamResult::Abort),
918 None if ch.eof => (None, rsapi::StreamResult::Eof),
919 None => (None, rsapi::StreamResult::Line), // empty but open: pump
920 });
921 match status {
922 rsapi::StreamResult::Abort => return rsapi::StreamResult::Abort,
923 rsapi::StreamResult::Eof => return rsapi::StreamResult::Eof,
924 rsapi::StreamResult::Line => {
925 if let Some(l) = line {
926 out.ptr = l.as_ptr();
927 out.len = l.len();
928 // Leak the String into the command's lifetime: lines are
929 // one-shot and the task dies soon; freeing across tasks
930 // is not worth the bookkeeping.
931 core::mem::forget(l);
932 return rsapi::StreamResult::Line;
933 }
934 // Channel empty: yield so the shell pump can refill.
935 rsnix::sched::yield_now();
936 }
937 }
938 }
828} 939}
829 940
830/// ABI callback: kernel log. 941/// ABI callback: kernel log.
@@ -837,8 +948,7 @@ fn abi_exit(code: i32) -> ! {
837 unsafe { 948 unsafe {
838 LAST_EXIT_CODE = code; 949 LAST_EXIT_CODE = code;
839 } 950 }
840 // Unwind the binary by longjmp-style task abort: the trampoline never 951 // Kill the calling task; exec_external sees the abort notification.
841 // returns here. We report the code via the static and kill the task.
842 rsnix::sched::abort_current("exit") 952 rsnix::sched::abort_current("exit")
843} 953}
844 954
@@ -871,37 +981,199 @@ fn exec_external(p: &Parsed) -> bool {
871 return true; 981 return true;
872 } 982 }
873 }; 983 };
984
985 // Grammar stays shell-side (ADR 0006): the binary gets clean argv and
986 // injected streams. Build its pipeline exactly like a built-in's.
987 let pipe = Pipeline::new(&p.filters, &p.output);
988 #[allow(unused_mut)]
989 // Input channel: pre-drain file sources; tty sources are pumped.
990 let mut channel = LineChannel::new();
991 match &p.input {
992 Input::None => {}
993 Input::Use(path) => {
994 drain_file_into(&mut channel, path, None);
995 channel.eof = true;
996 }
997 Input::UseTil(path, marker) => {
998 drain_file_into(&mut channel, path, Some(marker));
999 channel.eof = true;
1000 }
1001 Input::Til(marker) => {
1002 if p.args.is_empty() {
1003 // Interactive: read from tty until marker — the pump
1004 // checks each submitted line against it.
1005 channel.marker = Some(marker.clone());
1006 } else {
1007 for path in &p.args {
1008 drain_file_into(&mut channel, path, Some(marker));
1009 }
1010 channel.eof = true;
1011 }
1012 }
1013 }
1014
874 let mut argv: Vec<String> = Vec::with_capacity(p.args.len() + 1); 1015 let mut argv: Vec<String> = Vec::with_capacity(p.args.len() + 1);
875 argv.push(p.cmd.clone()); 1016 argv.push(p.cmd.clone());
876 argv.extend(p.args.iter().cloned()); 1017 argv.extend(p.args.iter().cloned());
877 let name = alloc::format!("{}", p.cmd); 1018 let name = alloc::format!("{}", p.cmd);
1019
1020 // Publish the invocation state before spawn (the command may call
1021 // put_line immediately).
1022 unsafe {
1023 *core::ptr::addr_of_mut!(ACTIVE_PIPELINE) = Some(pipe);
1024 *core::ptr::addr_of_mut!(INVOCATION) = Some(channel);
1025 }
1026
1027 let mut cmd_done = false;
1028 let mut exit_code: i64 = 0;
878 match rsnix::exec::spawn(image, &EXPORTS_V1, &argv, &name) { 1029 match rsnix::exec::spawn(image, &EXPORTS_V1, &argv, &name) {
879 Ok(id) => { 1030 Ok(_id) => {
880 // Wait for the completion message (TaskMsg::User(exit_code)). 1031 // Pumping mode: assemble canonical lines from our queue into
1032 // the channel while the command runs. The WM pump delivers
1033 // keys to us only while our window is focused.
881 loop { 1034 loop {
882 match rsnix::sched::recv_blocking() { 1035 match rsnix::sched::recv_blocking() {
883 rsnix::sched::TaskMsg::User(code) => { 1036 rsnix::sched::TaskMsg::User(code) => {
884 unsafe { 1037 exit_code = if code == u64::MAX { -1 } else { code as i64 };
885 LAST_EXIT_CODE = code as i32; 1038 cmd_done = true;
886 }
887 break;
888 } 1039 }
889 rsnix::sched::TaskMsg::Key(rsnix::ps2::KeyEvent::Ctrl('c')) => { 1040 rsnix::sched::TaskMsg::Key(rsnix::ps2::KeyEvent::Ctrl('c')) => {
890 // ^C while waiting: kill the command task. 1041 // ^C: mark the channel aborted; stream consumers stop
891 let _ = rsnix::sched::kill(id); 1042 // at their next get_line/put_line-false. Compute-
892 term_err("^C"); 1043 // bound commands stay blind to it (accepted policy).
893 break; 1044 with_invocation(|ch| ch.aborted = true);
1045 }
1046 rsnix::sched::TaskMsg::Key(ev) => {
1047 // Canonical assembly for the interactive source:
1048 // mirror tty::read_line's key handling, minus echo
1049 // (the terminal already echoed it? no — the shell
1050 // must echo here since nothing else does).
1051 pump_key(ev);
894 } 1052 }
895 _ => {} 1053 _ => {}
896 } 1054 }
1055 if cmd_done {
1056 break;
1057 }
897 } 1058 }
898 } 1059 }
899 Err(rsnix::exec::ExecError::Busy) => term_err(&alloc::format!("{}: task table full", p.cmd)), 1060 Err(rsnix::exec::ExecError::Busy) => {
1061 term_err(&alloc::format!("{}: task table full", p.cmd))
1062 }
900 Err(_) => term_err(&alloc::format!("{}: cannot start", p.cmd)), 1063 Err(_) => term_err(&alloc::format!("{}: cannot start", p.cmd)),
901 } 1064 }
1065
1066 unsafe {
1067 let pipe_ptr = core::ptr::addr_of_mut!(ACTIVE_PIPELINE);
1068 if let Some(mut pipe) = (*pipe_ptr).take() {
1069 pipe.end();
1070 }
1071 *core::ptr::addr_of_mut!(INVOCATION) = None;
1072 LAST_EXIT_CODE = exit_code as i32;
1073 }
902 true 1074 true
903} 1075}
904 1076
1077/// One key of canonical assembly during pumping. Lines land in the
1078/// channel; ^D closes it; Enter submits the line. Echo goes to the
1079/// terminal (this is the command's stdin).
1080fn pump_key(ev: rsnix::ps2::KeyEvent) {
1081 use rsnix::ps2::KeyEvent;
1082 // Static assembly buffer: only touched between keys by the shell.
1083 static mut PUMP_BUF: Option<String> = None;
1084 let buf = unsafe {
1085 let p = core::ptr::addr_of_mut!(PUMP_BUF);
1086 if (*p).is_none() {
1087 *p = Some(String::new());
1088 }
1089 (*p).as_mut().unwrap()
1090 };
1091 match ev {
1092 KeyEvent::Enter => {
1093 gop_println!();
1094 let line = core::mem::take(buf);
1095 with_invocation(|ch| {
1096 // Interactive TIL: the submitted line itself is checked
1097 // against the marker (it is consumed, not delivered).
1098 if let Some(m) = &ch.marker
1099 && line.as_str() == m
1100 {
1101 ch.eof = true;
1102 } else {
1103 ch.push(line.as_str());
1104 }
1105 });
1106 }
1107 KeyEvent::Ctrl('c') => {
1108 buf.clear();
1109 with_invocation(|ch| ch.aborted = true);
1110 }
1111 KeyEvent::Ctrl('d') => {
1112 if buf.is_empty() {
1113 with_invocation(|ch| ch.eof = true);
1114 }
1115 }
1116 KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => {
1117 buf.push(c);
1118 gop_print!("{}", c);
1119 }
1120 KeyEvent::Backspace => {
1121 if buf.pop().is_some() {
1122 gop_print!("\x08");
1123 }
1124 }
1125 _ => {}
1126 }
1127}
1128
1129/// Pre-drain a file into the channel; `marker` stops before that line
1130/// (USE <path> TIL sugar). The channel is left open — the caller sets
1131/// `eof` when this is the whole source.
1132fn drain_file_into(ch: &mut LineChannel, path: &str, marker: Option<&str>) {
1133 let h = match rsnix::fs::resolve(path) {
1134 Some(h) => h,
1135 None => {
1136 term_err(&alloc::format!("{}: no such file", path));
1137 ch.eof = true;
1138 return;
1139 }
1140 };
1141 drain_handle(ch, h, marker);
1142}
1143
1144/// Shared drain: read `h` in chunks, push complete lines; stop at marker.
1145fn drain_handle(ch: &mut LineChannel, h: usize, marker: Option<&str>) {
1146 let mut buf = [0u8; 512];
1147 let mut pos = 0u64;
1148 let mut pending = String::new();
1149 'outer: loop {
1150 match rsnix::fs::read(h, &mut buf, pos) {
1151 Ok(0) | Err(_) => break,
1152 Ok(n) => {
1153 pos += n as u64;
1154 pending.push_str(&String::from_utf8_lossy(&buf[..n]));
1155 while let Some(idx) = pending.find('\n') {
1156 let line: String = pending.drain(..=idx).collect();
1157 let line = line.trim_end_matches('\n').to_string();
1158 if let Some(m) = marker
1159 && line == m
1160 {
1161 break 'outer;
1162 }
1163 while !ch.push(&line) {
1164 rsnix::sched::yield_now();
1165 }
1166 }
1167 }
1168 }
1169 }
1170 if marker.is_none() && !pending.is_empty() {
1171 while !ch.push(&pending) {
1172 rsnix::sched::yield_now();
1173 }
1174 }
1175}
1176
905// --------------------------------------------------------------------------- 1177// ---------------------------------------------------------------------------
906// Built-in commands 1178// Built-in commands
907// --------------------------------------------------------------------------- 1179// ---------------------------------------------------------------------------
@@ -973,6 +1245,7 @@ an operator: echo \"WRITING\" prints WRITING.
973 1245
974fn cmd_echo(p: &Parsed) { 1246fn cmd_echo(p: &Parsed) {
975 let mut pipe = Pipeline::new(&p.filters, &p.output); 1247 let mut pipe = Pipeline::new(&p.filters, &p.output);
1248 #[allow(unused_mut)]
976 let line = p.args.join(" "); 1249 let line = p.args.join(" ");
977 let _ = pipe.line(&line); 1250 let _ = pipe.line(&line);
978 pipe.end(); 1251 pipe.end();
@@ -980,6 +1253,7 @@ fn cmd_echo(p: &Parsed) {
980 1253
981fn cmd_hello(p: &Parsed) { 1254fn cmd_hello(p: &Parsed) {
982 let mut pipe = Pipeline::new(&p.filters, &p.output); 1255 let mut pipe = Pipeline::new(&p.filters, &p.output);
1256 #[allow(unused_mut)]
983 let _ = pipe.line("World!"); 1257 let _ = pipe.line("World!");
984 pipe.end(); 1258 pipe.end();
985} 1259}
@@ -1036,6 +1310,7 @@ fn cmd_clear() {
1036 1310
1037fn cmd_info(p: &Parsed) { 1311fn cmd_info(p: &Parsed) {
1038 let mut pipe = Pipeline::new(&p.filters, &p.output); 1312 let mut pipe = Pipeline::new(&p.filters, &p.output);
1313 #[allow(unused_mut)]
1039 let mut s = String::from("rs-dos v"); 1314 let mut s = String::from("rs-dos v");
1040 s.push_str(env!("CARGO_PKG_VERSION")); 1315 s.push_str(env!("CARGO_PKG_VERSION"));
1041 s.push_str(" - rsnix kernel"); 1316 s.push_str(" - rsnix kernel");
@@ -1047,6 +1322,7 @@ fn cmd_info(p: &Parsed) {
1047fn cmd_ls(p: &Parsed) { 1322fn cmd_ls(p: &Parsed) {
1048 let path = p.args.first().map(|s| s.as_str()).unwrap_or("/"); 1323 let path = p.args.first().map(|s| s.as_str()).unwrap_or("/");
1049 let mut pipe = Pipeline::new(&p.filters, &p.output); 1324 let mut pipe = Pipeline::new(&p.filters, &p.output);
1325 #[allow(unused_mut)]
1050 match rsnix::fs::resolve(path) { 1326 match rsnix::fs::resolve(path) {
1051 None => term_err(&alloc::format!("ls: {}: no such file or directory", path)), 1327 None => term_err(&alloc::format!("ls: {}: no such file or directory", path)),
1052 Some(h) => { 1328 Some(h) => {
@@ -1111,6 +1387,7 @@ enum ReadResult {
1111 1387
1112fn cmd_cat(p: &Parsed) { 1388fn cmd_cat(p: &Parsed) {
1113 let mut pipe = Pipeline::new(&p.filters, &p.output); 1389 let mut pipe = Pipeline::new(&p.filters, &p.output);
1390 #[allow(unused_mut)]
1114 match &p.input { 1391 match &p.input {
1115 Input::None => { 1392 Input::None => {
1116 if p.args.is_empty() { 1393 if p.args.is_empty() {
@@ -1318,6 +1595,7 @@ impl HexCtx<'_> {
1318 1595
1319fn cmd_hexdump(p: &Parsed) { 1596fn cmd_hexdump(p: &Parsed) {
1320 let mut pipe = Pipeline::new(&p.filters, &p.output); 1597 let mut pipe = Pipeline::new(&p.filters, &p.output);
1598 #[allow(unused_mut)]
1321 match &p.input { 1599 match &p.input {
1322 Input::None => { 1600 Input::None => {
1323 if p.args.is_empty() { 1601 if p.args.is_empty() {
@@ -1633,6 +1911,7 @@ fn cmd_mem(p: &Parsed) {
1633 let ps = rsnix::memory::page_stats(); 1911 let ps = rsnix::memory::page_stats();
1634 1912
1635 let mut pipe = Pipeline::new(&p.filters, &p.output); 1913 let mut pipe = Pipeline::new(&p.filters, &p.output);
1914 #[allow(unused_mut)]
1636 let _ = pipe.line(&alloc::format!( 1915 let _ = pipe.line(&alloc::format!(
1637 "memory map (key=0x{:x}, {} entries)", 1916 "memory map (key=0x{:x}, {} entries)",
1638 key, 1917 key,
@@ -1699,6 +1978,7 @@ fn cmd_mem(p: &Parsed) {
1699fn cmd_mmap(p: &Parsed) { 1978fn cmd_mmap(p: &Parsed) {
1700 let (map, key) = rsnix::memory::get_map(); 1979 let (map, key) = rsnix::memory::get_map();
1701 let mut pipe = Pipeline::new(&p.filters, &p.output); 1980 let mut pipe = Pipeline::new(&p.filters, &p.output);
1981 #[allow(unused_mut)]
1702 let _ = pipe.line(&alloc::format!( 1982 let _ = pipe.line(&alloc::format!(
1703 "memory map (key=0x{:x}, {} entries):", 1983 "memory map (key=0x{:x}, {} entries):",
1704 key, 1984 key,
@@ -1744,6 +2024,7 @@ fn cmd_stress(p: &Parsed) {
1744 }; 2024 };
1745 2025
1746 let mut pipe = Pipeline::new(&p.filters, &p.output); 2026 let mut pipe = Pipeline::new(&p.filters, &p.output);
2027 #[allow(unused_mut)]
1747 let mut allocated = 0usize; 2028 let mut allocated = 0usize;
1748 { 2029 {
1749 let mut blocks: [Option<alloc::vec::Vec<u8>>; 64] = core::array::from_fn(|_| None); 2030 let mut blocks: [Option<alloc::vec::Vec<u8>>; 64] = core::array::from_fn(|_| None);
@@ -1931,6 +2212,7 @@ fn cmd_heapdbg(_p: &Parsed) {
1931fn cmd_date(p: &Parsed) { 2212fn cmd_date(p: &Parsed) {
1932 let t = rsnix::rtc::now(); 2213 let t = rsnix::rtc::now();
1933 let mut pipe = Pipeline::new(&p.filters, &p.output); 2214 let mut pipe = Pipeline::new(&p.filters, &p.output);
2215 #[allow(unused_mut)]
1934 let line = alloc::format!( 2216 let line = alloc::format!(
1935 "{}-{:02}-{:02} {:02}:{:02}:{:02}", 2217 "{}-{:02}-{:02} {:02}:{:02}:{:02}",
1936 t.year, 2218 t.year,
@@ -2184,6 +2466,7 @@ fn cmd_bg(p: &Parsed) {
2184 2466
2185fn cmd_jobs(p: &Parsed) { 2467fn cmd_jobs(p: &Parsed) {
2186 let mut pipe = Pipeline::new(&p.filters, &p.output); 2468 let mut pipe = Pipeline::new(&p.filters, &p.output);
2469 #[allow(unused_mut)]
2187 let lines = rsnix::sched::jobs(); 2470 let lines = rsnix::sched::jobs();
2188 if lines.is_empty() { 2471 if lines.is_empty() {
2189 let _ = pipe.line("jobs: no tasks"); 2472 let _ = pipe.line("jobs: no tasks");
@@ -2440,6 +2723,7 @@ fn usb_if_label(c: u8, s: u8, p: u8) -> &'static str {
2440fn cmd_pci(p: &Parsed) { 2723fn cmd_pci(p: &Parsed) {
2441 let devices = rsnix::pci::probe(); 2724 let devices = rsnix::pci::probe();
2442 let mut pipe = Pipeline::new(&p.filters, &p.output); 2725 let mut pipe = Pipeline::new(&p.filters, &p.output);
2726 #[allow(unused_mut)]
2443 let _ = pipe.line(&alloc::format!("pci: {} device(s)", devices.len())); 2727 let _ = pipe.line(&alloc::format!("pci: {} device(s)", devices.len()));
2444 let _ = pipe.line(" bus dev fn class vendor:device if"); 2728 let _ = pipe.line(" bus dev fn class vendor:device if");
2445 for d in &devices { 2729 for d in &devices {
@@ -2466,11 +2750,13 @@ fn cmd_usb(p: &Parsed) {
2466 rsnix::usb::rescan(); 2750 rsnix::usb::rescan();
2467 rsnix::ehci::rescan(); 2751 rsnix::ehci::rescan();
2468 let mut pipe = Pipeline::new(&p.filters, &p.output); 2752 let mut pipe = Pipeline::new(&p.filters, &p.output);
2753 #[allow(unused_mut)]
2469 let _ = pipe.line("usb: rescanning ports"); 2754 let _ = pipe.line("usb: rescanning ports");
2470 pipe.end(); 2755 pipe.end();
2471 return; 2756 return;
2472 } 2757 }
2473 let mut pipe = Pipeline::new(&p.filters, &p.output); 2758 let mut pipe = Pipeline::new(&p.filters, &p.output);
2759 #[allow(unused_mut)]
2474 for l in rsnix::usb::diagnose().lines() { 2760 for l in rsnix::usb::diagnose().lines() {
2475 if !pipe.line(l) { 2761 if !pipe.line(l) {
2476 break; 2762 break;
@@ -2479,6 +2765,7 @@ fn cmd_usb(p: &Parsed) {
2479 pipe.end(); 2765 pipe.end();
2480 // EHCI (USB2) controller status - the real hardware path. 2766 // EHCI (USB2) controller status - the real hardware path.
2481 let mut pipe = Pipeline::new(&p.filters, &p.output); 2767 let mut pipe = Pipeline::new(&p.filters, &p.output);
2768 #[allow(unused_mut)]
2482 for l in rsnix::ehci::diagnose().lines() { 2769 for l in rsnix::ehci::diagnose().lines() {
2483 if !pipe.line(l) { 2770 if !pipe.line(l) {
2484 break; 2771 break;
@@ -2487,6 +2774,7 @@ fn cmd_usb(p: &Parsed) {
2487 pipe.end(); 2774 pipe.end();
2488 // UHCI (USB1.1 companion) — full/low-speed keyboards and mice. 2775 // UHCI (USB1.1 companion) — full/low-speed keyboards and mice.
2489 let mut pipe = Pipeline::new(&p.filters, &p.output); 2776 let mut pipe = Pipeline::new(&p.filters, &p.output);
2777 #[allow(unused_mut)]
2490 for l in rsnix::uhci::diagnose().lines() { 2778 for l in rsnix::uhci::diagnose().lines() {
2491 if !pipe.line(l) { 2779 if !pipe.line(l) {
2492 break; 2780 break;