use anyhow::Result;
use colored::Colorize;
use std::sync::Arc;
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,
};
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_embedder::ExecutionProvider::CoreML => " (Metal GPU / ANE)",
trusty_embedder::ExecutionProvider::Cuda => " (CUDA GPU)",
trusty_embedder::ExecutionProvider::Cpu => "",
};
tracing::info!(
"embedder initialized: model=AllMiniLML6V2(Q) dim={dim} provider={provider}{metal_hint}"
);
Ok(std::sync::Arc::new(embedder))
}
pub async fn handle_start(port: u16, foreground: bool) -> Result<()> {
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 {
eprintln!(
"error: 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
);
std::process::exit(1);
}
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");
eprintln!(
"{} 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).",
"✗".red()
);
std::process::exit(1);
}
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()
);
for pid in &orphans {
#[cfg(unix)]
unsafe {
libc::kill(*pid as libc::pid_t, libc::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| unsafe { libc::kill(*p as libc::pid_t, 0) } == 0);
#[cfg(not(unix))]
let any_alive = false;
if !any_alive || std::time::Instant::now() >= deadline {
break;
}
}
#[cfg(unix)]
for pid in &orphans {
if unsafe { libc::kill(*pid as libc::pid_t, 0) } == 0 {
tracing::warn!("orphan pid {pid} ignored SIGTERM — sending SIGKILL");
unsafe {
libc::kill(*pid as libc::pid_t, libc::SIGKILL);
}
}
}
if let Ok(lock) = crate::service::daemon_lock_path() {
let _ = std::fs::remove_file(&lock);
}
if let Some(port) = crate::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 install_state = state.clone();
tokio::spawn(async move {
match build_embedder().await {
Ok(embedder) => {
install_state.install_embedder(Arc::clone(&embedder)).await;
tracing::info!("embedder ready — vector lane online");
restore_indexes(&install_state, &embedder).await;
}
Err(e) => {
tracing::error!(
"embedder failed to initialize: {e:#} — daemon will continue in BM25-only mode"
);
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()
);
}
}
});
match crate::service::run_daemon(state, port).await {
Ok(()) => {}
Err(crate::service::DaemonError::AlreadyRunning(p)) => {
tracing::info!(
"daemon already running (lock at {}), exiting cleanly",
p.display()
);
eprintln!(
"{} trusty-search daemon already running (lock at {}); nothing to do",
"✓".green(),
p.display()
);
return Ok(());
}
Err(e) => {
eprintln!("{} daemon failed: {e}", "✗".red());
std::process::exit(1);
}
}
Ok(())
}