use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use futures_util::StreamExt;
use tauri::{AppHandle, Runtime};
use zbus::message::Type as MessageType;
use zbus::zvariant::{OwnedFd, OwnedObjectPath, OwnedValue};
use zbus::{Connection, MatchRule, MessageStream};
use crate::Shared;
const LOGIND: &str = "org.freedesktop.login1";
const LOGIND_PATH: &str = "/org/freedesktop/login1";
const MANAGER: &str = "org.freedesktop.login1.Manager";
const SESSION: &str = "org.freedesktop.login1.Session";
const PROPERTIES: &str = "org.freedesktop.DBus.Properties";
const SLEEP_HOLD: Duration = Duration::from_secs(1);
const SHUTDOWN_HOLD: Duration = Duration::from_millis(2500);
pub(crate) async fn session_bus() -> Option<Connection> {
static SESSION_BUS: tokio::sync::OnceCell<Option<Connection>> = tokio::sync::OnceCell::const_new();
SESSION_BUS
.get_or_init(|| async {
match Connection::session().await {
Ok(conn) => Some(conn),
Err(err) => {
log::warn!("连接 D-Bus 会话总线失败,系统通知、启动器角标不可用:{err}");
None
}
}
})
.await
.clone()
}
pub(crate) fn desktop_entry<R: Runtime>(app: &AppHandle<R>) -> String {
static ENTRY: OnceLock<String> = OnceLock::new();
ENTRY
.get_or_init(|| {
let identifier = app.config().identifier.clone();
let mut candidates = vec![identifier.clone(), app.package_info().name.clone()];
if let Some(stem) = std::env::current_exe().ok().and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned())) {
candidates.push(stem);
}
let dirs = application_dirs();
pick_desktop_entry(&identifier, &candidates, |name| dirs.iter().any(|d| d.join(format!("{name}.desktop")).is_file()))
})
.clone()
}
pub(crate) fn pick_desktop_entry(identifier: &str, candidates: &[String], exists: impl Fn(&str) -> bool) -> String {
candidates.iter().find(|c| !c.is_empty() && exists(c)).cloned().unwrap_or_else(|| identifier.to_string())
}
fn application_dirs() -> Vec<std::path::PathBuf> {
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
let data_home = std::env::var_os("XDG_DATA_HOME").map(std::path::PathBuf::from).or_else(|| home.as_ref().map(|h| h.join(".local/share")));
let data_dirs = std::env::var("XDG_DATA_DIRS").unwrap_or_else(|_| "/usr/local/share:/usr/share".into());
let mut dirs: Vec<_> = data_home.into_iter().collect();
dirs.extend(data_dirs.split(':').filter(|d| !d.is_empty()).map(std::path::PathBuf::from));
dirs.push("/var/lib/flatpak/exports/share".into());
if let Some(h) = &home {
dirs.push(h.join(".local/share/flatpak/exports/share"));
}
dirs.into_iter().map(|d| d.join("applications")).collect()
}
pub(super) fn start<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
let (handle, state) = (app.clone(), shared.clone());
tauri::async_runtime::spawn(async move {
let conn = match Connection::system().await {
Ok(conn) => conn,
Err(err) => {
state.logs.plugin("warn", &format!("连接 D-Bus 系统总线失败,休眠、关机、锁屏只靠通用办法:{err}"));
return;
}
};
tokio::join!(watch_manager(&handle, &state, &conn), watch_session(&handle, &state, &conn));
});
let (handle, state) = (app.clone(), shared.clone());
tauri::async_runtime::spawn(async move {
if let Some(conn) = session_bus().await {
watch_screensaver(&handle, &state, &conn).await;
}
});
}
async fn signal_stream(conn: &Connection, sender: Option<&str>, path: Option<&str>, interface: Option<&str>, member: Option<&str>) -> zbus::Result<MessageStream> {
let mut rule = MatchRule::builder().msg_type(MessageType::Signal);
if let Some(sender) = sender {
rule = rule.sender(sender)?;
}
if let Some(path) = path {
rule = rule.path(path)?;
}
if let Some(interface) = interface {
rule = rule.interface(interface)?;
}
if let Some(member) = member {
rule = rule.member(member)?;
}
MessageStream::for_match_rule(rule.build(), conn, None).await
}
async fn inhibit(conn: &Connection, shared: &Shared) -> Option<OwnedFd> {
let reply = conn
.call_method(Some(LOGIND), LOGIND_PATH, Some(MANAGER), "Inhibit", &("shutdown:sleep", shared.identifier.as_str(), "退出前保存数据、关闭连接", "delay"))
.await;
match reply.and_then(|m| m.body().deserialize::<OwnedFd>()) {
Ok(fd) => Some(fd),
Err(err) => {
shared.logs.plugin("warn", &format!("logind 的延迟型抑制锁取不到,关机时引擎可能来不及收尾:{err}"));
None
}
}
}
fn release_later(fd: Option<OwnedFd>, after: Duration) {
if let Some(fd) = fd {
tauri::async_runtime::spawn(async move {
tokio::time::sleep(after).await;
drop(fd);
});
}
}
async fn watch_manager<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, conn: &Connection) {
let mut stream = match signal_stream(conn, Some(LOGIND), Some(LOGIND_PATH), Some(MANAGER), None).await {
Ok(stream) => stream,
Err(err) => {
shared.logs.plugin("warn", &format!("订阅 logind 的信号失败:{err}"));
return;
}
};
let mut lock = inhibit(conn, shared).await;
while let Some(msg) = stream.next().await {
let Ok(msg) = msg else { continue };
let header = msg.header();
let Some(member) = header.member() else { continue };
let Ok(start) = msg.body().deserialize::<bool>() else { continue };
match (member.as_str(), start) {
("PrepareForSleep", true) => {
super::on_sleep(shared, "logind");
release_later(lock.take(), SLEEP_HOLD);
}
("PrepareForSleep", false) => {
super::on_resume(shared, None, "logind");
if lock.is_none() {
lock = inhibit(conn, shared).await;
}
}
("PrepareForShutdown", true) => {
super::on_shutdown(app, shared, "logind");
release_later(lock.take(), SHUTDOWN_HOLD);
}
("PrepareForShutdown", false) if lock.is_none() => lock = inhibit(conn, shared).await,
_ => {}
}
}
}
async fn session_path(conn: &Connection) -> Option<OwnedObjectPath> {
let by_pid = conn.call_method(Some(LOGIND), LOGIND_PATH, Some(MANAGER), "GetSessionByPID", &(std::process::id(),)).await;
if let Ok(path) = by_pid.and_then(|m| m.body().deserialize::<OwnedObjectPath>()) {
return Some(path);
}
let id = std::env::var("XDG_SESSION_ID").ok().filter(|s| !s.is_empty()).unwrap_or_else(|| "auto".into());
let reply = conn.call_method(Some(LOGIND), LOGIND_PATH, Some(MANAGER), "GetSession", &(id.as_str(),)).await;
reply.and_then(|m| m.body().deserialize::<OwnedObjectPath>()).ok()
}
async fn watch_session<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, conn: &Connection) {
let Some(path) = session_path(conn).await else {
shared.logs.plugin("info", "找不到本进程的 logind 会话,锁屏只看屏幕保护程序");
return;
};
let mut stream = match signal_stream(conn, Some(LOGIND), Some(path.as_str()), None, None).await {
Ok(stream) => stream,
Err(err) => {
shared.logs.plugin("warn", &format!("订阅 logind 会话的信号失败:{err}"));
return;
}
};
let initial = conn.call_method(Some(LOGIND), path.as_str(), Some(PROPERTIES), "Get", &(SESSION, "LockedHint")).await;
if let Ok(Ok(true)) = initial.and_then(|m| m.body().deserialize::<OwnedValue>()).map(|v| v.downcast_ref::<bool>()) {
super::on_lock(app, shared, true, "logind");
}
while let Some(msg) = stream.next().await {
let Ok(msg) = msg else { continue };
let header = msg.header();
let Some(member) = header.member() else { continue };
match member.as_str() {
"Lock" => super::on_lock(app, shared, true, "logind"),
"Unlock" => super::on_lock(app, shared, false, "logind"),
"PropertiesChanged" => {
let Ok((interface, changed, _)) = msg.body().deserialize::<(String, HashMap<String, OwnedValue>, Vec<String>)>() else { continue };
if interface != SESSION {
continue;
}
if let Some(Ok(locked)) = changed.get("LockedHint").map(|v| v.downcast_ref::<bool>()) {
super::on_lock(app, shared, locked, "logind");
}
}
_ => {}
}
}
}
async fn watch_screensaver<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, conn: &Connection) {
let gnome = signal_stream(conn, None, None, Some("org.gnome.ScreenSaver"), Some("ActiveChanged")).await;
let freedesktop = signal_stream(conn, None, None, Some("org.freedesktop.ScreenSaver"), Some("ActiveChanged")).await;
let (Ok(gnome), Ok(freedesktop)) = (gnome, freedesktop) else {
shared.logs.plugin("warn", "订阅屏幕保护程序的信号失败");
return;
};
let mut merged = futures_util::stream::select(gnome, freedesktop);
while let Some(msg) = merged.next().await {
let Ok(msg) = msg else { continue };
if let Ok(active) = msg.body().deserialize::<bool>() {
super::on_lock(app, shared, active, "屏幕保护程序");
}
}
}
pub(super) fn interface_addrs() -> Vec<(String, IpAddr)> {
super::unix_interface_addrs()
}
pub(super) fn default_routes() -> Vec<String> {
let v4 = std::fs::read_to_string("/proc/net/route").unwrap_or_default();
let v6 = std::fs::read_to_string("/proc/net/ipv6_route").unwrap_or_default();
parse_default_routes(&v4, &v6)
}
pub(crate) fn parse_default_routes(v4: &str, v6: &str) -> Vec<String> {
let mut out = Vec::new();
for line in v4.lines().skip(1) {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() >= 8 && cols[1] == "00000000" && cols[7] == "00000000" {
out.push(format!("route4|{}|{}", cols[0], cols[2]));
}
}
for line in v6.lines() {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() >= 10 && cols[1] == "00" && cols[0].chars().all(|c| c == '0') && cols[9] != "lo" {
out.push(format!("route6|{}|{}", cols[9], cols[4]));
}
}
out
}
pub(super) fn os_version() -> String {
let kernel = std::fs::read_to_string("/proc/sys/kernel/osrelease").unwrap_or_default();
let release = std::fs::read_to_string("/etc/os-release").or_else(|_| std::fs::read_to_string("/usr/lib/os-release")).unwrap_or_default();
format_os_version(&kernel, &release)
}
pub(crate) fn format_os_version(kernel: &str, os_release: &str) -> String {
let number: String = kernel.trim().chars().take_while(|c| c.is_ascii_digit() || *c == '.').collect();
let number = number.trim_end_matches('.');
let field = |key: &str| {
os_release.lines().find_map(|line| line.strip_prefix(key).and_then(|v| v.strip_prefix('='))).map(|v| v.trim().trim_matches('"').trim_matches('\'').to_string())
};
let distro = match (field("NAME"), field("VERSION_ID")) {
(Some(name), Some(version)) if !version.is_empty() => format!("{name} {version}"),
(Some(name), _) => name,
_ => String::new(),
};
let mut out = String::from("Linux");
if !number.is_empty() {
out.push(' ');
out.push_str(number);
}
if !distro.is_empty() {
out.push_str(&format!(" ({distro})"));
}
out
}
pub(super) fn computer_name() -> String {
let pretty = std::fs::read_to_string("/etc/machine-info").ok().and_then(|text| {
text.lines().find_map(|l| l.strip_prefix("PRETTY_HOSTNAME=")).map(|v| v.trim().trim_matches('"').to_string()).filter(|v| !v.is_empty())
});
pretty
.or_else(|| first_line("/etc/hostname"))
.or_else(|| first_line("/proc/sys/kernel/hostname"))
.unwrap_or_default()
}
fn first_line(path: &str) -> Option<String> {
std::fs::read_to_string(path).ok().and_then(|t| t.lines().next().map(|l| l.trim().to_string())).filter(|s| !s.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn os_version_string() {
let release = "NAME=\"Ubuntu\"\nVERSION_ID=\"24.04\"\nID=ubuntu\n";
assert_eq!(format_os_version("6.8.0-45-generic\n", release), "Linux 6.8.0 (Ubuntu 24.04)");
assert_eq!(format_os_version("6.10.3-arch1-1", "NAME=\"Arch Linux\"\nID=arch\n"), "Linux 6.10.3 (Arch Linux)");
assert_eq!(format_os_version("5.15.0", ""), "Linux 5.15.0");
assert_eq!(format_os_version("6.1.0", "PRETTY_NAME=\"Debian GNU/Linux 12\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"12\""), "Linux 6.1.0 (Debian GNU/Linux 12)");
}
#[test]
fn default_routes_parsed() {
let v4 = "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n\
eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0\n\
eth0\t0001A8C0\t00000000\t0001\t0\t0\t100\t00FFFFFF\t0\t0\t0\n";
let v6 = "00000000000000000000000000000000 00 00000000000000000000000000000000 00 fe800000000000000000000000000001 00000400 00000001 00000000 00000003 wlan0\n\
00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 ffffffff 00000001 00000000 00200200 lo\n";
assert_eq!(parse_default_routes(v4, v6), vec!["route4|eth0|0101A8C0".to_string(), "route6|wlan0|fe800000000000000000000000000001".to_string()]);
}
#[test]
fn desktop_entry_choice() {
let candidates = vec!["com.example.im".to_string(), "Example IM".to_string(), "example-im".to_string()];
assert_eq!(pick_desktop_entry("com.example.im", &candidates, |n| n == "example-im"), "example-im");
assert_eq!(pick_desktop_entry("com.example.im", &candidates, |_| false), "com.example.im");
}
}