#![cfg(target_os = "windows")]
use std::path::PathBuf;
use tracing::{debug, warn};
const CLIENT_EXE_REL: &str = r"Kanade\kanade-client.exe";
const SHOW_NOTIFICATION_ARG: &str = "--show-notification";
fn client_exe_path() -> Option<PathBuf> {
let program_files =
std::env::var_os("ProgramW6432").or_else(|| std::env::var_os("ProgramFiles"))?;
let path = PathBuf::from(program_files).join(CLIENT_EXE_REL);
path.exists().then_some(path)
}
pub fn surface_emergency(notification_id: &str) {
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
const COOLDOWN_SECS: u64 = 10;
static LAST_LAUNCH_SECS: AtomicU64 = AtomicU64::new(0);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now.saturating_sub(LAST_LAUNCH_SECS.load(Ordering::Relaxed)) < COOLDOWN_SECS {
debug!("emergency fallback: client launch throttled (cooldown active)");
return;
}
LAST_LAUNCH_SECS.store(now, Ordering::Relaxed);
let id = notification_id.to_owned();
tokio::task::spawn_blocking(move || {
let Some(exe) = client_exe_path() else {
debug!("emergency fallback: Client App not installed; nothing to launch");
return;
};
match crate::process_as_user::launch_detached_in_user_session(
&exe,
&[SHOW_NOTIFICATION_ARG, &id],
) {
Ok(()) => debug!(
notification_id = %id,
"emergency fallback: launched Client App to toast the emergency",
),
Err(e) => warn!(
error = %e,
notification_id = %id,
"emergency fallback: failed to launch Client App",
),
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn surface_emergency_is_noop_safe_without_client() {
surface_emergency("notif-9f3a");
tokio::task::yield_now().await;
}
}