use anyhow::Result;
use colored::Colorize;
use std::sync::Arc;
use std::time::Duration;
use crate::core::registry::{IndexHandle, IndexId};
use crate::service::persistence::load_index_registry;
use crate::service::persistence_loader::build_indexer_with_persisted_state;
use crate::service::SearchAppState;
async fn restore_indexes(state: &SearchAppState, embedder: &Arc<dyn crate::core::Embedder>) {
let entries = match load_index_registry() {
Ok(e) => e,
Err(e) => {
tracing::warn!("could not read indexes.toml at startup: {e}");
return;
}
};
if entries.is_empty() {
return;
}
tracing::info!(
"warm-boot: restoring {} index registration(s) from indexes.toml",
entries.len()
);
for entry in entries {
let id = IndexId::new(entry.id.clone());
if state.registry.get(&id).is_some() {
continue;
}
let mut indexer =
build_indexer_with_persisted_state(&entry.id, entry.root_path.clone(), embedder).await;
let include_paths: Vec<std::path::PathBuf> = entry
.include_paths
.iter()
.filter(|p| !p.trim().is_empty() && p.trim() != ".")
.map(|p| entry.root_path.join(p.trim()))
.collect();
let extensions: Vec<String> = entry
.extensions
.iter()
.map(|e| e.trim_start_matches('.').to_string())
.filter(|e| !e.is_empty())
.collect();
indexer.set_domain_terms(entry.domain_terms.clone());
let handle = IndexHandle {
id: id.clone(),
indexer: Arc::new(tokio::sync::RwLock::new(indexer)),
root_path: entry.root_path,
include_paths,
exclude_globs: entry.exclude_globs,
extensions,
domain_terms: entry.domain_terms,
path_filter: entry.path_filter,
context_embedding: Arc::new(tokio::sync::RwLock::new(None)),
context_summary: Arc::new(tokio::sync::RwLock::new(None)),
};
state.registry.register(handle);
}
}
async fn build_embedder() -> Result<std::sync::Arc<dyn crate::core::Embedder>> {
let embedder = crate::core::FastEmbedder::new().await.map_err(|e| {
tracing::error!("FastEmbedder init failed: {e:#}");
anyhow::anyhow!("FastEmbedder init failed: {e}")
})?;
let dim = <crate::core::FastEmbedder as crate::core::Embedder>::dimension(&embedder);
let provider = embedder.provider();
let metal_hint = match provider {
trusty_common::embedder::ExecutionProvider::CoreML => " (Metal GPU / ANE)",
trusty_common::embedder::ExecutionProvider::Cuda => " (CUDA GPU)",
trusty_common::embedder::ExecutionProvider::Cpu => "",
};
tracing::info!(
"embedder initialized: model=AllMiniLML6V2(Q) dim={dim} provider={provider}{metal_hint}"
);
tune_batch_size_for_provider(provider);
Ok(std::sync::Arc::new(embedder))
}
fn tune_batch_size_for_provider(provider: trusty_common::embedder::ExecutionProvider) {
const GPU_BATCH_DEFAULT: usize = 512;
let is_gpu = matches!(
provider,
trusty_common::embedder::ExecutionProvider::Cuda
| trusty_common::embedder::ExecutionProvider::CoreML
);
if !is_gpu {
return;
}
if std::env::var("TRUSTY_MAX_BATCH_SIZE_EXPLICIT")
.map(|v| v == "1")
.unwrap_or(false)
{
tracing::info!(
"gpu_batch_tuning: TRUSTY_MAX_BATCH_SIZE_EXPLICIT=1 set, leaving batch size unchanged"
);
return;
}
let current = std::env::var("TRUSTY_MAX_BATCH_SIZE")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(128);
if current >= GPU_BATCH_DEFAULT {
return;
}
unsafe {
std::env::set_var("TRUSTY_MAX_BATCH_SIZE", GPU_BATCH_DEFAULT.to_string());
}
tracing::info!(
"gpu_batch_tuning: provider={provider} → TRUSTY_MAX_BATCH_SIZE={GPU_BATCH_DEFAULT} (was {current}); \
set TRUSTY_MAX_BATCH_SIZE_EXPLICIT=1 to keep your value"
);
}
pub async fn handle_start(port: u16, foreground: bool, device: &str) -> Result<()> {
if !foreground {
match device {
"auto" | "cpu" | "gpu" => {}
other => {
anyhow::bail!("invalid --device value '{other}'; expected one of: auto, cpu, gpu")
}
}
let exe = std::env::current_exe()
.map_err(|e| anyhow::anyhow!("could not resolve current_exe: {e}"))?;
let mut cmd = std::process::Command::new(&exe);
cmd.arg("start")
.arg("--foreground")
.arg("--port")
.arg(port.to_string())
.arg("--device")
.arg(device)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
let child = cmd
.spawn()
.map_err(|e| anyhow::anyhow!("could not spawn detached daemon: {e}"))?;
let pid = child.id();
eprintln!(
"{} Daemon starting in background (pid {pid}). Run `trusty-search status` to verify readiness.",
"✓".green()
);
return Ok(());
}
if std::env::var_os("TRUSTY_DEVICE").is_none() {
match device {
"cpu" | "gpu" => unsafe {
std::env::set_var("TRUSTY_DEVICE", device);
},
"auto" => {}
other => {
anyhow::bail!("invalid --device value '{other}'; expected one of: auto, cpu, gpu")
}
}
}
crate::service::save_daemon_env();
crate::service::load_daemon_env();
let policy = crate::core::MemoryPolicy::detect();
const MIN_RAM_MB: u64 = 16 * 1024;
if policy.total_ram_mb < MIN_RAM_MB {
anyhow::bail!(
"trusty-search requires at least 16 GB of RAM.\n\
Detected: {} MB ({:.1} GB)\n\
Indexing large codebases on machines with less memory is not supported.",
policy.total_ram_mb,
policy.total_ram_mb as f64 / 1024.0
);
}
policy.log_summary();
let _ = foreground;
if let Some(pid) = crate::service::running_daemon_pid() {
tracing::warn!("daemon already running (pid {pid}); refusing to start a second instance");
anyhow::bail!(
"Daemon already running (pid {pid}).\n\
Stop it first with `trusty-search stop`, then re-run `trusty-search start`.\n\
Replacing the binary while the daemon is running causes macOS to SIGKILL \
the process (Code Signature Invalid)."
);
}
let orphans = crate::commands::stop::find_daemon_pids();
if !orphans.is_empty() {
tracing::warn!(
"found {} existing trusty-search daemon process(es) not tracked by lockfile: {:?} — terminating before start",
orphans.len(),
orphans
);
eprintln!(
"{} found {} existing trusty-search daemon process(es) not tracked by lockfile — stopping them first",
"⚠".yellow(),
orphans.len()
);
#[cfg(unix)]
for pid in &orphans {
let _ = nix::sys::signal::kill(
nix::unistd::Pid::from_raw(*pid as i32),
nix::sys::signal::Signal::SIGTERM,
);
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
#[cfg(unix)]
let any_alive = orphans.iter().any(|p| {
nix::sys::signal::kill(nix::unistd::Pid::from_raw(*p as i32), None).is_ok()
});
#[cfg(not(unix))]
let any_alive = false;
if !any_alive || std::time::Instant::now() >= deadline {
break;
}
}
#[cfg(unix)]
for pid in &orphans {
if nix::sys::signal::kill(nix::unistd::Pid::from_raw(*pid as i32), None).is_ok() {
tracing::warn!("orphan pid {pid} ignored SIGTERM — sending SIGKILL");
let _ = nix::sys::signal::kill(
nix::unistd::Pid::from_raw(*pid as i32),
nix::sys::signal::Signal::SIGKILL,
);
}
}
if let Ok(lock) = crate::service::daemon_lock_path() {
let _ = std::fs::remove_file(&lock);
}
if let Some(port) = super::daemon_utils::daemon_port_path() {
let _ = std::fs::remove_file(&port);
}
}
let cfg = crate::service::load_user_config();
let state = crate::service::SearchAppState::new(crate::core::registry::IndexRegistry::new())
.with_local_model(cfg.local_model)
.with_openrouter_model(cfg.openrouter_model)
.with_openrouter_api_key(cfg.openrouter_api_key);
let init_timeout_secs: u64 = std::env::var("TRUSTY_EMBEDDER_INIT_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(60);
let install_state = state.clone();
tokio::spawn(async move {
let runtime_handle = tokio::runtime::Handle::current();
let init_handle = tokio::task::spawn_blocking(move || {
runtime_handle.block_on(build_embedder())
});
let init_result =
tokio::time::timeout(Duration::from_secs(init_timeout_secs), init_handle).await;
match init_result {
Ok(Ok(Ok(embedder))) => {
install_state.install_embedder(Arc::clone(&embedder)).await;
tracing::info!("embedder ready — vector lane online");
restore_indexes(&install_state, &embedder).await;
}
Ok(Ok(Err(e))) => {
let msg = format!("FastEmbedder init failed: {e}");
install_state.install_embedder_error(msg.clone()).await;
eprintln!(
"{} embedder failed to initialize: {e}\n\
Daemon is up but running BM25-only. Check the model cache at \
~/Library/Caches/trusty-search/models/ and network access.",
"✗".red()
);
}
Ok(Err(join_err)) => {
let msg = format!("embedder init task panicked: {join_err}");
install_state.install_embedder_error(msg).await;
}
Err(_elapsed) => {
tracing::error!(
"embedder init timed out after {init_timeout_secs}s — \
ONNX session did not start (try \
TRUSTY_EMBEDDER_INIT_TIMEOUT_SECS=120 or check ORT \
compatibility, see GitHub #121)"
);
let msg = format!("init timed out after {init_timeout_secs}s");
install_state.install_embedder_error(msg).await;
eprintln!(
"{} embedder init timed out after {init_timeout_secs}s — \
see GitHub #121. Daemon is up in BM25-only mode.",
"✗".red()
);
}
}
});
match crate::service::run_daemon(state, port).await {
Ok(()) => {}
Err(crate::service::DaemonError::AlreadyRunning(p)) => {
tracing::debug!(
"daemon already running (lock at {}), exiting non-zero to stop launchd respawn",
p.display()
);
std::process::exit(1);
}
Err(e) => anyhow::bail!("daemon failed: {e}"),
}
Ok(())
}