1use std::io::Write;
4
5use anyhow::Context;
6use carlog::Status;
7use console;
8use indicatif::{
9 ProgressBar,
10 ProgressDrawTarget,
11 ProgressStyle,
12};
13use portable_pty::{
14 CommandBuilder,
15 PtySize,
16 native_pty_system,
17};
18
19pub struct Logger {
25 progress_bar: Option<ProgressBar>,
26 line_count: usize,
27}
28
29impl Logger {
30 pub fn new() -> Self {
32 Self {
33 progress_bar: None,
34 line_count: 0,
35 }
36 }
37
38 #[allow(dead_code)] pub fn progress(&mut self, message: &str) {
44 let pb = ProgressBar::new_spinner();
45 pb.set_draw_target(ProgressDrawTarget::stderr());
46 pb.set_style(
47 ProgressStyle::default_spinner()
48 .template("{spinner:.green} {msg}")
49 .unwrap(),
50 );
51 pb.set_message(message.to_string());
52 pb.enable_steady_tick(std::time::Duration::from_millis(100));
53
54 self.progress_bar = Some(pb);
55 }
56
57 #[allow(dead_code)] pub fn set_progress_message(&self, message: &str) {
60 if let Some(pb) = &self.progress_bar {
61 pb.set_message(message.to_string());
62 }
63 }
64
65 pub fn status(&mut self, action: &str, target: &str) {
71 if let Some(pb) = self.progress_bar.take() {
73 pb.finish_and_clear();
74 }
75
76 use console::style;
78 let formatted_message = format!("{:>12} {}", style(action).cyan().bold(), target);
79
80 let pb = ProgressBar::new_spinner();
82 pb.set_draw_target(ProgressDrawTarget::stderr());
83 pb.set_style(ProgressStyle::default_spinner().template("{msg}").unwrap());
84 pb.set_message(formatted_message);
85
86 self.progress_bar = Some(pb);
87 self.line_count = 1;
88 }
89
90 #[allow(dead_code)] pub fn status_permanent(&self, action: &str, target: &str) {
98 let status = Status::new()
99 .bold()
100 .justify()
101 .color(carlog::CargoColor::Green)
102 .status(action);
103
104 let formatted_target = format!(" {}", target);
105
106 if let Some(pb) = &self.progress_bar {
108 pb.suspend(|| {
109 let _ = status.print_stderr(&formatted_target);
110 });
111 } else {
112 let _ = status.print_stderr(&formatted_target);
113 }
114 }
115
116 #[allow(dead_code)] pub fn print_message(&self, msg: &str) {
121 if let Some(pb) = &self.progress_bar {
122 pb.suspend(|| {
123 eprintln!("{}", msg);
124 });
125 } else {
126 eprintln!("{}", msg);
127 }
128 }
129
130 #[allow(dead_code)] pub fn info(&self, action: &str, target: &str) {
136 let status = Status::new()
137 .bold()
138 .justify()
139 .color(carlog::CargoColor::Cyan)
140 .status(action);
141
142 let formatted_target = format!(" {}", target);
143
144 if let Some(pb) = &self.progress_bar {
146 pb.suspend(|| {
147 let _ = status.print_stderr(&formatted_target);
148 });
149 } else {
150 let _ = status.print_stderr(&formatted_target);
151 }
152 }
153
154 pub fn warning(&self, action: &str, target: &str) {
159 let status = Status::new()
160 .bold()
161 .justify()
162 .color(carlog::CargoColor::Yellow)
163 .status(action);
164
165 let formatted_target = format!(" {}", target);
166
167 if let Some(pb) = &self.progress_bar {
169 pb.suspend(|| {
170 let _ = status.print_stderr(&formatted_target);
171 });
172 } else {
173 let _ = status.print_stderr(&formatted_target);
174 }
175 }
176
177 #[allow(dead_code)] pub fn error(&self, action: &str, target: &str) {
183 let status = Status::new()
184 .bold()
185 .justify()
186 .color(carlog::CargoColor::Red)
187 .status(action);
188
189 let formatted_target = format!(" {}", target);
190
191 if let Some(pb) = &self.progress_bar {
193 pb.suspend(|| {
194 let _ = status.print_stderr(&formatted_target);
195 });
196 } else {
197 let _ = status.print_stderr(&formatted_target);
198 }
199 }
200
201 pub fn clear_status(&mut self) {
205 if let Some(pb) = self.progress_bar.take() {
206 pb.finish_and_clear();
207 self.line_count = 0;
208 }
209 }
210
211 pub fn suspend<F, R>(&mut self, f: F) -> R
216 where
217 F: FnOnce() -> R,
218 {
219 if let Some(pb) = &self.progress_bar {
220 pb.suspend(f)
221 } else {
222 f()
223 }
224 }
225
226 pub fn finish(&mut self) {
228 if let Some(pb) = self.progress_bar.take() {
229 pb.finish_and_clear();
231 self.line_count = 0;
232 }
233 }
234}
235
236#[derive(Debug, Clone)]
238pub struct SubprocessOutput {
239 pub stdout: Vec<u8>,
241 pub stderr: Vec<u8>,
243 pub exit_code: u32,
245}
246
247impl SubprocessOutput {
248 pub fn stdout_str(&self) -> anyhow::Result<String> {
250 String::from_utf8(self.stdout.clone()).context("Failed to parse stdout as UTF-8")
251 }
252
253 pub fn stderr_str(&self) -> anyhow::Result<String> {
255 String::from_utf8(self.stderr.clone()).context("Failed to parse stderr as UTF-8")
256 }
257
258 pub fn success(&self) -> bool {
260 self.exit_code == 0
261 }
262
263 pub fn exit_code(&self) -> u32 {
265 self.exit_code
266 }
267}
268
269pub async fn run_subprocess<F>(
293 logger: &mut Logger,
294 cmd_builder: F,
295 stderr_lines: Option<usize>,
296) -> anyhow::Result<SubprocessOutput>
297where
298 F: FnOnce() -> CommandBuilder,
299{
300 let stderr_lines = stderr_lines.unwrap_or(5);
301
302 let term = console::Term::stderr();
303 let is_term = term.is_term();
304
305 if is_term {
309 if let Some(pb) = logger.progress_bar.take() {
311 pb.finish_and_clear();
312 }
313 if logger.line_count > 0 {
315 let _ = term.clear_last_lines(logger.line_count);
316 logger.line_count = 0;
317 }
318 }
319
320 let stderr_lines_u16 = stderr_lines as u16;
322 let lines_drawn = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
323 let lines_drawn_render = lines_drawn.clone();
324
325 let cmd = cmd_builder();
327
328 let pty_system = native_pty_system();
330 let pty_size = PtySize {
331 rows: stderr_lines_u16,
332 cols: 80,
333 pixel_width: 0,
334 pixel_height: 0,
335 };
336 let pty = pty_system
337 .openpty(pty_size)
338 .context("Failed to create PTY")?;
339
340 let mut child = pty
342 .slave
343 .spawn_command(cmd)
344 .context("Failed to spawn command in PTY")?;
345
346 let mut reader = pty
349 .master
350 .try_clone_reader()
351 .context("Failed to clone PTY reader")?;
352
353 let master = pty.master;
355
356 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
358 let tx_clone = tx.clone();
360
361 let collected_output = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
363 let collected_output_clone = collected_output.clone();
364
365 #[allow(clippy::excessive_nesting)]
370 let pty_task = tokio::spawn(async move {
371 tokio::task::spawn_blocking(move || {
372 let mut full_output = Vec::new();
373 let mut buffer = vec![0u8; 4096];
374
375 loop {
376 match reader.read(&mut buffer) {
377 Ok(0) => break, Ok(bytes_read) => {
379 let chunk = &buffer[..bytes_read];
380 full_output.extend_from_slice(chunk);
381 if let Ok(mut collected) = collected_output_clone.lock() {
383 collected.extend_from_slice(chunk);
384 }
385 let _ = tx.send(chunk.to_vec());
386 }
387 Err(err) => {
388 let error_msg = format!("<pty read error: {}>", err);
390 let error_bytes = error_msg.as_bytes();
391 full_output.extend_from_slice(error_bytes);
392 if let Ok(mut collected) = collected_output_clone.lock() {
393 collected.extend_from_slice(error_bytes);
394 }
395 let _ = tx.send(error_bytes.to_vec());
396 break;
397 }
398 }
399 }
400
401 drop(tx);
403
404 Ok::<Vec<u8>, anyhow::Error>(full_output)
405 })
406 .await
407 .context("Failed to join blocking PTY read task")?
408 });
409
410 let mut output_buffer = Vec::new();
412 let mut output_ring: Vec<Vec<u8>> = Vec::with_capacity(stderr_lines);
413
414 #[allow(clippy::excessive_nesting)]
418 let render_task = tokio::spawn(async move {
419 let mut current_lines_displayed: usize = 0;
420
421 while let Some(chunk) = rx.recv().await {
422 output_buffer.extend_from_slice(&chunk);
423
424 let mut lines: Vec<Vec<u8>> = Vec::new();
426 let mut current_line = Vec::new();
427 for byte in output_buffer.iter().copied() {
428 current_line.push(byte);
429 if byte == b'\n' {
430 lines.push(current_line);
431 current_line = Vec::new();
432 }
433 }
434 output_buffer = current_line;
435
436 for line in lines {
438 output_ring.push(line);
439 if output_ring.len() > stderr_lines {
440 output_ring.remove(0);
441 }
442 }
443
444 if is_term && !output_ring.is_empty() {
446 let mut stderr_handle = std::io::stderr();
447
448 if current_lines_displayed > 0 {
450 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
452 for _ in 0..current_lines_displayed {
453 write!(stderr_handle, "\x1b[2K\x1b[1B").ok(); }
455 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
457 }
458
459 for line_bytes in &output_ring {
461 let _ = stderr_handle.write_all(line_bytes);
462 }
463 let _ = stderr_handle.flush();
464
465 current_lines_displayed = output_ring.len();
466 lines_drawn_render
467 .store(current_lines_displayed, std::sync::atomic::Ordering::SeqCst);
468 }
469 }
470
471 if !output_buffer.is_empty() {
473 output_ring.push(output_buffer);
474 if output_ring.len() > stderr_lines {
475 output_ring.remove(0);
476 }
477 if is_term {
478 let mut stderr_handle = std::io::stderr();
479
480 if current_lines_displayed > 0 {
482 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
483 for _ in 0..current_lines_displayed {
484 write!(stderr_handle, "\x1b[2K\x1b[1B").ok();
485 }
486 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
487 }
488
489 for line_bytes in &output_ring {
491 let _ = stderr_handle.write_all(line_bytes);
492 }
493 let _ = stderr_handle.flush();
494
495 lines_drawn_render.store(output_ring.len(), std::sync::atomic::Ordering::SeqCst);
496 }
497 }
498
499 (output_ring, is_term)
500 });
501
502 let status = tokio::task::spawn_blocking(move || child.wait())
504 .await
505 .context("Failed to join process wait task")?
506 .context("Failed to wait for subprocess")?;
507
508 drop(master);
513
514 #[cfg(windows)]
516 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
517
518 let timeout_duration = if cfg!(windows) {
522 std::time::Duration::from_millis(500)
523 } else {
524 std::time::Duration::from_secs(10)
525 };
526 let pty_output = match tokio::time::timeout(timeout_duration, pty_task).await {
527 Ok(result) => {
528 result.context("Failed to join PTY task")??
530 }
531 Err(_) => {
532 drop(tx_clone);
539 collected_output.lock().unwrap().clone()
540 }
541 };
542 let render_timeout = if cfg!(windows) {
545 std::time::Duration::from_millis(500)
546 } else {
547 std::time::Duration::from_secs(5)
548 };
549 let (_final_output_ring, was_term) =
550 match tokio::time::timeout(render_timeout, render_task).await {
551 Ok(result) => result.context("Failed to join render task")?,
552 Err(_) => {
553 (Vec::new(), is_term)
557 }
558 };
559
560 let stdout_bytes = Vec::new(); let stderr_bytes = pty_output;
564
565 let exit_code = status.exit_code();
567 let final_lines_drawn = lines_drawn.load(std::sync::atomic::Ordering::SeqCst);
568
569 if was_term && final_lines_drawn > 0 {
570 let mut stderr_handle = std::io::stderr();
572 write!(stderr_handle, "\x1b[{}A", final_lines_drawn).ok();
573 for _ in 0..final_lines_drawn {
574 write!(stderr_handle, "\x1b[2K\x1b[1B").ok(); }
576 write!(stderr_handle, "\x1b[{}A", final_lines_drawn).ok();
578 let _ = stderr_handle.flush();
579 }
580
581 Ok(SubprocessOutput {
582 stdout: stdout_bytes,
583 stderr: stderr_bytes,
584 exit_code,
585 })
586}
587
588impl Default for Logger {
589 fn default() -> Self {
590 Self::new()
591 }
592}
593
594impl Drop for Logger {
595 fn drop(&mut self) {
596 if let Some(pb) = self.progress_bar.take() {
598 pb.finish_and_clear();
599 }
600
601 if self.line_count > 0 {
603 use console::Term;
604 let term = Term::stderr();
605 if term.is_term() {
606 let _ = term.clear_last_lines(self.line_count);
607 }
608 self.line_count = 0;
609 }
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 #[cfg(not(windows))]
616 use portable_pty::CommandBuilder;
617
618 use super::*;
619
620 #[tokio::test]
621 async fn test_logger_new() {
622 let logger = Logger::new();
623 assert!(logger.progress_bar.is_none());
624 assert_eq!(logger.line_count, 0);
625 }
626
627 #[tokio::test]
628 async fn test_logger_status() {
629 let mut logger = Logger::new();
630 logger.status("Building", "test-crate");
631 assert!(logger.progress_bar.is_some());
632 assert_eq!(logger.line_count, 1);
633 }
634
635 #[tokio::test]
636 async fn test_logger_clear_status() {
637 let mut logger = Logger::new();
638 logger.status("Building", "test-crate");
639 assert!(logger.progress_bar.is_some());
640 logger.clear_status();
641 assert!(logger.progress_bar.is_none());
642 assert_eq!(logger.line_count, 0);
643 }
644
645 #[tokio::test]
646 async fn test_logger_finish() {
647 let mut logger = Logger::new();
648 logger.status("Building", "test-crate");
649 logger.finish();
650 assert!(logger.progress_bar.is_none());
651 assert_eq!(logger.line_count, 0);
652 }
653
654 #[tokio::test]
655 async fn test_subprocess_output_success() {
656 let output = SubprocessOutput {
657 stdout: b"stdout content".to_vec(),
658 stderr: b"stderr content".to_vec(),
659 exit_code: 0,
660 };
661 assert!(output.success());
662 assert_eq!(output.exit_code(), 0);
663 assert_eq!(output.stdout_str().unwrap(), "stdout content");
664 assert_eq!(output.stderr_str().unwrap(), "stderr content");
665 }
666
667 #[tokio::test]
668 async fn test_subprocess_output_failure() {
669 let output = SubprocessOutput {
670 stdout: b"".to_vec(),
671 stderr: b"error message".to_vec(),
672 exit_code: 1,
673 };
674 assert!(!output.success());
675 assert_eq!(output.exit_code(), 1);
676 assert_eq!(output.stderr_str().unwrap(), "error message");
677 }
678
679 #[tokio::test]
680 #[cfg(not(windows))]
681 async fn test_run_subprocess_simple_success() {
682 let mut logger = Logger::new();
683 let output = run_subprocess(
684 &mut logger,
685 || {
686 let mut cmd = CommandBuilder::new("echo");
687 cmd.arg("hello world");
688 cmd
689 },
690 Some(3),
691 )
692 .await
693 .unwrap();
694
695 assert!(output.success());
696 assert_eq!(output.exit_code(), 0);
697 let stderr = output.stderr_str().unwrap();
699 assert!(stderr.contains("hello world") || stderr.is_empty());
700 }
701
702 #[tokio::test]
703 #[cfg(not(windows))]
704 async fn test_run_subprocess_simple_failure() {
705 let mut logger = Logger::new();
706 let output = run_subprocess(&mut logger, || CommandBuilder::new("false"), Some(3))
707 .await
708 .unwrap();
709
710 assert!(!output.success());
711 assert_ne!(output.exit_code(), 0);
712 }
713
714 #[tokio::test]
715 #[cfg(not(windows))]
716 async fn test_run_subprocess_multiline_output() {
717 let mut logger = Logger::new();
718 let output = run_subprocess(
719 &mut logger,
720 || {
721 let mut cmd = CommandBuilder::new("sh");
722 cmd.arg("-c");
723 cmd.arg("echo 'line 1'; echo 'line 2'; echo 'line 3'; echo 'line 4'; echo 'line 5'; echo 'line 6'");
724 cmd
725 },
726 Some(3), )
728 .await
729 .unwrap();
730
731 assert!(output.success());
732 let stderr = output.stderr_str().unwrap();
734 assert!(stderr.contains("line 1"));
735 assert!(stderr.contains("line 6"));
736 }
737
738 #[tokio::test]
739 #[cfg(not(windows))]
740 async fn test_run_subprocess_with_progress_bar() {
741 let mut logger = Logger::new();
742 logger.status("Preparing", "test");
743 assert!(logger.progress_bar.is_some());
744
745 let output = run_subprocess(
746 &mut logger,
747 || {
748 let mut cmd = CommandBuilder::new("echo");
749 cmd.arg("test output");
750 cmd
751 },
752 None,
753 )
754 .await
755 .unwrap();
756
757 assert!(output.success());
758 }
762
763 #[tokio::test]
764 #[cfg(not(windows))]
765 async fn test_run_subprocess_exit_code_preservation() {
766 let mut logger = Logger::new();
767 let output = run_subprocess(
768 &mut logger,
769 || {
770 let mut cmd = CommandBuilder::new("sh");
771 cmd.arg("-c");
772 cmd.arg("exit 42");
773 cmd
774 },
775 None,
776 )
777 .await
778 .unwrap();
779
780 assert!(!output.success());
781 assert_eq!(output.exit_code(), 42);
782 }
783
784 #[tokio::test]
785 #[cfg(not(windows))]
786 async fn test_run_subprocess_ansi_colors_preserved() {
787 let mut logger = Logger::new();
788 let output = run_subprocess(
789 &mut logger,
790 || {
791 let mut cmd = CommandBuilder::new("sh");
792 cmd.arg("-c");
793 cmd.arg("echo -e '\\033[31mred\\033[0m'");
794 cmd
795 },
796 None,
797 )
798 .await
799 .unwrap();
800
801 assert!(output.success());
802 let stderr = output.stderr_str().unwrap();
803 assert!(stderr.contains("\x1b[31m") || stderr.contains("red"));
805 }
806
807 #[tokio::test]
808 #[cfg(not(windows))]
809 async fn test_run_subprocess_default_stderr_lines() {
810 let mut logger = Logger::new();
811 let output = run_subprocess(
812 &mut logger,
813 || {
814 let mut cmd = CommandBuilder::new("echo");
815 cmd.arg("test");
816 cmd
817 },
818 None, )
820 .await
821 .unwrap();
822
823 assert!(output.success());
824 }
825
826 #[tokio::test]
827 #[cfg(not(windows))]
828 async fn test_run_subprocess_custom_stderr_lines() {
829 let mut logger = Logger::new();
830 let output = run_subprocess(
831 &mut logger,
832 || {
833 let mut cmd = CommandBuilder::new("echo");
834 cmd.arg("test");
835 cmd
836 },
837 Some(10), )
839 .await
840 .unwrap();
841
842 assert!(output.success());
843 }
844
845 #[tokio::test]
846 #[cfg(not(windows))]
847 async fn test_run_subprocess_nonexistent_command() {
848 let mut logger = Logger::new();
849 let result = run_subprocess(
850 &mut logger,
851 || CommandBuilder::new("nonexistent-command-xyz-123"),
852 None,
853 )
854 .await;
855
856 assert!(result.is_err());
857 }
858
859 #[tokio::test]
860 async fn test_subprocess_output_utf8_handling() {
861 let output = SubprocessOutput {
862 stdout: "hello 世界".as_bytes().to_vec(),
863 stderr: "error 错误".as_bytes().to_vec(),
864 exit_code: 0,
865 };
866
867 assert_eq!(output.stdout_str().unwrap(), "hello 世界");
868 assert_eq!(output.stderr_str().unwrap(), "error 错误");
869 }
870
871 #[tokio::test]
872 async fn test_subprocess_output_invalid_utf8() {
873 let output = SubprocessOutput {
874 stdout: vec![0xFF, 0xFE, 0xFD], stderr: vec![],
876 exit_code: 0,
877 };
878
879 assert!(output.stdout_str().is_err());
880 }
881
882 #[tokio::test]
883 async fn test_logger_suspend() {
884 let mut logger = Logger::new();
885 logger.status("Building", "test");
886 let result = logger.suspend(|| 42);
887 assert_eq!(result, 42);
888 }
889
890 #[tokio::test]
891 async fn test_logger_suspend_without_progress() {
892 let mut logger = Logger::new();
893 let result = logger.suspend(|| 42);
894 assert_eq!(result, 42);
895 }
896
897 #[tokio::test]
898 async fn test_logger_status_permanent() {
899 let logger = Logger::new();
900 logger.status_permanent("Compiling", "test-crate");
902 }
903
904 #[tokio::test]
905 async fn test_logger_warning() {
906 let logger = Logger::new();
907 logger.warning("Warning", "test message");
909 }
910
911 #[tokio::test]
912 async fn test_logger_info() {
913 let logger = Logger::new();
914 logger.info("Info", "test message");
916 }
917
918 #[tokio::test]
919 async fn test_logger_error() {
920 let logger = Logger::new();
921 logger.error("Error", "test message");
923 }
924
925 #[tokio::test]
926 async fn test_logger_print_message() {
927 let logger = Logger::new();
928 logger.print_message("test message");
930 }
931
932 #[tokio::test]
933 async fn test_logger_progress() {
934 let mut logger = Logger::new();
935 logger.progress("Processing...");
936 assert!(logger.progress_bar.is_some());
937 }
938
939 #[tokio::test]
940 async fn test_logger_set_progress_message() {
941 let mut logger = Logger::new();
942 logger.progress("Initial");
943 logger.set_progress_message("Updated");
944 assert!(logger.progress_bar.is_some());
945 }
946}