//! Loopback JSON-lines control server: one request per line, one reply per line. //! Agents and tests drive the app through it (adapted from CADCraft, MIT OR Apache-2.0). use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::mpsc::{Receiver, Sender, channel}; use std::time::Duration; use serde_json::{Value, json}; use solvecraft_ui_egui::ControlRequest; pub fn start(port: u16, ctx: egui::Context) -> Receiver { let (tx, rx) = channel::(); let listener = match TcpListener::bind(("127.0.0.1", port)) { Ok(l) => l, Err(e) => { eprintln!("solvecraft: control server failed to bind 127.0.0.1:{port}: {e}"); return rx; } }; eprintln!("solvecraft: control server listening on 127.0.0.1:{port}"); std::thread::spawn(move || { for stream in listener.incoming().flatten() { let tx = tx.clone(); let ctx = ctx.clone(); std::thread::spawn(move || serve(stream, tx, ctx)); } }); rx } fn serve(stream: TcpStream, tx: Sender, ctx: egui::Context) { let Ok(read) = stream.try_clone() else { return }; let mut out = stream; for line in BufReader::new(read).lines() { let Ok(line) = line else { break }; if line.trim().is_empty() { continue; } let reply = match serde_json::from_str::(&line) { Ok(msg) => { let id = msg.get("id").cloned().unwrap_or(Value::Null); let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string(); let params = msg.get("params").cloned().unwrap_or(json!({})); let (req, rrx) = ControlRequest::new(method, params); if tx.send(req).is_err() { break; } ctx.request_repaint(); let mut r = rrx.recv_timeout(Duration::from_secs(60)).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"})); if let Some(o) = r.as_object_mut() { o.insert("id".into(), id); } r } Err(e) => json!({"ok": false, "error": format!("bad JSON: {e}")}), }; if writeln!(out, "{reply}").is_err() { break; } } }