diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6458f34 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,107 @@ +use std::io::IsTerminal; +use std::process::ExitCode; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; + +use anyhow::Context as _; +use clap::Parser; +use tracing::{error, info, warn}; +use tracing_subscriber::EnvFilter; + +use gamescope_hyperhdr::capture::{self, CaptureConfig}; +use gamescope_hyperhdr::config::Config; +use gamescope_hyperhdr::hyperhdr::{client, ClientConfig}; +use gamescope_hyperhdr::slot::FrameSlot; +use gamescope_hyperhdr::stats::Stats; + +/// How long the network thread gets to send `Clear` and close on shutdown. +const NETWORK_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); + +fn init_logging(level: &str) { + let filter = EnvFilter::try_new(level).unwrap_or_else(|e| { + eprintln!("invalid log level {level:?} ({e}), using info"); + EnvFilter::new("info") + }); + // Under systemd, journald adds its own timestamps. + let journal = std::env::var_os("JOURNAL_STREAM").is_some(); + let builder = tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(filter) + .with_target(false) + .with_ansi(!journal && std::io::stderr().is_terminal()); + if journal { + builder.without_time().init(); + } else { + builder.init(); + } +} + +fn main() -> ExitCode { + let cfg = Config::parse(); + init_logging(&cfg.log_level); + + match cfg.validate() { + Ok(warnings) => warnings.iter().for_each(|w| warn!("{w}")), + Err(e) => { + error!("invalid configuration: {e}"); + return ExitCode::from(2); + } + } + info!( + host = %cfg.host, + port = cfg.port, + priority = cfg.priority, + origin = %cfg.origin, + size = %cfg.size, + supersample = cfg.supersample, + requested_size = %cfg.requested_size(), + max_fps = cfg.max_fps, + keepalive_ms = cfg.keepalive_ms, + idle_clear_secs = cfg.idle_clear_secs, + target = %cfg.target, + connect_timeout_ms = cfg.connect_timeout_ms, + write_timeout_ms = cfg.write_timeout_ms, + "gamescope-hyperhdr {} starting", + env!("CARGO_PKG_VERSION") + ); + + let slot = Arc::new(FrameSlot::new()); + let stats = Arc::new(Stats::default()); + let capture_cfg = CaptureConfig { + target: cfg.target.clone(), + requested: cfg.requested_size(), + output_max: cfg.size, + }; + let mut network = None; + let result = capture::run(capture_cfg, slot.clone(), stats.clone(), || { + let handle = client::spawn(ClientConfig::from_config(&cfg), slot.clone(), stats) + .context("cannot start the network thread")?; + network = Some(handle); + Ok(()) + }); + + slot.shutdown(); + if let Some(network) = network { + let deadline = Instant::now() + NETWORK_SHUTDOWN_TIMEOUT; + while !network.is_finished() && Instant::now() < deadline { + thread::sleep(Duration::from_millis(5)); + } + if network.is_finished() { + let _ = network.join(); + } else { + warn!("network thread did not finish within {NETWORK_SHUTDOWN_TIMEOUT:?}"); + } + } + + match result { + Ok(()) => { + info!("exiting"); + ExitCode::SUCCESS + } + Err(e) => { + error!("{e:#}"); + ExitCode::FAILURE + } + } +}