use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use serde_json::Value;
pub const NODE_BIN_ENV: &str = "SUPERCODE_NODE_BIN";
pub const SOCKET_FILE: &str = "orchestrator.sock";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Door {
Live,
Cold,
}
impl Door {
pub const fn as_str(self) -> &'static str {
match self {
Self::Live => "live",
Self::Cold => "cold",
}
}
}
#[derive(Debug, Clone)]
pub struct DoorAnswer {
pub ran: String,
pub door: Door,
pub result: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DoorError {
Refused(String),
Failed(String),
}
impl std::fmt::Display for DoorError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Refused(message) | Self::Failed(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for DoorError {}
type Result<T> = std::result::Result<T, DoorError>;
pub fn socket_path(root: &Path) -> PathBuf {
match std::env::var_os("SUPERCODE_ORCHESTRATOR_SOCK") {
Some(path) if !path.is_empty() => PathBuf::from(path),
_ => root.join(SOCKET_FILE),
}
}
pub fn daemon_is_live(root: &Path) -> bool {
crate::orchestrator::live_lease(root).is_some() && socket_path(root).exists()
}
pub fn call(root: &Path, op: &str, args: &Value, profile: &str) -> Result<DoorAnswer> {
if daemon_is_live(root) {
match call_live(root, op, args, profile) {
Ok(answer) => return Ok(answer),
Err(DoorError::Refused(message)) => return Err(DoorError::Refused(message)),
Err(DoorError::Failed(_)) => {}
}
}
call_cold(root, op, args, profile)
}
fn narrate_live(root: &Path, op: &str, args: &Value, profile: &str) -> String {
format!(
"{} {op} --profile {profile} --json {}",
socket_path(root).display(),
shell_quote(&args.to_string())
)
}
#[cfg(unix)]
fn call_live(root: &Path, op: &str, args: &Value, profile: &str) -> Result<DoorAnswer> {
use std::os::unix::net::UnixStream;
let ran = narrate_live(root, op, args, profile);
let path = socket_path(root);
let mut stream = UnixStream::connect(&path).map_err(|error| {
DoorError::Failed(format!(
"the orchestrator daemon is leased for `{}` but its socket `{}` did not accept a \
connection: {error}",
root.display(),
path.display()
))
})?;
let line = serde_json::json!({"op": op, "args": args, "profile": profile});
stream
.write_all(format!("{line}\n").as_bytes())
.and_then(|()| stream.flush())
.map_err(|error| DoorError::Failed(format!("`{ran}` could not be sent: {error}")))?;
let mut reader = BufReader::new(stream);
let mut answer = String::new();
reader
.read_line(&mut answer)
.map_err(|error| DoorError::Failed(format!("`{ran}` was not answered: {error}")))?;
if answer.trim().is_empty() {
return Err(DoorError::Failed(format!(
"`{ran}`: the orchestrator closed the connection without answering"
)));
}
interpret(&ran, Door::Live, answer.trim())
}
#[cfg(not(unix))]
fn call_live(root: &Path, op: &str, args: &Value, profile: &str) -> Result<DoorAnswer> {
Err(DoorError::Failed(format!(
"`{}`: the daemon's door is a Unix socket, which this platform has no client for; the \
cold path answers instead",
narrate_live(root, op, args, profile)
)))
}
fn call_cold(root: &Path, op: &str, args: &Value, profile: &str) -> Result<DoorAnswer> {
let entry = crate::orchestrator::daemon_entry().map_err(|error| {
DoorError::Failed(format!(
"the orchestrator's write door is its own package, and it could not be located: \
{error}"
))
})?;
let node = std::env::var_os(NODE_BIN_ENV)
.map(|value| value.to_string_lossy().trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "node".to_string());
let payload = args.to_string();
let arguments = vec![
entry.to_string_lossy().into_owned(),
op.to_string(),
"--root".to_string(),
root.to_string_lossy().into_owned(),
"--profile".to_string(),
profile.to_string(),
"--json".to_string(),
payload,
];
let ran = std::iter::once(node.clone())
.chain(arguments.iter().cloned())
.map(|part| shell_quote(&part))
.collect::<Vec<_>>()
.join(" ");
let output = std::process::Command::new(&node)
.args(&arguments)
.stdin(std::process::Stdio::null())
.output()
.map_err(|error| DoorError::Failed(format!("`{ran}` could not be executed: {error}")))?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let last = stdout.lines().rev().find(|line| !line.trim().is_empty());
let Some(last) = last else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(DoorError::Failed(format!(
"`{ran}` printed nothing ({}){}",
output.status,
if stderr.is_empty() {
String::new()
} else {
format!(": {stderr}")
}
)));
};
interpret(&ran, Door::Cold, last.trim())
}
fn interpret(ran: &str, door: Door, line: &str) -> Result<DoorAnswer> {
let value: Value = serde_json::from_str(line).map_err(|error| {
DoorError::Failed(format!(
"`{ran}` answered something that is not JSON: {error}"
))
})?;
if value.get("ok").and_then(Value::as_bool) == Some(true) {
return Ok(DoorAnswer {
ran: ran.to_string(),
door,
result: value.get("result").cloned().unwrap_or(Value::Null),
});
}
Err(DoorError::Refused(
value
.get("error")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("`{ran}` answered `{line}`")),
))
}
fn shell_quote(value: &str) -> String {
if !value.is_empty()
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_./:@=+,".contains(c))
{
return value.to_string();
}
format!("'{}'", value.replace('\'', "'\\''"))
}