use std::collections::{BTreeSet, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde_json::json;
use tauri::webview::{PageLoadEvent, PageLoadPayload};
use tauri::{AppHandle, ExitRequestApi, Manager, Runtime, Webview, WebviewUrl, WebviewWindowBuilder, WindowEvent};
use crate::hub::ENGINE_LABEL;
use crate::{Callbacks, EngineState, Shared};
const REBUILD_DELAY: Duration = Duration::from_secs(1);
const CRASH_WINDOW: Duration = Duration::from_secs(60);
const MAX_CRASHES: usize = 3;
const LOAD_TIMEOUT: Duration = Duration::from_secs(10);
const WAKE_INTERVAL: Duration = Duration::from_secs(10);
const WINDOW_CHECK_INTERVAL: Duration = Duration::from_secs(2);
const EXIT_WAIT: Duration = Duration::from_secs(2);
const ALIVE_TIMEOUT: Duration = Duration::from_secs(30);
struct Inner {
state: EngineState,
app_keys: BTreeSet<String>,
signed_in: BTreeSet<String>,
crashes: VecDeque<Instant>,
rebuilding: bool,
loading_since: Option<Instant>,
last_alive: Instant,
last_resume: Option<Instant>,
exiting: bool,
allow_exit: bool,
}
pub struct EngineCtl {
inner: Mutex<Inner>,
exit_done: tokio::sync::Notify,
window_states: Mutex<std::collections::HashMap<String, serde_json::Value>>,
}
impl Default for EngineCtl {
fn default() -> Self {
Self {
inner: Mutex::new(Inner {
state: EngineState::Starting,
app_keys: BTreeSet::new(),
signed_in: BTreeSet::new(),
crashes: VecDeque::new(),
rebuilding: false,
loading_since: None,
last_alive: Instant::now(),
last_resume: None,
exiting: false,
allow_exit: false,
}),
exit_done: tokio::sync::Notify::new(),
window_states: Mutex::new(std::collections::HashMap::new()),
}
}
}
impl EngineCtl {
pub fn state(&self) -> EngineState {
self.inner.lock().unwrap().state
}
pub fn app_keys(&self) -> Vec<String> {
self.inner.lock().unwrap().app_keys.iter().cloned().collect()
}
pub fn ready(&self, app_key: &str, signed_in: bool) -> bool {
let mut inner = self.inner.lock().unwrap();
inner.app_keys.insert(app_key.to_string());
if signed_in {
inner.signed_in.insert(app_key.to_string());
} else {
inner.signed_in.remove(app_key);
}
inner.loading_since = None;
inner.last_alive = Instant::now();
let changed = inner.state != EngineState::Ready;
inner.state = EngineState::Ready;
changed
}
pub fn any_signed_in(&self) -> bool {
!self.inner.lock().unwrap().signed_in.is_empty()
}
pub fn alive(&self) {
self.inner.lock().unwrap().last_alive = Instant::now();
}
pub fn note_resume(&self) {
let mut inner = self.inner.lock().unwrap();
inner.last_resume = Some(Instant::now());
inner.last_alive = Instant::now();
}
pub fn exit_done(&self) {
self.exit_done.notify_waiters();
self.exit_done.notify_one();
}
pub fn exiting(&self) -> bool {
self.inner.lock().unwrap().exiting
}
}
pub fn check_engine_url(url: &str) -> Result<(), String> {
let lower = url.trim().to_ascii_lowercase();
if lower.starts_with("http://") || lower.starts_with("https://") {
let local = ["http://localhost", "http://127.0.0.1", "http://[::1]"].iter().any(|p| lower.starts_with(p));
if cfg!(debug_assertions) && local {
return Ok(());
}
return Err(format!("tauri-plugin-deeprespond-im:engine_url 必须是打包在内的页面,不能是远程地址:{url}"));
}
if lower.contains("://") {
return Err(format!("tauri-plugin-deeprespond-im:engine_url 只能是前端资源中的路径:{url}"));
}
Ok(())
}
pub fn start<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) -> Result<(), Box<dyn std::error::Error>> {
app.add_capability(
tauri::ipc::CapabilityBuilder::new("deeprespond-im-engine")
.window(ENGINE_LABEL)
.permission("deeprespond-im:engine"),
)?;
if shared.another_instance {
set_state(shared, EngineState::Failed);
return Ok(());
}
{
let mut inner = shared.engine.inner.lock().unwrap();
inner.loading_since = Some(Instant::now());
}
watch_windows(app, shared);
let handle = app.clone();
let shared = shared.clone();
tauri::async_runtime::spawn(async move {
let mut wake = tokio::time::interval(WAKE_INTERVAL);
wake.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
wake.tick().await;
if shared.engine.exiting() {
continue;
}
shared.hub.send_engine(json!({ "t": "wake" }));
check_health(&handle, &shared);
}
});
Ok(())
}
pub fn on_ready<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
if shared.another_instance {
return;
}
let handle = app.clone();
let shared = shared.clone();
tauri::async_runtime::spawn(async move {
let creator = handle.clone();
let _ = handle.run_on_main_thread(move || {
if creator.get_webview_window(ENGINE_LABEL).is_some() {
return;
}
if let Err(err) = create_window(&creator, &shared) {
shared.logs.plugin("error", &format!("创建引擎窗口失败:{err}"));
set_state(&shared, EngineState::Failed);
}
});
});
}
fn create_window<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) -> tauri::Result<()> {
let url = WebviewUrl::App(shared.config.engine_url.clone().into());
let builder = WebviewWindowBuilder::new(app, ENGINE_LABEL, url)
.title("deeprespond-im-engine")
.visible(false)
.focused(false)
.skip_taskbar(true)
.resizable(false)
.inner_size(320.0, 240.0);
#[cfg(target_os = "macos")]
let builder = builder.background_throttling(tauri::utils::config::BackgroundThrottlingPolicy::Disabled);
let window = builder.build()?;
{
let mut inner = shared.engine.inner.lock().unwrap();
inner.loading_since = Some(Instant::now());
inner.last_alive = Instant::now();
inner.rebuilding = false;
}
watch_process(app, shared, &window);
shared.logs.plugin("info", "引擎窗口已创建");
Ok(())
}
#[allow(unused_variables)]
fn watch_process<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, window: &tauri::WebviewWindow<R>) {
#[cfg(target_os = "linux")]
{
let handle = app.clone();
let _ = window.with_webview(move |webview| {
use webkit2gtk::WebViewExt;
let handle = handle.clone();
webview.inner().connect_web_process_terminated(move |_, reason| {
if let Some(shared) = handle.try_state::<Arc<Shared>>() {
shared.logs.plugin("error", &format!("引擎窗口的网页进程终止:{reason:?}"));
crashed(&handle, &shared);
}
});
});
}
}
fn check_health<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
let mut fail = false;
let mut hung = false;
{
let mut inner = shared.engine.inner.lock().unwrap();
if let Some(since) = inner.loading_since {
if since.elapsed() >= LOAD_TIMEOUT && inner.state != EngineState::Ready && inner.state != EngineState::Failed {
inner.loading_since = None;
fail = true;
}
}
let resumed_recently = inner.last_resume.is_some_and(|t| t.elapsed() < Duration::from_secs(60));
if inner.state == EngineState::Ready && !inner.rebuilding && !resumed_recently && inner.last_alive.elapsed() >= ALIVE_TIMEOUT {
hung = true;
}
}
if fail {
shared.logs.plugin("error", "引擎页在 10 秒内没有创建引擎客户端(engine_url 写错或页面脚本出错),引擎状态置为 failed");
set_state(shared, EngineState::Failed);
}
if hung {
shared.logs.plugin("error", "引擎页 30 秒没有报告存活,按崩溃处理");
crashed(app, shared);
}
}
pub fn set_state(shared: &Shared, state: EngineState) {
let app_keys = {
let mut inner = shared.engine.inner.lock().unwrap();
if inner.state == state {
return;
}
inner.state = state;
if state != EngineState::Ready {
inner.app_keys.clear();
}
inner.app_keys.iter().cloned().collect::<Vec<_>>()
};
shared.hub.broadcast(json!({ "t": "engine", "state": state, "app_keys": app_keys }));
}
pub fn broadcast_state(shared: &Shared) {
let (state, app_keys) = {
let inner = shared.engine.inner.lock().unwrap();
(inner.state, inner.app_keys.iter().cloned().collect::<Vec<_>>())
};
shared.hub.broadcast(json!({ "t": "engine", "state": state, "app_keys": app_keys }));
}
pub fn crashed<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
if let Some(old) = shared.hub.detach(ENGINE_LABEL) {
crate::release_instance(shared, &old);
}
let give_up = {
let mut inner = shared.engine.inner.lock().unwrap();
if inner.exiting || inner.rebuilding {
return;
}
let now = Instant::now();
inner.crashes.push_back(now);
while inner.crashes.front().is_some_and(|t| now.duration_since(*t) > CRASH_WINDOW) {
inner.crashes.pop_front();
}
let give_up = inner.crashes.len() >= MAX_CRASHES;
if !give_up {
inner.rebuilding = true;
}
give_up
};
if give_up {
shared.logs.plugin("error", "引擎 1 分钟内崩溃 3 次,停止重建");
set_state(shared, EngineState::Failed);
return;
}
set_state(shared, EngineState::Restarting);
let handle = app.clone();
let shared = shared.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(REBUILD_DELAY).await;
if let Some(window) = handle.get_webview_window(ENGINE_LABEL) {
let _ = window.destroy();
for _ in 0..50 {
if handle.get_webview_window(ENGINE_LABEL).is_none() {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
}
let creator = handle.clone();
let shared2 = shared.clone();
let _ = handle.run_on_main_thread(move || {
if let Err(err) = create_window(&creator, &shared2) {
shared2.logs.plugin("error", &format!("重建引擎窗口失败:{err}"));
shared2.engine.inner.lock().unwrap().rebuilding = false;
set_state(&shared2, EngineState::Failed);
}
});
});
}
pub fn on_page_load<R: Runtime>(webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
if payload.event() != PageLoadEvent::Started {
return;
}
let Some(shared) = webview.try_state::<Arc<Shared>>() else { return };
let label = webview.label();
if let Some(old) = shared.hub.detach(label) {
crate::release_instance(&shared, &old);
if label == ENGINE_LABEL {
shared.logs.plugin("warn", "引擎页重新加载,按重建处理");
{
let mut inner = shared.engine.inner.lock().unwrap();
inner.loading_since = Some(Instant::now());
}
set_state(&shared, EngineState::Restarting);
}
}
}
pub fn on_window_event<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, label: &str, event: &WindowEvent) {
match event {
WindowEvent::Destroyed => {
shared.engine.window_states.lock().unwrap().remove(label);
if let Some(old) = shared.hub.detach(label) {
crate::release_instance(shared, &old);
}
if label == ENGINE_LABEL {
let unexpected = {
let inner = shared.engine.inner.lock().unwrap();
!inner.rebuilding && !inner.exiting && inner.state != EngineState::Failed
};
if unexpected {
shared.logs.plugin("error", "引擎窗口被意外关闭,重建");
crashed(app, shared);
}
return;
}
crate::badge::window_gone(app, shared, label);
if shared.config.exit_when_last_window_closed && !shared.engine.exiting() {
let remaining = app.webview_windows().keys().filter(|l| l.as_str() != ENGINE_LABEL && l.as_str() != label).count();
if remaining == 0 {
shared.logs.plugin("info", "最后一个界面窗口已关闭,退出");
begin_exit(app, shared, 0);
}
}
}
WindowEvent::Focused(_) | WindowEvent::Resized(_) | WindowEvent::Moved(_) => {
if label == ENGINE_LABEL {
return;
}
if let WindowEvent::Focused(true) = event {
crate::badge::window_focused(app, shared, label);
}
send_window_state(app, shared, label);
}
_ => {}
}
}
pub fn window_state<R: Runtime>(app: &AppHandle<R>, shared: &Shared, label: &str) -> serde_json::Value {
let window = app.get_webview_window(label);
let visible = window.as_ref().and_then(|w| w.is_visible().ok()).unwrap_or(false);
let minimized = window.as_ref().and_then(|w| w.is_minimized().ok()).unwrap_or(false);
let focused = window.as_ref().and_then(|w| w.is_focused().ok()).unwrap_or(false);
let locked = shared.system.screen_locked();
json!({ "visible": visible && !minimized && !locked, "focused": focused && !locked, "minimized": minimized, "screen_locked": locked })
}
pub fn send_window_state<R: Runtime>(app: &AppHandle<R>, shared: &Shared, label: &str) {
let state = window_state(app, shared, label);
{
let mut last = shared.engine.window_states.lock().unwrap();
if last.get(label) == Some(&state) {
return;
}
last.insert(label.to_string(), state.clone());
}
let mut event = state;
event["t"] = json!("window");
shared.hub.send(label, event);
}
fn watch_windows<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>) {
let handle = app.clone();
let shared = shared.clone();
tauri::async_runtime::spawn(async move {
let mut tick = tokio::time::interval(WINDOW_CHECK_INTERVAL);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tick.tick().await;
for instance in shared.hub.windows() {
send_window_state(&handle, &shared, &instance.label);
}
}
});
}
pub fn begin_exit<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, code: i32) {
{
let mut inner = shared.engine.inner.lock().unwrap();
if inner.exiting {
return;
}
inner.exiting = true;
}
let has_engine = shared.hub.send_engine(json!({ "t": "exit" }));
let handle = app.clone();
let shared = shared.clone();
tauri::async_runtime::spawn(async move {
if has_engine {
let waited = tokio::time::timeout(EXIT_WAIT, shared.engine.exit_done.notified()).await;
if waited.is_err() {
shared.logs.plugin("warn", "引擎 2 秒内没有完成收尾,直接退出");
}
}
shared.store.close_all();
shared.logs.flush();
shared.engine.inner.lock().unwrap().allow_exit = true;
handle.exit(code);
});
}
pub fn on_exit_requested<R: Runtime>(app: &AppHandle<R>, shared: &Arc<Shared>, code: Option<i32>, api: &ExitRequestApi) {
let (allow, rebuilding) = {
let inner = shared.engine.inner.lock().unwrap();
(inner.allow_exit, inner.rebuilding)
};
if allow {
return;
}
api.prevent_exit();
if rebuilding && code.is_none() {
return;
}
begin_exit(app, shared, code.unwrap_or(0));
}
pub fn show_main_window<R: Runtime>(app: &AppHandle<R>, shared: &Shared) {
match app.get_webview_window(&shared.config.main_window) {
Some(window) => {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
None => {
if let Some(callbacks) = app.try_state::<Callbacks<R>>() {
if let Some(f) = &callbacks.on_main_window_missing {
f(app);
return;
}
}
shared.logs.plugin("warn", "主窗口不存在,也没有注册 on_main_window_missing");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn engine_url_must_be_bundled() {
assert!(check_engine_url("engine.html").is_ok());
assert!(check_engine_url("/sub/engine.html").is_ok());
assert!(check_engine_url("https://example.com/engine.html").is_err());
assert!(check_engine_url("file:///tmp/engine.html").is_err());
assert_eq!(check_engine_url("http://localhost:1420/engine.html").is_ok(), cfg!(debug_assertions));
}
}