vrc-log 0.15.3

VRChat Local Avatar ID Logger
Documentation
#[macro_use]
extern crate tracing;

use std::{io::ErrorKind, sync::OnceLock};

use anyhow::Result;
use chrono::{Local, Offset};
#[cfg(feature = "title")]
use crossterm::{execute, terminal::SetTitle};
use derive_config::{ConfigError, DeriveTomlConfig};
use notify::PollWatcher;
use strum::IntoEnumIterator;
use terminal_link::Link;
use time::{macros::format_description, UtcOffset};
use tokio::signal;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{fmt::time::OffsetTime, EnvFilter};
use vrc_log::{
    provider,
    provider::{avtrdb::AvtrDBActor, prelude::*, ProviderKind},
    settings::Settings,
    vrchat::{VRCHAT_AMP_PATH, VRCHAT_LOW_PATH},
    CARGO_PKG_HOMEPAGE,
};

/* Watchers will stop working if they get dropped. */
static WATCHERS: OnceLock<Vec<PollWatcher>> = OnceLock::new();

#[allow(clippy::too_many_lines)]
#[tokio::main]
async fn main() -> Result<()> {
    #[cfg(feature = "title")]
    execute!(std::io::stdout(), SetTitle("VRC-LOG"))?;

    /* Debugging: RUST_LOG=vrc_log=debug */
    tracing_subscriber::fmt()
        .with_ansi(true)
        .with_ansi_sanitization(false)
        .with_env_filter(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .with_target(false)
        .with_timer(OffsetTime::new(
            UtcOffset::from_whole_seconds(Local::now().offset().fix().local_minus_utc())?,
            format_description!("[hour repr:12]:[minute]:[second] [period]"),
        ))
        .init();

    if vrc_log::check_for_updates().await? {
        let text = "An update is available";
        let link = Link::new(text, CARGO_PKG_HOMEPAGE);
        info!("{link}");
    }

    let mut args = std::env::args().collect::<Vec<_>>();
    let force_wizard = args.iter().any(|arg| arg == "--wizard" || arg == "-w");
    if force_wizard {
        args.retain(|arg| arg != "--wizard" && arg != "-w");
    }

    let mut settings = if force_wizard {
        info!("Setup wizard requested via flag");
        Settings::try_wizard().expect("Failed to setup wizard")
    } else {
        Settings::load().unwrap_or_else(|error| match error {
            ConfigError::Io(error) if error.kind() == ErrorKind::NotFound => {
                info!("Welcome to VRC-LOG! Please follow the setup wizard");
                Settings::try_wizard().expect("Failed to setup wizard")
            }
            error => {
                error!("There was an error loading the settings: {error}");
                error!("Most likely an update. Please follow the setup wizard");
                Settings::try_wizard().expect("Failed to setup wizard")
            }
        })
    };

    if settings.providers.len() != ProviderKind::iter().count() {
        info!("Additional providers have been added, triggering setup wizard");
        settings = Settings::try_wizard().expect("Failed to setup wizard");
    }

    let (tx, rx) = flume::unbounded();
    let _ = WATCHERS.set(vec![
        vrc_log::watch(tx.clone(), VRCHAT_AMP_PATH.as_path(), 100)?,
        vrc_log::watch(tx.clone(), VRCHAT_LOW_PATH.as_path(), 1_000)?,
    ]);

    #[cfg(windows)]
    if vrc_log::windows::is_elevated()? {
        vrc_log::windows::spawn_procmon_watcher();
        info!("Running with elevated privileges.");
        info!("Starting Process Monitor for additional logging.");
        info!("Close Process Monitor manually to begin scans; it will reopen automatically.");
    }

    settings.save()?;
    vrc_log::launch_game(&args)?;

    // This is a little wonky, but effectively we are creating a controlled memory leak,
    // which will be static for the rest of the programs runtime.
    let settings: &'static Settings = Box::leak(Box::new(settings));

    let (mut avtrdb_actor, avtrdb_sender) = AvtrDBActor::new(settings);

    let providers = settings
        .providers
        .iter()
        .filter(|(_, enabled)| **enabled)
        .map(|(provider, _)| match provider {
            #[cfg(feature = "nsvr")]
            ProviderKind::NSVR => provider!(NSVR::new(settings)),
            #[cfg(feature = "paw")]
            ProviderKind::PAW => provider!(Paw::new(settings)),
            #[cfg(feature = "vrcdb")]
            ProviderKind::VRCDB => provider!(VrcDB::new(settings)),
            #[cfg(feature = "vrcwb")]
            ProviderKind::VRCWB => provider!(VrcWB::new(settings)),
            #[cfg(feature = "avtrdb")]
            ProviderKind::AVTRDB => provider!(AvtrDB::new(avtrdb_sender.clone())),
            #[cfg(feature = "avtrzip")]
            ProviderKind::AVTRZIP => provider!(AvtrZip::default()),
        })
        .collect::<Vec<_>>();

    let avtrdb_handle = tokio::spawn(async move { avtrdb_actor.run().await });

    let handle = tokio::spawn(vrc_log::process_avatars(providers, settings, (tx, rx)));

    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to install signal handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        () = ctrl_c => {
            handle.abort();
            avtrdb_handle.abort();
        },
        () = terminate => {
            handle.abort();
            avtrdb_handle.abort();
        },
    }

    Ok(())
}