use std::sync::{Arc, Mutex};
use std::time::Duration;
use rightkit_child_process::OwnedCommand;
use rightkit_job_queue::{EngineClient, JobError};
use serde_json::Value;
use tauri::{AppHandle, Emitter, Wry};
use crate::EVENT_ENGINE;
pub type CommandFactory = Arc<dyn Fn() -> OwnedCommand + Send + Sync>;
#[derive(Clone)]
pub struct EngineSpec {
pub command: CommandFactory,
pub hello_timeout: Duration,
pub shutdown_grace: Duration,
pub call_timeout: Option<Duration>,
}
impl EngineSpec {
pub fn new(command: impl Fn() -> OwnedCommand + Send + Sync + 'static) -> Self {
Self {
command: Arc::new(command),
hello_timeout: Duration::from_secs(30),
shutdown_grace: Duration::from_secs(3),
call_timeout: Some(Duration::from_secs(60)),
}
}
}
pub struct EngineHost {
spec: EngineSpec,
client: Mutex<Option<Arc<EngineClient>>>,
closing: std::sync::atomic::AtomicBool,
lifecycle: Mutex<Value>,
}
impl EngineHost {
pub fn new(spec: EngineSpec) -> Self {
Self {
spec,
client: Mutex::new(None),
closing: Default::default(),
lifecycle: Mutex::new(serde_json::json!({"state": "starting"})),
}
}
pub fn start(&self) -> Result<Value, JobError> {
let client = Arc::new(EngineClient::spawn_with(
(self.spec.command)(),
self.spec.hello_timeout,
)?);
let hello = client.hello().clone();
*self.client.lock().unwrap_or_else(|e| e.into_inner()) = Some(client);
Ok(hello)
}
pub fn lifecycle(&self) -> Value {
self.lifecycle
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub(crate) fn set_lifecycle(&self, v: Value) {
*self.lifecycle.lock().unwrap_or_else(|e| e.into_inner()) = v;
}
pub fn hello(&self) -> Option<Value> {
self.client
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.map(|c| c.hello().clone())
}
fn client(&self) -> Result<Arc<EngineClient>, JobError> {
if self.closing.load(std::sync::atomic::Ordering::SeqCst) {
return Err(JobError::new("shutting_down", "the app is exiting"));
}
self.client
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or_else(|| JobError::new("engine_not_running", "engine has not started"))
}
pub fn call(
&self,
app: &AppHandle<Wry>,
method: &str,
params: Value,
) -> Result<Value, JobError> {
let attempt = |client: Arc<EngineClient>| {
let app = app.clone();
let name = method.to_string();
let on_event: Box<dyn Fn(&Value) + Send> = Box::new(move |ev| {
let _ = app.emit(EVENT_ENGINE, serde_json::json!({"call": name, "event": ev}));
});
match self.spec.call_timeout {
Some(t) => client.call_timeout(method, params.clone(), Some(on_event), t),
None => client.call_with_events(method, params.clone(), Some(on_event)),
}
};
let closing = || self.closing.load(std::sync::atomic::Ordering::SeqCst);
match attempt(self.client()?) {
Err(e) if matches!(e.code.as_str(), "io" | "engine_write") && !closing() => {
self.start()?;
attempt(self.client()?)
}
Err(e) if e.code == "engine_exited" && !closing() => {
let _ = self.start();
Err(e)
}
other => other,
}
}
pub fn shutdown(&self) {
self.closing
.store(true, std::sync::atomic::Ordering::SeqCst);
if let Some(c) = self.client.lock().unwrap_or_else(|e| e.into_inner()).take() {
c.shutdown(self.spec.shutdown_grace);
}
}
}