1use std::collections::HashMap;
2use std::env;
3use std::fs::File;
4use std::io::{self, Read, Write};
5use std::sync::{Arc, Mutex};
6use colored::*;
7pub use wasibox_core::IoContext;
8
9mod readline;
10pub use readline::*;
11
12pub type CommandFn = Arc<dyn Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync>;
21
22pub struct CommandRegistry {
44 commands: HashMap<String, CommandFn>,
45 fallback: Option<CommandFn>,
46}
47
48impl CommandRegistry {
49 pub fn new() -> Self {
54 Self {
55 commands: HashMap::new(),
56 fallback: Some(Arc::new(|args: &[String], ctx: &mut IoContext| {
57 wasibox_core::execute_with_context(args.iter().cloned(), ctx)
58 })),
59 }
60 }
61
62 pub fn with_builtins() -> Self {
66 let mut reg = Self::new();
67
68 reg.register("help", |_args, ctx| {
69 writeln!(ctx.stdout, "{}", "Available Commands:".yellow().bold()).map_err(|e| e.to_string())?;
70
71 let mut shell_builtins = vec!["cd", "help", "exit"];
72 #[cfg(feature = "clear")] shell_builtins.push("clear");
73 shell_builtins.sort();
74 writeln!(ctx.stdout, " Shell Built-ins: {}", shell_builtins.join(", ")).map_err(|e| e.to_string())?;
75
76 #[cfg(feature = "sl")]
77 writeln!(ctx.stdout, " Animations: sl").map_err(|e| e.to_string())?;
78
79 let mut utils = Vec::new();
80 #[cfg(feature = "arch")] utils.push("arch");
81 #[cfg(feature = "basename")] utils.push("basename");
82 #[cfg(feature = "cat")] utils.push("cat");
83 #[cfg(feature = "cp")] utils.push("cp");
84 #[cfg(feature = "dir")] utils.push("dir");
85 #[cfg(feature = "dirname")] utils.push("dirname");
86 #[cfg(feature = "echo")] utils.push("echo");
87 #[cfg(feature = "env")] utils.push("env");
88 #[cfg(feature = "false")] utils.push("false");
89 #[cfg(feature = "grep")] utils.push("grep");
90 #[cfg(feature = "head")] utils.push("head");
91 #[cfg(feature = "link")] utils.push("link");
92 #[cfg(feature = "ln")] utils.push("ln");
93 #[cfg(feature = "ls")] utils.push("ls");
94 #[cfg(feature = "mkdir")] utils.push("mkdir");
95 #[cfg(feature = "mv")] utils.push("mv");
96 #[cfg(feature = "pwd")] utils.push("pwd");
97 #[cfg(feature = "rm")] utils.push("rm");
98 #[cfg(feature = "rmdir")] utils.push("rmdir");
99 #[cfg(feature = "seq")] utils.push("seq");
100 #[cfg(feature = "sleep")] utils.push("sleep");
101 #[cfg(feature = "tail")] utils.push("tail");
102 #[cfg(feature = "tee")] utils.push("tee");
103 #[cfg(feature = "touch")] utils.push("touch");
104 #[cfg(feature = "tree")] utils.push("tree");
105 #[cfg(feature = "true")] utils.push("true");
106 #[cfg(feature = "uname")] utils.push("uname");
107 #[cfg(feature = "unlink")] utils.push("unlink");
108 #[cfg(feature = "wc")] utils.push("wc");
109 #[cfg(feature = "whoami")] utils.push("whoami");
110 #[cfg(feature = "yes")] utils.push("yes");
111
112 if !utils.is_empty() {
113 utils.sort();
114 writeln!(ctx.stdout, " Core Utilities: {}", utils.join(", ")).map_err(|e| e.to_string())?;
115 }
116 Ok(())
117 });
118
119 reg.register("cd", |args, _ctx| {
120 let new_dir = args.get(1).map(|s| s.as_str()).unwrap_or(".");
121 env::set_current_dir(new_dir).map_err(|e| format!("cd: {}", e))
122 });
123
124 reg.register("exit", |_args, _ctx| Ok(()));
125
126 #[cfg(feature = "clear")]
127 reg.register("clear", |_args, ctx| {
128 write!(ctx.stdout, "\x1B[2J\x1B[1;1H").map_err(|e| e.to_string())?;
129 ctx.stdout.flush().map_err(|e| e.to_string())
130 });
131
132 #[cfg(feature = "sl")]
133 reg.register("sl", |args, ctx| {
134 let _ = sl::run_with_token(args.iter().cloned(), Some(ctx.cancel_token.clone()));
135 Ok(())
136 });
137
138 #[cfg(feature = "arch")]
140 reg.register("arch", |args, ctx| wasibox_core::utils::arch::execute_with_context(args.iter().cloned(), ctx));
141 #[cfg(feature = "basename")]
142 reg.register("basename", |args, ctx| wasibox_core::utils::basename::execute_with_context(args.iter().cloned(), ctx));
143 #[cfg(feature = "cat")]
144 reg.register("cat", |args, ctx| wasibox_core::utils::cat::execute_with_context(args.iter().cloned(), ctx));
145 #[cfg(feature = "cp")]
146 reg.register("cp", |args, ctx| wasibox_core::utils::cp::execute_with_context(args.iter().cloned(), ctx));
147 #[cfg(feature = "dir")]
148 reg.register("dir", |args, ctx| wasibox_core::utils::dir::execute_with_context(args.iter().cloned(), ctx));
149 #[cfg(feature = "dirname")]
150 reg.register("dirname", |args, ctx| wasibox_core::utils::dirname::execute_with_context(args.iter().cloned(), ctx));
151 #[cfg(feature = "echo")]
152 reg.register("echo", |args, ctx| wasibox_core::utils::echo::execute_with_context(args.iter().cloned(), ctx));
153 #[cfg(feature = "env")]
154 reg.register("env", |args, ctx| wasibox_core::utils::env::execute_with_context(args.iter().cloned(), ctx));
155 #[cfg(feature = "false")]
156 reg.register("false", |args, ctx| wasibox_core::utils::r#false::execute_with_context(args.iter().cloned(), ctx));
157 #[cfg(feature = "grep")]
158 reg.register("grep", |args, ctx| wasibox_core::utils::grep::execute_with_context(args.iter().cloned(), ctx));
159 #[cfg(feature = "head")]
160 reg.register("head", |args, ctx| wasibox_core::utils::head::execute_with_context(args.iter().cloned(), ctx));
161 #[cfg(feature = "link")]
162 reg.register("link", |args, ctx| wasibox_core::utils::link::execute_with_context(args.iter().cloned(), ctx));
163 #[cfg(feature = "ln")]
164 reg.register("ln", |args, ctx| wasibox_core::utils::ln::execute_with_context(args.iter().cloned(), ctx));
165 #[cfg(feature = "ls")]
166 reg.register("ls", |args, ctx| wasibox_core::utils::ls::execute_with_context(args.iter().cloned(), ctx));
167 #[cfg(feature = "mkdir")]
168 reg.register("mkdir", |args, ctx| wasibox_core::utils::mkdir::execute_with_context(args.iter().cloned(), ctx));
169 #[cfg(feature = "mv")]
170 reg.register("mv", |args, ctx| wasibox_core::utils::mv::execute_with_context(args.iter().cloned(), ctx));
171 #[cfg(feature = "pwd")]
172 reg.register("pwd", |args, ctx| wasibox_core::utils::pwd::execute_with_context(args.iter().cloned(), ctx));
173 #[cfg(feature = "rm")]
174 reg.register("rm", |args, ctx| wasibox_core::utils::rm::execute_with_context(args.iter().cloned(), ctx));
175 #[cfg(feature = "rmdir")]
176 reg.register("rmdir", |args, ctx| wasibox_core::utils::rmdir::execute_with_context(args.iter().cloned(), ctx));
177 #[cfg(feature = "seq")]
178 reg.register("seq", |args, ctx| wasibox_core::utils::seq::execute_with_context(args.iter().cloned(), ctx));
179 #[cfg(feature = "sleep")]
180 reg.register("sleep", |args, ctx| wasibox_core::utils::sleep::execute_with_context(args.iter().cloned(), ctx));
181 #[cfg(feature = "tail")]
182 reg.register("tail", |args, ctx| wasibox_core::utils::tail::execute_with_context(args.iter().cloned(), ctx));
183 #[cfg(feature = "tee")]
184 reg.register("tee", |args, ctx| wasibox_core::utils::tee::execute_with_context(args.iter().cloned(), ctx));
185 #[cfg(feature = "touch")]
186 reg.register("touch", |args, ctx| wasibox_core::utils::touch::execute_with_context(args.iter().cloned(), ctx));
187 #[cfg(feature = "tree")]
188 reg.register("tree", |args, ctx| wasibox_core::utils::tree::execute_with_context(args.iter().cloned(), ctx));
189 #[cfg(feature = "true")]
190 reg.register("true", |args, ctx| wasibox_core::utils::r#true::execute_with_context(args.iter().cloned(), ctx));
191 #[cfg(feature = "uname")]
192 reg.register("uname", |args, ctx| wasibox_core::utils::uname::execute_with_context(args.iter().cloned(), ctx));
193 #[cfg(feature = "unlink")]
194 reg.register("unlink", |args, ctx| wasibox_core::utils::unlink::execute_with_context(args.iter().cloned(), ctx));
195 #[cfg(feature = "wc")]
196 reg.register("wc", |args, ctx| wasibox_core::utils::wc::execute_with_context(args.iter().cloned(), ctx));
197 #[cfg(feature = "whoami")]
198 reg.register("whoami", |args, ctx| wasibox_core::utils::whoami::execute_with_context(args.iter().cloned(), ctx));
199 #[cfg(feature = "yes")]
200 reg.register("yes", |args, ctx| wasibox_core::utils::yes::execute_with_context(args.iter().cloned(), ctx));
201
202 reg
203 }
204
205 pub fn command_names(&self) -> Vec<&str> {
208 self.commands.keys().map(|s| s.as_str()).collect()
209 }
210
211 pub fn register<F>(&mut self, name: impl Into<String>, handler: F)
213 where
214 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
215 {
216 self.commands.insert(name.into(), Arc::new(handler));
217 }
218
219 pub fn set_fallback<F>(&mut self, handler: F)
221 where
222 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
223 {
224 self.fallback = Some(Arc::new(handler));
225 }
226
227 pub fn remove_fallback(&mut self) {
229 self.fallback = None;
230 }
231
232 pub fn execute(&self, args: &[String], ctx: &mut IoContext) -> Result<(), String> {
234 if args.is_empty() {
235 return Ok(());
236 }
237 let cmd = &args[0];
238
239 if let Some(handler) = self.commands.get(cmd.as_str()) {
240 return handler(args, ctx);
241 }
242 if let Some(ref fallback) = self.fallback {
243 return fallback(args, ctx);
244 }
245 Err(format!("command not found: {}", cmd))
246 }
247}
248
249impl Default for CommandRegistry {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255fn is_broken_pipe(err: &str) -> bool {
261 err.contains("Broken pipe")
262 || err.contains("BrokenPipe")
263 || err.contains("broken pipe")
264}
265
266pub fn handle_pipeline(
271 line: &str,
272 initial_stdin: Box<dyn Read + Send + 'static>,
273 final_stdout: Box<dyn Write + Send + 'static>,
274 registry: &CommandRegistry,
275 cancel_token: wasibox_core::CancellationToken,
276) -> Result<(), String> {
277 let stages_str: Vec<&str> = line.split('|').collect();
278
279 let mut final_stdout = Some(final_stdout);
280
281 std::thread::scope(|s| {
282 let mut prev_reader: Box<dyn Read + Send> = initial_stdin;
283 let mut threads = Vec::new();
284
285 for (i, stage_str) in stages_str.iter().enumerate() {
286 let is_last = i == stages_str.len() - 1;
287 let mut tokens = shlex::split(stage_str.trim())
288 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
289
290 let stdin = std::mem::replace(&mut prev_reader, Box::new(io::empty()));
291 let (stdout, next_reader): (Box<dyn Write + Send>, Option<Box<dyn Read + Send>>) = if is_last {
292 let mut out: Box<dyn Write + Send> = final_stdout.take().unwrap();
293 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
294 let append = tokens[pos] == ">>";
295 let filename = tokens.get(pos + 1).ok_or("Error: Missing file for redirection")?;
296 let file = if append {
297 std::fs::OpenOptions::new().create(true).append(true).open(filename)
298 } else {
299 File::create(filename)
300 }.map_err(|e| format!("Error opening file: {}", e))?;
301 out = Box::new(file);
302 tokens.truncate(pos);
303 }
304 (out, None)
305 } else {
306 let (reader, writer) = create_pipe(cancel_token.clone());
307 (Box::new(writer), Some(Box::new(reader)))
308 };
309
310 if let Some(r) = next_reader {
311 prev_reader = r;
312 }
313
314 let cancel_clone = cancel_token.clone();
315 let thread = s.spawn(move || {
316 if cancel_clone.is_cancelled() {
317 return Err("Interrupted".to_string());
318 }
319 let wrapped_stdin = Box::new(CancelReader { inner: stdin, token: cancel_clone.clone() });
320 let wrapped_stdout = Box::new(CancelWriter { inner: stdout, token: cancel_clone.clone() });
321 let wrapped_stderr = Box::new(CancelWriter { inner: Box::new(io::stderr()), token: cancel_clone.clone() });
322
323 let mut ctx = IoContext::with_cancel(wrapped_stdin, wrapped_stdout, wrapped_stderr, cancel_clone.clone());
324 let res = registry.execute(&tokens, &mut ctx);
325 if cancel_clone.is_cancelled() {
326 return Err("Interrupted".to_string());
327 }
328 res
329 });
330 threads.push((i, thread));
331 }
332
333 let last_idx = stages_str.len() - 1;
336 let mut final_res = Ok(());
337 for (idx, thread) in threads {
338 match thread.join() {
339 Ok(Ok(())) => {}
340 Ok(Err(e)) => {
341 if idx != last_idx && is_broken_pipe(&e) {
342 continue;
343 }
344 if final_res.is_ok() {
345 final_res = Err(e);
346 }
347 }
348 Err(_) => {
349 if final_res.is_ok() {
350 final_res = Err("Thread panicked".to_string());
351 }
352 }
353 }
354 }
355 final_res
356 })
357}
358
359pub fn handle_parallel(
364 lines: Vec<String>,
365 initial_stdin: Box<dyn Read + Send + 'static>,
366 final_stdout: Box<dyn Write + Send + 'static>,
367 registry: Arc<CommandRegistry>,
368 cancel_token: wasibox_core::CancellationToken,
369) -> Vec<Result<(), String>> {
370 let mut handles = Vec::new();
371 let mut stdin_opt: Option<Box<dyn Read + Send>> = Some(initial_stdin);
372 let mut stdout_opt: Option<Box<dyn Write + Send>> = Some(final_stdout);
373
374 for line in lines {
375 let registry = Arc::clone(®istry);
376 let mut stdin_for_thread: Option<Box<dyn Read + Send>> = Some(stdin_opt.take().unwrap_or_else(|| Box::new(io::empty())));
377 let mut stdout_for_thread: Option<Box<dyn Write + Send>> = Some(stdout_opt.take().unwrap_or_else(|| Box::new(io::stdout())));
378
379 let cancel_clone = cancel_token.clone();
380 let handle = std::thread::spawn(move || {
381 let commands: Vec<&str> = line.split("&&").collect();
382 for cmd in commands {
383 let cmd = cmd.trim();
384 if cmd.is_empty() { continue; }
385 if cancel_clone.is_cancelled() { return Err("Interrupted".to_string()); }
386 let stdin = stdin_for_thread.take().unwrap_or_else(|| Box::new(io::empty()));
387 let stdout = stdout_for_thread.take().unwrap_or_else(|| Box::new(io::stdout()));
388 handle_pipeline(cmd, stdin, stdout, &*registry, cancel_clone.clone())?;
389 }
390 Ok(())
391 });
392 handles.push(handle);
393 }
394
395 handles.into_iter()
396 .map(|h| h.join().unwrap_or(Err("Thread panicked".to_string())))
397 .collect()
398}
399
400pub fn handle_command_line(
403 line: &str,
404 registry: &CommandRegistry,
405 cancel_token: wasibox_core::CancellationToken,
406) -> Result<(), String> {
407 let commands: Vec<&str> = line.split("&&").collect();
408 for cmd in commands {
409 let cmd = cmd.trim();
410 if cmd.is_empty() {
411 continue;
412 }
413 if cancel_token.is_cancelled() { return Err("Interrupted".to_string()); }
414 handle_pipeline(cmd, Box::new(io::stdin()), Box::new(io::stdout()), registry, cancel_token.clone())?;
415 }
416 Ok(())
417}
418
419struct CancelReader<R: Read> {
424 inner: R,
425 token: wasibox_core::CancellationToken,
426}
427impl<R: Read> Read for CancelReader<R> {
428 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
429 if self.token.is_cancelled() {
430 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
431 }
432 self.inner.read(buf)
433 }
434}
435
436struct CancelWriter<W: Write> {
437 inner: W,
438 token: wasibox_core::CancellationToken,
439}
440impl<W: Write> Write for CancelWriter<W> {
441 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
442 if self.token.is_cancelled() {
443 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
444 }
445 self.inner.write(buf)
446 }
447 fn flush(&mut self) -> io::Result<()> {
448 if self.token.is_cancelled() {
449 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
450 }
451 self.inner.flush()
452 }
453}
454
455struct PipeReader {
456 rx: std::sync::mpsc::Receiver<Vec<u8>>,
457 buffer: Vec<u8>,
458 pos: usize,
459 token: wasibox_core::CancellationToken,
460}
461impl Read for PipeReader {
462 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
463 if self.token.is_cancelled() {
464 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
465 }
466 if self.pos >= self.buffer.len() {
467 loop {
468 if self.token.is_cancelled() {
469 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
470 }
471 match self.rx.recv_timeout(std::time::Duration::from_millis(100)) {
472 Ok(data) => {
473 self.buffer = data;
474 self.pos = 0;
475 break;
476 }
477 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
478 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(0),
479 }
480 }
481 }
482 let available = self.buffer.len() - self.pos;
483 let to_copy = std::cmp::min(available, buf.len());
484 buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
485 self.pos += to_copy;
486 Ok(to_copy)
487 }
488}
489
490struct PipeWriter {
491 tx: std::sync::mpsc::SyncSender<Vec<u8>>,
492 token: wasibox_core::CancellationToken,
493}
494impl Write for PipeWriter {
495 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
496 if self.token.is_cancelled() {
497 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
498 }
499 self.tx.send(buf.to_vec()).map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e))?;
500 Ok(buf.len())
501 }
502 fn flush(&mut self) -> io::Result<()> { Ok(()) }
503}
504
505fn create_pipe(token: wasibox_core::CancellationToken) -> (PipeReader, PipeWriter) {
506 let (tx, rx) = std::sync::mpsc::sync_channel(1024);
507 (PipeReader { rx, buffer: Vec::new(), pos: 0, token: token.clone() }, PipeWriter { tx, token })
508}
509
510use std::sync::mpsc::{self, Sender, Receiver};
515
516pub struct StdinMultiplexer {
519 tx_subscribers: Mutex<Vec<Sender<Vec<u8>>>>,
520 cancel_token: wasibox_core::CancellationToken,
521}
522
523impl StdinMultiplexer {
524 pub fn new(cancel_token: wasibox_core::CancellationToken) -> Arc<Self> {
525 let mux = Arc::new(Self {
526 tx_subscribers: Mutex::new(Vec::new()),
527 cancel_token,
528 });
529
530 let mux_clone = Arc::clone(&mux);
531 std::thread::spawn(move || {
532 let mut buf = [0u8; 1024];
533 let mut stdin = io::stdin();
534 loop {
535 match stdin.read(&mut buf) {
536 Ok(0) => break, Ok(n) => {
538 let data = &buf[..n];
539 for &b in data {
541 if b == 3 {
542 mux_clone.cancel_token.cancel();
543 }
544 }
545 let mut subs = mux_clone.tx_subscribers.lock().unwrap();
547 subs.retain(|tx| {
548 tx.send(data.to_vec()).is_ok()
549 });
550 }
551 Err(_) => {
552 std::thread::sleep(std::time::Duration::from_millis(100));
555 }
556 }
557 }
558 });
559
560 mux
561 }
562
563 pub fn subscribe(&self) -> MultiplexedReader {
564 let (tx, rx) = mpsc::channel();
565 self.tx_subscribers.lock().unwrap().push(tx);
566 MultiplexedReader { rx, buffer: Vec::new(), pos: 0 }
567 }
568}
569
570pub struct MultiplexedReader {
571 rx: Receiver<Vec<u8>>,
572 buffer: Vec<u8>,
573 pos: usize,
574}
575
576impl Read for MultiplexedReader {
577 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
578 if self.pos >= self.buffer.len() {
579 match self.rx.recv() {
580 Ok(data) => {
581 self.buffer = data;
582 self.pos = 0;
583 }
584 Err(_) => return Ok(0),
585 }
586 }
587 let available = self.buffer.len() - self.pos;
588 let to_copy = std::cmp::min(available, buf.len());
589 buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
590 self.pos += to_copy;
591 Ok(to_copy)
592 }
593}
594
595pub struct ArcVecWriter {
597 pub inner: Arc<Mutex<Vec<u8>>>,
598}
599impl Write for ArcVecWriter {
600 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
601 let mut inner = self.inner.lock().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
602 inner.extend_from_slice(buf);
603 Ok(buf.len())
604 }
605 fn flush(&mut self) -> io::Result<()> { Ok(()) }
606}
607
608#[cfg(test)]
613mod tests {
614 use super::*;
615 use std::io::{BufRead, BufReader, Cursor};
616
617 fn get_temp_dir() -> tempfile::TempDir {
618 #[cfg(target_os = "wasi")]
619 {
620 tempfile::Builder::new()
621 .prefix("test_")
622 .tempdir_in(".")
623 .expect("Failed to create temp dir in current directory")
624 }
625 #[cfg(not(target_os = "wasi"))]
626 {
627 tempfile::tempdir().expect("Failed to create system temp dir")
628 }
629 }
630
631 fn builtins() -> CommandRegistry {
633 CommandRegistry::with_builtins()
634 }
635
636 #[test]
639 fn test_simple_echo() {
640 let out = Arc::new(Mutex::new(Vec::new()));
641 handle_pipeline("echo hello", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
642 let buf = out.lock().unwrap();
643 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
644 }
645
646 #[test]
647 fn test_pipe_echo_cat() {
648 let out = Arc::new(Mutex::new(Vec::new()));
649 handle_pipeline("echo hello | cat", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
650 let buf = out.lock().unwrap();
651 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
652 }
653
654 #[test]
655 fn test_pipe_grep() {
656 let out = Arc::new(Mutex::new(Vec::new()));
657 handle_pipeline("echo world | grep world", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
658 let buf = out.lock().unwrap();
659 assert_eq!(String::from_utf8_lossy(&buf).trim(), "world");
660 }
661
662 #[test]
663 fn test_redirection_create() {
664 let dir = get_temp_dir();
665 let file_path = dir.path().join("test_create.txt");
666 let cmd = format!("echo hello > \"{}\"", file_path.display());
667 handle_pipeline(&cmd, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
668 let content = std::fs::read_to_string(file_path).unwrap();
669 assert_eq!(content.trim(), "hello");
670 }
671
672 #[test]
673 fn test_redirection_append() {
674 let dir = get_temp_dir();
675 let file_path = dir.path().join("test_append.txt");
676 let cmd1 = format!("echo hello > \"{}\"", file_path.display());
677 handle_pipeline(&cmd1, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
678 let cmd2 = format!("echo world >> \"{}\"", file_path.display());
679 handle_pipeline(&cmd2, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
680 let content = std::fs::read_to_string(file_path).unwrap();
681 assert!(content.contains("hello"));
682 assert!(content.contains("world"));
683 }
684
685 #[test]
686 fn test_complex_pipeline() {
687 let out = Arc::new(Mutex::new(Vec::new()));
688 handle_pipeline("echo hello | grep h | wc -c", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
689 let buf = out.lock().unwrap();
690 assert_eq!(String::from_utf8_lossy(&buf).trim(), "6");
691 }
692
693 #[test]
696 fn test_custom_command() {
697 let mut reg = CommandRegistry::with_builtins();
698 reg.register("magic", |_args, ctx| {
699 write!(ctx.stdout, "magic happen").map_err(|e| e.to_string())
700 });
701
702 let out = Arc::new(Mutex::new(Vec::new()));
703 handle_pipeline("magic", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
704 let buf = out.lock().unwrap();
705 assert_eq!(String::from_utf8_lossy(&buf), "magic happen");
706 }
707
708 #[test]
709 fn test_custom_command_in_pipeline() {
710 let mut reg = CommandRegistry::with_builtins();
712 reg.register("double", |_args, ctx| {
713 for line in BufReader::new(&mut ctx.stdin).lines() {
714 let line = match line {
715 Ok(l) => l,
716 Err(_) => break,
717 };
718 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
719 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
720 }
721 Ok(())
722 });
723
724 let out = Arc::new(Mutex::new(Vec::new()));
725 handle_pipeline("echo hello | double", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
726 let buf = out.lock().unwrap();
727 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello\nhello");
728 }
729
730 #[test]
731 fn test_override_builtin() {
732 let mut reg = CommandRegistry::with_builtins();
734 reg.register("echo", |args, ctx| {
735 let msg = args[1..].join(" ").to_uppercase();
736 writeln!(ctx.stdout, "{}", msg).map_err(|e| e.to_string())
737 });
738
739 let out = Arc::new(Mutex::new(Vec::new()));
740 handle_pipeline("echo hello", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
741 let buf = out.lock().unwrap();
742 assert_eq!(String::from_utf8_lossy(&buf).trim(), "HELLO");
743 }
744
745 #[test]
748 fn test_all_external_pipeline() {
749 let mut reg = CommandRegistry::new();
750 reg.remove_fallback(); reg.register("count", |_args, ctx| {
754 for i in 1u64.. {
755 if writeln!(ctx.stdout, "{}", i).is_err() { break; }
756 }
757 Ok(())
758 });
759
760 reg.register("filter2", |_args, ctx| {
762 for line in BufReader::new(&mut ctx.stdin).lines() {
763 let line = match line {
764 Ok(l) => l,
765 Err(_) => break,
766 };
767 if line.contains('2') {
768 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
769 }
770 }
771 Ok(())
772 });
773
774 reg.register("take", |args, ctx| {
776 let n: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(5);
777 for (i, line) in BufReader::new(&mut ctx.stdin).lines().enumerate() {
778 if i >= n { break; }
779 let line = match line {
780 Ok(l) => l,
781 Err(_) => break,
782 };
783 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
784 }
785 Ok(())
786 });
787
788 let out = Arc::new(Mutex::new(Vec::new()));
789 handle_pipeline(
790 "count | filter2 | take 3",
791 Box::new(io::empty()),
792 Box::new(ArcVecWriter { inner: Arc::clone(&out) }),
793 ®,
794 wasibox_core::CancellationToken::new(),
795 ).unwrap();
796
797 let buf = out.lock().unwrap();
798 let result = String::from_utf8_lossy(&buf);
799 let lines: Vec<&str> = result.trim().lines().collect();
800 assert_eq!(lines.len(), 3);
801 for line in &lines {
802 assert!(line.contains('2'), "expected '2' in line, got: {}", line);
803 }
804 assert_eq!(lines[0], "2");
805 assert_eq!(lines[1], "12");
806 assert_eq!(lines[2], "20");
807 }
808
809 #[test]
812 fn test_parallel_execution() {
813 let mut reg = CommandRegistry::with_builtins();
814 reg.register("slow", |_args, _ctx| {
815 std::thread::sleep(std::time::Duration::from_millis(100));
816 Ok(())
817 });
818 let registry = Arc::new(reg);
819
820 let lines = vec!["slow".to_string(), "slow".to_string(), "slow".to_string()];
821 let start = std::time::Instant::now();
822 let results = handle_parallel(lines, Box::new(io::empty()), Box::new(io::sink()), registry, wasibox_core::CancellationToken::new());
823 let duration = start.elapsed();
824
825 assert_eq!(results.len(), 3);
826 for res in results {
827 assert!(res.is_ok());
828 }
829 assert!(duration < std::time::Duration::from_millis(250));
830 }
831
832 #[test]
835 fn test_streaming_pipeline() {
836 let out = Arc::new(Mutex::new(Vec::new()));
837 handle_pipeline("yes | head -n 2", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
838 let buf = out.lock().unwrap();
839 assert_eq!(String::from_utf8_lossy(&buf).trim(), "y\ny");
840 }
841
842 #[test]
843 fn test_seq_pipeline_head() {
844 let out = Arc::new(Mutex::new(Vec::new()));
845 handle_pipeline("seq | head -n 5", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
846 let buf = out.lock().unwrap();
847 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3\n4\n5");
848 }
849
850 #[test]
851 fn test_seq_pipeline_grep_head() {
852 let out = Arc::new(Mutex::new(Vec::new()));
853 handle_pipeline("seq | grep 2 | head -n 3", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
854 let buf = out.lock().unwrap();
855 let result = String::from_utf8_lossy(&buf);
856 let lines: Vec<&str> = result.trim().lines().collect();
857 assert_eq!(lines.len(), 3);
858 for line in &lines {
859 assert!(line.contains('2'));
860 }
861 assert_eq!(lines[0], "2");
862 assert_eq!(lines[1], "12");
863 assert_eq!(lines[2], "20");
864 }
865
866 #[test]
867 fn test_seq_pipeline_grep_head_5() {
868 let out = Arc::new(Mutex::new(Vec::new()));
869 handle_pipeline("seq | grep 2 | head -n 5", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
870 let buf = out.lock().unwrap();
871 let result = String::from_utf8_lossy(&buf);
872 let lines: Vec<&str> = result.trim().lines().collect();
873 assert_eq!(lines.len(), 5);
874 assert_eq!(lines[0], "2");
875 assert_eq!(lines[1], "12");
876 assert_eq!(lines[2], "20");
877 assert_eq!(lines[3], "21");
878 assert_eq!(lines[4], "22");
879 }
880
881 #[test]
882 fn test_seq_finite() {
883 let out = Arc::new(Mutex::new(Vec::new()));
884 handle_pipeline("seq 3", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
885 let buf = out.lock().unwrap();
886 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3");
887 }
888
889 #[test]
890 fn test_seq_range() {
891 let out = Arc::new(Mutex::new(Vec::new()));
892 handle_pipeline("seq 5 8", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
893 let buf = out.lock().unwrap();
894 assert_eq!(String::from_utf8_lossy(&buf).trim(), "5\n6\n7\n8");
895 }
896
897 #[test]
898 fn test_seq_step() {
899 let out = Arc::new(Mutex::new(Vec::new()));
900 handle_pipeline("seq 1 2 10", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
901 let buf = out.lock().unwrap();
902 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n3\n5\n7\n9");
903 }
904 #[test]
905 fn test_cd_and_ls() {
906 let dir = get_temp_dir();
907 std::fs::write(dir.path().join("aaa.txt"), "hello").unwrap();
909 std::fs::write(dir.path().join("bbb.txt"), "world").unwrap();
910
911 let original_cwd = env::current_dir().unwrap();
912 let reg = builtins();
913
914 let cd_cmd = format!("cd \"{}\"", dir.path().display());
916 handle_pipeline(&cd_cmd, Box::new(io::empty()), Box::new(io::sink()), ®, wasibox_core::CancellationToken::new()).unwrap();
917
918 let out = Arc::new(Mutex::new(Vec::new()));
920 handle_pipeline("ls", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
921
922 let buf = out.lock().unwrap();
923 let output = String::from_utf8_lossy(&buf);
924 assert!(output.contains("aaa.txt"), "expected aaa.txt in ls output, got: {}", output);
925 assert!(output.contains("bbb.txt"), "expected bbb.txt in ls output, got: {}", output);
926
927 env::set_current_dir(original_cwd).unwrap();
929 }
930
931 #[test]
934 fn test_command_line_sequence() {
935 let mut reg = CommandRegistry::with_builtins();
936 let counter = Arc::new(Mutex::new(0));
937 let c1 = Arc::clone(&counter);
938 reg.register("inc", move |_args, _ctx| {
939 *c1.lock().unwrap() += 1;
940 Ok(())
941 });
942
943 crate::handle_command_line("inc && inc && inc", ®, wasibox_core::CancellationToken::new()).unwrap();
945 assert_eq!(*counter.lock().unwrap(), 3);
946 }
947
948 #[test]
949 fn test_command_line_short_circuit() {
950 let mut reg = CommandRegistry::with_builtins();
951 let counter = Arc::new(Mutex::new(0));
952 let c1 = Arc::clone(&counter);
953 reg.register("inc", move |_args, _ctx| {
954 *c1.lock().unwrap() += 1;
955 Ok(())
956 });
957 reg.register("fail", |_args, _ctx| Err("simulated failure".to_string()));
958
959 let res = crate::handle_command_line("inc && fail && inc", ®, wasibox_core::CancellationToken::new());
962 assert!(res.is_err());
963 assert_eq!(res.unwrap_err(), "simulated failure");
964 assert_eq!(*counter.lock().unwrap(), 1); }
966
967 #[test]
968 fn test_handle_parallel_cancel() {
969 println!("STARTING TEST");
970 let registry = Arc::new(builtins());
971 let cancel_token = wasibox_core::CancellationToken::new();
972 let cancel_clone = cancel_token.clone();
973
974 let thread = std::thread::spawn(move || {
975 println!("THREAD SPAWNED");
976 super::handle_parallel(
977 vec!["yes".to_string()],
978 Box::new(std::io::empty()),
979 Box::new(std::io::sink()),
980 registry,
981 cancel_clone,
982 )
983 });
984
985 std::thread::sleep(std::time::Duration::from_millis(10));
986 println!("CANCELLING TOKEN");
987 cancel_token.cancel();
988
989 println!("JOINING THREAD");
990 let results = thread.join().unwrap();
991 println!("RESULTS: {:?}", results);
992 assert_eq!(results.len(), 1);
993 assert!(results[0].is_err());
994 assert_eq!(results[0].as_ref().unwrap_err(), "Interrupted");
995 }
996
997 #[test]
998 fn test_pipeline_cancel() {
999 let registry = builtins();
1000 let cancel_token = wasibox_core::CancellationToken::new();
1001 let cancel_clone = cancel_token.clone();
1002
1003 let thread = std::thread::spawn(move || {
1004 super::handle_pipeline(
1005 "yes | grep y",
1006 Box::new(std::io::empty()),
1007 Box::new(std::io::sink()),
1008 ®istry,
1009 cancel_clone,
1010 )
1011 });
1012
1013 std::thread::sleep(std::time::Duration::from_millis(10));
1014 cancel_token.cancel();
1015
1016 let result = thread.join().unwrap();
1017 assert!(result.is_err());
1018 assert_eq!(result.unwrap_err(), "Interrupted");
1019 }
1020
1021 #[test]
1022 fn test_seq_redirection_parallel_cancel() {
1023 let dir = get_temp_dir();
1024 let file_path = dir.path().join("i.txt");
1025 let cmd = format!("seq > \"{}\"", file_path.display());
1026
1027 let registry = Arc::new(builtins());
1028 let cancel_token = wasibox_core::CancellationToken::new();
1029 let cancel_clone = cancel_token.clone();
1030
1031 let thread = std::thread::spawn(move || {
1032 super::handle_parallel(
1033 vec![cmd],
1034 Box::new(std::io::empty()),
1035 Box::new(std::io::sink()),
1036 registry,
1037 cancel_clone,
1038 )
1039 });
1040
1041 std::thread::sleep(std::time::Duration::from_millis(50));
1043 cancel_token.cancel();
1044
1045 let results = thread.join().unwrap();
1046 assert_eq!(results.len(), 1);
1047 assert!(results[0].is_err());
1048 assert_eq!(results[0].as_ref().unwrap_err(), "Interrupted");
1049
1050 assert!(file_path.exists());
1052 let content = std::fs::read_to_string(&file_path).unwrap();
1053 assert!(!content.is_empty(), "File should not be empty");
1054
1055 println!("Final sequence number written: {}", content.lines().last().unwrap_or("none"));
1057 }
1058}
1059