tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! 系统事件(《2.功能概述文档》第 7 节,《4.实现与测试》第 3.4 节):休眠与唤醒、网络变化、锁屏、关机与注销,
//! 以及计算机名、系统版本和 macOS 的 App Nap。
//!
//! 唤醒和网络变化先用各平台都可靠的通用办法检测(后台线程比较墙上时钟、定时比较网卡地址),
//! 各平台的系统通知作为补充;两边都报告的同一次唤醒在这里去重,页面只收到一次。

use std::collections::BTreeSet;
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, SystemTime};

use serde::Serialize;
use serde_json::json;
use tauri::{AppHandle, Runtime};

use crate::Shared;

#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;

#[cfg(target_os = "linux")]
use linux as platform;
#[cfg(target_os = "macos")]
use macos as platform;
#[cfg(target_os = "windows")]
use self::windows as platform;

#[cfg(target_os = "linux")]
pub(crate) use linux::{desktop_entry, session_bus};

/// 通用的唤醒检测:后台线程每 2 秒醒一次。
const TICK: Duration = Duration::from_secs(2);
/// 墙上时钟比预期多走这么多,视为刚从休眠中唤醒。
const RESUME_GAP_MS: i64 = 10_000;
/// 同一次唤醒可能由时钟和系统通知各报告一次:这段时间内的第二次不再发出。
const RESUME_DEDUP: Duration = Duration::from_secs(30);
/// 网卡地址的比较间隔。
const NETWORK_POLL: Duration = Duration::from_secs(3);

#[derive(Default)]
pub struct SystemState {
    locked: AtomicBool,
    power: Mutex<Power>,
}

#[derive(Default)]
struct Power {
    /// 收到了“即将休眠”,还没有唤醒。
    sleeping: bool,
    sleep_at: Option<SystemTime>,
    /// 用墙上时钟:Linux、macOS 的单调时钟在休眠期间不走,不能用来判断“刚唤醒过”。
    last_resume: Option<SystemTime>,
}

impl SystemState {
    /// 系统锁屏中(窗口算不可见、没有焦点,《2.功能概述文档》第 5.3 节)。
    pub fn screen_locked(&self) -> bool {
        self.locked.load(Ordering::Relaxed)
    }
}

#[derive(Clone, Debug, Serialize)]
pub struct SystemInfo {
    pub os: &'static str,
    pub os_version: String,
    pub computer_name: String,
}

/// 系统名称、版本和计算机名:第一次调用时读取,之后用缓存(不会在运行中变化,变了也不影响)。
pub fn info() -> SystemInfo {
    static INFO: OnceLock<SystemInfo> = OnceLock::new();
    INFO.get_or_init(|| SystemInfo {
        os: if cfg!(windows) {
            "windows"
        } else if cfg!(target_os = "macos") {
            "macos"
        } else {
            "linux"
        },
        os_version: platform::os_version(),
        computer_name: platform::computer_name(),
    })
    .clone()
}

/// setup 中、引擎窗口创建之后调用:启动通用的唤醒和网络检测,以及平台的系统通知。
pub fn start<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
    spawn_tick(shared.clone());
    spawn_network(shared.clone());
    platform::start(app, shared);
}

/// macOS 登录期间阻止 App Nap(引擎就绪、登录状态变化时调用);其他平台没有这回事。
pub fn app_nap(hold: bool) {
    #[cfg(target_os = "macos")]
    macos::app_nap(hold);
    #[cfg(not(target_os = "macos"))]
    let _ = hold;
}

// ---- 事件:各平台的检测都汇集到这里 ----

/// 即将休眠(Linux PrepareForSleep、Windows PBT_APMSUSPEND、macOS willSleep)。
pub(crate) fn on_sleep(shared: &Shared, source: &str) {
    {
        let mut power = shared.system.power.lock().unwrap();
        if power.sleeping {
            return;
        }
        power.sleeping = true;
        power.sleep_at = Some(SystemTime::now());
        // 新的一轮休眠:之后的唤醒不算重复
        power.last_resume = None;
    }
    shared.logs.plugin("info", &format!("系统即将休眠({source})"));
    shared.hub.broadcast(json!({ "t": "sleep" }));
}

/// 从休眠中唤醒。slept_ms 由时钟检测给出;系统通知给不出的,按“即将休眠”的时间计算。
pub(crate) fn on_resume(shared: &Shared, slept_ms: Option<u64>, source: &str) {
    let slept = {
        let mut power = shared.system.power.lock().unwrap();
        let since = power.last_resume.and_then(|t| SystemTime::now().duration_since(t).ok());
        if resume_is_duplicate(power.sleeping, since) {
            return;
        }
        let from_sleep = power.sleep_at.take().and_then(|t| SystemTime::now().duration_since(t).ok()).map(|d| d.as_millis() as u64);
        power.sleeping = false;
        power.last_resume = Some(SystemTime::now());
        slept_ms.or(from_sleep).unwrap_or(0)
    };
    // 唤醒后的一段时间内引擎的存活信号可能迟到,不能按卡死重建
    shared.engine.note_resume();
    shared.logs.plugin("info", &format!("系统从休眠中唤醒({source}),约 {} 秒", slept / 1000));
    shared.hub.broadcast(json!({ "t": "resume", "slept_ms": slept }));
}

/// 锁屏与解锁:只影响“正被看”,所以同时把各窗口的可见性重新发一次。
pub(crate) fn on_lock<R: Runtime>(app: &AppHandle<R>, shared: &Shared, locked: bool, source: &str) {
    if shared.system.locked.swap(locked, Ordering::SeqCst) == locked {
        return;
    }
    shared.logs.plugin("info", &format!("{}({source})", if locked { "系统锁屏" } else { "系统解锁" }));
    shared.hub.broadcast(json!({ "t": "screen_lock", "locked": locked }));
    for window in shared.hub.windows() {
        crate::engine::send_window_state(app, shared, &window.label);
    }
}

/// 系统关机或注销:与 App 调用 exit 相同,引擎收尾(最多 2 秒)后退出。
pub(crate) fn on_shutdown<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, source: &str) {
    if shared.engine.exiting() {
        return;
    }
    shared.logs.plugin("info", &format!("系统关机或注销({source}),开始退出"));
    shared.hub.broadcast(json!({ "t": "shutdown" }));
    crate::engine::begin_exit(app, shared, 0);
}

// ---- 通用的唤醒检测 ----

fn spawn_tick(shared: Arc<Shared>) {
    let spawned = std::thread::Builder::new().name("drim-resume".into()).spawn(move || {
        let mut before = SystemTime::now();
        loop {
            std::thread::sleep(TICK);
            let now = SystemTime::now();
            let elapsed = wall_elapsed_ms(before, now);
            before = now;
            if let Some(slept) = detect_resume(TICK, elapsed) {
                on_resume(&shared, Some(slept), "时钟");
            }
        }
    });
    if let Err(err) = spawned {
        log::warn!("唤醒检测的线程创建失败:{err}");
    }
}

/// 两次读到的墙上时钟之差(毫秒,时钟被往回调时为负数)。
fn wall_elapsed_ms(before: SystemTime, after: SystemTime) -> i64 {
    match after.duration_since(before) {
        Ok(d) => d.as_millis() as i64,
        Err(e) => -(e.duration().as_millis() as i64),
    }
}

/// 线程睡了 expected,墙上时钟却走了 wall_elapsed_ms:多出 10 秒以上的视为系统休眠过,返回多出的毫秒数。
/// 休眠期间线程的计时(单调时钟)停止,墙上时钟照走,所以差值约等于休眠的时长。
/// 用户把时钟往前调 10 秒以上也会被当成唤醒,后果只是引擎多检查一次连接。
pub(crate) fn detect_resume(expected: Duration, wall_elapsed_ms: i64) -> Option<u64> {
    let extra = wall_elapsed_ms - expected.as_millis() as i64;
    (extra >= RESUME_GAP_MS).then_some(extra as u64)
}

/// 这次唤醒是不是刚报告过的那次(时钟和系统通知各报告一次)。收到过“即将休眠”的一定不是重复。
pub(crate) fn resume_is_duplicate(sleeping: bool, since_last_resume: Option<Duration>) -> bool {
    !sleeping && since_last_resume.is_some_and(|d| d < RESUME_DEDUP)
}

// ---- 通用的网络变化检测 ----

fn spawn_network(shared: Arc<Shared>) {
    let spawned = std::thread::Builder::new().name("drim-network".into()).spawn(move || {
        let mut last = network_snapshot();
        loop {
            std::thread::sleep(NETWORK_POLL);
            let now = network_snapshot();
            if now == last {
                continue;
            }
            let online = now.online;
            last = now;
            shared.logs.plugin("info", &format!("网络变化,{}", if online { "有可用地址" } else { "没有可用地址" }));
            shared.hub.broadcast(json!({ "t": "network", "online": online }));
        }
    });
    if let Err(err) = spawned {
        log::warn!("网络检测的线程创建失败:{err}");
    }
}

#[derive(PartialEq, Eq, Debug)]
struct NetworkSnapshot {
    /// 网卡名和地址(以及 Linux 的默认路由),任何一项变化都算网络变化。
    keys: BTreeSet<String>,
    online: bool,
}

fn network_snapshot() -> NetworkSnapshot {
    let addrs = platform::interface_addrs();
    let online = online_of(addrs.iter().map(|(_, a)| a));
    let mut keys: BTreeSet<String> = addrs.into_iter().filter(|(_, a)| !a.is_loopback()).map(|(name, a)| format!("{name}|{a}")).collect();
    keys.extend(platform::default_routes());
    NetworkSnapshot { keys, online }
}

/// 有非回环、非链路本地的地址就算在线(链路本地地址是没有拿到 DHCP 地址时系统自己配的,连不上外网)。
pub(crate) fn online_of<'a>(addrs: impl IntoIterator<Item = &'a IpAddr>) -> bool {
    addrs.into_iter().any(|addr| match addr {
        IpAddr::V4(v4) => !v4.is_loopback() && !v4.is_link_local() && !v4.is_unspecified(),
        IpAddr::V6(v6) => !v6.is_loopback() && !v6.is_unspecified() && (v6.segments()[0] & 0xffc0) != 0xfe80,
    })
}

/// Linux、macOS:用 getifaddrs 列出已启用网卡的 IPv4、IPv6 地址。
#[cfg(unix)]
pub(crate) fn unix_interface_addrs() -> Vec<(String, IpAddr)> {
    use std::ffi::CStr;
    use std::net::{Ipv4Addr, Ipv6Addr};

    let mut out = Vec::new();
    // SAFETY:getifaddrs 返回的链表在 freeifaddrs 之前一直有效;只按地址族读取对应的结构
    unsafe {
        let mut head: *mut libc::ifaddrs = std::ptr::null_mut();
        if libc::getifaddrs(&mut head) != 0 {
            return out;
        }
        let mut cur = head;
        while !cur.is_null() {
            let ifa = &*cur;
            cur = ifa.ifa_next;
            if ifa.ifa_addr.is_null() || ifa.ifa_flags & (libc::IFF_UP as libc::c_uint) == 0 {
                continue;
            }
            let addr = match i32::from((*ifa.ifa_addr).sa_family) {
                libc::AF_INET => {
                    let sin = &*(ifa.ifa_addr as *const libc::sockaddr_in);
                    IpAddr::V4(Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr)))
                }
                libc::AF_INET6 => {
                    let sin6 = &*(ifa.ifa_addr as *const libc::sockaddr_in6);
                    IpAddr::V6(Ipv6Addr::from(sin6.sin6_addr.s6_addr))
                }
                _ => continue,
            };
            let name = CStr::from_ptr(ifa.ifa_name).to_string_lossy().into_owned();
            out.push((name, addr));
        }
        libc::freeifaddrs(head);
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{Ipv4Addr, Ipv6Addr};

    #[test]
    fn resume_detected_only_after_big_jump() {
        // 按时醒来、稍有延迟、时钟往回调:都不算
        assert_eq!(detect_resume(TICK, 2_000), None);
        assert_eq!(detect_resume(TICK, 9_000), None);
        assert_eq!(detect_resume(TICK, -60_000), None);
        // 多走了 10 秒以上:算,返回多出的部分
        assert_eq!(detect_resume(TICK, 12_000), Some(10_000));
        assert_eq!(detect_resume(TICK, 3_602_000), Some(3_600_000));
    }

    #[test]
    fn wall_elapsed_handles_backward_clock() {
        let t = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
        assert_eq!(wall_elapsed_ms(t, t + Duration::from_secs(3)), 3_000);
        assert_eq!(wall_elapsed_ms(t + Duration::from_secs(3), t), -3_000);
    }

    #[test]
    fn resume_dedup() {
        // 刚报告过、没有新的休眠:重复
        assert!(resume_is_duplicate(false, Some(Duration::from_secs(1))));
        // 收到了新的“即将休眠”:不是重复
        assert!(!resume_is_duplicate(true, Some(Duration::from_secs(1))));
        // 很久以前报告过、从没报告过:不是重复
        assert!(!resume_is_duplicate(false, Some(Duration::from_secs(120))));
        assert!(!resume_is_duplicate(false, None));
    }

    #[test]
    fn online_needs_routable_address() {
        let lo = IpAddr::V4(Ipv4Addr::LOCALHOST);
        let ll4 = IpAddr::V4(Ipv4Addr::new(169, 254, 3, 4));
        let ll6 = IpAddr::V6("fe80::1".parse::<Ipv6Addr>().unwrap());
        let lan = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20));
        let v6 = IpAddr::V6("2001:db8::5".parse::<Ipv6Addr>().unwrap());
        assert!(!online_of(&[]));
        assert!(!online_of(&[lo, ll4, ll6, IpAddr::V6(Ipv6Addr::LOCALHOST)]));
        assert!(online_of(&[lo, lan]));
        assert!(online_of(&[ll6, v6]));
    }

    #[cfg(unix)]
    #[test]
    fn lists_loopback_interface() {
        // 容器中也至少有回环网卡
        let addrs = unix_interface_addrs();
        assert!(addrs.iter().any(|(_, a)| a.is_loopback()), "{addrs:?}");
    }

    #[test]
    fn info_is_cached_and_filled() {
        let a = info();
        let b = info();
        assert_eq!(a.os_version, b.os_version);
        assert!(!a.os_version.is_empty());
    }
}