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};
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>,
last_resume: Option<SystemTime>,
}
impl SystemState {
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()
}
pub fn start<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
spawn_tick(shared.clone());
spawn_network(shared.clone());
platform::start(app, shared);
}
pub fn app_nap(hold: bool) {
#[cfg(target_os = "macos")]
macos::app_nap(hold);
#[cfg(not(target_os = "macos"))]
let _ = hold;
}
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" }));
}
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);
}
}
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),
}
}
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 {
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 }
}
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,
})
}
#[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();
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);
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());
}
}