use std::io::{self, ErrorKind, Write};
use std::process::ExitCode;
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use tokio::net::UnixStream;
use waos_dev_protocol::{Connection, FromWaOS, OutputStream, RunEnd, ToWaOS};
use waos_package::ProgramId;
use crate::build::BuiltProgram;
use crate::waos_app;
const START_WAIT: Duration = Duration::from_secs(10);
const RETRY_EVERY: Duration = Duration::from_millis(200);
const INTERRUPTED: u8 = 130;
pub async fn run(id: ProgramId, program: &BuiltProgram) -> Result<ExitCode> {
let component = std::fs::read(&program.component)
.with_context(|| format!("reading {}", program.component.display()))?;
let mut connection = connect_or_start().await?;
connection
.agree_on_protocol()
.await
.context("the WaOS that answered")?;
let name = &program.name;
connection
.send(&ToWaOS::Run { id, component })
.await
.with_context(|| format!("sending {name} to WaOS"))?;
let interrupted = tokio::signal::ctrl_c();
tokio::pin!(interrupted);
loop {
tokio::select! {
received = connection.receive::<FromWaOS>() => match received.context("reading from WaOS")? {
Some(FromWaOS::Started) => eprintln!("cargo waos: {name} is running in WaOS"),
Some(FromWaOS::Output { stream, bytes }) => write_output(stream, &bytes)?,
Some(FromWaOS::Ended(end)) => return Ok(ended(name, &end)),
Some(FromWaOS::Refused(why)) => bail!("WaOS refused {name}: {why}"),
Some(other @ (FromWaOS::Opened
| FromWaOS::Screenshots(_)
| FromWaOS::InputDone { .. }
| FromWaOS::Unreached(_))) => {
bail!("WaOS answered {other:?}, which a run never asks for")
}
None => bail!("WaOS closed the connection before {name} ended"),
},
_ = &mut interrupted => {
drop(connection);
eprintln!("cargo waos: {name} stopped");
return Ok(ExitCode::from(INTERRUPTED));
}
}
}
}
async fn connect_or_start() -> Result<Connection<UnixStream>> {
let socket = waos_dev_protocol::socket_path();
match Connection::connect(&socket).await {
Ok(connection) => return Ok(connection),
Err(error) if nobody_answers(&error) => {}
Err(error) => {
return Err(error)
.with_context(|| format!("connecting to WaOS at {}", socket.display()));
}
}
eprintln!("cargo waos: no WaOS answers; starting WaOS.app");
let log = waos_app::start()?;
eprintln!("cargo waos: WaOS writes its log to {}", log.display());
let deadline = Instant::now() + START_WAIT;
loop {
tokio::time::sleep(RETRY_EVERY).await;
match Connection::connect(&socket).await {
Ok(connection) => return Ok(connection),
Err(error) if nobody_answers(&error) && Instant::now() < deadline => {}
Err(error) => {
return Err(error).with_context(|| {
format!(
"WaOS.app was started, but nothing answers at {}",
socket.display()
)
});
}
}
}
}
fn nobody_answers(error: &io::Error) -> bool {
matches!(
error.kind(),
ErrorKind::NotFound | ErrorKind::ConnectionRefused
)
}
fn write_output(stream: OutputStream, bytes: &[u8]) -> Result<()> {
let written = match stream {
OutputStream::Stdout => io::stdout()
.lock()
.write_all(bytes)
.and_then(|()| io::stdout().flush()),
OutputStream::Stderr => io::stderr()
.lock()
.write_all(bytes)
.and_then(|()| io::stderr().flush()),
};
written.context("writing the program's output")
}
fn ended(name: &str, end: &RunEnd) -> ExitCode {
let (said, status) = match end {
RunEnd::Returned { success: true } => (format!("{name} returned"), ExitCode::SUCCESS),
RunEnd::Returned { success: false } => {
(format!("{name} returned failure"), ExitCode::FAILURE)
}
RunEnd::Failed(error) => (format!("{name} failed: {error}"), ExitCode::FAILURE),
RunEnd::Stopped => (format!("{name} was stopped by WaOS"), ExitCode::FAILURE),
RunEnd::Replaced => (
format!("{name} was replaced by a later run"),
ExitCode::FAILURE,
),
};
eprintln!("cargo waos: {said}");
status
}