1use colored::*;
2use env_logger::{Builder, Logger};
3use indicatif::ProgressBar;
4use is_terminal::IsTerminal;
5use log::{Level, LevelFilter, Log, Record};
6use once_cell::sync::Lazy;
7#[cfg(feature = "sentry")]
8use sentry::{
9 integrations::panic::PanicIntegration,
10 types::{Dsn, Uuid},
11 Breadcrumb,
12};
13use simplelog::{CombinedLogger, SharedLogger};
14#[cfg(feature = "sentry")]
15use std::{borrow::Cow, error::Error, panic::PanicInfo, str::FromStr};
16use std::{
17 fmt::{self},
18 io::Write,
19 sync::{
20 atomic::{AtomicUsize, Ordering},
21 Arc, RwLock,
22 },
23};
24use terminal_size::{Height, Width};
25
26static MAX_WINDOW_WIDTH: AtomicUsize = AtomicUsize::new(0);
28
29static PROGRESS_BAR: Lazy<RwLock<Option<Arc<ProgressBar>>>> = Lazy::new(|| RwLock::new(None));
31
32#[cfg(feature = "sentry")]
33static LOG: Lazy<Arc<RwLock<Vec<Breadcrumb>>>> = Lazy::new(|| Arc::new(RwLock::new(vec![])));
34
35struct Padded<T> {
37 value: T,
38 width: usize,
39}
40
41impl<T: fmt::Display> fmt::Display for Padded<T> {
42 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43 write!(f, "{: <width$}", self.value, width = self.width)
44 }
45}
46
47fn max_target_width(target: &str) -> usize {
49 let max_width = MAX_WINDOW_WIDTH.load(Ordering::Relaxed);
50 if max_width < target.len() {
51 MAX_WINDOW_WIDTH.store(target.len(), Ordering::Relaxed);
52 target.len()
53 } else {
54 max_width
55 }
56}
57
58fn colored_level(level: Level) -> ColoredString {
60 match level {
61 Level::Trace => "TRACE".magenta().bold(),
62 Level::Debug => "DEBUG".blue().bold(),
63 Level::Info => " INFO".green().bold(),
64 Level::Warn => " WARN".yellow().bold(),
65 Level::Error => "ERROR".red().bold(),
66 }
67}
68
69struct ShareableLogger {
70 env_logger: Logger,
71 output_is_terminal: bool,
72}
73
74impl Log for ShareableLogger {
75 fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
76 metadata.level() <= self.env_logger.filter()
77 }
78
79 fn log(&self, record: &Record<'_>) {
80 if self.enabled(record.metadata()) {
81 if !self.output_is_terminal {
85 self.env_logger.log(record);
86 } else {
87 let guard = PROGRESS_BAR.write().unwrap();
88
89 if let Some(pb) = &*guard {
91 let target = record.target();
92 let max_width = max_target_width(target);
93
94 let level = colored_level(record.level());
95
96 let target = Padded {
97 value: target.bold(),
98 width: max_width,
99 };
100
101 pb.println(format!(" {} {} > {}", level, target, record.args()));
102 } else {
103 self.env_logger.log(record);
104 }
105 }
106 }
107 }
108
109 fn flush(&self) {
110 self.env_logger.flush();
111 }
112}
113
114impl SharedLogger for ShareableLogger {
115 fn level(&self) -> LevelFilter {
116 self.env_logger.filter()
117 }
118
119 fn config(&self) -> Option<&simplelog::Config> {
120 None
121 }
122
123 fn as_log(self: Box<Self>) -> Box<dyn log::Log> {
124 Box::new(*self)
125 }
126}
127
128pub fn init(level: Option<Level>) {
139 let mut log_builder = Builder::new();
142
143 if let Some(level) = level {
145 log_builder.filter_level(level.to_level_filter());
146 } else {
147 log_builder.filter_level(LevelFilter::Warn);
148 }
149
150 if let Ok(s) = ::std::env::var("RUST_LOG") {
152 log_builder.parse_filters(&s);
153 }
154
155 log_builder.format(move |f, record| {
157 let target = record.target();
158 let max_width = max_target_width(target);
159
160 let level = colored_level(record.level());
161
162 let mut style = f.style();
163 let target = style.set_bold(true).value(Padded {
164 value: target,
165 width: max_width,
166 });
167
168 writeln!(f, " {} {} > {}", level, target, record.args())
169 });
170
171 #[cfg(feature = "sentry")]
173 let mut sentry = {
174 let mut sentry = Builder::new();
175
176 sentry.filter_level(LevelFilter::Debug);
178
179 sentry.format(move |_f, record| {
181 let mut log_guard = LOG.write().unwrap();
182 log_guard.push(Breadcrumb {
183 level: match record.level() {
184 Level::Error => sentry::Level::Error,
185 Level::Warn => sentry::Level::Warning,
186 Level::Info => sentry::Level::Info,
187 Level::Debug => sentry::Level::Debug,
188 Level::Trace => sentry::Level::Debug,
190 },
191 category: Some(record.target().to_string()),
192 message: Some(format!("{}", record.args())),
193 ..Default::default()
194 });
195
196 Ok(())
197 });
198
199 sentry
200 };
201
202 let output_is_terminal = std::io::stderr().is_terminal();
203
204 CombinedLogger::init(vec![
205 Box::new(ShareableLogger {
206 env_logger: log_builder.build(),
207 output_is_terminal,
208 }),
209 #[cfg(feature = "sentry")]
210 Box::new(ShareableLogger {
211 env_logger: sentry.build(),
212 output_is_terminal,
213 }),
214 ])
215 .unwrap();
216}
217
218pub fn set_progress_bar(progress: Arc<ProgressBar>) {
220 let mut guard = PROGRESS_BAR.write().unwrap();
221 *guard = Some(progress);
222}
223
224pub fn clear_progress_bar() {
226 let mut guard = PROGRESS_BAR.write().unwrap();
227 *guard = None;
228}
229
230#[cfg(feature = "sentry")]
231fn send_logs() {
232 let mut log_guard = LOG.write().unwrap();
233
234 for breadcrumb in log_guard.drain(..) {
235 sentry::add_breadcrumb(breadcrumb);
236 }
237}
238
239#[cfg(feature = "sentry")]
240fn sentry_config(release: String) -> sentry::ClientOptions {
241 sentry::ClientOptions {
242 dsn: Some(
243 Dsn::from_str(
244 "https://820ae3cb7b524b59af68d652aeb8ac3a@o473674.ingest.sentry.io/5508777",
245 )
246 .unwrap(),
247 ),
248 release: Some(Cow::<'static>::Owned(release)),
249 #[cfg(debug_assertions)]
250 environment: Some(Cow::Borrowed("Development")),
251 #[cfg(not(debug_assertions))]
252 environment: Some(Cow::Borrowed("Production")),
253 default_integrations: false,
254 ..Default::default()
255 }
256}
257
258#[derive(Clone, Debug)]
259pub struct Metadata {
260 pub chip: Option<String>,
261 pub probe: Option<String>,
262 pub speed: Option<String>,
263 pub release: String,
264 pub commit: String,
265}
266
267#[cfg(feature = "sentry")]
268fn set_metadata(metadata: &Metadata) {
270 sentry::configure_scope(|scope| {
271 if let Some(chip) = metadata.chip.as_ref() {
272 scope.set_tag("chip", chip);
273 }
274 if let Some(probe) = metadata.probe.as_ref() {
275 scope.set_tag("probe", probe);
276 }
277 if let Some(speed) = metadata.speed.as_ref() {
278 scope.set_tag("speed", speed);
279 }
280 scope.set_tag("commit", &metadata.commit);
281 })
282}
283
284#[cfg(feature = "sentry")]
285const SENTRY_SUCCESS: &str = r"Your error was reported successfully. If you don't mind, please open an issue on Github and include the UUID:";
286
287#[cfg(feature = "sentry")]
288fn print_uuid(uuid: Uuid) {
289 let size = terminal_size::terminal_size();
290 if let Some((Width(w), Height(_h))) = size {
291 let lines = chunk_string(&format!("{SENTRY_SUCCESS} {uuid}"), w as usize - 14);
292
293 for (i, l) in lines.iter().enumerate() {
294 if i == 0 {
295 println!(" {} {}", "Thank You!".cyan().bold(), l);
296 } else {
297 println!(" {l}");
298 }
299 }
300 } else {
301 print!("{SENTRY_HINT}");
302 }
303}
304
305#[cfg(feature = "sentry")]
306pub fn capture_error<E>(metadata: &Metadata, error: &E)
308where
309 E: Error + ?Sized,
310{
311 let _guard = sentry::init(sentry_config(metadata.release.clone()));
312 set_metadata(metadata);
313 send_logs();
314 let uuid = sentry::capture_error(error);
315 print_uuid(uuid);
316}
317
318#[cfg(feature = "sentry")]
319pub fn capture_anyhow(metadata: &Metadata, error: &anyhow::Error) {
321 let _guard = sentry::init(sentry_config(metadata.release.clone()));
322 set_metadata(metadata);
323 send_logs();
324 let uuid = sentry::integrations::anyhow::capture_anyhow(error);
325 print_uuid(uuid);
326}
327
328#[cfg(feature = "sentry")]
329pub fn capture_panic(metadata: &Metadata, info: &PanicInfo<'_>) {
331 let _guard = sentry::init(sentry_config(metadata.release.clone()));
332 set_metadata(metadata);
333 send_logs();
334 let uuid = sentry::capture_event(PanicIntegration::new().event_from_panic_info(info));
335 print_uuid(uuid);
336}
337
338fn text() -> std::io::Result<String> {
340 let mut out = String::new();
343 std::io::stdin().read_line(&mut out)?;
344
345 if let Some(mut newline) = out.find('\n') {
347 if newline > 0 && out.as_bytes()[newline - 1] == b'\r' {
348 newline -= 1;
349 }
350 out.truncate(newline);
351 }
352
353 Ok(out)
354}
355
356const SENTRY_HINT: &str = r"Unfortunately probe-rs encountered an unhandled problem. To help the devs, you can automatically log the error to sentry.io. Your data will be transmitted completely anonymously and cannot be associated with you directly. To hide this message in the future, please set $PROBE_RS_SENTRY to 'true' or 'false'. Do you wish to transmit the data? Y/n: ";
357
358fn chunk_string(s: &str, max_width: usize) -> Vec<String> {
360 let string = s.chars().collect::<Vec<char>>();
361
362 let mut result = vec![];
363
364 let mut last_ws = 0;
365 let mut offset = 0;
366 let mut i = 0;
367 let mut t_max_width = max_width;
368 while i < string.len() {
369 let c = string[i];
370 if c.is_whitespace() {
371 last_ws = i;
372 }
373 if i > offset + t_max_width {
374 if last_ws > offset {
375 let s = string[offset..last_ws].iter().collect::<String>();
376 result.push(s);
377 t_max_width = max_width;
378 } else {
379 t_max_width += 1;
380 }
381
382 offset = last_ws + 1;
383 i = last_ws + 1;
384 } else {
385 i += 1;
386 }
387 }
388 result.push(string[offset..].iter().collect::<String>());
389 result
390}
391
392pub fn ask_to_log_crash() -> bool {
394 if let Ok(var) = std::env::var("PROBE_RS_SENTRY") {
395 var == "true"
396 } else {
397 let size = terminal_size::terminal_size();
398 if let Some((Width(w), Height(_h))) = size {
399 let lines = chunk_string(SENTRY_HINT, w as usize - 14);
400
401 for (i, l) in lines.iter().enumerate() {
402 if i == 0 {
403 println!(" {} {}", "Hint".blue().bold(), l);
404 } else if i == lines.len() - 1 {
405 print!(" {l}");
406 } else {
407 println!(" {l}");
408 }
409 }
410 } else {
411 print!("{SENTRY_HINT}");
412 }
413
414 std::io::stdout().flush().ok();
415 let result = if let Ok(s) = text() {
416 let s = s.to_lowercase();
417 "yes".starts_with(&s)
418 } else {
419 false
420 };
421
422 println!();
423
424 result
425 }
426}
427
428pub fn eprintln(message: impl AsRef<str>) {
431 if let Ok(guard) = PROGRESS_BAR.try_write() {
432 match guard.as_ref() {
433 Some(pb) if !pb.is_finished() => {
434 pb.println(message.as_ref());
435 }
436 _ => {
437 eprintln!("{}", message.as_ref());
438 }
439 }
440 } else {
441 eprintln!("{}", message.as_ref());
442 }
443}
444
445pub fn println(message: impl AsRef<str>) {
448 if let Ok(guard) = PROGRESS_BAR.try_write() {
449 match guard.as_ref() {
450 Some(pb) if !pb.is_finished() => {
451 pb.println(message.as_ref());
452 }
453 _ => {
454 println!("{}", message.as_ref());
455 }
456 }
457 } else {
458 println!("{}", message.as_ref());
459 }
460}