Skip to main content

rlean_search/
daemon.rs

1//! Tokio multi-client daemon serving JSONL / XML type search.
2
3use crate::index::{build_index, shared_index, SearchIndex, SharedIndex};
4use crate::protocol::{format_response, parse_request, ProtocolKind, Request, Response};
5use anyhow::{Context, Result};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
9use tokio::net::{TcpListener, TcpStream};
10pub struct DaemonConfig {
11    pub bind: String,
12    pub paths: Vec<PathBuf>,
13    pub cache_path: Option<PathBuf>,
14}
15
16pub async fn run_daemon(cfg: DaemonConfig) -> Result<()> {
17    let idx = if let Some(cache) = &cfg.cache_path {
18        crate::cache::load_or_build(&cfg.paths, cache, false)?
19    } else {
20        build_index(&cfg.paths)?
21    };
22    tracing::info!(
23        "indexed {} declarations from {} package(s)",
24        idx.len(),
25        idx.doc.packages.len()
26    );
27    let shared = shared_index(idx);
28    let paths = Arc::new(cfg.paths.clone());
29    let cache_path = cfg.cache_path.clone();
30    let listener = TcpListener::bind(&cfg.bind)
31        .await
32        .with_context(|| format!("bind {}", cfg.bind))?;
33    tracing::info!("rlean-search daemon listening on {}", cfg.bind);
34
35    loop {
36        let (socket, peer) = listener.accept().await?;
37        tracing::debug!("connection from {peer}");
38        let shared = Arc::clone(&shared);
39        let paths = Arc::clone(&paths);
40        let cache_path = cache_path.clone();
41        tokio::spawn(async move {
42            if let Err(e) = handle_client(socket, shared, paths, cache_path).await {
43                tracing::debug!("client error: {e}");
44            }
45        });
46    }
47}
48
49async fn handle_client(
50    socket: TcpStream,
51    shared: SharedIndex,
52    paths: Arc<Vec<PathBuf>>,
53    cache_path: Option<PathBuf>,
54) -> Result<()> {
55    let (reader, mut writer) = socket.into_split();
56    let mut lines = BufReader::new(reader).lines();
57
58    while let Some(line) = lines.next_line().await? {
59        if line.trim().is_empty() {
60            continue;
61        }
62        let (kind, req) = match parse_request(&line) {
63            Ok(v) => v,
64            Err(e) => {
65                let resp = Response::Error { message: e };
66                writer
67                    .write_all(format_response(ProtocolKind::detect(&line), &resp).as_bytes())
68                    .await?;
69                continue;
70            }
71        };
72        let resp = process_request(&req, &shared, &paths, cache_path.as_deref()).await;
73        writer
74            .write_all(format_response(kind, &resp).as_bytes())
75            .await?;
76    }
77    Ok(())
78}
79
80async fn process_request(
81    req: &Request,
82    shared: &SharedIndex,
83    paths: &[PathBuf],
84    cache_path: Option<&Path>,
85) -> Response {
86    match req {
87        Request::Ping {} => Response::Pong {},
88        Request::Stats {} => {
89            let idx = shared.read();
90            Response::Stats {
91                declarations: idx.len(),
92                packages: idx.doc.packages.len(),
93                source_hash: idx.doc.source_hash.clone(),
94            }
95        }
96        Request::Search { pattern, limit } => {
97            let idx = shared.read();
98            match idx.search(pattern, *limit) {
99                Ok(hits) => Response::Search {
100                    pattern: pattern.clone(),
101                    count: hits.len(),
102                    hits,
103                },
104                Err(e) => Response::Error {
105                    message: e.to_string(),
106                },
107            }
108        }
109        Request::Reload {} => {
110            let paths = paths.to_vec();
111            let cache_path = cache_path.map(|p| p.to_path_buf());
112            let result = tokio::task::spawn_blocking(move || {
113                if let Some(c) = &cache_path {
114                    crate::cache::load_or_build(&paths, c, true)
115                } else {
116                    build_index(&paths)
117                }
118            })
119            .await;
120            match result {
121                Ok(Ok(new_idx)) => {
122                    let n = new_idx.len();
123                    *shared.write() = new_idx;
124                    Response::Ok {
125                        message: format!("reloaded {n} declarations"),
126                    }
127                }
128                Ok(Err(e)) => Response::Error {
129                    message: e.to_string(),
130                },
131                Err(e) => Response::Error {
132                    message: e.to_string(),
133                },
134            }
135        }
136    }
137}
138
139/// One-shot TCP client helper used by CLI `query` against a running daemon.
140pub async fn client_query(addr: &str, line: &str) -> Result<String> {
141    let mut stream = TcpStream::connect(addr)
142        .await
143        .with_context(|| format!("connect {addr}"))?;
144    stream.write_all(line.as_bytes()).await?;
145    if !line.ends_with('\n') {
146        stream.write_all(b"\n").await?;
147    }
148    let mut reader = BufReader::new(stream);
149    let mut response = String::new();
150    reader.read_line(&mut response).await?;
151    // XML responses may be multi-line; read until we have a full root element or JSON line done
152    if response.trim_start().starts_with('<') && !response.contains("</rlean:response>") && !response.trim_end().ends_with("/>")
153    {
154        loop {
155            let mut more = String::new();
156            let n = reader.read_line(&mut more).await?;
157            if n == 0 {
158                break;
159            }
160            response.push_str(&more);
161            if response.contains("</rlean:response>") {
162                break;
163            }
164        }
165    }
166    Ok(response)
167}
168
169/// Process a single request against an in-memory index (no network).
170pub fn local_request(idx: &SearchIndex, line: &str) -> String {
171    match parse_request(line) {
172        Ok((kind, req)) => {
173            let resp = match req {
174                Request::Ping {} => Response::Pong {},
175                Request::Stats {} => Response::Stats {
176                    declarations: idx.len(),
177                    packages: idx.doc.packages.len(),
178                    source_hash: idx.doc.source_hash.clone(),
179                },
180                Request::Search { pattern, limit } => match idx.search(&pattern, limit) {
181                    Ok(hits) => Response::Search {
182                        pattern,
183                        count: hits.len(),
184                        hits,
185                    },
186                    Err(e) => Response::Error {
187                        message: e.to_string(),
188                    },
189                },
190                Request::Reload {} => Response::Error {
191                    message: "reload not supported in one-shot mode".into(),
192                },
193            };
194            format_response(kind, &resp)
195        }
196        Err(e) => format_response(
197            ProtocolKind::detect(line),
198            &Response::Error { message: e },
199        ),
200    }
201}