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::{LineReader, LineHandler, LoopAction};
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(args.iter().cloned());
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 register<F>(&mut self, name: impl Into<String>, handler: F)
207 where
208 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
209 {
210 self.commands.insert(name.into(), Arc::new(handler));
211 }
212
213 pub fn set_fallback<F>(&mut self, handler: F)
215 where
216 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
217 {
218 self.fallback = Some(Arc::new(handler));
219 }
220
221 pub fn remove_fallback(&mut self) {
223 self.fallback = None;
224 }
225
226 pub fn execute(&self, args: &[String], ctx: &mut IoContext) -> Result<(), String> {
228 if args.is_empty() {
229 return Ok(());
230 }
231 let cmd = &args[0];
232
233 if let Some(handler) = self.commands.get(cmd.as_str()) {
234 return handler(args, ctx);
235 }
236 if let Some(ref fallback) = self.fallback {
237 return fallback(args, ctx);
238 }
239 Err(format!("command not found: {}", cmd))
240 }
241}
242
243impl Default for CommandRegistry {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249fn is_broken_pipe(err: &str) -> bool {
255 err.contains("Broken pipe")
256 || err.contains("BrokenPipe")
257 || err.contains("broken pipe")
258}
259
260pub fn handle_pipeline(
265 line: &str,
266 initial_stdin: Box<dyn Read + Send + 'static>,
267 final_stdout: Box<dyn Write + Send + 'static>,
268 registry: &CommandRegistry,
269 cancel_token: wasibox_core::CancellationToken,
270) -> Result<(), String> {
271 let stages_str: Vec<&str> = line.split('|').collect();
272
273 let mut final_stdout = Some(final_stdout);
274
275 std::thread::scope(|s| {
276 let mut prev_reader: Box<dyn Read + Send> = initial_stdin;
277 let mut threads = Vec::new();
278
279 for (i, stage_str) in stages_str.iter().enumerate() {
280 let is_last = i == stages_str.len() - 1;
281 let mut tokens = shlex::split(stage_str.trim())
282 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
283
284 let stdin = std::mem::replace(&mut prev_reader, Box::new(io::empty()));
285 let (stdout, next_reader): (Box<dyn Write + Send>, Option<Box<dyn Read + Send>>) = if is_last {
286 let mut out: Box<dyn Write + Send> = final_stdout.take().unwrap();
287 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
288 let append = tokens[pos] == ">>";
289 let filename = tokens.get(pos + 1).ok_or("Error: Missing file for redirection")?;
290 let file = if append {
291 std::fs::OpenOptions::new().create(true).append(true).open(filename)
292 } else {
293 File::create(filename)
294 }.map_err(|e| format!("Error opening file: {}", e))?;
295 out = Box::new(file);
296 tokens.truncate(pos);
297 }
298 (out, None)
299 } else {
300 let (reader, writer) = create_pipe(cancel_token.clone());
301 (Box::new(writer), Some(Box::new(reader)))
302 };
303
304 if let Some(r) = next_reader {
305 prev_reader = r;
306 }
307
308 let cancel_clone = cancel_token.clone();
309 let thread = s.spawn(move || {
310 if cancel_clone.is_cancelled() {
311 return Err("Interrupted".to_string());
312 }
313 let wrapped_stdin = Box::new(CancelReader { inner: stdin, token: cancel_clone.clone() });
314 let wrapped_stdout = Box::new(CancelWriter { inner: stdout, token: cancel_clone.clone() });
315 let wrapped_stderr = Box::new(CancelWriter { inner: Box::new(io::stderr()), token: cancel_clone.clone() });
316
317 let mut ctx = IoContext::with_cancel(wrapped_stdin, wrapped_stdout, wrapped_stderr, cancel_clone.clone());
318 let res = registry.execute(&tokens, &mut ctx);
319 if cancel_clone.is_cancelled() {
320 return Err("Interrupted".to_string());
321 }
322 res
323 });
324 threads.push((i, thread));
325 }
326
327 let last_idx = stages_str.len() - 1;
330 let mut final_res = Ok(());
331 for (idx, thread) in threads {
332 match thread.join() {
333 Ok(Ok(())) => {}
334 Ok(Err(e)) => {
335 if idx != last_idx && is_broken_pipe(&e) {
336 continue;
337 }
338 if final_res.is_ok() {
339 final_res = Err(e);
340 }
341 }
342 Err(_) => {
343 if final_res.is_ok() {
344 final_res = Err("Thread panicked".to_string());
345 }
346 }
347 }
348 }
349 final_res
350 })
351}
352
353pub fn handle_parallel(
358 lines: Vec<String>,
359 initial_stdin: Box<dyn Read + Send + 'static>,
360 final_stdout: Box<dyn Write + Send + 'static>,
361 registry: Arc<CommandRegistry>,
362 cancel_token: wasibox_core::CancellationToken,
363) -> Vec<Result<(), String>> {
364 let mut handles = Vec::new();
365 let mut stdin_opt: Option<Box<dyn Read + Send>> = Some(initial_stdin);
366 let mut stdout_opt: Option<Box<dyn Write + Send>> = Some(final_stdout);
367
368 for line in lines {
369 let registry = Arc::clone(®istry);
370 let mut stdin_for_thread: Option<Box<dyn Read + Send>> = Some(stdin_opt.take().unwrap_or_else(|| Box::new(io::empty())));
371 let mut stdout_for_thread: Option<Box<dyn Write + Send>> = Some(stdout_opt.take().unwrap_or_else(|| Box::new(io::stdout())));
372
373 let cancel_clone = cancel_token.clone();
374 let handle = std::thread::spawn(move || {
375 let commands: Vec<&str> = line.split("&&").collect();
376 for cmd in commands {
377 let cmd = cmd.trim();
378 if cmd.is_empty() { continue; }
379 if cancel_clone.is_cancelled() { return Err("Interrupted".to_string()); }
380 let stdin = stdin_for_thread.take().unwrap_or_else(|| Box::new(io::empty()));
381 let stdout = stdout_for_thread.take().unwrap_or_else(|| Box::new(io::stdout()));
382 handle_pipeline(cmd, stdin, stdout, &*registry, cancel_clone.clone())?;
383 }
384 Ok(())
385 });
386 handles.push(handle);
387 }
388
389 handles.into_iter()
390 .map(|h| h.join().unwrap_or(Err("Thread panicked".to_string())))
391 .collect()
392}
393
394pub fn handle_command_line(
397 line: &str,
398 registry: &CommandRegistry,
399 cancel_token: wasibox_core::CancellationToken,
400) -> Result<(), String> {
401 let commands: Vec<&str> = line.split("&&").collect();
402 for cmd in commands {
403 let cmd = cmd.trim();
404 if cmd.is_empty() {
405 continue;
406 }
407 if cancel_token.is_cancelled() { return Err("Interrupted".to_string()); }
408 handle_pipeline(cmd, Box::new(io::stdin()), Box::new(io::stdout()), registry, cancel_token.clone())?;
409 }
410 Ok(())
411}
412
413struct CancelReader<R: Read> {
418 inner: R,
419 token: wasibox_core::CancellationToken,
420}
421impl<R: Read> Read for CancelReader<R> {
422 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
423 if self.token.is_cancelled() {
424 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
425 }
426 self.inner.read(buf)
427 }
428}
429
430struct CancelWriter<W: Write> {
431 inner: W,
432 token: wasibox_core::CancellationToken,
433}
434impl<W: Write> Write for CancelWriter<W> {
435 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
436 if self.token.is_cancelled() {
437 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
438 }
439 self.inner.write(buf)
440 }
441 fn flush(&mut self) -> io::Result<()> {
442 if self.token.is_cancelled() {
443 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
444 }
445 self.inner.flush()
446 }
447}
448
449struct PipeReader {
450 rx: std::sync::mpsc::Receiver<Vec<u8>>,
451 buffer: Vec<u8>,
452 pos: usize,
453 token: wasibox_core::CancellationToken,
454}
455impl Read for PipeReader {
456 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
457 if self.token.is_cancelled() {
458 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
459 }
460 if self.pos >= self.buffer.len() {
461 loop {
462 if self.token.is_cancelled() {
463 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
464 }
465 match self.rx.recv_timeout(std::time::Duration::from_millis(100)) {
466 Ok(data) => {
467 self.buffer = data;
468 self.pos = 0;
469 break;
470 }
471 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
472 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(0),
473 }
474 }
475 }
476 let available = self.buffer.len() - self.pos;
477 let to_copy = std::cmp::min(available, buf.len());
478 buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
479 self.pos += to_copy;
480 Ok(to_copy)
481 }
482}
483
484struct PipeWriter {
485 tx: std::sync::mpsc::SyncSender<Vec<u8>>,
486 token: wasibox_core::CancellationToken,
487}
488impl Write for PipeWriter {
489 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
490 if self.token.is_cancelled() {
491 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
492 }
493 self.tx.send(buf.to_vec()).map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e))?;
494 Ok(buf.len())
495 }
496 fn flush(&mut self) -> io::Result<()> { Ok(()) }
497}
498
499fn create_pipe(token: wasibox_core::CancellationToken) -> (PipeReader, PipeWriter) {
500 let (tx, rx) = std::sync::mpsc::sync_channel(1024);
501 (PipeReader { rx, buffer: Vec::new(), pos: 0, token: token.clone() }, PipeWriter { tx, token })
502}
503
504pub struct ArcVecWriter {
510 pub inner: Arc<Mutex<Vec<u8>>>,
511}
512impl Write for ArcVecWriter {
513 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
514 let mut inner = self.inner.lock().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
515 inner.extend_from_slice(buf);
516 Ok(buf.len())
517 }
518 fn flush(&mut self) -> io::Result<()> { Ok(()) }
519}
520
521#[cfg(test)]
526mod tests {
527 use super::*;
528 use std::io::{BufRead, BufReader, Cursor};
529
530 fn get_temp_dir() -> tempfile::TempDir {
531 #[cfg(target_os = "wasi")]
532 {
533 tempfile::Builder::new()
534 .prefix("test_")
535 .tempdir_in(".")
536 .expect("Failed to create temp dir in current directory")
537 }
538 #[cfg(not(target_os = "wasi"))]
539 {
540 tempfile::tempdir().expect("Failed to create system temp dir")
541 }
542 }
543
544 fn builtins() -> CommandRegistry {
546 CommandRegistry::with_builtins()
547 }
548
549 #[test]
552 fn test_simple_echo() {
553 let out = Arc::new(Mutex::new(Vec::new()));
554 handle_pipeline("echo hello", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
555 let buf = out.lock().unwrap();
556 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
557 }
558
559 #[test]
560 fn test_pipe_echo_cat() {
561 let out = Arc::new(Mutex::new(Vec::new()));
562 handle_pipeline("echo hello | cat", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
563 let buf = out.lock().unwrap();
564 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
565 }
566
567 #[test]
568 fn test_pipe_grep() {
569 let out = Arc::new(Mutex::new(Vec::new()));
570 handle_pipeline("echo world | grep world", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
571 let buf = out.lock().unwrap();
572 assert_eq!(String::from_utf8_lossy(&buf).trim(), "world");
573 }
574
575 #[test]
576 fn test_redirection_create() {
577 let dir = get_temp_dir();
578 let file_path = dir.path().join("test_create.txt");
579 let cmd = format!("echo hello > \"{}\"", file_path.display());
580 handle_pipeline(&cmd, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
581 let content = std::fs::read_to_string(file_path).unwrap();
582 assert_eq!(content.trim(), "hello");
583 }
584
585 #[test]
586 fn test_redirection_append() {
587 let dir = get_temp_dir();
588 let file_path = dir.path().join("test_append.txt");
589 let cmd1 = format!("echo hello > \"{}\"", file_path.display());
590 handle_pipeline(&cmd1, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
591 let cmd2 = format!("echo world >> \"{}\"", file_path.display());
592 handle_pipeline(&cmd2, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
593 let content = std::fs::read_to_string(file_path).unwrap();
594 assert!(content.contains("hello"));
595 assert!(content.contains("world"));
596 }
597
598 #[test]
599 fn test_complex_pipeline() {
600 let out = Arc::new(Mutex::new(Vec::new()));
601 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();
602 let buf = out.lock().unwrap();
603 assert_eq!(String::from_utf8_lossy(&buf).trim(), "6");
604 }
605
606 #[test]
609 fn test_custom_command() {
610 let mut reg = CommandRegistry::with_builtins();
611 reg.register("magic", |_args, ctx| {
612 write!(ctx.stdout, "magic happen").map_err(|e| e.to_string())
613 });
614
615 let out = Arc::new(Mutex::new(Vec::new()));
616 handle_pipeline("magic", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
617 let buf = out.lock().unwrap();
618 assert_eq!(String::from_utf8_lossy(&buf), "magic happen");
619 }
620
621 #[test]
622 fn test_custom_command_in_pipeline() {
623 let mut reg = CommandRegistry::with_builtins();
625 reg.register("double", |_args, ctx| {
626 for line in BufReader::new(&mut ctx.stdin).lines() {
627 let line = match line {
628 Ok(l) => l,
629 Err(_) => break,
630 };
631 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
632 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
633 }
634 Ok(())
635 });
636
637 let out = Arc::new(Mutex::new(Vec::new()));
638 handle_pipeline("echo hello | double", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
639 let buf = out.lock().unwrap();
640 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello\nhello");
641 }
642
643 #[test]
644 fn test_override_builtin() {
645 let mut reg = CommandRegistry::with_builtins();
647 reg.register("echo", |args, ctx| {
648 let msg = args[1..].join(" ").to_uppercase();
649 writeln!(ctx.stdout, "{}", msg).map_err(|e| e.to_string())
650 });
651
652 let out = Arc::new(Mutex::new(Vec::new()));
653 handle_pipeline("echo hello", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
654 let buf = out.lock().unwrap();
655 assert_eq!(String::from_utf8_lossy(&buf).trim(), "HELLO");
656 }
657
658 #[test]
661 fn test_all_external_pipeline() {
662 let mut reg = CommandRegistry::new();
663 reg.remove_fallback(); reg.register("count", |_args, ctx| {
667 for i in 1u64.. {
668 if writeln!(ctx.stdout, "{}", i).is_err() { break; }
669 }
670 Ok(())
671 });
672
673 reg.register("filter2", |_args, ctx| {
675 for line in BufReader::new(&mut ctx.stdin).lines() {
676 let line = match line {
677 Ok(l) => l,
678 Err(_) => break,
679 };
680 if line.contains('2') {
681 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
682 }
683 }
684 Ok(())
685 });
686
687 reg.register("take", |args, ctx| {
689 let n: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(5);
690 for (i, line) in BufReader::new(&mut ctx.stdin).lines().enumerate() {
691 if i >= n { break; }
692 let line = match line {
693 Ok(l) => l,
694 Err(_) => break,
695 };
696 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
697 }
698 Ok(())
699 });
700
701 let out = Arc::new(Mutex::new(Vec::new()));
702 handle_pipeline(
703 "count | filter2 | take 3",
704 Box::new(io::empty()),
705 Box::new(ArcVecWriter { inner: Arc::clone(&out) }),
706 ®,
707 wasibox_core::CancellationToken::new(),
708 ).unwrap();
709
710 let buf = out.lock().unwrap();
711 let result = String::from_utf8_lossy(&buf);
712 let lines: Vec<&str> = result.trim().lines().collect();
713 assert_eq!(lines.len(), 3);
714 for line in &lines {
715 assert!(line.contains('2'), "expected '2' in line, got: {}", line);
716 }
717 assert_eq!(lines[0], "2");
718 assert_eq!(lines[1], "12");
719 assert_eq!(lines[2], "20");
720 }
721
722 #[test]
725 fn test_parallel_execution() {
726 let mut reg = CommandRegistry::with_builtins();
727 reg.register("slow", |_args, _ctx| {
728 std::thread::sleep(std::time::Duration::from_millis(100));
729 Ok(())
730 });
731 let registry = Arc::new(reg);
732
733 let lines = vec!["slow".to_string(), "slow".to_string(), "slow".to_string()];
734 let start = std::time::Instant::now();
735 let results = handle_parallel(lines, Box::new(io::empty()), Box::new(io::sink()), registry, wasibox_core::CancellationToken::new());
736 let duration = start.elapsed();
737
738 assert_eq!(results.len(), 3);
739 for res in results {
740 assert!(res.is_ok());
741 }
742 assert!(duration < std::time::Duration::from_millis(250));
743 }
744
745 #[test]
748 fn test_streaming_pipeline() {
749 let out = Arc::new(Mutex::new(Vec::new()));
750 handle_pipeline("yes | head -n 2", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
751 let buf = out.lock().unwrap();
752 assert_eq!(String::from_utf8_lossy(&buf).trim(), "y\ny");
753 }
754
755 #[test]
756 fn test_seq_pipeline_head() {
757 let out = Arc::new(Mutex::new(Vec::new()));
758 handle_pipeline("seq | head -n 5", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
759 let buf = out.lock().unwrap();
760 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3\n4\n5");
761 }
762
763 #[test]
764 fn test_seq_pipeline_grep_head() {
765 let out = Arc::new(Mutex::new(Vec::new()));
766 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();
767 let buf = out.lock().unwrap();
768 let result = String::from_utf8_lossy(&buf);
769 let lines: Vec<&str> = result.trim().lines().collect();
770 assert_eq!(lines.len(), 3);
771 for line in &lines {
772 assert!(line.contains('2'));
773 }
774 assert_eq!(lines[0], "2");
775 assert_eq!(lines[1], "12");
776 assert_eq!(lines[2], "20");
777 }
778
779 #[test]
780 fn test_seq_pipeline_grep_head_5() {
781 let out = Arc::new(Mutex::new(Vec::new()));
782 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();
783 let buf = out.lock().unwrap();
784 let result = String::from_utf8_lossy(&buf);
785 let lines: Vec<&str> = result.trim().lines().collect();
786 assert_eq!(lines.len(), 5);
787 assert_eq!(lines[0], "2");
788 assert_eq!(lines[1], "12");
789 assert_eq!(lines[2], "20");
790 assert_eq!(lines[3], "21");
791 assert_eq!(lines[4], "22");
792 }
793
794 #[test]
795 fn test_seq_finite() {
796 let out = Arc::new(Mutex::new(Vec::new()));
797 handle_pipeline("seq 3", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
798 let buf = out.lock().unwrap();
799 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3");
800 }
801
802 #[test]
803 fn test_seq_range() {
804 let out = Arc::new(Mutex::new(Vec::new()));
805 handle_pipeline("seq 5 8", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
806 let buf = out.lock().unwrap();
807 assert_eq!(String::from_utf8_lossy(&buf).trim(), "5\n6\n7\n8");
808 }
809
810 #[test]
811 fn test_seq_step() {
812 let out = Arc::new(Mutex::new(Vec::new()));
813 handle_pipeline("seq 1 2 10", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins(), wasibox_core::CancellationToken::new()).unwrap();
814 let buf = out.lock().unwrap();
815 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n3\n5\n7\n9");
816 }
817 #[test]
818 fn test_cd_and_ls() {
819 let dir = get_temp_dir();
820 std::fs::write(dir.path().join("aaa.txt"), "hello").unwrap();
822 std::fs::write(dir.path().join("bbb.txt"), "world").unwrap();
823
824 let original_cwd = env::current_dir().unwrap();
825 let reg = builtins();
826
827 let cd_cmd = format!("cd \"{}\"", dir.path().display());
829 handle_pipeline(&cd_cmd, Box::new(io::empty()), Box::new(io::sink()), ®, wasibox_core::CancellationToken::new()).unwrap();
830
831 let out = Arc::new(Mutex::new(Vec::new()));
833 handle_pipeline("ls", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®, wasibox_core::CancellationToken::new()).unwrap();
834
835 let buf = out.lock().unwrap();
836 let output = String::from_utf8_lossy(&buf);
837 assert!(output.contains("aaa.txt"), "expected aaa.txt in ls output, got: {}", output);
838 assert!(output.contains("bbb.txt"), "expected bbb.txt in ls output, got: {}", output);
839
840 env::set_current_dir(original_cwd).unwrap();
842 }
843
844 #[test]
847 fn test_command_line_sequence() {
848 let mut reg = CommandRegistry::with_builtins();
849 let counter = Arc::new(Mutex::new(0));
850 let c1 = Arc::clone(&counter);
851 reg.register("inc", move |_args, _ctx| {
852 *c1.lock().unwrap() += 1;
853 Ok(())
854 });
855
856 crate::handle_command_line("inc && inc && inc", ®, wasibox_core::CancellationToken::new()).unwrap();
858 assert_eq!(*counter.lock().unwrap(), 3);
859 }
860
861 #[test]
862 fn test_command_line_short_circuit() {
863 let mut reg = CommandRegistry::with_builtins();
864 let counter = Arc::new(Mutex::new(0));
865 let c1 = Arc::clone(&counter);
866 reg.register("inc", move |_args, _ctx| {
867 *c1.lock().unwrap() += 1;
868 Ok(())
869 });
870 reg.register("fail", |_args, _ctx| Err("simulated failure".to_string()));
871
872 let res = crate::handle_command_line("inc && fail && inc", ®, wasibox_core::CancellationToken::new());
875 assert!(res.is_err());
876 assert_eq!(res.unwrap_err(), "simulated failure");
877 assert_eq!(*counter.lock().unwrap(), 1); }
879
880 #[test]
881 fn test_handle_parallel_cancel() {
882 println!("STARTING TEST");
883 let registry = Arc::new(builtins());
884 let cancel_token = wasibox_core::CancellationToken::new();
885 let cancel_clone = cancel_token.clone();
886
887 let thread = std::thread::spawn(move || {
888 println!("THREAD SPAWNED");
889 super::handle_parallel(
890 vec!["yes".to_string()],
891 Box::new(std::io::empty()),
892 Box::new(std::io::sink()),
893 registry,
894 cancel_clone,
895 )
896 });
897
898 std::thread::sleep(std::time::Duration::from_millis(10));
899 println!("CANCELLING TOKEN");
900 cancel_token.cancel();
901
902 println!("JOINING THREAD");
903 let results = thread.join().unwrap();
904 println!("RESULTS: {:?}", results);
905 assert_eq!(results.len(), 1);
906 assert!(results[0].is_err());
907 assert_eq!(results[0].as_ref().unwrap_err(), "Interrupted");
908 }
909
910 #[test]
911 fn test_pipeline_cancel() {
912 let registry = builtins();
913 let cancel_token = wasibox_core::CancellationToken::new();
914 let cancel_clone = cancel_token.clone();
915
916 let thread = std::thread::spawn(move || {
917 super::handle_pipeline(
918 "yes | grep y",
919 Box::new(std::io::empty()),
920 Box::new(std::io::sink()),
921 ®istry,
922 cancel_clone,
923 )
924 });
925
926 std::thread::sleep(std::time::Duration::from_millis(10));
927 cancel_token.cancel();
928
929 let result = thread.join().unwrap();
930 assert!(result.is_err());
931 assert_eq!(result.unwrap_err(), "Interrupted");
932 }
933}
934