2026-11-04 11:30:32 +00:00
|
|
|
// (Shared with relay/.)
|
|
|
|
|
#![allow(dead_code)]
|
|
|
|
|
// One frame on the lasting connection between an AnyBot and the account server (a WebSocket):
|
|
|
|
|
// a small JSON header, then the body as raw bytes (no base64), in one binary message.
|
|
|
|
|
//
|
|
|
|
|
// Headers ("t" says which):
|
|
|
|
|
// server → AnyBot job {id, method, path, query, user, role, z, z_ok} a request for this AnyBot
|
|
|
|
|
// AnyBot → server reply {id, status, ctype, headers, z} its answer
|
|
|
|
|
// AnyBot → server call {id, to, method, path, query, z} a request for another AnyBot of the account
|
|
|
|
|
// server → AnyBot ret {id, status, ctype, z} that one's answer
|
|
|
|
|
// AnyBot → server hello {brain} what this AnyBot shows
|
|
|
|
|
// "z": the body is deflate-compressed. "z_ok": the asker can read a compressed answer.
|
|
|
|
|
|
|
|
|
|
use serde_json::Value;
|
|
|
|
|
|
|
|
|
|
pub fn pack(head: &Value, body: &[u8]) -> Vec<u8> {
|
|
|
|
|
let h = head.to_string();
|
|
|
|
|
let mut out = Vec::with_capacity(4 + h.len() + body.len());
|
|
|
|
|
out.extend_from_slice(&(h.len() as u32).to_be_bytes());
|
|
|
|
|
out.extend_from_slice(h.as_bytes());
|
|
|
|
|
out.extend_from_slice(body);
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn unpack(b: &[u8]) -> Option<(Value, Vec<u8>)> {
|
|
|
|
|
let n = u32::from_be_bytes(b.get(..4)?.try_into().ok()?) as usize;
|
|
|
|
|
let head = serde_json::from_slice(b.get(4..4 + n)?).ok()?;
|
|
|
|
|
Some((head, b[4 + n..].to_vec()))
|
|
|
|
|
}
|
|
|
|
|
|
Remove what nothing uses and keep one copy of what was written several times
Unused (found by taking away the dead-code allowances; nothing else used them, tests aside):
computer::specs, quiet_button, the never-made Poke::Spin, ten icons and their SVGs, the
theme's chrome/bubble_agent colours, ImportRow.line, voice's multipart/transcribe/speak
wrappers, and the old keychain writers vault::save/remove/index_add with their live test
(the vault store replaced them; reading old keychain entries for the import stays).
One copy each: percent-encoding (httpd::url_encode, was four), tool arguments (tools::args_of,
was three), the WebSocket handshake (httpd::accept_websocket, was three), the read pacing
(wire::Pace, was two: relay and app), docker with input and a time limit (dk_with_input, was
two). Docs: FEATURES §看畫面 (VNC), protocol (v* frames, /screen, tickets), architecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:21:47 +00:00
|
|
|
/// How long a connection's loop waits to read before it sends what is queued: 2 ms while things
|
|
|
|
|
/// move (a live screen), 40 ms when quiet. A fixed 40 ms added up to 40 ms to every message on
|
|
|
|
|
/// each side of each hop.
|
|
|
|
|
pub struct Pace {
|
|
|
|
|
moved: std::time::Instant,
|
|
|
|
|
busy: Option<bool>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Pace {
|
|
|
|
|
pub fn new() -> Pace {
|
|
|
|
|
Pace { moved: std::time::Instant::now() - std::time::Duration::from_secs(60), busy: None }
|
|
|
|
|
}
|
|
|
|
|
/// Something was read or sent.
|
|
|
|
|
pub fn moved(&mut self) {
|
|
|
|
|
self.moved = std::time::Instant::now();
|
|
|
|
|
}
|
|
|
|
|
/// Set the socket's read wait for now (only when it changes).
|
|
|
|
|
pub fn tune(&mut self, c: &std::net::TcpStream) {
|
|
|
|
|
let busy = self.moved.elapsed() < std::time::Duration::from_secs(2);
|
|
|
|
|
if self.busy != Some(busy) {
|
|
|
|
|
self.busy = Some(busy);
|
|
|
|
|
let _ = c.set_read_timeout(Some(std::time::Duration::from_millis(if busy { 2 } else { 40 })));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-04 11:30:32 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
#[test]
|
|
|
|
|
fn frames_round_trip() {
|
|
|
|
|
let head = serde_json::json!({"t": "job", "id": "a"});
|
|
|
|
|
let f = super::pack(&head, b"\x00\x01body");
|
|
|
|
|
let (h, b) = super::unpack(&f).unwrap();
|
|
|
|
|
assert_eq!(h, head);
|
|
|
|
|
assert_eq!(b, b"\x00\x01body");
|
|
|
|
|
assert!(super::unpack(&f[..3]).is_none());
|
|
|
|
|
}
|
|
|
|
|
}
|