use super::{Capabilities, ErrorKind, ProviderError, ProviderResult};
use serde_json::{Value, json};
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
mod transport;
mod wire;
#[cfg(test)]
mod tests;
use transport::{Process, Shared, StderrMode, backoff_for, reader_loop, spawn_process};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(30_000);
pub struct StdioProvider {
name: String,
capabilities: Capabilities,
process: Mutex<Process>,
shared: Arc<Mutex<Shared>>,
current_id: AtomicU64,
timeout: Duration,
command: Vec<String>,
env: Vec<(String, String)>,
stderr_mode: StderrMode,
reader: Mutex<Option<JoinHandle<()>>>,
notice: Mutex<Option<String>>,
}
impl Drop for StdioProvider {
fn drop(&mut self) {
if let Ok(process) = self.process.get_mut() {
let _ = process.child.kill();
let _ = process.child.wait();
}
if let Ok(mut reader) = self.reader.lock()
&& let Some(handle) = reader.take()
{
let _ = handle.join();
}
}
}
impl StdioProvider {
pub fn spawn(command: &[String], timeout: Duration) -> ProviderResult<Self> {
Self::spawn_inner(command, timeout, &[], StderrMode::Null)
}
pub fn spawn_with_stderr(
command: &[String],
timeout: Duration,
inherit_stderr: bool,
) -> ProviderResult<Self> {
let mode = if inherit_stderr {
StderrMode::Inherit
} else {
StderrMode::Null
};
Self::spawn_inner(command, timeout, &[], mode)
}
#[cfg(test)]
fn spawn_with_env(
command: &[String],
timeout: Duration,
env: &[(&str, &str)],
) -> ProviderResult<Self> {
Self::spawn_inner(command, timeout, env, StderrMode::Null)
}
fn spawn_inner(
command: &[String],
timeout: Duration,
env: &[(&str, &str)],
stderr_mode: StderrMode,
) -> ProviderResult<Self> {
let (process, stdout) = spawn_process(command, env, stderr_mode)?;
let shared = Arc::new(Mutex::new(Shared::default()));
let reader = std::thread::spawn({
let shared = Arc::clone(&shared);
move || reader_loop(stdout, shared)
});
let provider = StdioProvider {
name: "stdio".into(),
capabilities: Capabilities {
orgs: true,
code_search: true,
},
process: Mutex::new(process),
shared,
current_id: AtomicU64::new(0),
timeout,
command: command.to_vec(),
env: env
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
stderr_mode,
reader: Mutex::new(Some(reader)),
notice: Mutex::new(None),
};
let reply = provider.handshake()?;
provider.initialize_from(reply)
}
fn handshake(&self) -> ProviderResult<Value> {
let reply = self.round_trip("initialize", json!({ "protocol": 1 }))?;
let protocol = reply.get("protocol").and_then(Value::as_u64).unwrap_or(1);
if protocol != 1 {
return Err(ProviderError::new(
ErrorKind::Provider,
format!("unsupported provider protocol {protocol}"),
));
}
Ok(reply)
}
fn initialize_from(mut self, reply: Value) -> ProviderResult<Self> {
if let Some(name) = reply.get("name").and_then(Value::as_str) {
self.name = format!("stdio:{name}");
}
if let Some(caps) = reply.get("capabilities") {
self.capabilities = Capabilities {
orgs: caps.get("orgs").and_then(Value::as_bool).unwrap_or(true),
code_search: caps
.get("code_search")
.and_then(Value::as_bool)
.unwrap_or(true),
};
}
Ok(self)
}
fn request(&self, method: &str, params: Value) -> ProviderResult<Value> {
self.ensure_alive()?;
self.round_trip(method, params)
}
fn ensure_alive(&self) -> ProviderResult<()> {
if !self.shared.lock().expect("provider shared poisoned").dead {
return Ok(());
}
self.respawn()
}
fn respawn(&self) -> ProviderResult<()> {
let mut shared = self.shared.lock().expect("provider shared poisoned");
if !shared.dead {
return Ok(()); }
let attempt = shared.restarts + 1;
std::thread::sleep(backoff_for(attempt));
if let Some(handle) = self.reader.lock().expect("reader poisoned").take() {
let _ = handle.join();
}
let env: Vec<(&str, &str)> = self
.env
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let (process, stdout) = spawn_process(&self.command, &env, self.stderr_mode)?;
let reader = std::thread::spawn({
let shared = Arc::clone(&self.shared);
move || reader_loop(stdout, shared)
});
*self.process.lock().expect("provider process poisoned") = process;
*self.reader.lock().expect("reader poisoned") = Some(reader);
shared.dead = false;
shared.restarts = attempt;
drop(shared);
if let Err(e) = self.handshake() {
self.shared.lock().expect("provider shared poisoned").dead = true;
return Err(e);
}
*self.notice.lock().expect("notice poisoned") = Some(format!(
"provider restarted (attempt {attempt}, backoff {}s)",
backoff_for(attempt).as_secs()
));
Ok(())
}
}