use std::io::{BufRead, BufReader, Read, Write};
use std::process::{Child, ChildStderr, Command, Stdio};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{Sender, channel};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use onetaskgraph_plugin_api::SourceError;
use serde_json::Value;
use super::wire::{Request, Response};
const QUOTED: usize = 200;
pub const MAX_LINE: u64 = 16 * 1024 * 1024;
const KEPT_DIAGNOSTICS: usize = 4096;
pub(crate) enum Line {
Read(String),
Ended,
TooLong,
Failed(std::io::Error),
}
pub(crate) fn read_line(reader: &mut (impl BufRead + ?Sized)) -> Line {
let mut line = String::new();
match reader.take(MAX_LINE).read_line(&mut line) {
Err(error) => Line::Failed(error),
Ok(0) => Line::Ended,
Ok(_) if !line.ends_with('\n') => Line::TooLong,
Ok(_) => Line::Read(line),
}
}
pub(crate) struct Connection {
jobs: Mutex<Option<Sender<Job>>>,
diagnostics: Arc<Mutex<String>>,
next_id: AtomicU64,
child: Arc<Mutex<Option<Child>>>,
deadline: Duration,
}
struct Job {
line: String,
slot: Arc<Slot>,
}
impl Connection {
pub(crate) fn adopt(peer: Peer) -> Self {
let Peer {
child,
mut writer,
mut reader,
stderr,
request_deadline,
handshake_deadline: _,
} = peer;
let diagnostics = Arc::new(Mutex::new(String::new()));
if let Some(stderr) = stderr {
drain(stderr, Arc::clone(&diagnostics));
}
let (sender, receiver) = channel::<Job>();
std::thread::spawn(move || {
for job in &receiver {
let answer = exchange(&mut writer, &mut reader, &job.line);
let fatal = answer.is_err();
job.slot.fill(answer);
if fatal {
break;
}
}
for job in receiver.try_iter() {
job.slot.fill(Err(SourceError::Unavailable {
message: "the plugin connection closed before this request was sent".to_owned(),
}));
}
});
Self {
jobs: Mutex::new(Some(sender)),
diagnostics,
next_id: AtomicU64::new(1),
child,
deadline: request_deadline,
}
}
pub(crate) async fn call(&self, method: &str, params: Value) -> Result<Value, SourceError> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed).to_string();
let request = Request {
id: id.clone(),
method: method.to_owned(),
params,
};
let line = serde_json::to_string(&request).expect("a request is plain data");
let slot = Arc::new(Slot::empty());
self.dispatch(Job {
line,
slot: Arc::clone(&slot),
})?;
let timed = Arc::clone(&slot);
let child = Arc::clone(&self.child);
let deadline = self.deadline;
let timed_method = method.to_owned();
std::thread::spawn(move || {
std::thread::sleep(deadline);
let expired = timed.fill_if_empty(Err(SourceError::Unavailable {
message: format!(
"the plugin did not answer {timed_method:?} within {} milliseconds",
deadline.as_millis()
),
}));
if expired
&& let Ok(mut child) = child.lock()
&& let Some(child) = child.as_mut()
{
let _ = child.kill();
}
});
let answer = Answer { slot }.await?;
self.interpret(&id, &answer)
}
fn dispatch(&self, job: Job) -> Result<(), SourceError> {
let mut jobs = self
.jobs
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let Some(sender) = jobs.as_ref() else {
return Err(self.closed());
};
if sender.send(job).is_err() {
*jobs = None;
return Err(self.closed());
}
Ok(())
}
fn closed(&self) -> SourceError {
SourceError::Unavailable {
message: format!("the plugin stopped answering{}", self.said()),
}
}
fn said(&self) -> String {
let diagnostics = self
.diagnostics
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let said = diagnostics.trim();
if said.is_empty() {
String::new()
} else {
format!("; it wrote: {said}")
}
}
fn interpret(&self, id: &str, line: &str) -> Result<Value, SourceError> {
let response: Response = serde_json::from_str(line).map_err(|error| {
self.violation(
format!("the plugin answered with a line that is not a response envelope: {error}"),
line,
)
})?;
if response.id != id {
return Err(self.violation(
format!(
"the plugin answered request {id:?} with an envelope addressed to {:?}",
response.id
),
line,
));
}
match response.outcome() {
Some(outcome) => outcome,
None => Err(self.violation(
"the plugin answered with an envelope carrying both a result and an error, \
or neither"
.to_owned(),
line,
)),
}
}
fn violation(&self, problem: String, line: &str) -> SourceError {
SourceError::Malformed {
message: format!("{problem}: {}{}", quoted(line), self.said()),
}
}
}
impl Drop for Connection {
fn drop(&mut self) {
if let Ok(mut jobs) = self.jobs.lock() {
*jobs = None;
}
if let Ok(mut child) = self.child.lock()
&& let Some(child) = child.as_mut()
{
let _ = child.kill();
let _ = child.wait();
}
}
}
pub(crate) struct Peer {
pub(crate) child: Arc<Mutex<Option<Child>>>,
pub(crate) writer: Box<dyn Write + Send>,
pub(crate) reader: Box<dyn BufRead + Send>,
pub(crate) stderr: Option<ChildStderr>,
pub(crate) request_deadline: Duration,
pub(crate) handshake_deadline: Option<Duration>,
}
impl Peer {
pub(crate) fn spawn(
program: &str,
args: &[String],
deadline: Duration,
) -> Result<Self, SourceError> {
let mut child = Command::new(program)
.args(args)
.env_clear()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|error| SourceError::Unavailable {
message: format!("could not run the plugin program {program:?}: {error}"),
})?;
let writer = Box::new(child.stdin.take().expect("stdin was piped"));
let reader = Box::new(BufReader::new(
child.stdout.take().expect("stdout was piped"),
));
let stderr = child.stderr.take().expect("stderr was piped");
Ok(Self {
child: Arc::new(Mutex::new(Some(child))),
writer,
reader,
stderr: Some(stderr),
request_deadline: deadline,
handshake_deadline: Some(deadline),
})
}
pub(crate) fn over(
writer: impl Write + Send + 'static,
reader: impl Read + Send + 'static,
deadline: Duration,
) -> Self {
Self {
child: Arc::new(Mutex::new(None)),
writer: Box::new(writer),
reader: Box::new(BufReader::new(reader)),
stderr: None,
request_deadline: deadline,
handshake_deadline: None,
}
}
pub(crate) fn exchange(&mut self, line: &str) -> Result<String, SourceError> {
let Some(deadline) = self.handshake_deadline else {
return exchange(&mut self.writer, &mut self.reader, line);
};
let finished = Arc::new(AtomicBool::new(false));
let timed_out = Arc::new(AtomicBool::new(false));
let watched = Arc::clone(&self.child);
let done = Arc::clone(&finished);
let expired = Arc::clone(&timed_out);
std::thread::spawn(move || {
std::thread::sleep(deadline);
if !done.load(Ordering::Acquire) {
expired.store(true, Ordering::Release);
if let Ok(mut child) = watched.lock()
&& let Some(child) = child.as_mut()
{
let _ = child.kill();
}
}
});
let answer = exchange(&mut self.writer, &mut self.reader, line);
finished.store(true, Ordering::Release);
if timed_out.load(Ordering::Acquire) {
Err(SourceError::Unavailable {
message: format!(
"the plugin did not answer the initialize request within {} milliseconds",
deadline.as_millis()
),
})
} else {
answer
}
}
pub(crate) fn said(&mut self) -> String {
if let Ok(mut child) = self.child.lock()
&& let Some(child) = child.as_mut()
{
let _ = child.kill();
let _ = child.wait();
}
let Some(stderr) = self.stderr.as_mut() else {
return String::new();
};
let mut said = String::new();
let mut reader = BufReader::new(stderr);
while said.len() < KEPT_DIAGNOSTICS {
let mut line = String::new();
let room = (KEPT_DIAGNOSTICS - said.len()) as u64;
match (&mut reader).take(room).read_line(&mut line) {
Ok(0) | Err(_) => break,
Ok(_) => said.push_str(&line),
}
}
said.trim().to_owned()
}
}
fn exchange(
writer: &mut (impl Write + ?Sized),
reader: &mut (impl BufRead + ?Sized),
line: &str,
) -> Result<String, SourceError> {
writeln!(writer, "{line}")
.and_then(|()| writer.flush())
.map_err(|error| SourceError::Unavailable {
message: format!("could not send a request to the plugin: {error}"),
})?;
match read_line(reader) {
Line::Read(answer) => Ok(answer),
Line::Ended => Err(SourceError::Unavailable {
message: "the plugin closed its output without answering".to_owned(),
}),
Line::TooLong => Err(SourceError::Malformed {
message: format!(
"the plugin wrote more than {MAX_LINE} bytes without ending the line; a \
response is one line and this engine will not hold an unbounded one"
),
}),
Line::Failed(error) => Err(SourceError::Unavailable {
message: format!("could not read the plugin's answer: {error}"),
}),
}
}
fn drain(stderr: ChildStderr, into: Arc<Mutex<String>>) {
std::thread::spawn(move || {
let mut reader = BufReader::new(stderr);
loop {
let mut line = String::new();
match (&mut reader).take(MAX_LINE).read_line(&mut line) {
Ok(0) | Err(_) => return,
Ok(_) => {}
}
let mut kept = into.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let room = KEPT_DIAGNOSTICS.saturating_sub(kept.len());
if room > 0 {
let end = line
.char_indices()
.nth(room)
.map_or(line.len(), |(at, _)| at);
kept.push_str(&line[..end]);
}
}
});
}
fn quoted(line: &str) -> String {
let line = line.trim();
match line.char_indices().nth(QUOTED) {
None => format!("{line:?}"),
Some((at, _)) => format!("{:?} (truncated)", &line[..at]),
}
}
struct Slot {
state: Mutex<SlotState>,
}
#[derive(Default)]
struct SlotState {
answer: Option<Result<String, SourceError>>,
completed: bool,
waker: Option<Waker>,
}
impl Slot {
fn empty() -> Self {
Self {
state: Mutex::new(SlotState::default()),
}
}
fn fill(&self, answer: Result<String, SourceError>) {
let _ = self.fill_if_empty(answer);
}
fn fill_if_empty(&self, answer: Result<String, SourceError>) -> bool {
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if state.completed {
return false;
}
state.completed = true;
state.answer = Some(answer);
let waker = state.waker.take();
drop(state);
if let Some(waker) = waker {
waker.wake();
}
true
}
}
struct Answer {
slot: Arc<Slot>,
}
impl Future for Answer {
type Output = Result<String, SourceError>;
fn poll(self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
let mut state = self
.slot
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
match state.answer.take() {
Some(answer) => Poll::Ready(answer),
None => {
state.waker = Some(context.waker().clone());
Poll::Pending
}
}
}
}