tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! macOS 的系统事件:NSWorkspace 的 willSleep、didWake、willPowerOff 通知,分布式通知 com.apple.screenIsLocked、
//! com.apple.screenIsUnlocked(锁屏),以及登录期间用 NSProcessInfo.beginActivity 阻止 App Nap(《2.功能概述文档》第 4.5 节)。

use std::net::IpAddr;
use std::process::Command;
use std::ptr::NonNull;
use std::sync::{Arc, Mutex};

use block2::RcBlock;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_app_kit::{NSWorkspace, NSWorkspaceDidWakeNotification, NSWorkspaceWillPowerOffNotification, NSWorkspaceWillSleepNotification};
use objc2_foundation::{
    ns_string, NSActivityOptions, NSDistributedNotificationCenter, NSNotification, NSNotificationCenter, NSObjectProtocol, NSProcessInfo, NSString,
};
use tauri::{AppHandle, Runtime};

use crate::Shared;

pub(super) fn start<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
    let workspace = NSWorkspace::sharedWorkspace().notificationCenter();
    let state = shared.clone();
    // SAFETY:这些通知名是 AppKit 导出的常量字符串
    let (will_sleep, did_wake, power_off) = unsafe { (NSWorkspaceWillSleepNotification, NSWorkspaceDidWakeNotification, NSWorkspaceWillPowerOffNotification) };
    observe(&workspace, will_sleep, move || super::on_sleep(&state, "NSWorkspace"));
    let state = shared.clone();
    observe(&workspace, did_wake, move || super::on_resume(&state, None, "NSWorkspace"));
    let (handle, state) = (app.clone(), shared.clone());
    observe(&workspace, power_off, move || super::on_shutdown(&handle, &state, "NSWorkspace"));

    let distributed = NSDistributedNotificationCenter::defaultCenter();
    let (handle, state) = (app.clone(), shared.clone());
    observe(&distributed, ns_string!("com.apple.screenIsLocked"), move || super::on_lock(&handle, &state, true, "分布式通知"));
    let (handle, state) = (app.clone(), shared.clone());
    observe(&distributed, ns_string!("com.apple.screenIsUnlocked"), move || super::on_lock(&handle, &state, false, "分布式通知"));
}

/// 注册一个通知的处理块。通知中心一直持有返回的观察者,插件从不注销,所以不保存它。
fn observe(center: &NSNotificationCenter, name: &NSString, f: impl Fn() + 'static) {
    let block = RcBlock::new(move |_notification: NonNull<NSNotification>| f());
    // SAFETY:object、queue 为空表示任何发送者、在发送通知的线程上调用
    let _observer = unsafe { center.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) };
}

/// beginActivity 返回的令牌:在 endActivity 之前一直持有。NSProcessInfo 的活动接口是线程安全的。
struct Activity(Retained<ProtocolObject<dyn NSObjectProtocol>>);
// SAFETY:令牌只被保存和交回 endActivity,不在线程间并发使用(由 Mutex 保护)
unsafe impl Send for Activity {}

static ACTIVITY: Mutex<Option<Activity>> = Mutex::new(None);

/// 登录期间阻止 App Nap(UserInitiatedAllowingIdleSystemSleep:不阻止系统空闲休眠);退出登录后结束。
pub(super) fn app_nap(hold: bool) {
    let mut slot = ACTIVITY.lock().unwrap();
    let info = NSProcessInfo::processInfo();
    match (hold, slot.is_some()) {
        (true, false) => {
            let reason = NSString::from_str("深应智能 IM:登录期间保持连接与心跳");
            let token = info.beginActivityWithOptions_reason(NSActivityOptions::UserInitiatedAllowingIdleSystemSleep, &reason);
            *slot = Some(Activity(token));
        }
        (false, true) => {
            if let Some(activity) = slot.take() {
                // SAFETY:令牌来自同一个 NSProcessInfo 的 beginActivity
                unsafe { info.endActivity(&activity.0) };
            }
        }
        _ => {}
    }
}

pub(super) fn interface_addrs() -> Vec<(String, IpAddr)> {
    super::unix_interface_addrs()
}

/// 默认路由的变化体现在网卡地址上,不另外读取。
pub(super) fn default_routes() -> Vec<String> {
    Vec::new()
}

/// “macOS 14.5”(补丁号不为 0 时为“macOS 14.5.1”)。
pub(super) fn os_version() -> String {
    let v = NSProcessInfo::processInfo().operatingSystemVersion();
    if v.patchVersion > 0 {
        format!("macOS {}.{}.{}", v.majorVersion, v.minorVersion, v.patchVersion)
    } else {
        format!("macOS {}.{}", v.majorVersion, v.minorVersion)
    }
}

/// “系统设置 > 通用 > 关于本机”中的名称(SCDynamicStoreCopyComputerName 的命令行版本),取不到的用主机名。
pub(super) fn computer_name() -> String {
    let scutil = Command::new("/usr/sbin/scutil").args(["--get", "ComputerName"]).output().ok().filter(|o| o.status.success());
    if let Some(name) = scutil.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()).filter(|s| !s.is_empty()) {
        return name;
    }
    let host = NSProcessInfo::processInfo().hostName().to_string();
    host.strip_suffix(".local").unwrap_or(&host).to_string()
}