1use colored::*;
2use std::collections::HashMap;
3use std::env;
4use std::fs::File;
5use std::io::{self, Read, Write};
6use std::sync::{Arc, Mutex};
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())
70 .map_err(|e| e.to_string())?;
71
72 let mut shell_builtins = vec!["cd", "help", "exit"];
73 #[cfg(feature = "clear")]
74 shell_builtins.push("clear");
75 shell_builtins.sort();
76 writeln!(
77 ctx.stdout,
78 " Shell Built-ins: {}",
79 shell_builtins.join(", ")
80 )
81 .map_err(|e| e.to_string())?;
82
83 #[cfg(feature = "sl")]
84 writeln!(ctx.stdout, " Animations: sl").map_err(|e| e.to_string())?;
85
86 let mut utils = Vec::new();
87 #[cfg(feature = "arch")]
88 utils.push("arch");
89 #[cfg(feature = "basename")]
90 utils.push("basename");
91 #[cfg(feature = "cat")]
92 utils.push("cat");
93 #[cfg(feature = "cp")]
94 utils.push("cp");
95 #[cfg(feature = "dir")]
96 utils.push("dir");
97 #[cfg(feature = "dirname")]
98 utils.push("dirname");
99 #[cfg(feature = "echo")]
100 utils.push("echo");
101 #[cfg(feature = "env")]
102 utils.push("env");
103 #[cfg(feature = "false")]
104 utils.push("false");
105 #[cfg(feature = "grep")]
106 utils.push("grep");
107 #[cfg(feature = "head")]
108 utils.push("head");
109 #[cfg(feature = "link")]
110 utils.push("link");
111 #[cfg(feature = "ln")]
112 utils.push("ln");
113 #[cfg(feature = "ls")]
114 utils.push("ls");
115 #[cfg(feature = "mkdir")]
116 utils.push("mkdir");
117 #[cfg(feature = "mv")]
118 utils.push("mv");
119 #[cfg(feature = "pwd")]
120 utils.push("pwd");
121 #[cfg(feature = "rm")]
122 utils.push("rm");
123 #[cfg(feature = "rmdir")]
124 utils.push("rmdir");
125 #[cfg(feature = "seq")]
126 utils.push("seq");
127 #[cfg(feature = "sleep")]
128 utils.push("sleep");
129 #[cfg(feature = "tail")]
130 utils.push("tail");
131 #[cfg(feature = "tee")]
132 utils.push("tee");
133 #[cfg(feature = "touch")]
134 utils.push("touch");
135 #[cfg(feature = "tree")]
136 utils.push("tree");
137 #[cfg(feature = "true")]
138 utils.push("true");
139 #[cfg(feature = "uname")]
140 utils.push("uname");
141 #[cfg(feature = "unlink")]
142 utils.push("unlink");
143 #[cfg(feature = "wc")]
144 utils.push("wc");
145 #[cfg(feature = "whoami")]
146 utils.push("whoami");
147 #[cfg(feature = "yes")]
148 utils.push("yes");
149
150 if !utils.is_empty() {
151 utils.sort();
152 writeln!(ctx.stdout, " Core Utilities: {}", utils.join(", "))
153 .map_err(|e| e.to_string())?;
154 }
155 Ok(())
156 });
157
158 reg.register("cd", |args, _ctx| {
159 let new_dir = args.get(1).map(|s| s.as_str()).unwrap_or(".");
160 env::set_current_dir(new_dir).map_err(|e| format!("cd: {}", e))
161 });
162
163 reg.register("exit", |_args, _ctx| Ok(()));
164
165 #[cfg(feature = "clear")]
166 reg.register("clear", |_args, ctx| {
167 write!(ctx.stdout, "\x1B[2J\x1B[1;1H").map_err(|e| e.to_string())?;
168 ctx.stdout.flush().map_err(|e| e.to_string())
169 });
170
171 #[cfg(feature = "sl")]
172 reg.register("sl", |args, ctx| {
173 let _ = sl::run_with_token(args.iter().cloned(), Some(ctx.cancel_token.clone()));
174 Ok(())
175 });
176
177 #[cfg(feature = "arch")]
179 reg.register("arch", |args, ctx| {
180 wasibox_core::utils::arch::execute_with_context(args.iter().cloned(), ctx)
181 });
182 #[cfg(feature = "basename")]
183 reg.register("basename", |args, ctx| {
184 wasibox_core::utils::basename::execute_with_context(args.iter().cloned(), ctx)
185 });
186 #[cfg(feature = "cat")]
187 reg.register("cat", |args, ctx| {
188 wasibox_core::utils::cat::execute_with_context(args.iter().cloned(), ctx)
189 });
190 #[cfg(feature = "cp")]
191 reg.register("cp", |args, ctx| {
192 wasibox_core::utils::cp::execute_with_context(args.iter().cloned(), ctx)
193 });
194 #[cfg(feature = "dir")]
195 reg.register("dir", |args, ctx| {
196 wasibox_core::utils::dir::execute_with_context(args.iter().cloned(), ctx)
197 });
198 #[cfg(feature = "dirname")]
199 reg.register("dirname", |args, ctx| {
200 wasibox_core::utils::dirname::execute_with_context(args.iter().cloned(), ctx)
201 });
202 #[cfg(feature = "echo")]
203 reg.register("echo", |args, ctx| {
204 wasibox_core::utils::echo::execute_with_context(args.iter().cloned(), ctx)
205 });
206 #[cfg(feature = "env")]
207 reg.register("env", |args, ctx| {
208 wasibox_core::utils::env::execute_with_context(args.iter().cloned(), ctx)
209 });
210 #[cfg(feature = "false")]
211 reg.register("false", |args, ctx| {
212 wasibox_core::utils::r#false::execute_with_context(args.iter().cloned(), ctx)
213 });
214 #[cfg(feature = "grep")]
215 reg.register("grep", |args, ctx| {
216 wasibox_core::utils::grep::execute_with_context(args.iter().cloned(), ctx)
217 });
218 #[cfg(feature = "head")]
219 reg.register("head", |args, ctx| {
220 wasibox_core::utils::head::execute_with_context(args.iter().cloned(), ctx)
221 });
222 #[cfg(feature = "link")]
223 reg.register("link", |args, ctx| {
224 wasibox_core::utils::link::execute_with_context(args.iter().cloned(), ctx)
225 });
226 #[cfg(feature = "ln")]
227 reg.register("ln", |args, ctx| {
228 wasibox_core::utils::ln::execute_with_context(args.iter().cloned(), ctx)
229 });
230 #[cfg(feature = "ls")]
231 reg.register("ls", |args, ctx| {
232 wasibox_core::utils::ls::execute_with_context(args.iter().cloned(), ctx)
233 });
234 #[cfg(feature = "mkdir")]
235 reg.register("mkdir", |args, ctx| {
236 wasibox_core::utils::mkdir::execute_with_context(args.iter().cloned(), ctx)
237 });
238 #[cfg(feature = "mv")]
239 reg.register("mv", |args, ctx| {
240 wasibox_core::utils::mv::execute_with_context(args.iter().cloned(), ctx)
241 });
242 #[cfg(feature = "pwd")]
243 reg.register("pwd", |args, ctx| {
244 wasibox_core::utils::pwd::execute_with_context(args.iter().cloned(), ctx)
245 });
246 #[cfg(feature = "rm")]
247 reg.register("rm", |args, ctx| {
248 wasibox_core::utils::rm::execute_with_context(args.iter().cloned(), ctx)
249 });
250 #[cfg(feature = "rmdir")]
251 reg.register("rmdir", |args, ctx| {
252 wasibox_core::utils::rmdir::execute_with_context(args.iter().cloned(), ctx)
253 });
254 #[cfg(feature = "seq")]
255 reg.register("seq", |args, ctx| {
256 wasibox_core::utils::seq::execute_with_context(args.iter().cloned(), ctx)
257 });
258 #[cfg(feature = "sleep")]
259 reg.register("sleep", |args, ctx| {
260 wasibox_core::utils::sleep::execute_with_context(args.iter().cloned(), ctx)
261 });
262 #[cfg(feature = "tail")]
263 reg.register("tail", |args, ctx| {
264 wasibox_core::utils::tail::execute_with_context(args.iter().cloned(), ctx)
265 });
266 #[cfg(feature = "tee")]
267 reg.register("tee", |args, ctx| {
268 wasibox_core::utils::tee::execute_with_context(args.iter().cloned(), ctx)
269 });
270 #[cfg(feature = "touch")]
271 reg.register("touch", |args, ctx| {
272 wasibox_core::utils::touch::execute_with_context(args.iter().cloned(), ctx)
273 });
274 #[cfg(feature = "tree")]
275 reg.register("tree", |args, ctx| {
276 wasibox_core::utils::tree::execute_with_context(args.iter().cloned(), ctx)
277 });
278 #[cfg(feature = "true")]
279 reg.register("true", |args, ctx| {
280 wasibox_core::utils::r#true::execute_with_context(args.iter().cloned(), ctx)
281 });
282 #[cfg(feature = "uname")]
283 reg.register("uname", |args, ctx| {
284 wasibox_core::utils::uname::execute_with_context(args.iter().cloned(), ctx)
285 });
286 #[cfg(feature = "unlink")]
287 reg.register("unlink", |args, ctx| {
288 wasibox_core::utils::unlink::execute_with_context(args.iter().cloned(), ctx)
289 });
290 #[cfg(feature = "wc")]
291 reg.register("wc", |args, ctx| {
292 wasibox_core::utils::wc::execute_with_context(args.iter().cloned(), ctx)
293 });
294 #[cfg(feature = "whoami")]
295 reg.register("whoami", |args, ctx| {
296 wasibox_core::utils::whoami::execute_with_context(args.iter().cloned(), ctx)
297 });
298 #[cfg(feature = "yes")]
299 reg.register("yes", |args, ctx| {
300 wasibox_core::utils::yes::execute_with_context(args.iter().cloned(), ctx)
301 });
302
303 reg
304 }
305
306 pub fn command_names(&self) -> Vec<&str> {
309 self.commands.keys().map(|s| s.as_str()).collect()
310 }
311
312 pub fn register<F>(&mut self, name: impl Into<String>, handler: F)
314 where
315 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
316 {
317 self.commands.insert(name.into(), Arc::new(handler));
318 }
319
320 pub fn set_fallback<F>(&mut self, handler: F)
322 where
323 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
324 {
325 self.fallback = Some(Arc::new(handler));
326 }
327
328 pub fn remove_fallback(&mut self) {
330 self.fallback = None;
331 }
332
333 pub fn execute(&self, args: &[String], ctx: &mut IoContext) -> Result<(), String> {
335 if args.is_empty() {
336 return Ok(());
337 }
338 let cmd = &args[0];
339
340 if let Some(handler) = self.commands.get(cmd.as_str()) {
341 return handler(args, ctx);
342 }
343 if let Some(ref fallback) = self.fallback {
344 return fallback(args, ctx);
345 }
346 Err(format!("command not found: {}", cmd))
347 }
348}
349
350impl Default for CommandRegistry {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356fn is_broken_pipe(err: &str) -> bool {
362 err.contains("Broken pipe") || err.contains("BrokenPipe") || err.contains("broken pipe")
363}
364
365pub fn handle_pipeline(
370 line: &str,
371 initial_stdin: Box<dyn Read + Send + 'static>,
372 final_stdout: Box<dyn Write + Send + 'static>,
373 registry: &CommandRegistry,
374 cancel_token: wasibox_core::CancellationToken,
375) -> Result<(), String> {
376 let stages_str: Vec<&str> = line.split('|').collect();
377
378 let mut final_stdout = Some(final_stdout);
379
380 if stages_str.len() == 1 {
381 let mut tokens = shlex::split(stages_str[0].trim())
382 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
383 let mut stdout: Box<dyn Write + Send> = final_stdout.take().unwrap();
384 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
385 let append = tokens[pos] == ">>";
386 let filename = tokens
387 .get(pos + 1)
388 .ok_or("Error: Missing file for redirection")?;
389 let file = if append {
390 std::fs::OpenOptions::new()
391 .create(true)
392 .append(true)
393 .open(filename)
394 } else {
395 File::create(filename)
396 }
397 .map_err(|e| format!("Error opening file: {}", e))?;
398 stdout = Box::new(file);
399 tokens.truncate(pos);
400 }
401
402 if cancel_token.is_cancelled() {
403 return Err("Interrupted".to_string());
404 }
405 let wrapped_stdin = Box::new(CancelReader {
406 inner: initial_stdin,
407 token: cancel_token.clone(),
408 });
409 let wrapped_stdout = Box::new(CancelWriter {
410 inner: stdout,
411 token: cancel_token.clone(),
412 });
413 let wrapped_stderr = Box::new(CancelWriter {
414 inner: Box::new(io::stderr()),
415 token: cancel_token.clone(),
416 });
417
418 let mut ctx = IoContext::with_cancel(
419 wrapped_stdin,
420 wrapped_stdout,
421 wrapped_stderr,
422 cancel_token.clone(),
423 );
424 let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
425 registry.execute(&tokens, &mut ctx)
426 }))
427 .unwrap_or_else(|_| Err("Thread panicked".to_string()));
428 if cancel_token.is_cancelled() {
429 return Err("Interrupted".to_string());
430 }
431 return res;
432 }
433
434 std::thread::scope(|s| {
435 let mut prev_reader: Box<dyn Read + Send> = initial_stdin;
436 let mut threads = Vec::new();
437
438 for (i, stage_str) in stages_str.iter().enumerate() {
439 let is_last = i == stages_str.len() - 1;
440 let mut tokens = shlex::split(stage_str.trim())
441 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
442
443 let stdin = std::mem::replace(&mut prev_reader, Box::new(io::empty()));
444 let (stdout, next_reader): (Box<dyn Write + Send>, Option<Box<dyn Read + Send>>) =
445 if is_last {
446 let mut out: Box<dyn Write + Send> = final_stdout.take().unwrap();
447 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
448 let append = tokens[pos] == ">>";
449 let filename = tokens
450 .get(pos + 1)
451 .ok_or("Error: Missing file for redirection")?;
452 let file = if append {
453 std::fs::OpenOptions::new()
454 .create(true)
455 .append(true)
456 .open(filename)
457 } else {
458 File::create(filename)
459 }
460 .map_err(|e| format!("Error opening file: {}", e))?;
461 out = Box::new(file);
462 tokens.truncate(pos);
463 }
464 (out, None)
465 } else {
466 let (reader, writer) = create_pipe(cancel_token.clone());
467 (Box::new(writer), Some(Box::new(reader)))
468 };
469
470 if let Some(r) = next_reader {
471 prev_reader = r;
472 }
473
474 let cancel_clone = cancel_token.clone();
475 let thread = s.spawn(move || {
476 if cancel_clone.is_cancelled() {
477 return Err("Interrupted".to_string());
478 }
479 let wrapped_stdin = Box::new(CancelReader {
480 inner: stdin,
481 token: cancel_clone.clone(),
482 });
483 let wrapped_stdout = Box::new(CancelWriter {
484 inner: stdout,
485 token: cancel_clone.clone(),
486 });
487 let wrapped_stderr = Box::new(CancelWriter {
488 inner: Box::new(io::stderr()),
489 token: cancel_clone.clone(),
490 });
491
492 let mut ctx = IoContext::with_cancel(
493 wrapped_stdin,
494 wrapped_stdout,
495 wrapped_stderr,
496 cancel_clone.clone(),
497 );
498 let res = registry.execute(&tokens, &mut ctx);
499 if cancel_clone.is_cancelled() {
500 return Err("Interrupted".to_string());
501 }
502 res
503 });
504 threads.push((i, thread));
505 }
506
507 let last_idx = stages_str.len() - 1;
510 let mut final_res = Ok(());
511 for (idx, thread) in threads {
512 match thread.join() {
513 Ok(Ok(())) => {}
514 Ok(Err(e)) => {
515 if idx != last_idx && is_broken_pipe(&e) {
516 continue;
517 }
518 if final_res.is_ok() {
519 final_res = Err(e);
520 }
521 }
522 Err(_) => {
523 if final_res.is_ok() {
524 final_res = Err("Thread panicked".to_string());
525 }
526 }
527 }
528 }
529 final_res
530 })
531}
532
533pub fn handle_parallel(
538 mut lines: Vec<String>,
539 initial_stdin: Box<dyn Read + Send + 'static>,
540 final_stdout: Box<dyn Write + Send + 'static>,
541 registry: Arc<CommandRegistry>,
542 cancel_token: wasibox_core::CancellationToken,
543) -> Vec<Result<(), String>> {
544 if lines.len() == 1 {
545 let line = lines.pop().unwrap();
546 let mut stdin_opt: Option<Box<dyn Read + Send>> = Some(initial_stdin);
547 let mut stdout_opt: Option<Box<dyn Write + Send>> = Some(final_stdout);
548 let result = (|| {
549 let commands: Vec<&str> = line.split("&&").collect();
550 for cmd in commands {
551 let cmd = cmd.trim();
552 if cmd.is_empty() {
553 continue;
554 }
555 if cancel_token.is_cancelled() {
556 return Err("Interrupted".to_string());
557 }
558 let stdin = stdin_opt.take().unwrap_or_else(|| Box::new(io::empty()));
559 let stdout = stdout_opt.take().unwrap_or_else(|| Box::new(io::stdout()));
560 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
561 handle_pipeline(cmd, stdin, stdout, &*registry, cancel_token.clone())
562 }))
563 .unwrap_or_else(|_| Err("Thread panicked".to_string()))?;
564 }
565 Ok(())
566 })();
567 return vec![result];
568 }
569
570 let mut handles = Vec::new();
571 let mut stdin_opt: Option<Box<dyn Read + Send>> = Some(initial_stdin);
572 let mut stdout_opt: Option<Box<dyn Write + Send>> = Some(final_stdout);
573
574 for line in lines {
575 let registry = Arc::clone(®istry);
576 let mut stdin_for_thread: Option<Box<dyn Read + Send>> =
577 Some(stdin_opt.take().unwrap_or_else(|| Box::new(io::empty())));
578 let mut stdout_for_thread: Option<Box<dyn Write + Send>> =
579 Some(stdout_opt.take().unwrap_or_else(|| Box::new(io::stdout())));
580
581 let cancel_clone = cancel_token.clone();
582 let handle = std::thread::spawn(move || {
583 let commands: Vec<&str> = line.split("&&").collect();
584 for cmd in commands {
585 let cmd = cmd.trim();
586 if cmd.is_empty() {
587 continue;
588 }
589 if cancel_clone.is_cancelled() {
590 return Err("Interrupted".to_string());
591 }
592 let stdin = stdin_for_thread
593 .take()
594 .unwrap_or_else(|| Box::new(io::empty()));
595 let stdout = stdout_for_thread
596 .take()
597 .unwrap_or_else(|| Box::new(io::stdout()));
598 handle_pipeline(cmd, stdin, stdout, &*registry, cancel_clone.clone())?;
599 }
600 Ok(())
601 });
602 handles.push(handle);
603 }
604
605 handles
606 .into_iter()
607 .map(|h| h.join().unwrap_or(Err("Thread panicked".to_string())))
608 .collect()
609}
610
611pub fn handle_command_line(
614 line: &str,
615 registry: &CommandRegistry,
616 cancel_token: wasibox_core::CancellationToken,
617) -> Result<(), String> {
618 let commands: Vec<&str> = line.split("&&").collect();
619 for cmd in commands {
620 let cmd = cmd.trim();
621 if cmd.is_empty() {
622 continue;
623 }
624 if cancel_token.is_cancelled() {
625 return Err("Interrupted".to_string());
626 }
627 handle_pipeline(
628 cmd,
629 Box::new(io::stdin()),
630 Box::new(io::stdout()),
631 registry,
632 cancel_token.clone(),
633 )?;
634 }
635 Ok(())
636}
637
638struct CancelReader<R: Read> {
643 inner: R,
644 token: wasibox_core::CancellationToken,
645}
646impl<R: Read> Read for CancelReader<R> {
647 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
648 if self.token.is_cancelled() {
649 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
650 }
651 self.inner.read(buf)
652 }
653}
654
655struct CancelWriter<W: Write> {
656 inner: W,
657 token: wasibox_core::CancellationToken,
658}
659impl<W: Write> Write for CancelWriter<W> {
660 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
661 if self.token.is_cancelled() {
662 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
663 }
664 self.inner.write(buf)
665 }
666 fn flush(&mut self) -> io::Result<()> {
667 if self.token.is_cancelled() {
668 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
669 }
670 self.inner.flush()
671 }
672}
673
674struct PipeReader {
675 rx: std::sync::mpsc::Receiver<Vec<u8>>,
676 buffer: Vec<u8>,
677 pos: usize,
678 token: wasibox_core::CancellationToken,
679}
680impl Read for PipeReader {
681 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
682 if self.token.is_cancelled() {
683 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
684 }
685 if self.pos >= self.buffer.len() {
686 loop {
687 if self.token.is_cancelled() {
688 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
689 }
690 match self.rx.recv_timeout(std::time::Duration::from_millis(100)) {
691 Ok(data) => {
692 self.buffer = data;
693 self.pos = 0;
694 break;
695 }
696 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
697 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(0),
698 }
699 }
700 }
701 let available = self.buffer.len() - self.pos;
702 let to_copy = std::cmp::min(available, buf.len());
703 buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
704 self.pos += to_copy;
705 Ok(to_copy)
706 }
707}
708
709struct PipeWriter {
710 tx: std::sync::mpsc::SyncSender<Vec<u8>>,
711 token: wasibox_core::CancellationToken,
712}
713impl Write for PipeWriter {
714 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
715 if self.token.is_cancelled() {
716 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Interrupted"));
717 }
718 self.tx
719 .send(buf.to_vec())
720 .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e))?;
721 Ok(buf.len())
722 }
723 fn flush(&mut self) -> io::Result<()> {
724 Ok(())
725 }
726}
727
728fn create_pipe(token: wasibox_core::CancellationToken) -> (PipeReader, PipeWriter) {
729 let (tx, rx) = std::sync::mpsc::sync_channel(1024);
730 (
731 PipeReader {
732 rx,
733 buffer: Vec::new(),
734 pos: 0,
735 token: token.clone(),
736 },
737 PipeWriter { tx, token },
738 )
739}
740
741use std::sync::mpsc::{self, Receiver, Sender};
746
747pub struct StdinMultiplexer {
750 tx_subscribers: Mutex<Vec<Sender<Vec<u8>>>>,
751 cancel_token: wasibox_core::CancellationToken,
752}
753
754impl StdinMultiplexer {
755 pub fn new(cancel_token: wasibox_core::CancellationToken) -> Arc<Self> {
756 let mux = Arc::new(Self {
757 tx_subscribers: Mutex::new(Vec::new()),
758 cancel_token,
759 });
760
761 let mux_clone = Arc::clone(&mux);
762 std::thread::spawn(move || {
763 let mut buf = [0u8; 1024];
764 let mut stdin = io::stdin();
765 loop {
766 match stdin.read(&mut buf) {
767 Ok(0) => break, Ok(n) => {
769 let data = &buf[..n];
770 for &b in data {
772 if b == 3 {
773 mux_clone.cancel_token.cancel();
774 }
775 }
776 let mut subs = mux_clone.tx_subscribers.lock().unwrap();
778 subs.retain(|tx| tx.send(data.to_vec()).is_ok());
779 }
780 Err(_) => {
781 std::thread::sleep(std::time::Duration::from_millis(100));
784 }
785 }
786 }
787 });
788
789 mux
790 }
791
792 pub fn subscribe(&self) -> MultiplexedReader {
793 let (tx, rx) = mpsc::channel();
794 self.tx_subscribers.lock().unwrap().push(tx);
795 MultiplexedReader {
796 rx,
797 buffer: Vec::new(),
798 pos: 0,
799 }
800 }
801}
802
803pub struct MultiplexedReader {
804 rx: Receiver<Vec<u8>>,
805 buffer: Vec<u8>,
806 pos: usize,
807}
808
809impl Read for MultiplexedReader {
810 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
811 if self.pos >= self.buffer.len() {
812 match self.rx.recv() {
813 Ok(data) => {
814 self.buffer = data;
815 self.pos = 0;
816 }
817 Err(_) => return Ok(0),
818 }
819 }
820 let available = self.buffer.len() - self.pos;
821 let to_copy = std::cmp::min(available, buf.len());
822 buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
823 self.pos += to_copy;
824 Ok(to_copy)
825 }
826}
827
828pub struct ArcVecWriter {
830 pub inner: Arc<Mutex<Vec<u8>>>,
831}
832impl Write for ArcVecWriter {
833 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
834 let mut inner = self
835 .inner
836 .lock()
837 .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
838 inner.extend_from_slice(buf);
839 Ok(buf.len())
840 }
841 fn flush(&mut self) -> io::Result<()> {
842 Ok(())
843 }
844}
845
846#[cfg(test)]
851mod tests {
852 use super::*;
853 use std::io::{BufRead, BufReader, Cursor};
854
855 fn get_temp_dir() -> tempfile::TempDir {
856 #[cfg(target_os = "wasi")]
857 {
858 tempfile::Builder::new()
859 .prefix("test_")
860 .tempdir_in(".")
861 .expect("Failed to create temp dir in current directory")
862 }
863 #[cfg(not(target_os = "wasi"))]
864 {
865 tempfile::tempdir().expect("Failed to create system temp dir")
866 }
867 }
868
869 fn builtins() -> CommandRegistry {
871 CommandRegistry::with_builtins()
872 }
873
874 #[test]
877 fn test_simple_echo() {
878 let out = Arc::new(Mutex::new(Vec::new()));
879 handle_pipeline(
880 "echo hello",
881 Box::new(Cursor::new("")),
882 Box::new(ArcVecWriter {
883 inner: Arc::clone(&out),
884 }),
885 &builtins(),
886 wasibox_core::CancellationToken::new(),
887 )
888 .unwrap();
889 let buf = out.lock().unwrap();
890 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
891 }
892
893 #[test]
894 fn test_single_stage_pipeline_runs_inline() {
895 let caller_thread = format!("{:?}", std::thread::current().id());
896 let observed_thread = Arc::new(Mutex::new(None));
897 let observed_thread_clone = Arc::clone(&observed_thread);
898
899 let mut reg = CommandRegistry::new();
900 reg.remove_fallback();
901 reg.register("record-thread", move |_args, _ctx| {
902 *observed_thread_clone.lock().unwrap() =
903 Some(format!("{:?}", std::thread::current().id()));
904 Ok(())
905 });
906
907 handle_pipeline(
908 "record-thread",
909 Box::new(Cursor::new("")),
910 Box::new(io::sink()),
911 ®,
912 wasibox_core::CancellationToken::new(),
913 )
914 .unwrap();
915
916 assert_eq!(
917 observed_thread.lock().unwrap().as_deref(),
918 Some(caller_thread.as_str())
919 );
920 }
921
922 #[test]
923 fn test_single_stage_pipeline_catches_panics() {
924 let mut reg = CommandRegistry::new();
925 reg.remove_fallback();
926 reg.register("panic", |_args, _ctx| {
927 panic!("intentional test panic");
928 });
929
930 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
931 handle_pipeline(
932 "panic",
933 Box::new(Cursor::new("")),
934 Box::new(io::sink()),
935 ®,
936 wasibox_core::CancellationToken::new(),
937 )
938 }));
939
940 assert!(
941 result.is_ok(),
942 "single-stage panic should be converted to Err"
943 );
944 assert_eq!(result.unwrap(), Err("Thread panicked".to_string()));
945 }
946
947 #[test]
948 fn test_pipe_echo_cat() {
949 let out = Arc::new(Mutex::new(Vec::new()));
950 handle_pipeline(
951 "echo hello | cat",
952 Box::new(Cursor::new("")),
953 Box::new(ArcVecWriter {
954 inner: Arc::clone(&out),
955 }),
956 &builtins(),
957 wasibox_core::CancellationToken::new(),
958 )
959 .unwrap();
960 let buf = out.lock().unwrap();
961 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
962 }
963
964 #[test]
965 fn test_pipe_grep() {
966 let out = Arc::new(Mutex::new(Vec::new()));
967 handle_pipeline(
968 "echo world | grep world",
969 Box::new(Cursor::new("")),
970 Box::new(ArcVecWriter {
971 inner: Arc::clone(&out),
972 }),
973 &builtins(),
974 wasibox_core::CancellationToken::new(),
975 )
976 .unwrap();
977 let buf = out.lock().unwrap();
978 assert_eq!(String::from_utf8_lossy(&buf).trim(), "world");
979 }
980
981 #[test]
982 fn test_redirection_create() {
983 let dir = get_temp_dir();
984 let file_path = dir.path().join("test_create.txt");
985 let cmd = format!("echo hello > \"{}\"", file_path.display());
986 handle_pipeline(
987 &cmd,
988 Box::new(Cursor::new("")),
989 Box::new(io::sink()),
990 &builtins(),
991 wasibox_core::CancellationToken::new(),
992 )
993 .unwrap();
994 let content = std::fs::read_to_string(file_path).unwrap();
995 assert_eq!(content.trim(), "hello");
996 }
997
998 #[test]
999 fn test_redirection_append() {
1000 let dir = get_temp_dir();
1001 let file_path = dir.path().join("test_append.txt");
1002 let cmd1 = format!("echo hello > \"{}\"", file_path.display());
1003 handle_pipeline(
1004 &cmd1,
1005 Box::new(Cursor::new("")),
1006 Box::new(io::sink()),
1007 &builtins(),
1008 wasibox_core::CancellationToken::new(),
1009 )
1010 .unwrap();
1011 let cmd2 = format!("echo world >> \"{}\"", file_path.display());
1012 handle_pipeline(
1013 &cmd2,
1014 Box::new(Cursor::new("")),
1015 Box::new(io::sink()),
1016 &builtins(),
1017 wasibox_core::CancellationToken::new(),
1018 )
1019 .unwrap();
1020 let content = std::fs::read_to_string(file_path).unwrap();
1021 assert!(content.contains("hello"));
1022 assert!(content.contains("world"));
1023 }
1024
1025 #[test]
1026 fn test_complex_pipeline() {
1027 let out = Arc::new(Mutex::new(Vec::new()));
1028 handle_pipeline(
1029 "echo hello | grep h | wc -c",
1030 Box::new(Cursor::new("")),
1031 Box::new(ArcVecWriter {
1032 inner: Arc::clone(&out),
1033 }),
1034 &builtins(),
1035 wasibox_core::CancellationToken::new(),
1036 )
1037 .unwrap();
1038 let buf = out.lock().unwrap();
1039 assert_eq!(String::from_utf8_lossy(&buf).trim(), "6");
1040 }
1041
1042 #[test]
1045 fn test_custom_command() {
1046 let mut reg = CommandRegistry::with_builtins();
1047 reg.register("magic", |_args, ctx| {
1048 write!(ctx.stdout, "magic happen").map_err(|e| e.to_string())
1049 });
1050
1051 let out = Arc::new(Mutex::new(Vec::new()));
1052 handle_pipeline(
1053 "magic",
1054 Box::new(io::empty()),
1055 Box::new(ArcVecWriter {
1056 inner: Arc::clone(&out),
1057 }),
1058 ®,
1059 wasibox_core::CancellationToken::new(),
1060 )
1061 .unwrap();
1062 let buf = out.lock().unwrap();
1063 assert_eq!(String::from_utf8_lossy(&buf), "magic happen");
1064 }
1065
1066 #[test]
1067 fn test_custom_command_in_pipeline() {
1068 let mut reg = CommandRegistry::with_builtins();
1070 reg.register("double", |_args, ctx| {
1071 for line in BufReader::new(&mut ctx.stdin).lines() {
1072 let line = match line {
1073 Ok(l) => l,
1074 Err(_) => break,
1075 };
1076 if writeln!(ctx.stdout, "{}", line).is_err() {
1077 break;
1078 }
1079 if writeln!(ctx.stdout, "{}", line).is_err() {
1080 break;
1081 }
1082 }
1083 Ok(())
1084 });
1085
1086 let out = Arc::new(Mutex::new(Vec::new()));
1087 handle_pipeline(
1088 "echo hello | double",
1089 Box::new(io::empty()),
1090 Box::new(ArcVecWriter {
1091 inner: Arc::clone(&out),
1092 }),
1093 ®,
1094 wasibox_core::CancellationToken::new(),
1095 )
1096 .unwrap();
1097 let buf = out.lock().unwrap();
1098 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello\nhello");
1099 }
1100
1101 #[test]
1102 fn test_override_builtin() {
1103 let mut reg = CommandRegistry::with_builtins();
1105 reg.register("echo", |args, ctx| {
1106 let msg = args[1..].join(" ").to_uppercase();
1107 writeln!(ctx.stdout, "{}", msg).map_err(|e| e.to_string())
1108 });
1109
1110 let out = Arc::new(Mutex::new(Vec::new()));
1111 handle_pipeline(
1112 "echo hello",
1113 Box::new(io::empty()),
1114 Box::new(ArcVecWriter {
1115 inner: Arc::clone(&out),
1116 }),
1117 ®,
1118 wasibox_core::CancellationToken::new(),
1119 )
1120 .unwrap();
1121 let buf = out.lock().unwrap();
1122 assert_eq!(String::from_utf8_lossy(&buf).trim(), "HELLO");
1123 }
1124
1125 #[test]
1128 fn test_all_external_pipeline() {
1129 let mut reg = CommandRegistry::new();
1130 reg.remove_fallback(); reg.register("count", |_args, ctx| {
1134 for i in 1u64.. {
1135 if writeln!(ctx.stdout, "{}", i).is_err() {
1136 break;
1137 }
1138 }
1139 Ok(())
1140 });
1141
1142 reg.register("filter2", |_args, ctx| {
1144 for line in BufReader::new(&mut ctx.stdin).lines() {
1145 let line = match line {
1146 Ok(l) => l,
1147 Err(_) => break,
1148 };
1149 if line.contains('2') {
1150 if writeln!(ctx.stdout, "{}", line).is_err() {
1151 break;
1152 }
1153 }
1154 }
1155 Ok(())
1156 });
1157
1158 reg.register("take", |args, ctx| {
1160 let n: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(5);
1161 for (i, line) in BufReader::new(&mut ctx.stdin).lines().enumerate() {
1162 if i >= n {
1163 break;
1164 }
1165 let line = match line {
1166 Ok(l) => l,
1167 Err(_) => break,
1168 };
1169 if writeln!(ctx.stdout, "{}", line).is_err() {
1170 break;
1171 }
1172 }
1173 Ok(())
1174 });
1175
1176 let out = Arc::new(Mutex::new(Vec::new()));
1177 handle_pipeline(
1178 "count | filter2 | take 3",
1179 Box::new(io::empty()),
1180 Box::new(ArcVecWriter {
1181 inner: Arc::clone(&out),
1182 }),
1183 ®,
1184 wasibox_core::CancellationToken::new(),
1185 )
1186 .unwrap();
1187
1188 let buf = out.lock().unwrap();
1189 let result = String::from_utf8_lossy(&buf);
1190 let lines: Vec<&str> = result.trim().lines().collect();
1191 assert_eq!(lines.len(), 3);
1192 for line in &lines {
1193 assert!(line.contains('2'), "expected '2' in line, got: {}", line);
1194 }
1195 assert_eq!(lines[0], "2");
1196 assert_eq!(lines[1], "12");
1197 assert_eq!(lines[2], "20");
1198 }
1199
1200 #[test]
1203 fn test_parallel_execution() {
1204 let mut reg = CommandRegistry::with_builtins();
1205 reg.register("slow", |_args, _ctx| {
1206 std::thread::sleep(std::time::Duration::from_millis(100));
1207 Ok(())
1208 });
1209 let registry = Arc::new(reg);
1210
1211 let lines = vec!["slow".to_string(), "slow".to_string(), "slow".to_string()];
1212 let start = std::time::Instant::now();
1213 let results = handle_parallel(
1214 lines,
1215 Box::new(io::empty()),
1216 Box::new(io::sink()),
1217 registry,
1218 wasibox_core::CancellationToken::new(),
1219 );
1220 let duration = start.elapsed();
1221
1222 assert_eq!(results.len(), 3);
1223 for res in results {
1224 assert!(res.is_ok());
1225 }
1226 assert!(duration < std::time::Duration::from_millis(250));
1227 }
1228
1229 #[test]
1230 fn test_single_parallel_command_runs_inline() {
1231 let caller_thread = std::thread::current().id();
1232 let observed_thread = Arc::new(Mutex::new(None));
1233 let observed_thread_for_cmd = Arc::clone(&observed_thread);
1234
1235 let mut reg = CommandRegistry::new();
1236 reg.remove_fallback();
1237 reg.register("record-thread", move |_args, _ctx| {
1238 *observed_thread_for_cmd.lock().unwrap() = Some(std::thread::current().id());
1239 Ok(())
1240 });
1241
1242 let results = handle_parallel(
1243 vec!["record-thread".to_string()],
1244 Box::new(io::empty()),
1245 Box::new(io::sink()),
1246 Arc::new(reg),
1247 wasibox_core::CancellationToken::new(),
1248 );
1249
1250 assert_eq!(results, vec![Ok(())]);
1251 assert_eq!(*observed_thread.lock().unwrap(), Some(caller_thread));
1252 }
1253
1254 #[test]
1255 fn test_single_parallel_command_catches_panics() {
1256 let mut reg = CommandRegistry::new();
1257 reg.remove_fallback();
1258 reg.register("panic", |_args, _ctx| {
1259 panic!("intentional test panic");
1260 });
1261
1262 let results = handle_parallel(
1263 vec!["panic".to_string()],
1264 Box::new(io::empty()),
1265 Box::new(io::sink()),
1266 Arc::new(reg),
1267 wasibox_core::CancellationToken::new(),
1268 );
1269
1270 assert_eq!(results, vec![Err("Thread panicked".to_string())]);
1271 }
1272
1273 #[test]
1276 fn test_streaming_pipeline() {
1277 let out = Arc::new(Mutex::new(Vec::new()));
1278 handle_pipeline(
1279 "yes | head -n 2",
1280 Box::new(io::empty()),
1281 Box::new(ArcVecWriter {
1282 inner: Arc::clone(&out),
1283 }),
1284 &builtins(),
1285 wasibox_core::CancellationToken::new(),
1286 )
1287 .unwrap();
1288 let buf = out.lock().unwrap();
1289 assert_eq!(String::from_utf8_lossy(&buf).trim(), "y\ny");
1290 }
1291
1292 #[test]
1293 fn test_seq_pipeline_head() {
1294 let out = Arc::new(Mutex::new(Vec::new()));
1295 handle_pipeline(
1296 "seq | head -n 5",
1297 Box::new(io::empty()),
1298 Box::new(ArcVecWriter {
1299 inner: Arc::clone(&out),
1300 }),
1301 &builtins(),
1302 wasibox_core::CancellationToken::new(),
1303 )
1304 .unwrap();
1305 let buf = out.lock().unwrap();
1306 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3\n4\n5");
1307 }
1308
1309 #[test]
1310 fn test_seq_pipeline_grep_head() {
1311 let out = Arc::new(Mutex::new(Vec::new()));
1312 handle_pipeline(
1313 "seq | grep 2 | head -n 3",
1314 Box::new(io::empty()),
1315 Box::new(ArcVecWriter {
1316 inner: Arc::clone(&out),
1317 }),
1318 &builtins(),
1319 wasibox_core::CancellationToken::new(),
1320 )
1321 .unwrap();
1322 let buf = out.lock().unwrap();
1323 let result = String::from_utf8_lossy(&buf);
1324 let lines: Vec<&str> = result.trim().lines().collect();
1325 assert_eq!(lines.len(), 3);
1326 for line in &lines {
1327 assert!(line.contains('2'));
1328 }
1329 assert_eq!(lines[0], "2");
1330 assert_eq!(lines[1], "12");
1331 assert_eq!(lines[2], "20");
1332 }
1333
1334 #[test]
1335 fn test_seq_pipeline_grep_head_5() {
1336 let out = Arc::new(Mutex::new(Vec::new()));
1337 handle_pipeline(
1338 "seq | grep 2 | head -n 5",
1339 Box::new(io::empty()),
1340 Box::new(ArcVecWriter {
1341 inner: Arc::clone(&out),
1342 }),
1343 &builtins(),
1344 wasibox_core::CancellationToken::new(),
1345 )
1346 .unwrap();
1347 let buf = out.lock().unwrap();
1348 let result = String::from_utf8_lossy(&buf);
1349 let lines: Vec<&str> = result.trim().lines().collect();
1350 assert_eq!(lines.len(), 5);
1351 assert_eq!(lines[0], "2");
1352 assert_eq!(lines[1], "12");
1353 assert_eq!(lines[2], "20");
1354 assert_eq!(lines[3], "21");
1355 assert_eq!(lines[4], "22");
1356 }
1357
1358 #[test]
1359 fn test_seq_finite() {
1360 let out = Arc::new(Mutex::new(Vec::new()));
1361 handle_pipeline(
1362 "seq 3",
1363 Box::new(io::empty()),
1364 Box::new(ArcVecWriter {
1365 inner: Arc::clone(&out),
1366 }),
1367 &builtins(),
1368 wasibox_core::CancellationToken::new(),
1369 )
1370 .unwrap();
1371 let buf = out.lock().unwrap();
1372 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3");
1373 }
1374
1375 #[test]
1376 fn test_seq_range() {
1377 let out = Arc::new(Mutex::new(Vec::new()));
1378 handle_pipeline(
1379 "seq 5 8",
1380 Box::new(io::empty()),
1381 Box::new(ArcVecWriter {
1382 inner: Arc::clone(&out),
1383 }),
1384 &builtins(),
1385 wasibox_core::CancellationToken::new(),
1386 )
1387 .unwrap();
1388 let buf = out.lock().unwrap();
1389 assert_eq!(String::from_utf8_lossy(&buf).trim(), "5\n6\n7\n8");
1390 }
1391
1392 #[test]
1393 fn test_seq_step() {
1394 let out = Arc::new(Mutex::new(Vec::new()));
1395 handle_pipeline(
1396 "seq 1 2 10",
1397 Box::new(io::empty()),
1398 Box::new(ArcVecWriter {
1399 inner: Arc::clone(&out),
1400 }),
1401 &builtins(),
1402 wasibox_core::CancellationToken::new(),
1403 )
1404 .unwrap();
1405 let buf = out.lock().unwrap();
1406 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n3\n5\n7\n9");
1407 }
1408 #[test]
1409 fn test_cd_and_ls() {
1410 let dir = get_temp_dir();
1411 std::fs::write(dir.path().join("aaa.txt"), "hello").unwrap();
1413 std::fs::write(dir.path().join("bbb.txt"), "world").unwrap();
1414
1415 let original_cwd = env::current_dir().unwrap();
1416 let reg = builtins();
1417
1418 let cd_cmd = format!("cd \"{}\"", dir.path().display());
1420 handle_pipeline(
1421 &cd_cmd,
1422 Box::new(io::empty()),
1423 Box::new(io::sink()),
1424 ®,
1425 wasibox_core::CancellationToken::new(),
1426 )
1427 .unwrap();
1428
1429 let out = Arc::new(Mutex::new(Vec::new()));
1431 handle_pipeline(
1432 "ls",
1433 Box::new(io::empty()),
1434 Box::new(ArcVecWriter {
1435 inner: Arc::clone(&out),
1436 }),
1437 ®,
1438 wasibox_core::CancellationToken::new(),
1439 )
1440 .unwrap();
1441
1442 let buf = out.lock().unwrap();
1443 let output = String::from_utf8_lossy(&buf);
1444 assert!(
1445 output.contains("aaa.txt"),
1446 "expected aaa.txt in ls output, got: {}",
1447 output
1448 );
1449 assert!(
1450 output.contains("bbb.txt"),
1451 "expected bbb.txt in ls output, got: {}",
1452 output
1453 );
1454
1455 env::set_current_dir(original_cwd).unwrap();
1457 }
1458
1459 #[test]
1462 fn test_command_line_sequence() {
1463 let mut reg = CommandRegistry::with_builtins();
1464 let counter = Arc::new(Mutex::new(0));
1465 let c1 = Arc::clone(&counter);
1466 reg.register("inc", move |_args, _ctx| {
1467 *c1.lock().unwrap() += 1;
1468 Ok(())
1469 });
1470
1471 crate::handle_command_line(
1473 "inc && inc && inc",
1474 ®,
1475 wasibox_core::CancellationToken::new(),
1476 )
1477 .unwrap();
1478 assert_eq!(*counter.lock().unwrap(), 3);
1479 }
1480
1481 #[test]
1482 fn test_command_line_short_circuit() {
1483 let mut reg = CommandRegistry::with_builtins();
1484 let counter = Arc::new(Mutex::new(0));
1485 let c1 = Arc::clone(&counter);
1486 reg.register("inc", move |_args, _ctx| {
1487 *c1.lock().unwrap() += 1;
1488 Ok(())
1489 });
1490 reg.register("fail", |_args, _ctx| Err("simulated failure".to_string()));
1491
1492 let res = crate::handle_command_line(
1495 "inc && fail && inc",
1496 ®,
1497 wasibox_core::CancellationToken::new(),
1498 );
1499 assert!(res.is_err());
1500 assert_eq!(res.unwrap_err(), "simulated failure");
1501 assert_eq!(*counter.lock().unwrap(), 1); }
1503
1504 #[test]
1505 fn test_handle_parallel_cancel() {
1506 println!("STARTING TEST");
1507 let registry = Arc::new(builtins());
1508 let cancel_token = wasibox_core::CancellationToken::new();
1509 let cancel_clone = cancel_token.clone();
1510
1511 let thread = std::thread::spawn(move || {
1512 println!("THREAD SPAWNED");
1513 super::handle_parallel(
1514 vec!["yes".to_string()],
1515 Box::new(std::io::empty()),
1516 Box::new(std::io::sink()),
1517 registry,
1518 cancel_clone,
1519 )
1520 });
1521
1522 std::thread::sleep(std::time::Duration::from_millis(10));
1523 println!("CANCELLING TOKEN");
1524 cancel_token.cancel();
1525
1526 println!("JOINING THREAD");
1527 let results = thread.join().unwrap();
1528 println!("RESULTS: {:?}", results);
1529 assert_eq!(results.len(), 1);
1530 assert!(results[0].is_err());
1531 assert_eq!(results[0].as_ref().unwrap_err(), "Interrupted");
1532 }
1533
1534 #[test]
1535 fn test_pipeline_cancel() {
1536 let registry = builtins();
1537 let cancel_token = wasibox_core::CancellationToken::new();
1538 let cancel_clone = cancel_token.clone();
1539
1540 let thread = std::thread::spawn(move || {
1541 super::handle_pipeline(
1542 "yes | grep y",
1543 Box::new(std::io::empty()),
1544 Box::new(std::io::sink()),
1545 ®istry,
1546 cancel_clone,
1547 )
1548 });
1549
1550 std::thread::sleep(std::time::Duration::from_millis(10));
1551 cancel_token.cancel();
1552
1553 let result = thread.join().unwrap();
1554 assert!(result.is_err());
1555 assert_eq!(result.unwrap_err(), "Interrupted");
1556 }
1557
1558 #[test]
1559 fn test_seq_redirection_parallel_cancel() {
1560 let dir = get_temp_dir();
1561 let file_path = dir.path().join("i.txt");
1562 let cmd = format!("seq > \"{}\"", file_path.display());
1563
1564 let registry = Arc::new(builtins());
1565 let cancel_token = wasibox_core::CancellationToken::new();
1566 let cancel_clone = cancel_token.clone();
1567
1568 let thread = std::thread::spawn(move || {
1569 super::handle_parallel(
1570 vec![cmd],
1571 Box::new(std::io::empty()),
1572 Box::new(std::io::sink()),
1573 registry,
1574 cancel_clone,
1575 )
1576 });
1577
1578 std::thread::sleep(std::time::Duration::from_millis(50));
1580 cancel_token.cancel();
1581
1582 let results = thread.join().unwrap();
1583 assert_eq!(results.len(), 1);
1584 assert!(results[0].is_err());
1585 assert_eq!(results[0].as_ref().unwrap_err(), "Interrupted");
1586
1587 assert!(file_path.exists());
1589 let content = std::fs::read_to_string(&file_path).unwrap();
1590 assert!(!content.is_empty(), "File should not be empty");
1591
1592 println!(
1594 "Final sequence number written: {}",
1595 content.lines().last().unwrap_or("none")
1596 );
1597 }
1598}