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
19use crate::scrolling::{
20 clear_scrolling_region,
21 get_terminal_size,
22 move_cursor_to_line,
23 reset_scrolling_region,
24 set_scrolling_region,
25};
26
27pub struct Logger {
33 progress_bar: Option<ProgressBar>,
34 line_count: usize,
35}
36
37impl Logger {
38 pub fn new() -> Self {
40 Self {
41 progress_bar: None,
42 line_count: 0,
43 }
44 }
45
46 #[allow(dead_code)] pub fn progress(&mut self, message: &str) {
52 let pb = ProgressBar::new_spinner();
53 pb.set_draw_target(ProgressDrawTarget::stderr());
54 pb.set_style(
55 ProgressStyle::default_spinner()
56 .template("{spinner:.green} {msg}")
57 .unwrap(),
58 );
59 pb.set_message(message.to_string());
60 pb.enable_steady_tick(std::time::Duration::from_millis(100));
61
62 self.progress_bar = Some(pb);
63 }
64
65 #[allow(dead_code)] pub fn set_progress_message(&self, message: &str) {
68 if let Some(pb) = &self.progress_bar {
69 pb.set_message(message.to_string());
70 }
71 }
72
73 pub fn status(&mut self, action: &str, target: &str) {
79 if let Some(pb) = self.progress_bar.take() {
81 pb.finish_and_clear();
82 }
83
84 use console::style;
86 let formatted_message = format!("{:>12} {}", style(action).cyan().bold(), target);
87
88 let pb = ProgressBar::new_spinner();
90 pb.set_draw_target(ProgressDrawTarget::stderr());
91 pb.set_style(ProgressStyle::default_spinner().template("{msg}").unwrap());
92 pb.set_message(formatted_message);
93
94 self.progress_bar = Some(pb);
95 self.line_count = 1;
96 }
97
98 #[allow(dead_code)] pub fn status_permanent(&self, action: &str, target: &str) {
106 let status = Status::new()
107 .bold()
108 .justify()
109 .color(carlog::CargoColor::Green)
110 .status(action);
111
112 let formatted_target = format!(" {}", target);
113
114 if let Some(pb) = &self.progress_bar {
116 pb.suspend(|| {
117 let _ = status.print_stderr(&formatted_target);
118 });
119 } else {
120 let _ = status.print_stderr(&formatted_target);
121 }
122 }
123
124 #[allow(dead_code)] pub fn print_message(&self, msg: &str) {
129 if let Some(pb) = &self.progress_bar {
130 pb.suspend(|| {
131 eprintln!("{}", msg);
132 });
133 } else {
134 eprintln!("{}", msg);
135 }
136 }
137
138 #[allow(dead_code)] pub fn info(&self, action: &str, target: &str) {
144 let status = Status::new()
145 .bold()
146 .justify()
147 .color(carlog::CargoColor::Cyan)
148 .status(action);
149
150 let formatted_target = format!(" {}", target);
151
152 if let Some(pb) = &self.progress_bar {
154 pb.suspend(|| {
155 let _ = status.print_stderr(&formatted_target);
156 });
157 } else {
158 let _ = status.print_stderr(&formatted_target);
159 }
160 }
161
162 pub fn warning(&self, action: &str, target: &str) {
167 let status = Status::new()
168 .bold()
169 .justify()
170 .color(carlog::CargoColor::Yellow)
171 .status(action);
172
173 let formatted_target = format!(" {}", target);
174
175 if let Some(pb) = &self.progress_bar {
177 pb.suspend(|| {
178 let _ = status.print_stderr(&formatted_target);
179 });
180 } else {
181 let _ = status.print_stderr(&formatted_target);
182 }
183 }
184
185 #[allow(dead_code)] pub fn error(&self, action: &str, target: &str) {
191 let status = Status::new()
192 .bold()
193 .justify()
194 .color(carlog::CargoColor::Red)
195 .status(action);
196
197 let formatted_target = format!(" {}", target);
198
199 if let Some(pb) = &self.progress_bar {
201 pb.suspend(|| {
202 let _ = status.print_stderr(&formatted_target);
203 });
204 } else {
205 let _ = status.print_stderr(&formatted_target);
206 }
207 }
208
209 pub fn clear_status(&mut self) {
213 if let Some(pb) = self.progress_bar.take() {
214 pb.finish_and_clear();
215 self.line_count = 0;
216 }
217 }
218
219 pub fn suspend<F, R>(&mut self, f: F) -> R
224 where
225 F: FnOnce() -> R,
226 {
227 if let Some(pb) = &self.progress_bar {
228 pb.suspend(f)
229 } else {
230 f()
231 }
232 }
233
234 pub fn finish(&mut self) {
236 if let Some(pb) = self.progress_bar.take() {
237 pb.finish_and_clear();
239 self.line_count = 0;
240 }
241 }
242}
243
244#[derive(Debug, Clone)]
246pub struct SubprocessOutput {
247 pub stdout: Vec<u8>,
249 pub stderr: Vec<u8>,
251 pub exit_code: u32,
253}
254
255impl SubprocessOutput {
256 pub fn stdout_str(&self) -> anyhow::Result<String> {
258 String::from_utf8(self.stdout.clone()).context("Failed to parse stdout as UTF-8")
259 }
260
261 pub fn stderr_str(&self) -> anyhow::Result<String> {
263 String::from_utf8(self.stderr.clone()).context("Failed to parse stderr as UTF-8")
264 }
265
266 pub fn success(&self) -> bool {
268 self.exit_code == 0
269 }
270
271 pub fn exit_code(&self) -> u32 {
273 self.exit_code
274 }
275}
276
277pub async fn run_subprocess<F>(
301 logger: &mut Logger,
302 cmd_builder: F,
303 stderr_lines: Option<usize>,
304) -> anyhow::Result<SubprocessOutput>
305where
306 F: FnOnce() -> CommandBuilder,
307{
308 let stderr_lines = stderr_lines.unwrap_or(5);
309 let had_progress = logger.progress_bar.is_some();
311 if had_progress {
312 logger.clear_status();
313 }
314
315 let term = console::Term::stderr();
316 let is_term = term.is_term();
317
318 let (term_rows, _term_cols) = if is_term {
320 get_terminal_size().unwrap_or((24u16, 80u16))
321 } else {
322 (24u16, 80u16) };
324
325 let stderr_lines_u16 = stderr_lines as u16;
328 let region_top = if stderr_lines_u16 < term_rows {
329 term_rows - stderr_lines_u16 + 1 } else {
331 1 };
333 let region_bottom = term_rows;
334
335 if is_term {
337 set_scrolling_region(region_top, region_bottom)
338 .context("Failed to set scrolling region")?;
339 move_cursor_to_line(region_top).context("Failed to move cursor to scrolling region")?;
341 }
342
343 let cmd = cmd_builder();
345
346 let pty_system = native_pty_system();
348 let pty_size = PtySize {
349 rows: stderr_lines_u16,
350 cols: 80,
351 pixel_width: 0,
352 pixel_height: 0,
353 };
354 let pty = pty_system
355 .openpty(pty_size)
356 .context("Failed to create PTY")?;
357
358 let mut child = pty
360 .slave
361 .spawn_command(cmd)
362 .context("Failed to spawn command in PTY")?;
363
364 let mut reader = pty
367 .master
368 .try_clone_reader()
369 .context("Failed to clone PTY reader")?;
370
371 let master = pty.master;
373
374 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
376 let tx_clone = tx.clone();
378
379 let collected_output = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
381 let collected_output_clone = collected_output.clone();
382
383 #[allow(clippy::excessive_nesting)]
388 let pty_task = tokio::spawn(async move {
389 tokio::task::spawn_blocking(move || {
390 let mut full_output = Vec::new();
391 let mut buffer = vec![0u8; 4096];
392
393 loop {
394 match reader.read(&mut buffer) {
395 Ok(0) => break, Ok(bytes_read) => {
397 let chunk = &buffer[..bytes_read];
398 full_output.extend_from_slice(chunk);
399 if let Ok(mut collected) = collected_output_clone.lock() {
401 collected.extend_from_slice(chunk);
402 }
403 let _ = tx.send(chunk.to_vec());
404 }
405 Err(err) => {
406 let error_msg = format!("<pty read error: {}>", err);
408 let error_bytes = error_msg.as_bytes();
409 full_output.extend_from_slice(error_bytes);
410 if let Ok(mut collected) = collected_output_clone.lock() {
411 collected.extend_from_slice(error_bytes);
412 }
413 let _ = tx.send(error_bytes.to_vec());
414 break;
415 }
416 }
417 }
418
419 drop(tx);
421
422 Ok::<Vec<u8>, anyhow::Error>(full_output)
423 })
424 .await
425 .context("Failed to join blocking PTY read task")?
426 });
427
428 let mut output_buffer = Vec::new();
430 let mut output_ring: Vec<Vec<u8>> = Vec::with_capacity(stderr_lines);
431
432 let render_task = tokio::spawn(async move {
434 while let Some(chunk) = rx.recv().await {
435 output_buffer.extend_from_slice(&chunk);
436
437 let mut lines: Vec<Vec<u8>> = Vec::new();
439 let mut current_line = Vec::new();
440 for byte in output_buffer.iter().copied() {
441 current_line.push(byte);
442 if byte == b'\n' {
443 lines.push(current_line);
444 current_line = Vec::new();
445 }
446 }
447 output_buffer = current_line;
448
449 for line in lines {
451 output_ring.push(line);
452 if output_ring.len() > stderr_lines {
453 output_ring.remove(0);
454 }
455 }
456
457 if is_term && !output_ring.is_empty() {
459 move_cursor_to_line(region_top).ok();
461 clear_scrolling_region().ok();
462
463 let mut stderr_handle = std::io::stderr();
465 for line_bytes in &output_ring {
466 let _ = stderr_handle.write_all(line_bytes);
467 }
468 let _ = stderr_handle.flush();
469 }
470 }
471
472 if !output_buffer.is_empty() {
474 output_ring.push(output_buffer);
475 if output_ring.len() > stderr_lines {
476 output_ring.remove(0);
477 }
478 if is_term {
479 move_cursor_to_line(region_top).ok();
481 clear_scrolling_region().ok();
482 let mut stderr_handle = std::io::stderr();
483 for line_bytes in &output_ring {
484 let _ = stderr_handle.write_all(line_bytes);
485 }
486 let _ = stderr_handle.flush();
487 }
488 }
489
490 (output_ring, is_term)
491 });
492
493 let status = tokio::task::spawn_blocking(move || child.wait())
495 .await
496 .context("Failed to join process wait task")?
497 .context("Failed to wait for subprocess")?;
498
499 drop(master);
504
505 #[cfg(windows)]
507 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
508
509 let timeout_duration = if cfg!(windows) {
513 std::time::Duration::from_millis(500)
514 } else {
515 std::time::Duration::from_secs(10)
516 };
517 let pty_output = match tokio::time::timeout(timeout_duration, pty_task).await {
518 Ok(result) => {
519 result.context("Failed to join PTY task")??
521 }
522 Err(_) => {
523 drop(tx_clone);
530 collected_output.lock().unwrap().clone()
531 }
532 };
533 let render_timeout = if cfg!(windows) {
536 std::time::Duration::from_millis(500)
537 } else {
538 std::time::Duration::from_secs(5)
539 };
540 let (_final_output_ring, was_term) =
541 match tokio::time::timeout(render_timeout, render_task).await {
542 Ok(result) => result.context("Failed to join render task")?,
543 Err(_) => {
544 (Vec::new(), is_term)
548 }
549 };
550
551 let stdout_bytes = Vec::new(); let stderr_bytes = pty_output;
555
556 let exit_code = status.exit_code();
558 let success = exit_code == 0;
559
560 if was_term {
561 if success {
562 clear_scrolling_region().ok();
564 } else {
565 reset_scrolling_region().ok();
568 }
569 }
570
571 Ok(SubprocessOutput {
572 stdout: stdout_bytes,
573 stderr: stderr_bytes,
574 exit_code,
575 })
576}
577
578impl Default for Logger {
579 fn default() -> Self {
580 Self::new()
581 }
582}
583
584impl Drop for Logger {
585 fn drop(&mut self) {
586 if let Some(pb) = self.progress_bar.take() {
588 pb.finish_and_clear();
589 }
590
591 if self.line_count > 0 {
593 use console::Term;
594 let term = Term::stderr();
595 if term.is_term() {
596 let _ = term.clear_last_lines(self.line_count);
597 }
598 self.line_count = 0;
599 }
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 #[cfg(not(windows))]
606 use portable_pty::CommandBuilder;
607
608 use super::*;
609
610 #[tokio::test]
611 async fn test_logger_new() {
612 let logger = Logger::new();
613 assert!(logger.progress_bar.is_none());
614 assert_eq!(logger.line_count, 0);
615 }
616
617 #[tokio::test]
618 async fn test_logger_status() {
619 let mut logger = Logger::new();
620 logger.status("Building", "test-crate");
621 assert!(logger.progress_bar.is_some());
622 assert_eq!(logger.line_count, 1);
623 }
624
625 #[tokio::test]
626 async fn test_logger_clear_status() {
627 let mut logger = Logger::new();
628 logger.status("Building", "test-crate");
629 assert!(logger.progress_bar.is_some());
630 logger.clear_status();
631 assert!(logger.progress_bar.is_none());
632 assert_eq!(logger.line_count, 0);
633 }
634
635 #[tokio::test]
636 async fn test_logger_finish() {
637 let mut logger = Logger::new();
638 logger.status("Building", "test-crate");
639 logger.finish();
640 assert!(logger.progress_bar.is_none());
641 assert_eq!(logger.line_count, 0);
642 }
643
644 #[tokio::test]
645 async fn test_subprocess_output_success() {
646 let output = SubprocessOutput {
647 stdout: b"stdout content".to_vec(),
648 stderr: b"stderr content".to_vec(),
649 exit_code: 0,
650 };
651 assert!(output.success());
652 assert_eq!(output.exit_code(), 0);
653 assert_eq!(output.stdout_str().unwrap(), "stdout content");
654 assert_eq!(output.stderr_str().unwrap(), "stderr content");
655 }
656
657 #[tokio::test]
658 async fn test_subprocess_output_failure() {
659 let output = SubprocessOutput {
660 stdout: b"".to_vec(),
661 stderr: b"error message".to_vec(),
662 exit_code: 1,
663 };
664 assert!(!output.success());
665 assert_eq!(output.exit_code(), 1);
666 assert_eq!(output.stderr_str().unwrap(), "error message");
667 }
668
669 #[tokio::test]
670 #[cfg(not(windows))]
671 async fn test_run_subprocess_simple_success() {
672 let mut logger = Logger::new();
673 let output = run_subprocess(
674 &mut logger,
675 || {
676 let mut cmd = CommandBuilder::new("echo");
677 cmd.arg("hello world");
678 cmd
679 },
680 Some(3),
681 )
682 .await
683 .unwrap();
684
685 assert!(output.success());
686 assert_eq!(output.exit_code(), 0);
687 let stderr = output.stderr_str().unwrap();
689 assert!(stderr.contains("hello world") || stderr.is_empty());
690 }
691
692 #[tokio::test]
693 #[cfg(not(windows))]
694 async fn test_run_subprocess_simple_failure() {
695 let mut logger = Logger::new();
696 let output = run_subprocess(&mut logger, || CommandBuilder::new("false"), Some(3))
697 .await
698 .unwrap();
699
700 assert!(!output.success());
701 assert_ne!(output.exit_code(), 0);
702 }
703
704 #[tokio::test]
705 #[cfg(not(windows))]
706 async fn test_run_subprocess_multiline_output() {
707 let mut logger = Logger::new();
708 let output = run_subprocess(
709 &mut logger,
710 || {
711 let mut cmd = CommandBuilder::new("sh");
712 cmd.arg("-c");
713 cmd.arg("echo 'line 1'; echo 'line 2'; echo 'line 3'; echo 'line 4'; echo 'line 5'; echo 'line 6'");
714 cmd
715 },
716 Some(3), )
718 .await
719 .unwrap();
720
721 assert!(output.success());
722 let stderr = output.stderr_str().unwrap();
724 assert!(stderr.contains("line 1"));
725 assert!(stderr.contains("line 6"));
726 }
727
728 #[tokio::test]
729 #[cfg(not(windows))]
730 async fn test_run_subprocess_with_progress_bar() {
731 let mut logger = Logger::new();
732 logger.status("Preparing", "test");
733 assert!(logger.progress_bar.is_some());
734
735 let output = run_subprocess(
736 &mut logger,
737 || {
738 let mut cmd = CommandBuilder::new("echo");
739 cmd.arg("test output");
740 cmd
741 },
742 None,
743 )
744 .await
745 .unwrap();
746
747 assert!(output.success());
748 }
752
753 #[tokio::test]
754 #[cfg(not(windows))]
755 async fn test_run_subprocess_exit_code_preservation() {
756 let mut logger = Logger::new();
757 let output = run_subprocess(
758 &mut logger,
759 || {
760 let mut cmd = CommandBuilder::new("sh");
761 cmd.arg("-c");
762 cmd.arg("exit 42");
763 cmd
764 },
765 None,
766 )
767 .await
768 .unwrap();
769
770 assert!(!output.success());
771 assert_eq!(output.exit_code(), 42);
772 }
773
774 #[tokio::test]
775 #[cfg(not(windows))]
776 async fn test_run_subprocess_ansi_colors_preserved() {
777 let mut logger = Logger::new();
778 let output = run_subprocess(
779 &mut logger,
780 || {
781 let mut cmd = CommandBuilder::new("sh");
782 cmd.arg("-c");
783 cmd.arg("echo -e '\\033[31mred\\033[0m'");
784 cmd
785 },
786 None,
787 )
788 .await
789 .unwrap();
790
791 assert!(output.success());
792 let stderr = output.stderr_str().unwrap();
793 assert!(stderr.contains("\x1b[31m") || stderr.contains("red"));
795 }
796
797 #[tokio::test]
798 #[cfg(not(windows))]
799 async fn test_run_subprocess_default_stderr_lines() {
800 let mut logger = Logger::new();
801 let output = run_subprocess(
802 &mut logger,
803 || {
804 let mut cmd = CommandBuilder::new("echo");
805 cmd.arg("test");
806 cmd
807 },
808 None, )
810 .await
811 .unwrap();
812
813 assert!(output.success());
814 }
815
816 #[tokio::test]
817 #[cfg(not(windows))]
818 async fn test_run_subprocess_custom_stderr_lines() {
819 let mut logger = Logger::new();
820 let output = run_subprocess(
821 &mut logger,
822 || {
823 let mut cmd = CommandBuilder::new("echo");
824 cmd.arg("test");
825 cmd
826 },
827 Some(10), )
829 .await
830 .unwrap();
831
832 assert!(output.success());
833 }
834
835 #[tokio::test]
836 #[cfg(not(windows))]
837 async fn test_run_subprocess_nonexistent_command() {
838 let mut logger = Logger::new();
839 let result = run_subprocess(
840 &mut logger,
841 || CommandBuilder::new("nonexistent-command-xyz-123"),
842 None,
843 )
844 .await;
845
846 assert!(result.is_err());
847 }
848
849 #[tokio::test]
850 async fn test_subprocess_output_utf8_handling() {
851 let output = SubprocessOutput {
852 stdout: "hello 世界".as_bytes().to_vec(),
853 stderr: "error 错误".as_bytes().to_vec(),
854 exit_code: 0,
855 };
856
857 assert_eq!(output.stdout_str().unwrap(), "hello 世界");
858 assert_eq!(output.stderr_str().unwrap(), "error 错误");
859 }
860
861 #[tokio::test]
862 async fn test_subprocess_output_invalid_utf8() {
863 let output = SubprocessOutput {
864 stdout: vec![0xFF, 0xFE, 0xFD], stderr: vec![],
866 exit_code: 0,
867 };
868
869 assert!(output.stdout_str().is_err());
870 }
871
872 #[tokio::test]
873 async fn test_logger_suspend() {
874 let mut logger = Logger::new();
875 logger.status("Building", "test");
876 let result = logger.suspend(|| 42);
877 assert_eq!(result, 42);
878 }
879
880 #[tokio::test]
881 async fn test_logger_suspend_without_progress() {
882 let mut logger = Logger::new();
883 let result = logger.suspend(|| 42);
884 assert_eq!(result, 42);
885 }
886
887 #[tokio::test]
888 async fn test_logger_status_permanent() {
889 let logger = Logger::new();
890 logger.status_permanent("Compiling", "test-crate");
892 }
893
894 #[tokio::test]
895 async fn test_logger_warning() {
896 let logger = Logger::new();
897 logger.warning("Warning", "test message");
899 }
900
901 #[tokio::test]
902 async fn test_logger_info() {
903 let logger = Logger::new();
904 logger.info("Info", "test message");
906 }
907
908 #[tokio::test]
909 async fn test_logger_error() {
910 let logger = Logger::new();
911 logger.error("Error", "test message");
913 }
914
915 #[tokio::test]
916 async fn test_logger_print_message() {
917 let logger = Logger::new();
918 logger.print_message("test message");
920 }
921
922 #[tokio::test]
923 async fn test_logger_progress() {
924 let mut logger = Logger::new();
925 logger.progress("Processing...");
926 assert!(logger.progress_bar.is_some());
927 }
928
929 #[tokio::test]
930 async fn test_logger_set_progress_message() {
931 let mut logger = Logger::new();
932 logger.progress("Initial");
933 logger.set_progress_message("Updated");
934 assert!(logger.progress_bar.is_some());
935 }
936}