use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use colored::Colorize;
use super::embedder::build_embedder;
use super::restore::restore_indexes;
use crate::commands::prior_index_count::load_prior_index_count;
pub async fn handle_start(
port: u16,
foreground: bool,
device: &str,
data_dir: Option<&std::path::Path>,
verbose: bool,
no_auto_discover: bool,
) -> Result<()> {
if std::env::var_os("TRUSTY_DATA_DIR").is_none() {
if let Some(dir) = data_dir {
let dir = dir.to_path_buf();
anyhow::ensure!(
dir.is_absolute(),
"--data-dir must be an absolute path (got: {})",
dir.display()
);
std::fs::create_dir_all(&dir)
.with_context(|| format!("create --data-dir directory: {}", dir.display()))?;
if std::fs::read_dir(&dir)
.map(|mut d| d.next().is_none())
.unwrap_or(false)
{
tracing::warn!(
"--data-dir {} is empty (no existing indexes); daemon will start fresh",
dir.display()
);
}
unsafe {
std::env::set_var("TRUSTY_DATA_DIR", &dir);
}
tracing::info!("data-dir override: {}", dir.display());
}
}
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);
if no_auto_discover {
cmd.arg("--no-auto-discover");
}
if let Some(dir) = data_dir {
cmd.arg("--data-dir").arg(dir);
}
cmd.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(());
}
let (log_buffer, _error_store) = trusty_common::init_tracing_with_buffer_and_capture(
if verbose { 2 } else { 0 },
trusty_common::log_buffer::DEFAULT_LOG_CAPACITY,
"trusty-search",
env!("CARGO_PKG_VERSION"),
);
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();
crate::commands::startup_checks::warn_if_stale_cpu_device_on_apple_silicon();
let policy = crate::core::MemoryPolicy::detect();
const MIN_RAM_MB: u64 = 16 * 1024;
if policy.total_ram_mb < MIN_RAM_MB
&& std::env::var("TRUSTY_SKIP_RAM_CHECK").as_deref() != Ok("1")
{
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.\n\
To bypass on this host set TRUSTY_SKIP_RAM_CHECK=1 in the daemon environment.",
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::super::daemon_utils::daemon_port_path() {
let _ = std::fs::remove_file(&port);
}
}
let cfg = crate::service::load_user_config();
let metrics_state = match crate::service::metrics::install_recorder() {
Ok(state) => Some(state),
Err(e) => {
tracing::warn!("could not install prometheus recorder: {e} (metrics disabled)");
None
}
};
let mut 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)
.with_log_buffer(log_buffer);
if let Some(m) = metrics_state {
state = state.with_metrics(m);
}
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, pid_slot)))) => {
if let Some(slot) = pid_slot {
install_state.install_embedderd_pid_slot(slot).await;
}
install_state.install_embedder(Arc::clone(&embedder)).await;
let tracker = Arc::clone(&install_state.embedder_stall_tracker);
use crate::service::embed_pool::EmbedPool;
let base = EmbedPool::with_autotune(Arc::clone(&embedder));
let pool = std::sync::Arc::new(base.with_stall_tracker(tracker));
install_state.install_embed_pool(pool).await;
tracing::info!("embedder ready — vector lane online");
let prior = load_prior_index_count(); install_state
.prior_index_count
.store(prior, std::sync::atomic::Ordering::Relaxed);
restore_indexes(&install_state, &embedder).await;
if std::env::var("TRUSTY_DISABLE_MIGRATIONS").as_deref() != Ok("1") {
let registry =
std::sync::Arc::new(crate::core::migration::MigrationRegistry::new());
for index_id in install_state.registry.list() {
let Some(handle) = install_state.registry.get(&index_id) else {
continue;
};
let reg = std::sync::Arc::clone(®istry);
tokio::spawn(async move {
if let Err(e) =
crate::core::migration::run_migrations(&handle, ®).await
{
tracing::warn!(
index_id = %handle.id,
"schema migration failed (index kept at current schema): {e:#}"
);
}
});
}
}
crate::service::metrics::set_index_count(install_state.registry.list().len());
if !no_auto_discover {
tokio::spawn(crate::commands::discover::auto_discover_and_index());
} else {
tracing::info!(
"auto-discover: disabled via --no-auto-discover / TRUSTY_NO_AUTO_DISCOVER"
);
}
}
Ok(Ok(Err(e))) => {
let msg = format!("embedder 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()
);
}
}
});
{
let update_available = state.update_available.clone();
tokio::spawn(async move {
let crate_name = env!("CARGO_PKG_NAME");
let current = env!("CARGO_PKG_VERSION");
if let Some(info) = trusty_common::update::check_throttled(crate_name, current).await {
tracing::info!(
latest = %info.latest,
"update available: {}",
trusty_common::update::notice(&info)
);
eprintln!("{}", trusty_common::update::notice(&info));
if let Ok(mut guard) = update_available.lock() {
*guard = Some(info.latest);
}
}
});
}
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(())
}