use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tauri::{plugin::PluginApi, AppHandle, Emitter, Manager, Runtime};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
use crate::error::Error;
use crate::models::*;
struct SidecarProcess {
child: Child,
stdin_tx: tokio::sync::mpsc::Sender<String>,
}
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<Tauribun<R>> {
Ok(Tauribun {
app: app.clone(),
sidecars: Arc::new(Mutex::new(HashMap::new())),
})
}
pub struct Tauribun<R: Runtime> {
app: AppHandle<R>,
sidecars: Arc<Mutex<HashMap<String, SidecarProcess>>>,
}
impl<R: Runtime> Tauribun<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
Ok(PingResponse {
value: payload.value,
})
}
fn get_sidecar_path(&self, binary_path: &str) -> crate::Result<PathBuf> {
let target_triple = tauri::utils::platform::target_triple()
.map_err(|e| Error::BinaryNotFound(format!("Failed to get target triple: {}", e)))?;
let binary_name = std::path::Path::new(binary_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(binary_path);
let suffixed_name = if cfg!(target_os = "windows") {
format!("{}-{}.exe", binary_path, target_triple)
} else {
format!("{}-{}", binary_path, target_triple)
};
let unsuffixed_name = if cfg!(target_os = "windows") {
format!("{}.exe", binary_name)
} else {
binary_name.to_string()
};
log::info!(
"Looking for sidecar: binary_path={}, binary_name={}, suffixed={}, unsuffixed={}",
binary_path,
binary_name,
suffixed_name,
unsuffixed_name
);
let mut search_paths = Vec::new();
if let Ok(resource_dir) = self.app.path().resource_dir() {
log::info!("Resource dir: {:?}", resource_dir);
search_paths.push(resource_dir.join(&suffixed_name));
search_paths.push(resource_dir.join(&unsuffixed_name));
}
if let Ok(exe_path) = std::env::current_exe() {
log::info!("Executable path: {:?}", exe_path);
if let Some(exe_dir) = exe_path.parent() {
let src_tauri_dir = exe_dir.join("../..");
search_paths.push(src_tauri_dir.join(&suffixed_name));
search_paths.push(exe_dir.join(&suffixed_name));
search_paths.push(exe_dir.join(&unsuffixed_name));
}
}
if let Ok(sidecar_path) = std::env::var(format!(
"TAURI_SIDECAR_{}",
binary_path.to_uppercase().replace(['/', '-'], "_")
)) {
search_paths.insert(0, PathBuf::from(sidecar_path));
}
log::info!("Searching for sidecar in {} paths", search_paths.len());
for path in &search_paths {
log::info!("Checking path: {:?}", path);
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
if canonical.exists() {
log::info!("Found sidecar at {:?}", canonical);
return Ok(canonical);
}
}
Err(Error::BinaryNotFound(format!(
"Sidecar '{}' not found. Searched paths:\n{}",
binary_path,
search_paths
.iter()
.map(|p| format!(" - {:?}", p))
.collect::<Vec<_>>()
.join("\n")
)))
}
pub async fn spawn_server(
&self,
payload: SpawnServerRequest,
) -> crate::Result<SpawnServerResponse> {
let name = payload.name.clone();
{
let sidecars = self.sidecars.lock().await;
if sidecars.contains_key(&name) {
return Err(Error::ServerAlreadyExists(name));
}
}
let sidecar_path = self.get_sidecar_path(&payload.binary_path)?;
log::info!("Spawning sidecar '{}' from {:?}", name, sidecar_path);
let mut child = Command::new(&sidecar_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| Error::SpawnFailed(e.to_string()))?;
let stdin = child
.stdin
.take()
.ok_or_else(|| Error::SpawnFailed("Failed to capture stdin".to_string()))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| Error::SpawnFailed("Failed to capture stdout".to_string()))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| Error::SpawnFailed("Failed to capture stderr".to_string()))?;
let (stdin_tx, mut stdin_rx) = tokio::sync::mpsc::channel::<String>(100);
tokio::spawn(async move {
let mut stdin = stdin;
while let Some(message) = stdin_rx.recv().await {
if let Err(e) = stdin.write_all(message.as_bytes()).await {
log::error!("Failed to write to stdin: {}", e);
break;
}
if let Err(e) = stdin.flush().await {
log::error!("Failed to flush stdin: {}", e);
break;
}
}
});
let app_handle = self.app.clone();
let server_name = name.clone();
tokio::spawn(async move {
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
if line.contains("__ORPC__") {
let event_name = format!("tauribun://{}/message", server_name);
if let Err(e) = app_handle.emit(&event_name, &line) {
log::error!("Failed to emit event: {}", e);
}
} else {
let trimmed = line.trim();
if !trimmed.is_empty() {
log::info!("[{}] {}", server_name, trimmed);
}
}
}
});
let server_name_stderr = name.clone();
tokio::spawn(async move {
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
log::warn!("[{}] stderr: {}", server_name_stderr, line);
}
});
{
let mut sidecars = self.sidecars.lock().await;
sidecars.insert(name.clone(), SidecarProcess { child, stdin_tx });
}
log::info!("Sidecar '{}' spawned successfully", name);
Ok(SpawnServerResponse {
success: true,
name,
})
}
pub async fn send_message(
&self,
payload: SendMessageRequest,
) -> crate::Result<SendMessageResponse> {
let sidecars = self.sidecars.lock().await;
let sidecar = sidecars
.get(&payload.name)
.ok_or_else(|| Error::ServerNotFound(payload.name.clone()))?;
sidecar
.stdin_tx
.send(payload.message)
.await
.map_err(|e| Error::SendFailed(e.to_string()))?;
Ok(SendMessageResponse { success: true })
}
pub async fn kill_server(
&self,
payload: KillServerRequest,
) -> crate::Result<KillServerResponse> {
let mut sidecars = self.sidecars.lock().await;
let mut sidecar = sidecars
.remove(&payload.name)
.ok_or_else(|| Error::ServerNotFound(payload.name.clone()))?;
sidecar
.child
.kill()
.await
.map_err(|e| Error::KillFailed(e.to_string()))?;
log::info!("Sidecar '{}' killed successfully", payload.name);
Ok(KillServerResponse { success: true })
}
}