comindly_engine/
indexer.rs1use std::collections::BTreeMap;
4use std::process::Stdio;
5use std::sync::Arc;
6
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
10
11use crate::context::{Cancelled, IndexContext, Settings};
12use crate::events::{Event, EventSink, Kind, Level};
13
14pub const COMMAND: &str = "index-job";
16
17#[derive(Serialize, Deserialize)]
19pub struct Spec {
20 pub root: String,
21 pub repos: Vec<crate::config::RepoEntry>,
22 pub llm: bool,
23 pub llm_key: Option<String>,
24 pub git: BTreeMap<String, String>,
26 #[serde(default)]
28 pub rewrite: Vec<crate::prompts::Task>,
29 #[serde(default)]
31 pub paths: Vec<String>,
32}
33
34pub async fn run(spec: &Spec, job: Arc<crate::jobs::Running>) -> Result<()> {
36 let mut cmd = tokio::process::Command::new(std::env::current_exe()?);
37 cmd.arg(COMMAND)
38 .stdin(Stdio::piped())
39 .stdout(Stdio::piped())
40 .stderr(Stdio::piped())
41 .kill_on_drop(true);
42 #[cfg(unix)]
43 cmd.process_group(0);
44 let mut child = cmd.spawn().context("start the indexer")?;
45 let pid = child.id();
46 let mut stdin = child.stdin.take().context("indexer stdin")?;
48 stdin.write_all(&serde_json::to_vec(spec)?).await?;
49 stdin.write_all(b"\n").await?;
50 let out = child.stdout.take().context("indexer stdout")?;
51 let err = child.stderr.take().context("indexer stderr")?;
52 let events = async {
53 let mut failure = None;
54 let mut lines = BufReader::new(out).lines();
55 while let Ok(Some(l)) = lines.next_line().await {
56 match serde_json::from_str::<Event>(&l) {
57 Ok(e) => {
58 if e.level == Level::Error {
59 failure = Some(e.message.clone());
60 }
61 job.emit(e);
62 }
63 Err(_) if !l.trim().is_empty() => job.emit(Event::new(Level::Info, Kind::Note, l)),
64 Err(_) => {}
65 }
66 }
67 failure
68 };
69 let logs = async {
70 let mut lines = BufReader::new(err).lines();
71 while let Ok(Some(l)) = lines.next_line().await {
72 crate::telemetry::write(&l);
73 }
74 };
75 let exit = async {
76 tokio::select! {
77 s = child.wait() => s,
78 _ = job.cancel.cancelled() => {
79 signal(pid, Signal::Term);
80 match tokio::time::timeout(std::time::Duration::from_secs(60), child.wait()).await {
81 Ok(s) => s,
82 Err(_) => {
83 signal(pid, Signal::Kill);
84 child.wait().await
85 }
86 }
87 }
88 }
89 };
90 let (failure, (), status) = tokio::join!(events, logs, exit);
91 drop(stdin);
92 let status = status.context("wait for the indexer")?;
93 if status.success() {
94 return Ok(());
95 }
96 if job.cancel.is_cancelled() {
97 return Err(Cancelled.into());
98 }
99 Err(anyhow::anyhow!(failure.unwrap_or_else(|| stopped(&status))))
100}
101
102fn stopped(status: &std::process::ExitStatus) -> String {
103 #[cfg(unix)]
104 if std::os::unix::process::ExitStatusExt::signal(status) == Some(9) {
105 return "the indexer was killed, most likely for running out of memory".into();
106 }
107 format!("the indexer stopped ({status})")
108}
109
110enum Signal {
111 Term,
112 Kill,
113}
114
115fn signal(pid: Option<u32>, sig: Signal) {
117 #[cfg(unix)]
118 if let Some(pid) = pid.and_then(|p| i32::try_from(p).ok()) {
119 let (to, sig) = match sig {
120 Signal::Term => (pid, libc::SIGTERM),
121 Signal::Kill => (-pid, libc::SIGKILL),
122 };
123 unsafe {
125 libc::kill(to, sig);
126 }
127 }
128 #[cfg(not(unix))]
129 let _ = (pid, sig);
130}
131
132struct Stdout;
134
135impl EventSink for Stdout {
136 fn emit(&self, e: Event) {
137 use std::io::Write;
138 if let Ok(line) = serde_json::to_string(&e) {
139 let mut out = std::io::stdout().lock();
140 let _ = writeln!(out, "{line}");
141 let _ = out.flush();
142 }
143 }
144}
145
146pub fn child() -> Result<()> {
148 crate::rt::shared().block_on(async {
149 let mut stdin = BufReader::new(tokio::io::stdin());
150 let mut line = String::new();
151 stdin.read_line(&mut line).await?;
152 let spec: Spec = serde_json::from_str(&line).context("read the index job")?;
153 let team = crate::team::load(&spec.root)
154 .await
155 .map(|t| t.settings)
156 .unwrap_or_default();
157 let mut settings = Settings::resolve(&team, crate::config::env);
158 settings.rewrite = spec.rewrite.iter().copied().collect();
159 if !spec.llm {
160 settings.enrich = false;
161 settings.flows = false;
162 settings.guide = false;
163 }
164 let client = if settings.enrich || settings.flows || settings.guide {
165 crate::llm::LlmClient::new(
166 settings.llm_base_url.as_deref(),
167 &settings.llm_model,
168 spec.llm_key.as_deref(),
169 )
170 .ok()
171 .map(|c| c.with_models(settings.task_models.clone()))
172 } else {
173 None
174 };
175 let scip = settings.scip;
176 let git = spec.git;
177 let creds = move |host: &str| {
178 git.get(&host.to_lowercase())
179 .cloned()
180 .or_else(|| crate::config::env_token(host))
181 };
182 let ctx = IndexContext::new(
183 spec.root,
184 settings,
185 Arc::new(Stdout),
186 crate::config::cache_dir(),
187 )
188 .with_llm(client)
189 .with_git(Arc::new(creds));
190 let cancel = ctx.cancel.clone();
191 tokio::spawn(async move {
192 let mut rest = Vec::new();
193 #[cfg(unix)]
194 {
195 use tokio::signal::unix::{signal, SignalKind};
196 let mut term = signal(SignalKind::terminate()).ok();
197 tokio::select! {
198 _ = async { match term.as_mut() { Some(t) => { t.recv().await; } None => std::future::pending().await } } => {}
199 _ = stdin.read_to_end(&mut rest) => {}
200 }
201 }
202 #[cfg(not(unix))]
203 let _ = stdin.read_to_end(&mut rest).await;
204 cancel.cancel();
205 });
206 let run = async {
207 let sources = if spec.paths.is_empty() {
208 crate::pipeline::checkout(&ctx, &crate::config::expanded(&spec.repos)).await?
209 } else {
210 spec.paths.iter().map(crate::pipeline::Source::at).collect()
211 };
212 crate::pipeline::link(&ctx, sources, true, scip).await
213 };
214 match run.await {
215 Ok(_) => Ok(()),
216 Err(e) if e.is::<Cancelled>() => {
217 ctx.report.warn("Cancelled; what was already saved stays.");
218 Err(e)
219 }
220 Err(e) => {
221 ctx.report.error(crate::storage::explain(&format!("{e:#}")));
222 Err(e)
223 }
224 }
225 })
226}