mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
The desktop app installed no logger, so every log::warn!/log::error! in the workspace was discarded: the crash guard's panic report, a failed crash-recovery save, PowerPoint parts skipped on import, audio output problems. A launch from a desktop menu has no terminal either, so there was nothing to attach to a bug report. apps/deckcraft/src/logging.rs (no new dependency): - standard error plus <settings dir>/logs/deckcraft.log (beside ui.json and prefs.json); each launch rotates the previous log to deckcraft.1.log and .2.log, so the log of a run that crashed survives the next start; the file is capped at 16 MiB - records logged before the settings directory is known are buffered (bounded) and written once the file is attached - default levels: info for deckcraft*, warn for everything else; RUST_LOG replaces them with env_logger-style directives - never panics: an unusable directory or a failed write leaves standard error as the only sink; a poisoned lock is tolerated - a record holds the lock for one write of an already formatted line The cpal error callback runs on the realtime audio thread (on ALSA in a loop while the device stays broken), so it no longer logs: it keeps the first error without blocking, and the UI thread logs it when the stream stops. The logger is installed first in main (before the engine's panic hook) and the file is opened after argument parsing (--version leaves no file). The settings directory moves out of prefs_path into config_dir. The start-up eprintln!s (sample, files that fail to open, control server) now go through the logger. Documented in README.md (Getting started > Logs). Co-authored-by: Brandon Thomas <echelon@gmail.com>
63 lines
2.3 KiB
Rust
63 lines
2.3 KiB
Rust
//! Loopback JSON-lines control server: one request per line, one reply per line.
|
|
//! This is the transport the MCP server (`deckcraft-cli mcp --connect`) wraps.
|
|
|
|
use std::io::{BufRead, BufReader, Write};
|
|
use std::net::{TcpListener, TcpStream};
|
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
|
use std::time::Duration;
|
|
|
|
use deckcraft_ui_egui::ControlRequest;
|
|
use serde_json::{Value, json};
|
|
|
|
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
|
let (tx, rx) = channel::<ControlRequest>();
|
|
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
|
Ok(l) => l,
|
|
Err(e) => {
|
|
log::error!("control server failed to bind 127.0.0.1:{port}: {e}");
|
|
return rx;
|
|
}
|
|
};
|
|
log::info!("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<ControlRequest>, 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::<Value>(&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;
|
|
}
|
|
}
|
|
}
|