1use std::{
17 collections::VecDeque,
18 fmt,
19 io::{self, BufRead as _, Read, Write as _},
20 mem,
21 process::{ChildStderr, ChildStdout},
22};
23
24use futures_lite::{AsyncRead, AsyncReadExt};
25use zng_txt::{ToTxt as _, Txt, formatx};
26
27pub struct StdoutTap(StdTap<false>);
31impl fmt::Debug for StdoutTap {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_tuple("StdoutTap").finish_non_exhaustive()
34 }
35}
36impl StdoutTap {
37 pub fn new_blocking(stream: ChildStdout) -> Self {
39 Self(StdTap::new_blocking(stream))
40 }
41
42 pub fn new(stream: super::ChildStdout) -> Self {
44 Self(StdTap::new(stream))
45 }
46}
47
48pub struct StderrTap(StdTap<true>);
52impl fmt::Debug for StderrTap {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.debug_tuple("StderrTap").finish_non_exhaustive()
55 }
56}
57impl StderrTap {
58 pub fn new_blocking(stream: ChildStderr) -> Self {
60 Self(StdTap::new_blocking(stream))
61 }
62
63 pub fn new(stream: super::ChildStderr) -> Self {
65 Self(StdTap::new(stream))
66 }
67
68 pub fn into_panic_blocking(self) -> Result<PanicInfo, Txt> {
75 let s = self.into_string_blocking(false);
76 match PanicInfo::find(&s) {
77 Some(p) => Ok(p),
78 None => Err(s.into()),
79 }
80 }
81
82 pub async fn into_panic(self) -> Result<PanicInfo, Txt> {
89 blocking::unblock(move || self.into_panic_blocking()).await
90 }
91}
92
93macro_rules! impl_common {
94 ($($StreamTap:ident;)+) => {
95 $(
96impl $StreamTap {
97 pub fn dummy() -> Self {
99 Self(StdTap::dummy())
100 }
101
102 pub fn into_string_blocking(self, remove_ansi_csi: bool) -> String {
104 let s = deque_to_string(self.0.capture());
105 if remove_ansi_csi && contains_ansi_csi(&s) {
106 self::remove_ansi_csi_str(&s)
107 } else {
108 s
109 }
110 }
111
112 pub async fn into_string(self, remove_ansi_csi: bool) -> String {
114 blocking::unblock(move || self.into_string_blocking(remove_ansi_csi)).await
115 }
116
117 pub fn into_txt_blocking(self, remove_ansi_csi: bool) -> Txt {
119 self.into_string_blocking(remove_ansi_csi).into()
120 }
121
122 pub async fn into_txt(self, remove_ansi_csi: bool) -> Txt {
124 blocking::unblock(move || self.into_txt_blocking(remove_ansi_csi)).await
125 }
126}
127 )+
128 };
129}
130impl_common! {
131 StdoutTap;
132 StderrTap;
133}
134
135struct StdTap<const E: bool>(Option<std::thread::JoinHandle<VecDeque<u8>>>);
136
137impl<const E: bool> StdTap<E> {
138 fn new_blocking(std_stream: impl Read + Send + 'static) -> Self {
139 Self(Some(tap(std_stream, E)))
140 }
141
142 fn new(stream: impl AsyncRead + Send + Unpin + 'static) -> Self {
143 Self(Some(tap_async(stream, E)))
144 }
145
146 fn dummy() -> Self {
147 Self(None)
148 }
149
150 fn capture(self) -> VecDeque<u8> {
151 match self.0 {
152 Some(j) => match j.join() {
153 Ok(d) => d,
154 Err(p) => std::panic::resume_unwind(p),
155 },
156 None => VecDeque::new(),
157 }
158 }
159}
160
161fn tap(mut stream: impl Read + Send + 'static, is_err: bool) -> std::thread::JoinHandle<VecDeque<u8>> {
162 tap_thread(is_err)
163 .spawn(move || tap_read_loop(&mut stream, is_err))
164 .expect("failed to spawn thread")
165}
166fn tap_thread(is_err: bool) -> std::thread::Builder {
167 std::thread::Builder::new()
168 .name(format!("{}-reader", if is_err { "stderr" } else { "stdout" }))
169 .stack_size(256 * 1024)
170}
171fn tap_read_loop(stream: &mut dyn Read, is_err: bool) -> VecDeque<u8> {
172 let mut tap = Tap::new();
173 loop {
174 let r = stream.read(&mut tap.buffer);
175 if tap.push(r, is_err) {
176 break;
177 }
178 }
179 tap.rec
180}
181
182fn tap_async(mut stream: impl AsyncRead + Send + Unpin + 'static, is_err: bool) -> std::thread::JoinHandle<VecDeque<u8>> {
183 tap_thread(is_err)
184 .spawn(move || tap_async_read_loop(&mut stream, is_err))
185 .expect("failed to spawn thread")
186}
187
188fn tap_async_read_loop(stream: &mut (dyn AsyncRead + Unpin), is_err: bool) -> VecDeque<u8> {
189 let mut tap = Tap::new();
190 loop {
191 let r = crate::block_on(stream.read(&mut tap.buffer));
192 if tap.push(r, is_err) {
193 break;
194 }
195 }
196 tap.rec
197}
198struct Tap {
199 rec: VecDeque<u8>,
200 buffer: [u8; 16_384],
201}
202impl Tap {
203 fn new() -> Self {
204 Self {
205 rec: VecDeque::with_capacity(16_384),
206 buffer: [0; 16_384],
207 }
208 }
209
210 fn push(&mut self, read_r: io::Result<usize>, is_err: bool) -> bool {
211 const MAX_CAPTURE: usize = 8_388_608;
212
213 match read_r {
214 Ok(n) => {
215 if n == 0 {
216 return true;
217 }
218
219 let new = &self.buffer[..n];
220 let next_len = self.rec.len() + new.len();
221 if next_len > MAX_CAPTURE {
222 let overflow = self.rec.len() + new.len() - MAX_CAPTURE;
223 self.rec.drain(..overflow);
224 }
225 self.rec.extend(new);
226
227 let r = if is_err {
228 let mut s = std::io::stderr();
229 s.write_all(new).and_then(|_| s.flush())
230 } else {
231 let mut s = std::io::stdout();
232 s.write_all(new).and_then(|_| s.flush())
233 };
234 if let Err(e) = r {
235 panic!("{} write error, {}", if is_err { "stderr" } else { "stdout" }, e)
236 }
237 }
238 Err(e) => panic!("{} read error, {}", if is_err { "stderr" } else { "stdout" }, e),
239 }
240
241 false
242 }
243}
244
245fn deque_to_string(deq: VecDeque<u8>) -> String {
246 let deq: Vec<u8> = deq.into();
247 match String::from_utf8_lossy(&deq) {
248 std::borrow::Cow::Borrowed(_) => {
249 unsafe { String::from_utf8_unchecked(deq) }
251 }
252 std::borrow::Cow::Owned(s) => s,
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
263#[non_exhaustive]
264pub struct PanicInfo {
265 pub thread: Txt,
267 pub message: Txt,
269 pub file: Txt,
271 pub line: u32,
273 pub column: u32,
275 pub widget_path: Txt,
279 pub backtrace: Txt,
281}
282
283impl fmt::Display for PanicInfo {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 fmt::Display::fmt(&self.display_no_backtrace(), f)?;
289 if f.alternate() {
290 writeln!(f, "stack backtrace:\n{}", self.backtrace)
291 } else {
292 writeln!(f, "stack backtrace:")?;
293 let mut snippet = 9;
294 for frame in self.backtrace_frames().skip_while(|f| f.is_after_panic) {
295 write!(f, "{frame}")?;
296 if snippet > 0 {
297 let code = frame.code_snippet();
298 if !code.is_empty() {
299 snippet -= 1;
300 writeln!(f, "{code}")?;
301 }
302 }
303 }
304 Ok(())
305 }
306 }
307}
308impl PanicInfo {
309 pub fn display_no_backtrace(&self) -> impl fmt::Display {
311 struct D<'a>(&'a PanicInfo);
312 impl<'a> fmt::Display for D<'a> {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 let p = &self.0;
315 writeln!(f, "thread '{}' panicked at {}:{}:{}:", p.thread, p.file, p.line, p.column)?;
316 for line in p.message.lines() {
317 writeln!(f, " {line}")?;
318 }
319 if !p.widget_path.is_empty() {
320 writeln!(f, "widget path:\n {}", p.widget_path)?;
321 }
322 Ok(())
323 }
324 }
325 D(self)
326 }
327}
328impl PanicInfo {
329 pub fn contains(stderr: &str) -> bool {
333 Self::find_impl(stderr, false).is_some()
334 }
335
336 pub fn contains_widget(stderr: &str) -> bool {
340 match Self::find_impl(stderr, false) {
341 Some(p) => !p.widget_path.is_empty(),
342 None => false,
343 }
344 }
345
346 pub fn find(stderr: &str) -> Option<Self> {
351 Self::find_impl(stderr, true)
352 }
353
354 fn find_impl(stderr: &str, parse: bool) -> Option<Self> {
355 let mut thread = "";
356 let mut location = "";
357 let mut message = "";
358 let mut widget_path = "";
359 let mut backtrace = "";
360
361 let mut nl_message = false;
362 let mut nl_widget_path = false;
363 let mut nl_backtrace = false;
364
365 for line in stderr.lines() {
366 if let Some(panic) = line.strip_prefix("thread '")
367 && let Some((t, l)) = panic.split_once(" panicked at ")
368 {
369 thread = t;
370 location = l;
371 message = "";
372 nl_message = true;
373 widget_path = "";
374 nl_widget_path = false;
375 backtrace = "";
376 nl_backtrace = false;
377 } else if line == "widget path:" {
378 nl_widget_path = true;
379 } else if line == "stack backtrace:" {
380 nl_backtrace = true;
381 } else if mem::take(&mut nl_message) {
382 let i = line.as_ptr() as usize - stderr.as_ptr() as usize;
383 message = &stderr[i..];
384 } else if mem::take(&mut nl_widget_path) {
385 widget_path = line.trim();
386 } else if mem::take(&mut nl_backtrace) {
387 let i = line.as_ptr() as usize - stderr.as_ptr() as usize;
388 backtrace = &stderr[i..];
389 }
390 }
391
392 if thread.is_empty() {
393 return None;
394 }
395
396 if !parse {
397 return Some(Self {
398 thread: Txt::from(""),
399 message: Txt::from(""),
400 file: Txt::from(""),
401 line: 0,
402 column: 0,
403 widget_path: if !widget_path.is_empty() {
404 Txt::from("true")
405 } else {
406 Txt::from("")
407 },
408 backtrace: Txt::from(""),
409 });
410 }
411
412 let mut location = location.rsplitn(3, ':');
413 let column: u32 = location.next().unwrap_or("0").parse().unwrap_or(0);
414 let line: u32 = location.next().unwrap_or("0").parse().unwrap_or(0);
415 let file = location.next().unwrap_or("");
416
417 let mut thread = thread.split('\'');
418 let mut thread_name = thread.next().unwrap_or("<unnamed>");
419 let thread_id = thread.next().unwrap_or("");
420 if thread_name == "<unnamed>"
421 && let Some(id) = thread_id.strip_prefix('(')
422 && let Some(id) = id.strip_suffix(')')
423 {
424 thread_name = id;
425 }
426
427 let mut m = String::new();
428 let mut sep = "";
429 for line in message.lines() {
430 if let Some(line) = line.strip_prefix(" ") {
431 m.push_str(sep);
432 m.push_str(line);
433 sep = "\n";
434 } else {
435 if m.is_empty() && line != "widget path:" && line != "stack backtrace:" {
436 line.clone_into(&mut m);
438 }
439 break;
440 }
441 }
442 let message = m;
443
444 let mut backtrace_end = backtrace.len();
445 'backtrace_seek: for line in backtrace.lines() {
446 let s = line.trim_start();
447 if s.is_empty() {
448 break;
449 } else if !s.starts_with("at ") {
450 for c in s.chars() {
451 if !c.is_ascii_digit() {
452 if c != ':' {
453 break 'backtrace_seek;
454 }
455 break;
456 }
457 }
458 }
459 backtrace_end = line.as_ptr() as usize - backtrace.as_ptr() as usize + line.len();
461 }
462 backtrace = &backtrace[..backtrace_end];
463
464 Some(Self {
465 thread: thread_name.to_txt(),
466 message: message.into(),
467 file: file.to_txt(),
468 line,
469 column,
470 widget_path: widget_path.to_txt(),
471 backtrace: backtrace.to_txt(),
472 })
473 }
474
475 pub fn backtrace_frames(&self) -> impl Iterator<Item = BacktraceFrame> + '_ {
477 BacktraceFrame::parse(&self.backtrace)
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
483#[non_exhaustive]
484pub struct BacktraceFrame {
485 pub n: usize,
487
488 pub name: Txt,
490 pub file: Txt,
492 pub line: u32,
494
495 pub is_after_panic: bool,
497}
498impl fmt::Display for BacktraceFrame {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 writeln!(f, "{:>4}: {}", self.n, self.name)?;
501 if !self.file.is_empty() {
502 writeln!(f, " at {}:{}", self.file, self.line)?;
503 }
504 Ok(())
505 }
506}
507impl BacktraceFrame {
508 pub fn parse(mut backtrace: &str) -> impl Iterator<Item = BacktraceFrame> + '_ {
510 let mut is_after_panic = backtrace.lines().any(|l| l.ends_with("core::panicking::panic_fmt"));
511 std::iter::from_fn(move || {
512 if backtrace.is_empty() {
513 None
514 } else {
515 let n_name = backtrace.lines().next().unwrap();
516 let (n, name) = if let Some((n, name)) = n_name.split_once(':') {
517 let n = match n.trim_start().parse() {
518 Ok(n) => n,
519 Err(_) => {
520 backtrace = "";
521 return None;
522 }
523 };
524 let name = name.trim();
525 if name.is_empty() {
526 backtrace = "";
527 return None;
528 }
529 (n, name)
530 } else {
531 backtrace = "";
532 return None;
533 };
534
535 backtrace = backtrace.get(n_name.len() + 1..).unwrap_or("");
536 let r = if backtrace.trim_start().starts_with("at ") {
537 let file_line = backtrace.lines().next().unwrap();
538 let (file, line) = if let Some((file, line)) = file_line.rsplit_once(':') {
539 let file = file.trim_start().strip_prefix("at ").unwrap();
540 let line = match line.trim_end().parse() {
541 Ok(l) => l,
542 Err(_) => {
543 backtrace = "";
544 return None;
545 }
546 };
547 (file, line)
548 } else {
549 backtrace = "";
550 return None;
551 };
552
553 backtrace = &backtrace[file_line.len() + 1..];
554
555 BacktraceFrame {
556 n,
557 name: name.to_txt(),
558 file: file.to_txt(),
559 line,
560 is_after_panic,
561 }
562 } else {
563 BacktraceFrame {
564 n,
565 name: name.to_txt(),
566 file: Txt::from(""),
567 line: 0,
568 is_after_panic,
569 }
570 };
571
572 if is_after_panic && name.ends_with("core::panicking::panic_fmt") {
573 is_after_panic = false;
574 }
575
576 Some(r)
577 }
578 })
579 }
580
581 pub fn code_snippet(&self) -> Txt {
583 if !self.file.is_empty()
584 && self.line > 0
585 && let Ok(file) = std::fs::File::open(&self.file)
586 {
587 use std::fmt::Write as _;
588 let mut r = String::new();
589
590 let reader = std::io::BufReader::new(file);
591
592 let line_s = self.line - 2.min(self.line - 1);
593 let lines = reader.lines().skip(line_s as usize - 1).take(5);
594 for (line, line_n) in lines.zip(line_s..) {
595 let line = match line {
596 Ok(l) => l,
597 Err(_) => return Txt::from(""),
598 };
599
600 if line_n == self.line {
601 writeln!(&mut r, " {line_n:>4} > {line}").unwrap();
602 } else {
603 writeln!(&mut r, " {line_n:>4} │ {line}").unwrap();
604 }
605 }
606
607 return r.into();
608 }
609 Txt::from("")
610 }
611}
612impl PanicInfo {
613 pub fn set_hook(widget_trace_path: impl Fn() -> Txt + Send + Sync + 'static) {
622 std::panic::set_hook(Box::new(move |a| {
623 let path = widget_trace_path();
624 Self::eprint_panic(a, &path);
625 }));
626 }
627
628 pub fn eprint_panic(info: &std::panic::PanicHookInfo, widget_trace_path: &str) {
634 let backtrace = std::backtrace::Backtrace::capture();
635 let panic = PanicFromHook::from_hook(info);
636 if widget_trace_path.is_empty() {
637 eprintln!("{panic}\nstack backtrace:\n{backtrace}");
638 } else {
639 eprintln!("{panic}widget path:\n {widget_trace_path}\nstack backtrace:\n{backtrace}");
640 }
641 }
642}
643
644#[derive(Debug)]
645pub(crate) struct PanicFromHook {
646 pub thread: Txt,
647 pub msg: Txt,
648 pub file: Txt,
649 pub line: u32,
650 pub column: u32,
651}
652impl PanicFromHook {
653 pub fn from_hook(info: &std::panic::PanicHookInfo) -> Self {
654 let current_thread = std::thread::current();
655 let thread = match current_thread.name() {
656 Some(n) => n.to_txt(),
657 None => formatx!("{:?}", std::thread::current().id()),
658 };
659 let msg = crate::extract_panic_message(info.payload()).unwrap_or("Box<dyn Any>").to_txt();
660
661 let (file, line, column) = if let Some(l) = info.location() {
662 (l.file(), l.line(), l.column())
663 } else {
664 ("<unknown>", 0, 0)
665 };
666 Self {
667 thread: thread.to_txt(),
668 msg,
669 file: file.to_txt(),
670 line,
671 column,
672 }
673 }
674}
675impl std::error::Error for PanicFromHook {}
676impl fmt::Display for PanicFromHook {
677 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678 writeln!(
679 f,
680 "thread '{}' panicked at {}:{}:{}:",
681 self.thread, self.file, self.line, self.column
682 )?;
683 for line in self.msg.lines() {
684 writeln!(f, " {line}")?;
685 }
686 Ok(())
687 }
688}
689
690fn remove_ansi_csi_str(mut s: &str) -> String {
691 fn is_esc_end(byte: u8) -> bool {
692 (0x40..=0x7e).contains(&byte)
693 }
694
695 let mut r = String::new();
696 while let Some(i) = s.find(CSI) {
697 r.push_str(&s[..i]);
698 s = &s[i + CSI.len()..];
699 let mut esc_end = 0;
700 while esc_end < s.len() && !is_esc_end(s.as_bytes()[esc_end]) {
701 esc_end += 1;
702 }
703 esc_end += 1;
704 s = &s[esc_end..];
705 }
706 r.push_str(s);
707 r
708}
709
710pub fn remove_ansi_csi(s: &str) -> Txt {
712 remove_ansi_csi_str(s).into()
713}
714
715pub fn contains_ansi_csi(s: &str) -> bool {
717 s.contains(CSI)
718}
719
720const CSI: &str = "\x1b[";
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 #[test]
727 fn remove_ansi() {
728 let r = remove_ansi_csi_str(
729 "\x1b[32m INFO\x1b[0m \x1b[2mzng_env::process\x1b[0m\x1b[2m:\x1b[0m pid: 16196, name: crash-dialog-process",
730 );
731 assert_eq!(r, " INFO zng_env::process: pid: 16196, name: crash-dialog-process");
732 }
733}