use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use thiserror::Error;
use super::super::{ENV_CONV_BRANCH, ENV_CONV_REPO};
use super::dispatch::EnvLookup;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Input {
agent: String,
content: String,
}
#[derive(Debug, Serialize, PartialEq, Eq)]
struct Output<'a> {
status: &'a str,
}
const STATUS_DEPOSITED: &str = "deposited";
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid input JSON: {0}")]
InvalidJson(#[source] serde_json::Error),
#[error("read input from stdin: {0}")]
StdinRead(#[source] io::Error),
#[error("missing env var {0:?} (set by the harness per ARCH §3.3)")]
MissingEnv(&'static str),
#[error("spawn lernie message: {0}")]
Spawn(#[source] io::Error),
#[error("lernie message failed (exit {exit}): {stderr}")]
MessageExit { exit: i32, stderr: String },
#[error("write to stdout: {0}")]
Write(#[source] io::Error),
}
#[derive(Debug)]
pub struct SendOutput {
pub stderr: String,
pub exit: i32,
}
pub trait Sender {
fn send(
&self,
workspace: &Path,
agent: &str,
content: &str,
sender: &str,
) -> Result<SendOutput, io::Error>;
}
pub struct SubprocessSender {
exe: PathBuf,
}
impl SubprocessSender {
pub fn with_exe(exe: PathBuf) -> Self {
Self { exe }
}
}
impl Sender for SubprocessSender {
fn send(
&self,
workspace: &Path,
agent: &str,
content: &str,
sender: &str,
) -> Result<SendOutput, io::Error> {
let out = Command::new(&self.exe)
.arg("message")
.arg(workspace)
.arg(agent)
.arg(content)
.env(ENV_CONV_BRANCH, sender)
.output()?;
Ok(SendOutput {
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
exit: out.status.code().unwrap_or(-1),
})
}
}
pub fn run<R: Read, W: Write>(
stdin: &mut R,
stdout: &mut W,
env: &dyn EnvLookup,
sender_impl: &dyn Sender,
) -> Result<(), Error> {
let mut buf = Vec::new();
stdin.read_to_end(&mut buf).map_err(Error::StdinRead)?;
let input: Input = serde_json::from_slice(&buf).map_err(Error::InvalidJson)?;
let repo = require_env(env, ENV_CONV_REPO)?;
let branch = require_env(env, ENV_CONV_BRANCH)?;
let repo_path = PathBuf::from(repo);
let sender = branch
.into_string()
.map_err(|_| Error::MissingEnv(ENV_CONV_BRANCH))?;
let captured = sender_impl
.send(&repo_path, &input.agent, &input.content, &sender)
.map_err(Error::Spawn)?;
if captured.exit != 0 {
return Err(Error::MessageExit {
exit: captured.exit,
stderr: captured.stderr,
});
}
let payload = Output {
status: STATUS_DEPOSITED,
};
let bytes = serde_json::to_vec(&payload).expect("Output is always serializable");
stdout.write_all(&bytes).map_err(Error::Write)
}
fn require_env(env: &dyn EnvLookup, key: &'static str) -> Result<OsString, Error> {
env.get(key).ok_or(Error::MissingEnv(key))
}
#[cfg(test)]
mod tests;