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 #[cfg(windows)]
343 let mut input = pty
344 .master
345 .take_writer()
346 .context("Failed to open PTY input")?;
347
348 let mut child = pty
350 .slave
351 .spawn_command(cmd)
352 .context("Failed to spawn command in PTY")?;
353
354 drop(pty.slave);
356
357 let mut reader = pty
360 .master
361 .try_clone_reader()
362 .context("Failed to clone PTY reader")?;
363
364 let master = pty.master;
366
367 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
369
370 #[allow(clippy::excessive_nesting)]
375 let pty_task = tokio::spawn(async move {
376 tokio::task::spawn_blocking(move || {
377 let mut full_output = Vec::new();
378 let mut buffer = vec![0u8; 4096];
379 #[cfg(windows)]
380 let mut cursor_query = crate::cursor_query::CursorQuery::default();
381
382 loop {
383 match reader.read(&mut buffer) {
384 Ok(0) => break, Ok(bytes_read) => {
386 let chunk = &buffer[..bytes_read];
387 #[cfg(windows)]
388 for _ in 0..cursor_query.consume(chunk) {
389 input
390 .write_all(b"\x1b[1;1R")
391 .context("Failed to answer ConPTY cursor query")?;
392 }
393 full_output.extend_from_slice(chunk);
394 let _ = tx.send(chunk.to_vec());
395 }
396 Err(err) => {
397 let error_msg = format!("<pty read error: {}>", err);
399 let error_bytes = error_msg.as_bytes();
400 full_output.extend_from_slice(error_bytes);
401 let _ = tx.send(error_bytes.to_vec());
402 break;
403 }
404 }
405 }
406
407 drop(tx);
409
410 Ok::<Vec<u8>, anyhow::Error>(full_output)
411 })
412 .await
413 .context("Failed to join blocking PTY read task")?
414 });
415
416 let mut output_buffer = Vec::new();
418 let mut output_ring: Vec<Vec<u8>> = Vec::with_capacity(stderr_lines);
419
420 #[allow(clippy::excessive_nesting)]
424 let render_task = tokio::spawn(async move {
425 let mut current_lines_displayed: usize = 0;
426
427 while let Some(chunk) = rx.recv().await {
428 output_buffer.extend_from_slice(&chunk);
429
430 let mut lines: Vec<Vec<u8>> = Vec::new();
432 let mut current_line = Vec::new();
433 for byte in output_buffer.iter().copied() {
434 current_line.push(byte);
435 if byte == b'\n' {
436 lines.push(current_line);
437 current_line = Vec::new();
438 }
439 }
440 output_buffer = current_line;
441
442 for line in lines {
444 output_ring.push(line);
445 if output_ring.len() > stderr_lines {
446 output_ring.remove(0);
447 }
448 }
449
450 if is_term && !output_ring.is_empty() {
452 let mut stderr_handle = std::io::stderr();
453
454 if current_lines_displayed > 0 {
456 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
458 for _ in 0..current_lines_displayed {
459 write!(stderr_handle, "\x1b[2K\x1b[1B").ok(); }
461 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
463 }
464
465 for line_bytes in &output_ring {
467 let _ = stderr_handle.write_all(line_bytes);
468 }
469 let _ = stderr_handle.flush();
470
471 current_lines_displayed = output_ring.len();
472 lines_drawn_render
473 .store(current_lines_displayed, std::sync::atomic::Ordering::SeqCst);
474 }
475 }
476
477 if !output_buffer.is_empty() {
479 output_ring.push(output_buffer);
480 if output_ring.len() > stderr_lines {
481 output_ring.remove(0);
482 }
483 if is_term {
484 let mut stderr_handle = std::io::stderr();
485
486 if current_lines_displayed > 0 {
488 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
489 for _ in 0..current_lines_displayed {
490 write!(stderr_handle, "\x1b[2K\x1b[1B").ok();
491 }
492 write!(stderr_handle, "\x1b[{}A", current_lines_displayed).ok();
493 }
494
495 for line_bytes in &output_ring {
497 let _ = stderr_handle.write_all(line_bytes);
498 }
499 let _ = stderr_handle.flush();
500
501 lines_drawn_render.store(output_ring.len(), std::sync::atomic::Ordering::SeqCst);
502 }
503 }
504
505 (output_ring, is_term)
506 });
507
508 let status = tokio::task::spawn_blocking(move || child.wait())
510 .await
511 .context("Failed to join process wait task")?
512 .context("Failed to wait for subprocess")?;
513
514 drop(master);
519
520 let pty_output = pty_task.await.context("Failed to join PTY task")??;
522 let (_final_output_ring, was_term) = render_task.await.context("Failed to join render task")?;
523
524 let stdout_bytes = Vec::new(); let stderr_bytes = pty_output;
528
529 let exit_code = status.exit_code();
531 let final_lines_drawn = lines_drawn.load(std::sync::atomic::Ordering::SeqCst);
532
533 if was_term && final_lines_drawn > 0 {
534 let mut stderr_handle = std::io::stderr();
536 write!(stderr_handle, "\x1b[{}A", final_lines_drawn).ok();
537 for _ in 0..final_lines_drawn {
538 write!(stderr_handle, "\x1b[2K\x1b[1B").ok(); }
540 write!(stderr_handle, "\x1b[{}A", final_lines_drawn).ok();
542 let _ = stderr_handle.flush();
543 }
544
545 Ok(SubprocessOutput {
546 stdout: stdout_bytes,
547 stderr: stderr_bytes,
548 exit_code,
549 })
550}
551
552impl Default for Logger {
553 fn default() -> Self {
554 Self::new()
555 }
556}
557
558impl Drop for Logger {
559 fn drop(&mut self) {
560 if let Some(pb) = self.progress_bar.take() {
562 pb.finish_and_clear();
563 }
564
565 if self.line_count > 0 {
567 use console::Term;
568 let term = Term::stderr();
569 if term.is_term() {
570 let _ = term.clear_last_lines(self.line_count);
571 }
572 self.line_count = 0;
573 }
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 #[cfg(not(windows))]
580 use portable_pty::CommandBuilder;
581
582 use super::*;
583
584 #[tokio::test]
585 async fn test_logger_new() {
586 let logger = Logger::new();
587 assert!(logger.progress_bar.is_none());
588 assert_eq!(logger.line_count, 0);
589 }
590
591 #[tokio::test]
592 async fn test_logger_status() {
593 let mut logger = Logger::new();
594 logger.status("Building", "test-crate");
595 assert!(logger.progress_bar.is_some());
596 assert_eq!(logger.line_count, 1);
597 }
598
599 #[tokio::test]
600 async fn test_logger_clear_status() {
601 let mut logger = Logger::new();
602 logger.status("Building", "test-crate");
603 assert!(logger.progress_bar.is_some());
604 logger.clear_status();
605 assert!(logger.progress_bar.is_none());
606 assert_eq!(logger.line_count, 0);
607 }
608
609 #[tokio::test]
610 async fn test_logger_finish() {
611 let mut logger = Logger::new();
612 logger.status("Building", "test-crate");
613 logger.finish();
614 assert!(logger.progress_bar.is_none());
615 assert_eq!(logger.line_count, 0);
616 }
617
618 #[tokio::test]
619 async fn test_subprocess_output_success() {
620 let output = SubprocessOutput {
621 stdout: b"stdout content".to_vec(),
622 stderr: b"stderr content".to_vec(),
623 exit_code: 0,
624 };
625 assert!(output.success());
626 assert_eq!(output.exit_code(), 0);
627 assert_eq!(output.stdout_str().unwrap(), "stdout content");
628 assert_eq!(output.stderr_str().unwrap(), "stderr content");
629 }
630
631 #[tokio::test]
632 async fn test_subprocess_output_failure() {
633 let output = SubprocessOutput {
634 stdout: b"".to_vec(),
635 stderr: b"error message".to_vec(),
636 exit_code: 1,
637 };
638 assert!(!output.success());
639 assert_eq!(output.exit_code(), 1);
640 assert_eq!(output.stderr_str().unwrap(), "error message");
641 }
642
643 #[tokio::test]
644 #[cfg(not(windows))]
645 async fn test_run_subprocess_simple_success() {
646 let mut logger = Logger::new();
647 let output = run_subprocess(
648 &mut logger,
649 || {
650 let mut cmd = CommandBuilder::new("echo");
651 cmd.arg("hello world");
652 cmd
653 },
654 Some(3),
655 )
656 .await
657 .unwrap();
658
659 assert!(output.success());
660 assert_eq!(output.exit_code(), 0);
661 let stderr = output.stderr_str().unwrap();
663 assert!(stderr.contains("hello world") || stderr.is_empty());
664 }
665
666 #[tokio::test]
667 #[cfg(not(windows))]
668 async fn test_run_subprocess_simple_failure() {
669 let mut logger = Logger::new();
670 let output = run_subprocess(&mut logger, || CommandBuilder::new("false"), Some(3))
671 .await
672 .unwrap();
673
674 assert!(!output.success());
675 assert_ne!(output.exit_code(), 0);
676 }
677
678 #[tokio::test]
679 #[cfg(not(windows))]
680 async fn test_run_subprocess_multiline_output() {
681 let mut logger = Logger::new();
682 let output = run_subprocess(
683 &mut logger,
684 || {
685 let mut cmd = CommandBuilder::new("sh");
686 cmd.arg("-c");
687 cmd.arg("echo 'line 1'; echo 'line 2'; echo 'line 3'; echo 'line 4'; echo 'line 5'; echo 'line 6'");
688 cmd
689 },
690 Some(3), )
692 .await
693 .unwrap();
694
695 assert!(output.success());
696 let stderr = output.stderr_str().unwrap();
698 assert!(stderr.contains("line 1"));
699 assert!(stderr.contains("line 6"));
700 }
701
702 #[tokio::test]
703 #[cfg(not(windows))]
704 async fn test_run_subprocess_with_progress_bar() {
705 let mut logger = Logger::new();
706 logger.status("Preparing", "test");
707 assert!(logger.progress_bar.is_some());
708
709 let output = run_subprocess(
710 &mut logger,
711 || {
712 let mut cmd = CommandBuilder::new("echo");
713 cmd.arg("test output");
714 cmd
715 },
716 None,
717 )
718 .await
719 .unwrap();
720
721 assert!(output.success());
722 }
726
727 #[tokio::test]
728 #[cfg(not(windows))]
729 async fn test_run_subprocess_exit_code_preservation() {
730 let mut logger = Logger::new();
731 let output = run_subprocess(
732 &mut logger,
733 || {
734 let mut cmd = CommandBuilder::new("sh");
735 cmd.arg("-c");
736 cmd.arg("exit 42");
737 cmd
738 },
739 None,
740 )
741 .await
742 .unwrap();
743
744 assert!(!output.success());
745 assert_eq!(output.exit_code(), 42);
746 }
747
748 #[tokio::test]
749 #[cfg(not(windows))]
750 async fn test_run_subprocess_ansi_colors_preserved() {
751 let mut logger = Logger::new();
752 let output = run_subprocess(
753 &mut logger,
754 || {
755 let mut cmd = CommandBuilder::new("sh");
756 cmd.arg("-c");
757 cmd.arg("echo -e '\\033[31mred\\033[0m'");
758 cmd
759 },
760 None,
761 )
762 .await
763 .unwrap();
764
765 assert!(output.success());
766 let stderr = output.stderr_str().unwrap();
767 assert!(stderr.contains("\x1b[31m") || stderr.contains("red"));
769 }
770
771 #[tokio::test]
772 #[cfg(not(windows))]
773 async fn test_run_subprocess_default_stderr_lines() {
774 let mut logger = Logger::new();
775 let output = run_subprocess(
776 &mut logger,
777 || {
778 let mut cmd = CommandBuilder::new("echo");
779 cmd.arg("test");
780 cmd
781 },
782 None, )
784 .await
785 .unwrap();
786
787 assert!(output.success());
788 }
789
790 #[tokio::test]
791 #[cfg(not(windows))]
792 async fn test_run_subprocess_custom_stderr_lines() {
793 let mut logger = Logger::new();
794 let output = run_subprocess(
795 &mut logger,
796 || {
797 let mut cmd = CommandBuilder::new("echo");
798 cmd.arg("test");
799 cmd
800 },
801 Some(10), )
803 .await
804 .unwrap();
805
806 assert!(output.success());
807 }
808
809 #[tokio::test]
810 #[cfg(not(windows))]
811 async fn test_run_subprocess_nonexistent_command() {
812 let mut logger = Logger::new();
813 let result = run_subprocess(
814 &mut logger,
815 || CommandBuilder::new("nonexistent-command-xyz-123"),
816 None,
817 )
818 .await;
819
820 assert!(result.is_err());
821 }
822
823 #[tokio::test]
824 async fn test_subprocess_output_utf8_handling() {
825 let output = SubprocessOutput {
826 stdout: "hello 世界".as_bytes().to_vec(),
827 stderr: "error 错误".as_bytes().to_vec(),
828 exit_code: 0,
829 };
830
831 assert_eq!(output.stdout_str().unwrap(), "hello 世界");
832 assert_eq!(output.stderr_str().unwrap(), "error 错误");
833 }
834
835 #[tokio::test]
836 async fn test_subprocess_output_invalid_utf8() {
837 let output = SubprocessOutput {
838 stdout: vec![0xFF, 0xFE, 0xFD], stderr: vec![],
840 exit_code: 0,
841 };
842
843 assert!(output.stdout_str().is_err());
844 }
845
846 #[tokio::test]
847 async fn test_logger_suspend() {
848 let mut logger = Logger::new();
849 logger.status("Building", "test");
850 let result = logger.suspend(|| 42);
851 assert_eq!(result, 42);
852 }
853
854 #[tokio::test]
855 async fn test_logger_suspend_without_progress() {
856 let mut logger = Logger::new();
857 let result = logger.suspend(|| 42);
858 assert_eq!(result, 42);
859 }
860
861 #[tokio::test]
862 async fn test_logger_status_permanent() {
863 let logger = Logger::new();
864 logger.status_permanent("Compiling", "test-crate");
866 }
867
868 #[tokio::test]
869 async fn test_logger_warning() {
870 let logger = Logger::new();
871 logger.warning("Warning", "test message");
873 }
874
875 #[tokio::test]
876 async fn test_logger_info() {
877 let logger = Logger::new();
878 logger.info("Info", "test message");
880 }
881
882 #[tokio::test]
883 async fn test_logger_error() {
884 let logger = Logger::new();
885 logger.error("Error", "test message");
887 }
888
889 #[tokio::test]
890 async fn test_logger_print_message() {
891 let logger = Logger::new();
892 logger.print_message("test message");
894 }
895
896 #[tokio::test]
897 async fn test_logger_progress() {
898 let mut logger = Logger::new();
899 logger.progress("Processing...");
900 assert!(logger.progress_bar.is_some());
901 }
902
903 #[tokio::test]
904 async fn test_logger_set_progress_message() {
905 let mut logger = Logger::new();
906 logger.progress("Initial");
907 logger.set_progress_message("Updated");
908 assert!(logger.progress_bar.is_some());
909 }
910}