diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-27 16:58:31 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-27 16:58:31 +0200 |
| commit | 300c0acbd7ab3fae8596ff839c39d9a17c70c68d (patch) | |
| tree | 5a600b24c037dc798766e3ce3fc004956e517abb | |
| parent | exec: PE32+ loader, versioned ABI, first external binaries (v0.6.0) (diff) | |
| download | rs-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.lock | 7 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | bins/count/Cargo.toml | 10 | ||||
| -rw-r--r-- | bins/count/src/main.rs | 64 | ||||
| -rw-r--r-- | bins/hello/src/main.rs | 12 | ||||
| -rw-r--r-- | rsapi/src/lib.rs | 3 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 2 | ||||
| -rw-r--r-- | rsnix/src/load.rs | 68 | ||||
| -rw-r--r-- | shell/src/lib.rs | 342 |
9 files changed, 459 insertions, 51 deletions
| @@ -10,6 +10,13 @@ dependencies = [ | |||
| 10 | ] | 10 | ] |
| 11 | 11 | ||
| 12 | [[package]] | 12 | [[package]] |
| 13 | name = "count" | ||
| 14 | version = "0.6.0" | ||
| 15 | dependencies = [ | ||
| 16 | "rsapi", | ||
| 17 | ] | ||
| 18 | |||
| 19 | [[package]] | ||
| 13 | name = "hello" | 20 | name = "hello" |
| 14 | version = "0.6.0" | 21 | version = "0.6.0" |
| 15 | dependencies = [ | 22 | dependencies = [ |
| @@ -1,6 +1,6 @@ | |||
| 1 | [workspace] | 1 | [workspace] |
| 2 | resolver = "2" | 2 | resolver = "2" |
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit", "rspaint", "rsapi", "bins/hello", "bins/boom"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit", "rspaint", "rsapi", "bins/hello", "bins/boom", "bins/count"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.6.0" | 6 | version = "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] | ||
| 2 | name = "count" | ||
| 3 | version.workspace = true | ||
| 4 | edition.workspace = true | ||
| 5 | |||
| 6 | [lints] | ||
| 7 | workspace = true | ||
| 8 | |||
| 9 | [dependencies] | ||
| 10 | rsapi = { 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 | |||
| 10 | use rsapi::{Exports, StreamResult}; | ||
| 11 | |||
| 12 | #[panic_handler] | ||
| 13 | fn panic(_info: &core::panic::PanicInfo) -> ! { | ||
| 14 | loop { | ||
| 15 | core::hint::spin_loop(); | ||
| 16 | } | ||
| 17 | } | ||
| 18 | |||
| 19 | fn 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)] | ||
| 45 | pub 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)] |
| 22 | pub extern "C" fn efi_main(exports: &'static Exports, _argc: usize, _argv: *const rsapi::Arg) -> i32 { | 22 | pub 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. |
| 23 | pub type EntryPoint = extern "C" fn(exports: &'static Exports, argc: usize, argv: *const Arg) -> i32; | 23 | pub 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; | |||
| 5 | pub mod ahci; | 5 | pub mod ahci; |
| 6 | pub mod cursor; | 6 | pub mod cursor; |
| 7 | pub mod ehci; | 7 | pub mod ehci; |
| 8 | pub mod exec; | ||
| 8 | pub mod fat; | 9 | pub mod fat; |
| 9 | pub mod font; | 10 | pub mod font; |
| 10 | pub mod fs; | 11 | pub mod fs; |
| @@ -12,7 +13,6 @@ pub mod gdt; | |||
| 12 | pub mod gop; | 13 | pub mod gop; |
| 13 | pub mod interrupts; | 14 | pub mod interrupts; |
| 14 | pub mod klog; | 15 | pub mod klog; |
| 15 | pub mod exec; | ||
| 16 | pub mod load; | 16 | pub mod load; |
| 17 | pub mod memory; | 17 | pub mod memory; |
| 18 | pub mod paging; | 18 | pub 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 | ||
| 52 | fn u16_at(b: &[u8], off: usize) -> Option<u16> { | 52 | fn 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 | } |
| 55 | fn u32_at(b: &[u8], off: usize) -> Option<u32> { | 56 | fn 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). | ||
| 93 | fn 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 | } | ||
| 91 | fn parse_and_relocate(img: &[u8], mem: usize) -> Result<usize, LoadError> { | 108 | fn 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). |
| 251 | fn zero_bss_tails(img: &[u8], mem: usize) { | 277 | fn 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`). |
| 713 | fn accepts_input(cmd: &str) -> bool { | 713 | fn 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 | ||
| 717 | fn execute(s: &str) { | 723 | fn 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 | ||
| 812 | static mut LAST_EXIT_CODE: i32 = 0; | 828 | static mut LAST_EXIT_CODE: i32 = 0; |
| 813 | 829 | ||
| 830 | /// The pipeline of the running external command (None when idle). | ||
| 831 | static 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). |
| 815 | pub fn last_exit_code() -> i32 { | 834 | pub 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. |
| 839 | struct 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 | |||
| 851 | impl 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. | ||
| 886 | static 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. | ||
| 891 | fn 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. | ||
| 820 | fn abi_put_line(line: rsapi::Line) -> bool { | 902 | fn 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. |
| 826 | fn abi_get_line(_out: &mut rsapi::Line) -> rsapi::StreamResult { | 913 | fn 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). | ||
| 1080 | fn 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. | ||
| 1132 | fn 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. | ||
| 1145 | fn 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 | ||
| 974 | fn cmd_echo(p: &Parsed) { | 1246 | fn 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 | ||
| 981 | fn cmd_hello(p: &Parsed) { | 1254 | fn 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 | ||
| 1037 | fn cmd_info(p: &Parsed) { | 1311 | fn 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) { | |||
| 1047 | fn cmd_ls(p: &Parsed) { | 1322 | fn 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 | ||
| 1112 | fn cmd_cat(p: &Parsed) { | 1388 | fn 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 | ||
| 1319 | fn cmd_hexdump(p: &Parsed) { | 1596 | fn 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) { | |||
| 1699 | fn cmd_mmap(p: &Parsed) { | 1978 | fn 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) { | |||
| 1931 | fn cmd_date(p: &Parsed) { | 2212 | fn 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 | ||
| 2185 | fn cmd_jobs(p: &Parsed) { | 2467 | fn 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 { | |||
| 2440 | fn cmd_pci(p: &Parsed) { | 2723 | fn 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; |
