use super::language::{Line, help, parse};
pub struct Terminal {
pub url: String,
pub token: Option<String>,
pub city: String,
pub client: String,
pub bind: SocketAddr,
pub records: super::stream::Records,
}
use kernel::Address;
use std::io::{BufRead, Write};
use std::net::SocketAddr;
use std::sync::Arc;
pub(crate) use wire::Answering;
pub(crate) struct Inside {
pub(crate) desk: Arc<accounting::worker::CommandDesk>,
pub(crate) answering: Answering,
pub(crate) remote: Result<crate::outside::console::Remote, kernel::AxError>,
}
pub(crate) fn serving(
terminal: &Terminal,
vitals: Option<&wire::MetricsAnswer>,
pid: u32,
) -> String {
let reach = if terminal.bind.ip().is_loopback() {
"this machine only"
} else if terminal.bind.ip().is_unspecified() {
"every interface on this machine"
} else {
"one interface, reachable from the network"
};
let door = match (terminal.token.is_some(), terminal.bind.ip().is_loopback()) {
(true, _) => "a pairing key is required",
(false, true) => "none - a loopback listener asks for nothing",
(false, false) => "NONE - anyone who can reach this address can drive this city",
};
let counts = match vitals {
Some(v) => format!(
" runs {} active, {} frozen\n \
waiting {} approval(s), {} signal(s)\n \
holds {} building(s), {} event(s), {} discard(s) outstanding\n",
v.runs_active,
v.runs_frozen,
v.approvals_waiting,
v.signals_waiting,
v.buildings,
v.events,
v.discards_outstanding
),
None => " runs the city did not answer; its views may be rebuilding\n".to_owned(),
};
format!(
"\n sprawling is serving.\n\n \
city {}\n \
WebUI {}\n \
bound {}, {reach}\n \
door {door}\n \
client {}\n \
process pid {pid} - `cargo xtask mem {pid}` weighs it\n\n\
{counts}",
terminal.city, terminal.url, terminal.bind, terminal.client
)
}
pub(crate) fn web_url(terminal: &Terminal) -> String {
match &terminal.token {
None => terminal.url.clone(),
Some(token) => format!("{}/?token={token}", terminal.url.trim_end_matches('/')),
}
}
pub(crate) fn start(
terminal: Terminal,
inside: Inside,
watching: tokio::sync::broadcast::Receiver<wire::Committed>,
) {
super::stream::print(terminal.records, watching);
std::thread::spawn(move || {
let stdin = std::io::stdin();
drive(
&terminal,
&inside,
&mut stdin.lock(),
&mut std::io::stdout(),
);
});
}
fn say<W: Write>(out: &mut W, line: &str) {
drop(out.write_all(line.as_bytes()));
drop(out.write_all(b"\n"));
}
pub(super) fn drive<R: BufRead, W: Write>(
terminal: &Terminal,
inside: &Inside,
input: &mut R,
out: &mut W,
) {
let Inside {
desk, answering, ..
} = inside;
let mut keys = match LineKeys::drawn() {
Ok(keys) => keys,
Err(err) => {
say(out, &format!(" {err}"));
say(out, &format!(" {}", err.recovery()));
return;
}
};
let mut selected: Option<Address> = None;
let mut typed = String::new();
loop {
typed.clear();
if write!(out, "> ").is_err() || out.flush().is_err() {
return;
}
match input.read_line(&mut typed) {
Ok(0) | Err(_) => return,
Ok(_) => {}
}
let idem = keys.next();
match parse(&typed, selected.as_ref(), idem) {
Line::Nothing => {}
Line::Help => {
say(out, &help(selected.as_ref()));
}
Line::OpenWeb => {
let url = web_url(terminal);
say(out, &format!(" {url}"));
drop(crate::firstrun::open_in_browser(&url));
}
Line::Serving => {
let vitals = match answering(wire::Query::Metrics) {
(_, Ok(wire::Answer::Metrics(vitals))) => Some(*vitals),
(_, Ok(_) | Err(_)) => None,
};
say(out, &serving(terminal, vitals.as_ref(), std::process::id()));
}
Line::Select(addr) => {
say(out, &format!(" work goes to {}", addr.as_str()));
selected = Some(addr);
}
Line::Remote(line) => {
say(out, &crate::outside::console::carry(&inside.remote, line));
}
Line::Quit => {
say(out, " the console is closing; the city keeps serving");
return;
}
Line::Unknown { verb, nearest } => {
say(out, &format!(" no verb `{verb}`"));
if !nearest.is_empty() {
say(out, &format!(" did you mean: {}", nearest.join(", ")));
}
}
Line::Malformed { verb, reason } => {
say(out, &format!(" `/{verb}` cannot be read: {reason}"));
say(
out,
" the body is the wire's own JSON; `idem` may be left out",
);
}
Line::Frame(frame) => post(desk, answering, *frame, out),
Line::Work(task) => {
let Some(addr) = selected.clone() else {
continue;
};
post(desk, answering, dispatch(&addr, &task, idem), out);
}
}
}
}
fn post<W: Write>(
desk: &accounting::worker::CommandDesk,
answering: &Answering,
frame: wire::ClientFrame,
out: &mut W,
) {
match frame {
wire::ClientFrame::Command(command) => {
desk.post(
(*command).into(),
wire::Reply::to(move |error: kernel::AxError| {
eprintln!(" {error}");
eprintln!(" {}", error.recovery());
wire::Delivered::ToThePeer
}),
);
}
wire::ClientFrame::Ask(ask) => answer(answering, ask.query, out),
wire::ClientFrame::Hello(_) | wire::ClientFrame::Monitor(_) => {
say(out, " this console is already inside the city");
}
}
}
fn answer<W: Write>(answering: &Answering, query: wire::Query, out: &mut W) {
let (_as_of, answered) = answering(query);
match answered {
Ok(answer) => match serde_json::to_string(&answer) {
Ok(text) => {
say(out, &text);
}
Err(err) => {
say(out, &format!(" the answer could not be rendered: {err}"));
}
},
Err(error) => {
say(out, &format!(" {error}"));
say(out, &format!(" {}", error.recovery()));
}
}
}
fn dispatch(addr: &Address, task: &str, idem: kernel::IdemKey) -> wire::ClientFrame {
wire::ClientFrame::Command(Box::new(wire::WireCommand::Dispatch {
addr: addr.clone(),
task: task.to_owned(),
goal: String::new(),
policy: wire::RunPolicy::of(wire::Mode::Work),
idem,
session: None,
effort: None,
model: None,
}))
}
struct LineKeys {
origin: [u8; 16],
lines: kernel::Seq,
}
impl LineKeys {
fn drawn() -> Result<LineKeys, kernel::AxError> {
let mut origin = [0u8; 16];
getrandom::fill(&mut origin).map_err(|err| {
kernel::AxError::failure(
kernel::AxCode::ConfigInvalid,
"draw an origin for this console's keys",
err.to_string(),
)
.with_recovery(
"this machine's entropy source refused; the city keeps serving, drive it from the WebUI or `sprawling call`",
)
})?;
Ok(LineKeys {
origin,
lines: kernel::Seq::FIRST,
})
}
fn next(&mut self) -> kernel::IdemKey {
let key = kernel::IdemKey::derive(&kernel::RunId::CITY, self.lines, &self.origin);
self.lines = self.lines.next().unwrap_or(self.lines);
key
}
}