1use std::{
2 backtrace::Backtrace,
3 env,
4 error::Error,
5 fs,
6 io,
7 panic,
8 path::PathBuf,
9 sync::{Arc, atomic},
10};
11
12use chrono::{Datelike, Timelike};
13use thiserror::Error;
14use tokio::runtime;
15use tracing::level_filters::LevelFilter;
16use tracing_subscriber::{
17 EnvFilter,
18 Layer,
19 filter::FromEnvError,
20 layer::SubscriberExt,
21 util::{SubscriberInitExt, TryInitError},
22};
23
24const LOG_ENABLED_ENV_VAR: &'static str = "THEDES_LOG";
25const LOG_LEVEL_ENV_VAR: &'static str = "THEDES_LOG_LEVEL";
26const LOG_PATH_ENV_VAR: &'static str = "THEDES_LOG_PATH";
27
28const THREAD_STACK_SIZE: usize = 4 * 1024 * 1024;
29
30#[derive(Debug, Error)]
31enum ProgramError {
32 #[error("Failed to open log file {}", .path.display())]
33 OpenLogFile {
34 path: PathBuf,
35 #[source]
36 cause: io::Error,
37 },
38 #[error("Failed to get log filter")]
39 LogFilter(
40 #[source]
41 #[from]
42 FromEnvError,
43 ),
44 #[error("Failed to init logger")]
45 LogInit(
46 #[source]
47 #[from]
48 TryInitError,
49 ),
50 #[error("Failed to start asynchronous runtime")]
51 AsyncRuntime(#[source] io::Error),
52 #[error(transparent)]
53 TuiApp(#[from] thedes_app::Error),
54 #[error(transparent)]
55 TuiRuntime(#[from] thedes_tui::core::runtime::Error),
56}
57
58async fn async_runtime_main() -> Result<(), ProgramError> {
59 let config = thedes_tui::core::runtime::Config::new();
60 let runtime_future = config.run(thedes_app::run);
61 runtime_future.await??;
62 Ok(())
63}
64
65fn setup_logger() -> Result<(), ProgramError> {
66 let mut options = fs::OpenOptions::new();
67
68 options.write(true).append(true).create(true).truncate(false);
69
70 let path = match env::var_os(LOG_PATH_ENV_VAR) {
71 Some(path) => path.into(),
72 None => {
73 let now = chrono::Local::now();
74 let stem = format!(
75 "log_{:04}-{:02}-{:02}_{:02}-{:02}-{:02}.txt",
76 now.year(),
77 now.month(),
78 now.day(),
79 now.hour(),
80 now.minute(),
81 now.second(),
82 );
83 match directories::ProjectDirs::from(
84 "io.github",
85 "brunoczim",
86 "Thedes",
87 ) {
88 Some(dirs) => dirs.cache_dir().join(stem),
89 None => stem.into(),
90 }
91 },
92 };
93
94 if let Some(dir) = path.parent() {
95 fs::create_dir_all(&dir).map_err(|cause| {
96 ProgramError::OpenLogFile { path: path.clone(), cause }
97 })?;
98 }
99
100 let file = options
101 .open(&path)
102 .map_err(|cause| ProgramError::OpenLogFile { path, cause })?;
103
104 tracing_subscriber::registry()
105 .with(
106 tracing_subscriber::fmt::layer()
107 .with_writer(Arc::new(file))
108 .with_filter(
109 EnvFilter::builder()
110 .with_default_directive(LevelFilter::INFO.into())
111 .with_env_var(LOG_LEVEL_ENV_VAR)
112 .from_env()?,
113 ),
114 )
115 .try_init()?;
116
117 Ok(())
118}
119
120fn setup_panic_handler() {
121 panic::set_hook(Box::new(|info| {
122 tracing::error!("{}\n", info);
123 let backtrace = Backtrace::capture();
124 tracing::error!("backtrace:\n{}\n", backtrace);
125 eprintln!("{}", info);
126 eprintln!("backtrace:\n{}\n", backtrace);
127 }));
128}
129
130fn try_main() -> Result<(), ProgramError> {
131 setup_panic_handler();
132
133 let mut log_enabled = false;
134 if env::var_os(LOG_LEVEL_ENV_VAR).is_some()
135 || env::var_os(LOG_PATH_ENV_VAR).is_some()
136 {
137 log_enabled = true;
138 }
139 if let Some(log_enabled_var) = env::var_os(LOG_ENABLED_ENV_VAR) {
140 if log_enabled_var.eq_ignore_ascii_case("true")
141 || log_enabled_var.eq_ignore_ascii_case("t")
142 || log_enabled_var.eq_ignore_ascii_case("on")
143 || log_enabled_var.eq_ignore_ascii_case("yes")
144 || log_enabled_var.eq_ignore_ascii_case("y")
145 || log_enabled_var == "1"
146 {
147 log_enabled = true;
148 } else if log_enabled_var.eq_ignore_ascii_case("false")
149 || log_enabled_var.eq_ignore_ascii_case("f")
150 || log_enabled_var.eq_ignore_ascii_case("off")
151 || log_enabled_var.eq_ignore_ascii_case("no")
152 || log_enabled_var.eq_ignore_ascii_case("n")
153 || log_enabled_var == "0"
154 {
155 log_enabled = false;
156 }
157 }
158
159 if log_enabled {
160 setup_logger()?;
161 }
162
163 let runtime = runtime::Builder::new_multi_thread()
164 .enable_time()
165 .thread_stack_size(THREAD_STACK_SIZE)
166 .build()
167 .map_err(ProgramError::AsyncRuntime)?;
168
169 runtime.block_on(async_runtime_main())
170}
171
172fn main() {
173 unsafe {
174 env::set_var("RUST_BACKTRACE", "1");
175 atomic::fence(atomic::Ordering::SeqCst);
176 }
177
178 if let Err(error) = try_main() {
179 eprintln!("thedes found a fatal error!");
180 let mut current_error = Some(&error as &dyn Error);
181 while let Some(error) = current_error {
182 eprintln!("caused by:");
183 eprintln!(" {error}");
184 current_error = error.source();
185 }
186 }
187}