thedes_bin/
main.rs

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}