1use 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
139pub 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 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
169pub 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}