use std::fs;
use std::hash::{BuildHasher, Hasher, RandomState};
use std::io::{Read, Write};
use std::net::{Ipv4Addr, TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant, SystemTime};
use serde::Deserialize as _;
use serde_json::{Value, json};
use crate::error::{
EXIT_CANCELLED, EXIT_FORMAT_MISMATCH, EXIT_INPUT_DECODE, EXIT_MODEL_NOT_FOUND, EXIT_TIMEOUT,
EXIT_USAGE, FocrError, FocrResult,
};
use crate::native_engine::got::{FrameColor, Region};
use crate::native_engine::tromr::{MusicWarning, StaffBBox, StaffSkip};
use crate::native_engine::{
ChartPageMeta, DecodeOverrides, DecodeReport, ModelIdentity, MusicPageMeta, PreprocessOverrides,
};
use crate::{LayoutSpan, OcrEngine, RecognizedDocument};
const DEFAULT_IDLE: Duration = Duration::from_secs(600);
const DEFAULT_CLIENT_READ_TIMEOUT: Duration = Duration::from_secs(600);
const REPLY_SLACK: Duration = Duration::from_secs(30);
const CANCEL_GRACE: Duration = Duration::from_secs(5);
const CLIENT_WATCH_TICK: Duration = Duration::from_millis(100);
const CLIENT_ACK_WAIT: Duration = Duration::from_secs(5);
const ACK_KIND: &str = "accepted";
const STAGE_KIND: &str = "stage";
const ACCEPT_QUEUE: usize = 16;
const DEFAULT_SPAWN_WAIT: Duration = Duration::from_secs(30);
const MAX_REPLY_BYTES: usize = 64 * 1024 * 1024;
const MAX_REQUEST_BYTES: usize = 1024 * 1024;
const PROTOCOL: u64 = 8;
const MAX_CONFIG_SECS: u64 = 31_536_000;
const ENV_FINGERPRINT_EXEMPT: &[&str] = &[
"FOCR_NO_RESIDENT",
"FOCR_RESIDENT_IDLE_SECS",
"FOCR_RESIDENT_DIR",
"FOCR_RESIDENT_LOG",
"FOCR_RESIDENT_SPAWN_WAIT_SECS",
"FOCR_RESIDENT_CLIENT_TIMEOUT_SECS",
"FOCR_NO_PROGRESS",
"FOCR_RUN_STORE",
"FOCR_MANIFEST_URL",
"FOCR_TIMING",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ReplyWait {
Fixed(Duration),
Staged(Duration),
}
impl ReplyWait {
fn after_stage(
self,
started: Instant,
now: Instant,
budget: Option<Duration>,
current: Option<Instant>,
) -> Option<Instant> {
match self {
Self::Fixed(_) => current,
Self::Staged(floor) => budget.map(|budget| {
let bound = budget
.saturating_add(crate::native_engine::document_budget_grace(budget))
.saturating_add(REPLY_SLACK)
.min(Duration::from_secs(MAX_CONFIG_SECS));
(now + bound).max(started + floor)
}),
}
}
fn first(self, started: Instant) -> Instant {
match self {
Self::Fixed(wait) | Self::Staged(wait) => started + wait,
}
}
}
fn client_reply_wait() -> ReplyWait {
std::env::var("FOCR_RESIDENT_CLIENT_TIMEOUT_SECS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|&seconds| seconds > 0)
.map_or(ReplyWait::Staged(DEFAULT_CLIENT_READ_TIMEOUT), |seconds| {
ReplyWait::Fixed(Duration::from_secs(seconds.min(MAX_CONFIG_SECS)))
})
}
fn spawn_wait() -> Duration {
std::env::var("FOCR_RESIDENT_SPAWN_WAIT_SECS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.map_or(DEFAULT_SPAWN_WAIT, |seconds| {
Duration::from_secs(seconds.min(MAX_CONFIG_SECS))
})
}
fn idle_period() -> Duration {
std::env::var("FOCR_RESIDENT_IDLE_SECS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|&seconds| seconds > 0)
.map_or(DEFAULT_IDLE, |seconds| {
Duration::from_secs(seconds.min(MAX_CONFIG_SECS))
})
}
#[must_use]
pub fn enabled(no_resident_flag: bool) -> bool {
if no_resident_flag {
return false;
}
!matches!(
std::env::var("FOCR_NO_RESIDENT").ok().as_deref(),
Some("1") | Some("true")
)
}
fn resident_dir() -> Option<PathBuf> {
if let Ok(dir) = std::env::var("FOCR_RESIDENT_DIR") {
return Some(PathBuf::from(dir));
}
crate::dist::cache_root()
}
fn root_digest(root: &Path) -> u64 {
let canonical = fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in canonical.to_string_lossy().as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
fn state_path(root: &Path) -> Option<PathBuf> {
resident_dir().map(|dir| dir.join(format!("resident-{:016x}.json", root_digest(root))))
}
struct DaemonState {
port: u16,
token: String,
}
fn read_state(root: &Path) -> Option<DaemonState> {
let path = state_path(root)?;
let raw = fs::read_to_string(path).ok()?;
let value: Value = serde_json::from_str(&raw).ok()?;
Some(DaemonState {
port: u16::try_from(value.get("port")?.as_u64()?).ok()?,
token: value.get("token")?.as_str()?.to_owned(),
})
}
fn write_state(root: &Path, port: u16, token: &str) -> std::io::Result<PathBuf> {
let path = state_path(root).ok_or_else(|| {
std::io::Error::other("no cache directory and no FOCR_RESIDENT_DIR; cannot go resident")
})?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let body = json!({
"port": port,
"token": token,
"pid": std::process::id(),
"version": env!("CARGO_PKG_VERSION"),
"model_root": root.to_string_lossy(),
})
.to_string();
let staging = path.with_extension("json.tmp");
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&staging)?;
file.write_all(body.as_bytes())?;
}
#[cfg(not(unix))]
fs::write(&staging, &body)?;
fs::rename(&staging, &path)?;
Ok(path)
}
fn fresh_token() -> String {
let mut token = String::with_capacity(32);
for _ in 0..2 {
let mut hasher = RandomState::new().build_hasher();
hasher.write_u128(std::time::UNIX_EPOCH.elapsed().map_or(0, |d| d.as_nanos()));
hasher.write_u32(std::process::id());
token.push_str(&format!("{:016x}", hasher.finish()));
}
token
}
fn artifact_stamp(root: &Path) -> (u64, u64) {
let Ok(meta) = fs::metadata(root) else {
return (0, 0);
};
let mtime = meta
.modified()
.ok()
.and_then(|time| time.duration_since(SystemTime::UNIX_EPOCH).ok())
.map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX));
(mtime, meta.len())
}
fn env_fingerprint() -> Value {
let mut map = std::collections::BTreeMap::new();
for (key, value) in std::env::vars() {
if key.starts_with("FOCR_") && !ENV_FINGERPRINT_EXEMPT.contains(&key.as_str()) {
map.insert(key, Value::String(value));
}
}
Value::Object(map.into_iter().collect())
}
pub struct ResidentReply {
pub document: RecognizedDocument,
pub report: Option<DecodeReport>,
pub model: Option<ModelIdentity>,
pub music: Option<MusicPageMeta>,
pub chart: Option<ChartPageMeta>,
}
fn chart_to_wire(meta: &ChartPageMeta) -> Value {
json!({
"pred_locs": meta.pred_locs,
"reliable_distance": meta.reliable_distance,
"reliable": meta.reliable,
})
}
fn chart_from_wire(value: &Value) -> Option<ChartPageMeta> {
let pred_locs = value
.get("pred_locs")?
.as_array()?
.iter()
.map(|slot| slot.as_f64().map(|v| v as f32))
.collect::<Option<Vec<f32>>>()?;
let reliable_distance = match value.get("reliable_distance")? {
Value::Null => None,
distance => Some(distance.as_f64()?),
};
let reliable = match value.get("reliable")? {
Value::Null => None,
verdict => Some(verdict.as_bool()?),
};
Some(ChartPageMeta {
pred_locs,
reliable_distance,
reliable,
})
}
fn bbox_to_wire(bbox: StaffBBox) -> Value {
json!([bbox.0, bbox.1, bbox.2, bbox.3])
}
fn bbox_from_wire(value: &Value) -> Option<StaffBBox> {
let edges = value.as_array()?;
let edge = |index: usize| -> Option<usize> {
edges
.get(index)?
.as_u64()
.and_then(|e| usize::try_from(e).ok())
};
Some((edge(0)?, edge(1)?, edge(2)?, edge(3)?))
}
fn music_to_wire(meta: &MusicPageMeta) -> Value {
let staves: Vec<Value> = meta
.staves
.iter()
.map(|(index, bbox)| json!({ "index": index, "bbox": bbox_to_wire(*bbox) }))
.collect();
let skips: Vec<Value> = meta
.skips
.iter()
.map(|skip| {
json!({ "index": skip.index, "bbox": bbox_to_wire(skip.bbox), "reason": skip.reason })
})
.collect();
let warnings: Vec<Value> = meta
.warnings
.iter()
.map(|warning| {
json!({
"kind": warning.kind,
"part": warning.part,
"measure": warning.measure,
"detail": warning.detail,
})
})
.collect();
json!({ "staves": staves, "skips": skips, "warnings": warnings })
}
fn music_from_wire(value: &Value) -> Option<MusicPageMeta> {
let uint = |item: &Value, key: &str| -> Option<usize> {
item.get(key)?
.as_u64()
.and_then(|n| usize::try_from(n).ok())
};
let text =
|item: &Value, key: &str| -> Option<String> { item.get(key)?.as_str().map(str::to_owned) };
let list = |key: &str| value.get(key).and_then(Value::as_array);
let staves = list("staves")?
.iter()
.map(|staff| Some((uint(staff, "index")?, bbox_from_wire(staff.get("bbox")?)?)))
.collect::<Option<Vec<_>>>()?;
let skips = list("skips")?
.iter()
.map(|skip| {
Some(StaffSkip {
index: uint(skip, "index")?,
bbox: bbox_from_wire(skip.get("bbox")?)?,
reason: text(skip, "reason")?,
})
})
.collect::<Option<Vec<_>>>()?;
let warnings = list("warnings")?
.iter()
.map(|warning| {
Some(MusicWarning {
kind: crate::native_engine::tromr::music_warning_kind(
warning.get("kind")?.as_str()?,
)?,
part: uint(warning, "part")?,
measure: uint(warning, "measure")?,
detail: text(warning, "detail")?,
})
})
.collect::<Option<Vec<_>>>()?;
Some(MusicPageMeta {
staves,
skips,
warnings,
})
}
fn model_from_reply(reply: &Value) -> Option<ModelIdentity> {
let arch = reply
.get("model_id")
.and_then(Value::as_str)
.and_then(crate::native_engine::model_arch::arch_by_id)?;
Some(ModelIdentity {
model_id: arch.id(),
source_sha256: reply
.get("source_sha256")
.and_then(Value::as_str)
.map(str::to_owned),
})
}
pub struct ResidentRequest<'a> {
pub image: &'a Path,
pub model: PathBuf,
pub decode: DecodeOverrides,
pub preprocess: PreprocessOverrides,
pub format: bool,
pub square_pad: bool,
pub region: Option<Region>,
pub question: Option<&'a str>,
pub on_stage: Option<OnStage<'a>>,
}
pub type OnStage<'a> = &'a dyn Fn(&str, Option<Duration>);
fn options_value(request: &ResidentRequest<'_>) -> Value {
json!({
"decode": {
"max_length": request.decode.max_length,
"temperature": request.decode.temperature,
"no_repeat_ngram": request.decode.no_repeat_ngram,
"ngram_window": request.decode.ngram_window,
"max_page_tokens": request.decode.max_page_tokens,
},
"preprocess": {
"base_size": request.preprocess.base_size,
"image_size": request.preprocess.image_size,
"gundam": request.preprocess.gundam,
},
"format": request.format,
"square_pad": request.square_pad,
"region": region_to_wire(request.region),
"question": request.question,
})
}
fn region_to_wire(region: Option<Region>) -> Value {
match region {
None => Value::Null,
Some(Region::Box(coords)) => json!({ "box": coords }),
Some(Region::Point(coords)) => json!({ "point": coords }),
Some(Region::Color(color)) => json!({ "color": color.name() }),
}
}
fn region_from_wire(value: Option<&Value>) -> Option<Region> {
let value = value?;
let coords = |key: &str| -> Option<Vec<f64>> {
value
.get(key)?
.as_array()?
.iter()
.map(Value::as_f64)
.collect()
};
if let Some(coords) = coords("box") {
return coords.try_into().ok().map(Region::Box);
}
if let Some(coords) = coords("point") {
return coords.try_into().ok().map(Region::Point);
}
FrameColor::from_name(value.get("color")?.as_str()?).map(Region::Color)
}
pub fn try_recognize(request: &ResidentRequest<'_>) -> FocrResult<Option<ResidentReply>> {
let Some(admission) = crate::native_engine::resident_admission(&request.model) else {
return Ok(None);
};
let outcome = match connect(&request.model) {
Some(stream) => roundtrip(stream, request),
None => Ok(None),
};
if !matches!(outcome, Ok(None)) {
admission.announce();
}
Ok(outcome?.map(|mut reply| {
if let Some(report) = &mut reply.report {
report.admission_warnings = admission.warnings;
}
reply
}))
}
enum ConnectFailure {
NoState,
Refused,
Other,
}
fn connect(root: &Path) -> Option<TcpStream> {
state_path(root)?;
let mut last = ConnectFailure::NoState;
for attempt in 0..3 {
if attempt > 0 {
std::thread::sleep(Duration::from_millis(300));
}
match connect_once(root) {
Ok(stream) => return Some(stream),
Err(ConnectFailure::NoState) => {
last = ConnectFailure::NoState;
break;
}
Err(failure) => last = failure,
}
}
match last {
ConnectFailure::Refused => {
if let Some(path) = state_path(root)
&& path.exists()
{
let _ = fs::remove_file(&path);
}
}
ConnectFailure::NoState => {}
ConnectFailure::Other => return None,
}
spawn_daemon(root)?;
let deadline = Instant::now() + spawn_wait();
while Instant::now() < deadline {
if let Ok(stream) = connect_once(root) {
return Some(stream);
}
std::thread::sleep(Duration::from_millis(50));
}
None
}
fn connect_once(root: &Path) -> Result<TcpStream, ConnectFailure> {
let state = read_state(root).ok_or(ConnectFailure::NoState)?;
let stream = TcpStream::connect_timeout(
&(Ipv4Addr::LOCALHOST, state.port).into(),
Duration::from_millis(1000),
)
.map_err(|error| {
if error.kind() == std::io::ErrorKind::ConnectionRefused {
ConnectFailure::Refused
} else {
ConnectFailure::Other
}
})?;
stream
.set_read_timeout(Some(Duration::from_millis(500)))
.map_err(|_| ConnectFailure::Other)?;
stream.set_nodelay(true).ok();
Ok(stream)
}
fn spawn_daemon(root: &Path) -> Option<()> {
let exe = std::env::current_exe().ok()?;
let mut command = Command::new(exe); command
.arg("resident-daemon")
.arg("--model-root")
.arg(root)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
if let Ok(log) = std::env::var("FOCR_RESIDENT_LOG")
&& let Ok(file) = fs::OpenOptions::new().create(true).append(true).open(&log)
&& let Ok(err) = file.try_clone()
{
command.stdout(file).stderr(err);
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
command.creation_flags(0x0000_0008 | 0x0800_0000);
}
command.spawn().ok().map(|_child| ())
}
fn roundtrip(
mut stream: TcpStream,
request: &ResidentRequest<'_>,
) -> FocrResult<Option<ResidentReply>> {
let Some(state) = read_state(&request.model) else {
return Ok(None);
};
let image = std::path::absolute(request.image).unwrap_or_else(|_| request.image.to_path_buf());
let header = json!({
"protocol": PROTOCOL,
"op": "ocr",
"token": state.token,
"version": env!("CARGO_PKG_VERSION"),
"model_root": request.model.to_string_lossy(),
"image": image.to_string_lossy(),
"options": options_value(request),
"env": env_fingerprint(),
});
if stream
.write_all(format!("{header}\n").as_bytes())
.and_then(|()| stream.flush())
.is_err()
{
return Ok(None);
}
let waits = (CLIENT_ACK_WAIT, client_reply_wait());
let on_stage = |name: &str, budget: Option<Duration>| {
if let Some(on_stage) = request.on_stage {
on_stage(name, budget);
}
};
let Some(line) = read_reply(&mut stream, waits, crate::cancel_checkpoint, on_stage)? else {
return Ok(None);
};
let reply = reply_from_wire(&line);
if crate::shutdown_requested() {
return answer_after_cancel(reply);
}
reply
}
fn answer_after_cancel(
reply: FocrResult<Option<ResidentReply>>,
) -> FocrResult<Option<ResidentReply>> {
match reply {
Ok(Some(reply))
if reply.report.as_ref().is_some_and(|report| {
report.run_finish_reason() == crate::native_engine::FinishReason::Cancelled
}) =>
{
Ok(Some(reply))
}
_ => Err(FocrError::Cancelled),
}
}
fn reply_from_wire(line: &[u8]) -> FocrResult<Option<ResidentReply>> {
let Ok(reply) = serde_json::from_slice::<Value>(line) else {
return Ok(None);
};
if reply.get("ok").and_then(Value::as_bool) == Some(true) {
let markdown = reply
.get("markdown")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let Some(raw) = reply.get("raw").and_then(Value::as_str) else {
return Ok(None);
};
let layout = reply
.get("layout")
.and_then(Value::as_array)
.map(|spans| spans.iter().filter_map(LayoutSpan::from_json).collect())
.unwrap_or_default();
let report = match reply.get("report") {
None | Some(Value::Null) => None,
Some(wire) => match DecodeReport::deserialize(wire) {
Ok(report) => Some(report),
Err(_) => return Ok(None),
},
};
let music = match reply.get("music").map(music_from_wire) {
None => None,
Some(Some(music)) => Some(music),
Some(None) => return Ok(None),
};
let chart = match reply.get("chart").map(chart_from_wire) {
None => None,
Some(Some(chart)) => Some(chart),
Some(None) => return Ok(None),
};
return Ok(Some(ResidentReply {
document: RecognizedDocument {
markdown,
raw: raw.to_owned(),
layout,
},
report,
model: model_from_reply(&reply),
music,
chart,
}));
}
match reply.get("kind").and_then(Value::as_str) {
Some("ocr") => {
let code = reply
.get("exit_code")
.and_then(Value::as_i64)
.and_then(|c| i32::try_from(c).ok())
.unwrap_or(crate::error::EXIT_GENERIC);
let message = reply
.get("message")
.and_then(Value::as_str)
.unwrap_or("resident recognition failed")
.to_owned();
Err(wire_error(code, message))
}
_ => Ok(None),
}
}
fn read_reply(
stream: &mut TcpStream,
(ack_wait, reply_wait): (Duration, ReplyWait),
checkpoint: impl Fn() -> FocrResult<()>,
on_stage: impl Fn(&str, Option<Duration>),
) -> FocrResult<Option<Vec<u8>>> {
let started = Instant::now();
let mut deadline = Some(reply_wait.first(started));
let mut acknowledged = false;
let mut cancelled = false;
let mut buffer: Vec<u8> = Vec::new();
let mut chunk = [0_u8; 65536];
loop {
if !cancelled {
match checkpoint() {
Ok(()) => {}
Err(FocrError::Cancelled) => {
cancelled = true;
deadline = Some(Instant::now() + CANCEL_GRACE);
let _ = stream.shutdown(std::net::Shutdown::Write);
}
Err(error) => return Err(error),
}
}
let no_reply = || {
if cancelled {
Err(FocrError::Cancelled)
} else {
Ok(None)
}
};
if let Some(end) = buffer.iter().position(|&byte| byte == b'\n') {
let mut line: Vec<u8> = buffer.drain(..=end).collect();
line.pop();
if !acknowledged && is_acknowledgement(&line) {
acknowledged = true;
continue;
}
if let Some((name, budget)) = stage_from_wire(&line) {
if !cancelled {
deadline = reply_wait.after_stage(started, Instant::now(), budget, deadline);
on_stage(&name, budget);
}
continue;
}
return Ok(Some(line));
}
if buffer.len() >= MAX_REPLY_BYTES {
return no_reply();
}
let now = Instant::now();
if deadline.is_some_and(|deadline| now >= deadline)
|| (!acknowledged && !cancelled && now.duration_since(started) >= ack_wait)
{
return no_reply();
}
match stream.read(&mut chunk) {
Ok(0) => return no_reply(),
Ok(count) => buffer.extend_from_slice(&chunk[..count]),
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) => {}
Err(_) => return no_reply(),
}
}
}
fn is_acknowledgement(line: &[u8]) -> bool {
serde_json::from_slice::<Value>(line)
.is_ok_and(|reply| reply.get("kind").and_then(Value::as_str) == Some(ACK_KIND))
}
fn stage_from_wire(line: &[u8]) -> Option<(String, Option<Duration>)> {
let line = serde_json::from_slice::<Value>(line).ok()?;
if line.get("kind")?.as_str()? != STAGE_KIND {
return None;
}
let budget = line.get("budget_ms").and_then(Value::as_u64);
Some((
line.get("name")?.as_str()?.to_owned(),
budget.map(Duration::from_millis),
))
}
fn wire_message(error: &FocrError) -> String {
match error {
FocrError::Usage(message)
| FocrError::ModelNotFound(message)
| FocrError::InputDecode(message)
| FocrError::Timeout(message)
| FocrError::FormatMismatch(message) => message.clone(),
other => other.to_string(),
}
}
fn wire_error(exit_code: i32, message: String) -> FocrError {
match exit_code {
EXIT_USAGE => FocrError::Usage(message),
EXIT_MODEL_NOT_FOUND => FocrError::ModelNotFound(message),
EXIT_INPUT_DECODE => FocrError::InputDecode(message),
EXIT_TIMEOUT => FocrError::Timeout(message),
EXIT_CANCELLED => FocrError::Cancelled,
EXIT_FORMAT_MISMATCH => FocrError::FormatMismatch(message),
_ => FocrError::Other(anyhow::anyhow!(message)),
}
}
struct ResidentEngine {
engine: OcrEngine,
fingerprint: String,
stamp: (u64, u64),
}
fn accept_connections(listener: &TcpListener, queue: &std::sync::mpsc::SyncSender<TcpStream>) {
let acknowledgement = format!("{}\n", json!({ "ok": true, "kind": ACK_KIND }));
for stream in listener.incoming() {
let Ok(mut stream) = stream else {
return;
};
let _ = stream.write_all(acknowledgement.as_bytes());
if queue.send(stream).is_err() {
return;
}
}
}
pub fn run_daemon(model_root: &Path) -> FocrResult<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
.map_err(|error| FocrError::Other(anyhow::anyhow!("resident daemon bind: {error}")))?;
let port = listener
.local_addr()
.map_err(|error| FocrError::Other(anyhow::anyhow!("resident daemon addr: {error}")))?
.port();
let token = fresh_token();
let state_file = write_state(model_root, port, &token)
.map_err(|error| FocrError::Other(anyhow::anyhow!("resident state write: {error}")))?;
let (queue, connections) = std::sync::mpsc::sync_channel(ACCEPT_QUEUE);
std::thread::Builder::new()
.name("focr-resident-accept".into())
.spawn(move || accept_connections(&listener, &queue))
.map_err(|error| FocrError::Other(anyhow::anyhow!("resident acceptor: {error}")))?;
eprintln!(
"focr resident daemon serving {} on 127.0.0.1:{port}",
model_root.display()
);
let idle = idle_period();
let mut resident: Option<ResidentEngine> = None;
let mut deadline = Instant::now() + idle;
loop {
match connections.recv_timeout(deadline.saturating_duration_since(Instant::now())) {
Ok(stream) => {
crate::reset_shutdown();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handle_connection(stream, model_root, &token, port, &mut resident);
}));
if outcome.is_err() {
eprintln!(
"focr resident daemon: request panicked; connection dropped, model kept"
);
}
deadline = Instant::now() + idle;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
eprintln!("focr resident daemon idle exit");
remove_state_if_ours(&state_file, port);
return Ok(());
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
remove_state_if_ours(&state_file, port);
return Ok(());
}
}
}
}
fn remove_state_if_ours(state_file: &Path, port: u16) {
let ours = fs::read_to_string(state_file)
.ok()
.and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
.and_then(|value| value.get("port")?.as_u64())
.is_some_and(|recorded| recorded == u64::from(port));
if ours {
let _ = fs::remove_file(state_file);
}
}
fn decode_overrides_from_wire(options: &Value) -> DecodeOverrides {
let decode = options.get("decode").cloned().unwrap_or(Value::Null);
let usize_of = |key: &str| {
decode
.get(key)
.and_then(Value::as_u64)
.and_then(|n| usize::try_from(n).ok())
};
DecodeOverrides {
max_length: usize_of("max_length"),
temperature: decode
.get("temperature")
.and_then(Value::as_f64)
.map(|t| t as f32),
no_repeat_ngram: usize_of("no_repeat_ngram"),
ngram_window: usize_of("ngram_window"),
max_page_tokens: usize_of("max_page_tokens"),
}
}
fn preprocess_overrides_from_wire(options: &Value) -> PreprocessOverrides {
let preprocess = options.get("preprocess").cloned().unwrap_or(Value::Null);
let usize_of = |key: &str| {
preprocess
.get(key)
.and_then(Value::as_u64)
.and_then(|n| usize::try_from(n).ok())
};
PreprocessOverrides {
base_size: usize_of("base_size"),
image_size: usize_of("image_size"),
gundam: preprocess.get("gundam").and_then(Value::as_bool),
}
}
fn load_fingerprint(options: &Value) -> String {
json!({
"decode": options.get("decode").cloned().unwrap_or(Value::Null),
"preprocess": options.get("preprocess").cloned().unwrap_or(Value::Null),
})
.to_string()
}
fn stop_when_client_leaves(stream: &TcpStream, done: &AtomicBool, stop: impl Fn()) {
let _ = stream.set_read_timeout(Some(CLIENT_WATCH_TICK));
let mut reader = stream;
let mut byte = [0_u8; 1];
while !done.load(Ordering::Acquire) {
let gone = match reader.read(&mut byte) {
Ok(count) => count == 0,
Err(error) => !matches!(
error.kind(),
std::io::ErrorKind::WouldBlock
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::Interrupted
),
};
if gone {
if !done.load(Ordering::Acquire) {
stop();
}
return;
}
}
}
fn handle_connection(
stream: TcpStream,
model_root: &Path,
token: &str,
port: u16,
resident: &mut Option<ResidentEngine>,
) {
let _ = stream.set_nonblocking(false);
let _ = stream.set_read_timeout(Some(Duration::from_secs(10)));
let _ = stream.set_write_timeout(Some(Duration::from_secs(60)));
let _ = stream.set_nodelay(true);
const REQUEST_DEADLINE: Duration = Duration::from_secs(30);
let mut stream = stream;
let deadline = Instant::now() + REQUEST_DEADLINE;
let mut buffer: Vec<u8> = Vec::new();
let mut chunk = [0_u8; 4096];
let newline = loop {
if Instant::now() >= deadline {
return;
}
match stream.read(&mut chunk) {
Ok(0) => return,
Ok(count) => {
buffer.extend_from_slice(&chunk[..count]);
if let Some(position) = buffer.iter().position(|&byte| byte == b'\n') {
break position;
}
if buffer.len() >= MAX_REQUEST_BYTES {
let reply =
json!({ "ok": false, "kind": "request", "message": "request too large" });
let _ = stream.write_all(format!("{reply}\n").as_bytes());
return;
}
}
Err(_) => return,
}
};
let Ok(request) = serde_json::from_slice::<Value>(&buffer[..newline]) else {
return;
};
let refuse = |stream: &mut TcpStream, kind: &str, message: &str| {
let reply = json!({ "ok": false, "kind": kind, "message": message });
let _ = stream.write_all(format!("{reply}\n").as_bytes());
};
let token_matches = |candidate: &str| {
let (a, b) = (candidate.as_bytes(), token.as_bytes());
if a.len() != b.len() {
return false;
}
a.iter().zip(b).fold(0_u8, |acc, (x, y)| acc | (x ^ y)) == 0
};
if !request
.get("token")
.and_then(Value::as_str)
.is_some_and(token_matches)
{
refuse(&mut stream, "auth", "bad token");
return;
}
if request.get("protocol").and_then(Value::as_u64) != Some(PROTOCOL)
|| request.get("version").and_then(Value::as_str) != Some(env!("CARGO_PKG_VERSION"))
{
refuse(&mut stream, "version", "resident daemon version mismatch");
if let Some(state) = state_path(model_root) {
remove_state_if_ours(&state, port);
}
std::process::exit(0);
}
if let Some(wire_root) = request.get("model_root").and_then(Value::as_str) {
let wire_canonical =
fs::canonicalize(wire_root).unwrap_or_else(|_| PathBuf::from(wire_root));
let own_canonical =
fs::canonicalize(model_root).unwrap_or_else(|_| model_root.to_path_buf());
if wire_canonical != own_canonical {
refuse(
&mut stream,
"request",
"resident daemon serves a different model",
);
return;
}
}
if request.get("env").cloned().unwrap_or_else(|| json!({})) != env_fingerprint() {
refuse(
&mut stream,
"env",
"inference environment differs from the resident daemon's",
);
return;
}
let stamp_now = artifact_stamp(model_root);
if let Some(engine) = resident.as_ref()
&& engine.stamp != stamp_now
{
refuse(&mut stream, "stale", "model artifact changed since load");
if let Some(state) = state_path(model_root) {
remove_state_if_ours(&state, port);
}
std::process::exit(0);
}
let options = request.get("options").cloned().unwrap_or_else(|| json!({}));
let fingerprint = load_fingerprint(&options);
if resident
.as_ref()
.is_some_and(|engine| engine.fingerprint != fingerprint)
{
*resident = None;
}
crate::native_engine::set_decode_overrides(decode_overrides_from_wire(&options));
crate::native_engine::set_preprocess_overrides(preprocess_overrides_from_wire(&options));
crate::native_engine::force_got_format(
options
.get("format")
.and_then(Value::as_bool)
.unwrap_or(false),
);
crate::native_engine::force_got_square_pad(
options
.get("square_pad")
.and_then(Value::as_bool)
.unwrap_or(false),
);
crate::native_engine::set_got_region(region_from_wire(options.get("region")));
crate::native_engine::set_smolvlm2_question(
options
.get("question")
.and_then(Value::as_str)
.map(str::to_owned),
);
if resident.is_none() {
match OcrEngine::new() {
Ok(engine) => {
*resident = Some(ResidentEngine {
engine,
fingerprint,
stamp: stamp_now,
});
}
Err(error) => {
refuse(
&mut stream,
"engine",
&format!("resident engine start failed: {error}"),
);
return;
}
}
}
let Some(engine) = resident.as_ref() else {
return;
};
let image = PathBuf::from(request.get("image").and_then(Value::as_str).unwrap_or(""));
if image.as_os_str().is_empty() {
refuse(&mut stream, "request", "missing image path");
return;
}
if let Ok(client) = stream.try_clone() {
engine
.engine
.set_stage_sink(Some(Box::new(move |name, budget| {
let mut line = json!({ "kind": STAGE_KIND, "name": name });
if let Some(budget) = budget {
line["budget_ms"] =
u64::try_from(budget.as_millis()).unwrap_or(u64::MAX).into();
}
let _ = (&client).write_all(format!("{line}\n").as_bytes());
})));
}
let take_run_meta = || {
(
engine.engine.take_last_decode_report(),
engine.engine.take_music_page_meta(),
engine.engine.take_chart_page_meta(),
)
};
drop(take_run_meta());
let done = AtomicBool::new(false);
let watch = stream.try_clone().ok();
let outcome = std::thread::scope(|scope| {
if let Some(watch) = &watch {
scope.spawn(|| stop_when_client_leaves(watch, &done, crate::request_shutdown));
}
let outcome = engine
.engine
.recognize_with_layout_model(model_root, &image);
done.store(true, Ordering::Release);
if let Some(watch) = &watch {
let _ = watch.shutdown(std::net::Shutdown::Read);
}
outcome
});
engine.engine.set_stage_sink(None);
let (report, music, chart) = take_run_meta();
match outcome {
Ok(document) => {
let layout: Vec<Value> = document.layout.iter().map(LayoutSpan::to_json).collect();
let mut reply = json!({
"ok": true,
"markdown": document.markdown,
"raw": document.raw,
"layout": layout,
"report": report,
});
if let Some(music) = music {
reply["music"] = music_to_wire(&music);
}
if let Some(chart) = chart {
reply["chart"] = chart_to_wire(&chart);
}
if let Some(model) = engine.engine.loaded_model_identity() {
model.merge_into(&mut reply);
}
let _ = stream.write_all(format!("{reply}\n").as_bytes());
let _ = stream.flush();
}
Err(error) => {
let reply = json!({
"ok": false,
"kind": "ocr",
"exit_code": error.exit_code(),
"message": wire_message(&error),
});
let _ = stream.write_all(format!("{reply}\n").as_bytes());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_digest_distinguishes_roots_and_is_stable() {
let a = root_digest(Path::new("/tmp/model-a.focrq"));
let b = root_digest(Path::new("/tmp/model-b.focrq"));
assert_ne!(a, b);
assert_eq!(a, root_digest(Path::new("/tmp/model-a.focrq")));
}
#[test]
fn tokens_are_distinct_and_hex() {
let one = fresh_token();
let two = fresh_token();
assert_eq!(one.len(), 32);
assert!(one.bytes().all(|b| b.is_ascii_hexdigit()));
assert_ne!(one, two, "two RandomState-seeded tokens collided");
}
const ACK: &[u8] = b"{\"ok\":true,\"kind\":\"accepted\"}\n";
const LONG: Duration = Duration::from_secs(30);
const SHORT: Duration = Duration::from_millis(500);
const LONG_WAIT: ReplyWait = ReplyWait::Fixed(LONG);
const HOLD: Duration = Duration::from_secs(8);
fn read(stream: &mut TcpStream, waits: (Duration, ReplyWait)) -> Option<Vec<u8>> {
read_reply(stream, waits, || Ok(()), |_, _| {}).unwrap()
}
fn client_of(daemon: impl FnOnce(TcpStream) + Send + 'static) -> TcpStream {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let port = listener.local_addr().unwrap().port();
std::thread::spawn(move || daemon(listener.accept().unwrap().0));
let stream = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
stream
.set_read_timeout(Some(Duration::from_millis(10)))
.unwrap();
stream
}
#[test]
fn a_silent_daemon_is_given_up_on_at_the_ack_wait() {
let mut stream = client_of(|stream| {
std::thread::sleep(HOLD);
drop(stream);
});
let started = Instant::now();
assert_eq!(read(&mut stream, (SHORT, LONG_WAIT)), None);
assert!(started.elapsed() < HOLD);
}
#[test]
fn a_cancelled_wait_without_an_answer_ends_cancelled() {
let mut stream = client_of(|mut stream| {
stream.write_all(ACK).unwrap();
stream
.write_all(b"{\"budget_ms\":600000,\"kind\":\"stage\",\"name\":\"forward\"}\n")
.unwrap();
std::thread::sleep(HOLD);
});
let started = Instant::now();
let out = read_reply(
&mut stream,
(LONG, ReplyWait::Staged(SHORT)),
|| Err(FocrError::Cancelled),
|_, _| {},
);
let waited = started.elapsed();
assert!(matches!(out, Err(FocrError::Cancelled)), "{out:?}");
assert!(waited >= CANCEL_GRACE && waited < HOLD, "{waited:?}");
}
#[test]
fn a_daemon_that_hangs_up_on_a_cancelled_wait_leaves_it_cancelled() {
let mut stream = client_of(|mut stream| stream.write_all(ACK).unwrap());
let started = Instant::now();
let out = read_reply(
&mut stream,
(LONG, LONG_WAIT),
|| Err(FocrError::Cancelled),
|_, _| {},
);
assert!(matches!(out, Err(FocrError::Cancelled)), "{out:?}");
assert!(started.elapsed() < CANCEL_GRACE);
}
#[test]
fn a_cancelled_wait_reads_the_answer_its_cancel_produced() {
let mut stream = client_of(|mut stream| {
stream.write_all(ACK).unwrap();
stream.set_read_timeout(Some(HOLD)).unwrap();
assert_eq!(stream.read(&mut [0_u8; 1]).unwrap(), 0);
stream
.write_all(b"{\"kind\":\"stage\",\"name\":\"forward\"}\n")
.unwrap();
stream
.write_all(b"{\"ok\":true,\"raw\":\"so far\"}\n")
.unwrap();
std::thread::sleep(HOLD);
});
let heard = std::cell::RefCell::new(Vec::new());
let reply = read_reply(
&mut stream,
(LONG, LONG_WAIT),
|| Err(FocrError::Cancelled),
|name, _| heard.borrow_mut().push(name.to_owned()),
);
assert_eq!(reply.unwrap().unwrap(), b"{\"ok\":true,\"raw\":\"so far\"}");
assert!(heard.into_inner().is_empty());
}
#[test]
fn only_a_cancelled_answer_stands_after_a_cancel() {
use crate::native_engine::{FinishReason, PageFinish, decode_warnings, partial_output};
let served = |report: DecodeReport| {
let reply = json!({ "ok": true, "markdown": "m", "raw": "so far", "report": report });
reply_from_wire(reply.to_string().as_bytes())
};
let mut cancelled = DecodeReport {
finish_reason: FinishReason::Cancelled,
..budget_report()
};
if let Some(document) = &mut cancelled.document {
document.page_finish = vec![PageFinish::Cancelled];
}
let kept = answer_after_cancel(served(cancelled))
.unwrap()
.expect("the page so far");
assert_eq!(kept.document.raw, "so far");
let report = kept.report.expect("its report");
let model = ModelIdentity {
model_id: "unlimited-ocr-2",
source_sha256: None,
};
let exit =
partial_output(&report, &decode_warnings(&report), &model).map(|e| e.exit_code());
assert_eq!(exit, Some(EXIT_CANCELLED));
let finished = DecodeReport {
finish_reason: FinishReason::Stop,
length_stops: 0,
..budget_report()
};
for (case, reply) in [
("a page the daemon finished", served(finished)),
("a page its budget stopped", served(budget_report())),
("no answer", Ok(None)),
(
"a daemon error",
Err(FocrError::Other(anyhow::anyhow!("x"))),
),
] {
let out = answer_after_cancel(reply);
assert!(matches!(out, Err(FocrError::Cancelled)), "{case}");
}
}
fn connection() -> (TcpStream, TcpStream) {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
(listener.accept().unwrap().0, client)
}
#[test]
fn a_client_that_hangs_up_stops_the_request() {
let (daemon, client) = connection();
let (done, stopped) = (AtomicBool::new(false), AtomicBool::new(false));
let stopped_early = std::thread::scope(|scope| {
let watcher = scope.spawn(|| {
stop_when_client_leaves(&daemon, &done, || stopped.store(true, Ordering::Release));
});
std::thread::sleep(3 * CLIENT_WATCH_TICK);
let early = stopped.load(Ordering::Acquire);
drop(client);
watcher.join().unwrap();
early
});
assert!(!stopped_early, "stopped while its client still waited");
assert!(stopped.load(Ordering::Acquire));
}
#[test]
fn a_finished_request_is_not_stopped_by_its_client_leaving() {
let (daemon, client) = connection();
let (done, stopped) = (AtomicBool::new(false), AtomicBool::new(false));
std::thread::scope(|scope| {
let watcher = scope.spawn(|| {
stop_when_client_leaves(&daemon, &done, || stopped.store(true, Ordering::Release));
});
done.store(true, Ordering::Release);
let _ = daemon.shutdown(std::net::Shutdown::Read);
watcher.join().unwrap();
});
drop(client);
assert!(!stopped.load(Ordering::Acquire));
}
#[test]
fn stages_reach_the_client_on_the_way_to_the_reply() {
let mut stream = client_of(|mut stream| {
stream.write_all(ACK).unwrap();
stream
.write_all(b"{\"kind\":\"stage\",\"name\":\"load\"}\n")
.unwrap();
stream
.write_all(b"{\"budget_ms\":600000,\"kind\":\"stage\",\"name\":\"forward\"}\n")
.unwrap();
stream.write_all(b"{\"ok\":true,\"raw\":\"r\"}\n").unwrap();
});
let heard = std::cell::RefCell::new(Vec::new());
let reply = read_reply(
&mut stream,
(LONG, LONG_WAIT),
|| Ok(()),
|name, budget| heard.borrow_mut().push((name.to_owned(), budget)),
)
.unwrap()
.unwrap();
assert_eq!(reply, b"{\"ok\":true,\"raw\":\"r\"}");
assert_eq!(
heard.into_inner(),
[
("load".to_owned(), None),
("forward".to_owned(), Some(Duration::from_secs(600))),
]
);
}
fn sample_music() -> MusicPageMeta {
MusicPageMeta {
staves: vec![(0, (1, 2, 3, 4)), (2, (5, 6, 7, 8))],
skips: vec![StaffSkip {
index: 1,
bbox: (9, 10, 11, 12),
reason: "width guard".into(),
}],
warnings: vec![
MusicWarning {
kind: "overfull_bar",
part: 1,
measure: 3,
detail: "5/4 in 4/4".into(),
},
MusicWarning {
kind: "key_mismatch",
part: 2,
measure: 0,
detail: "G against D".into(),
},
],
}
}
#[test]
fn music_metadata_crosses_the_wire_whole() {
let wire = music_to_wire(&sample_music());
let back = music_from_wire(&wire).expect("the wire shape parses");
assert_eq!(music_to_wire(&back), wire);
assert_eq!(back.staves, sample_music().staves);
assert_eq!(back.skips[0].reason, "width guard");
assert_eq!(back.warnings[1].kind, "key_mismatch");
}
#[test]
fn music_metadata_that_cannot_be_read_is_not_half_reported() {
let wire = music_to_wire(&sample_music());
for (pointer, broken) in [
("/staves/0/bbox", json!([1, 2, 3])),
("/skips/0/reason", json!(7)),
("/warnings/0/kind", json!("no_such_warning")),
("/warnings/1/part", json!(-1)),
] {
let mut bad = wire.clone();
*bad.pointer_mut(pointer).expect(pointer) = broken;
assert!(music_from_wire(&bad).is_none(), "{pointer}");
}
assert!(music_from_wire(&json!({ "staves": [] })).is_none());
}
#[test]
fn a_chart_head_crosses_the_wire_whole_or_not_at_all() {
let meta = ChartPageMeta {
pred_locs: vec![0.25, 1.0],
reliable_distance: Some(0.5),
reliable: Some(false),
};
assert_eq!(chart_from_wire(&chart_to_wire(&meta)), Some(meta.clone()));
let unverifiable = ChartPageMeta {
reliable_distance: None,
reliable: None,
..meta
};
assert_eq!(
chart_from_wire(&chart_to_wire(&unverifiable)),
Some(unverifiable.clone())
);
let wire = chart_to_wire(&unverifiable);
for (key, broken) in [
("pred_locs", json!(["x"])),
("reliable_distance", json!("far")),
("reliable", json!(1)),
] {
let mut bad = wire.clone();
bad[key] = broken;
assert!(chart_from_wire(&bad).is_none(), "{key}");
}
let reply = json!({ "ok": true, "markdown": "m", "raw": "r", "chart": { "reliable": 1 } });
let served = reply_from_wire(reply.to_string().as_bytes()).unwrap();
assert!(served.is_none(), "an unreadable head falls back inline");
}
#[test]
fn a_served_run_reports_music_it_can_read_and_otherwise_runs_inline() {
let served = |music: Value| {
let reply = json!({ "ok": true, "markdown": "m", "raw": "r", "music": music });
reply_from_wire(reply.to_string().as_bytes()).unwrap()
};
let reply = served(music_to_wire(&sample_music())).expect("a whole reply");
assert_eq!(reply.music.expect("music").staves.len(), 2);
assert!(served(json!({ "staves": 1 })).is_none());
let plain = json!({ "ok": true, "markdown": "m", "raw": "r" });
let reply = reply_from_wire(plain.to_string().as_bytes()).unwrap();
assert!(reply.expect("a whole reply").music.is_none());
}
#[test]
fn a_served_error_reads_as_the_inline_one_does() {
let errors = [
FocrError::Usage("bad flag".into()),
FocrError::ModelNotFound("gone".into()),
FocrError::InputDecode("not an image".into()),
FocrError::Timeout("forward stage exceeded 1ms budget".into()),
FocrError::FormatMismatch("newer format".into()),
FocrError::Cancelled,
FocrError::Other(anyhow::anyhow!("broke")),
];
for error in errors {
let served = wire_error(error.exit_code(), wire_message(&error));
assert_eq!(served.to_string(), error.to_string());
assert_eq!(served.exit_code(), error.exit_code());
}
}
#[test]
fn an_acknowledged_request_may_outlast_the_ack_wait() {
let mut stream = client_of(|mut stream| {
stream.write_all(ACK).unwrap();
std::thread::sleep(SHORT * 2);
stream.write_all(b"{\"ok\":true}\n").unwrap();
});
let reply = read(&mut stream, (SHORT, LONG_WAIT));
assert_eq!(reply.as_deref(), Some(&b"{\"ok\":true}"[..]));
}
#[test]
fn an_acknowledged_request_is_still_bounded_by_the_reply_wait() {
for wait in [ReplyWait::Fixed(SHORT * 2), ReplyWait::Staged(SHORT * 2)] {
let mut stream = client_of(|mut stream| {
stream.write_all(ACK).unwrap();
std::thread::sleep(HOLD);
drop(stream);
});
let started = Instant::now();
assert_eq!(read(&mut stream, (SHORT, wait)), None, "{wait:?}");
assert!(started.elapsed() < HOLD, "{wait:?}");
}
}
#[test]
fn an_acknowledgement_and_a_reply_in_one_read_are_both_taken() {
let mut stream = client_of(|mut stream| {
stream
.write_all(&[ACK, b"{\"ok\":true}\n"].concat())
.unwrap();
std::thread::sleep(HOLD);
});
let reply = read(&mut stream, (SHORT, LONG_WAIT));
assert_eq!(reply.as_deref(), Some(&b"{\"ok\":true}"[..]));
}
#[test]
fn a_refusal_needs_no_acknowledgement() {
let refusal = b"{\"ok\":false,\"kind\":\"version\"}\n";
let mut stream = client_of(move |mut stream| {
stream.write_all(refusal).unwrap();
std::thread::sleep(HOLD);
});
let reply = read(&mut stream, (SHORT, LONG_WAIT));
assert_eq!(reply.as_deref(), Some(&refusal[..refusal.len() - 1]));
}
#[test]
fn a_busy_worker_does_not_delay_the_acknowledgement() {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let (queue, _connections) = std::sync::mpsc::sync_channel(ACCEPT_QUEUE);
std::thread::spawn(move || accept_connections(&listener, &queue));
for _ in 0..2 {
let mut stream = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
stream.set_read_timeout(Some(LONG)).unwrap();
let mut line = vec![0_u8; ACK.len()];
stream.read_exact(&mut line).unwrap();
assert!(is_acknowledgement(line.trim_ascii_end()), "{line:?}");
}
}
#[test]
fn wire_errors_keep_their_exit_class() {
let cases: &[(i32, i32)] = &[
(EXIT_USAGE, EXIT_USAGE),
(EXIT_MODEL_NOT_FOUND, EXIT_MODEL_NOT_FOUND),
(EXIT_INPUT_DECODE, EXIT_INPUT_DECODE),
(EXIT_TIMEOUT, EXIT_TIMEOUT),
(EXIT_CANCELLED, EXIT_CANCELLED),
(EXIT_FORMAT_MISMATCH, EXIT_FORMAT_MISMATCH),
(crate::error::EXIT_GENERIC, crate::error::EXIT_GENERIC),
(99, crate::error::EXIT_GENERIC),
];
for &(wire, expected) in cases {
assert_eq!(wire_error(wire, "x".into()).exit_code(), expected);
}
}
#[test]
fn options_round_trip_through_the_wire_shape() {
let request = ResidentRequest {
image: Path::new("/tmp/x.png"),
model: PathBuf::from("/tmp/m.focrq"),
decode: DecodeOverrides {
max_length: Some(4096),
temperature: Some(0.5),
no_repeat_ngram: None,
ngram_window: Some(512),
max_page_tokens: Some(0),
},
preprocess: PreprocessOverrides {
base_size: Some(1024),
image_size: None,
gundam: Some(true),
},
format: true,
square_pad: true,
region: Some(Region::Box([10.0, 20.5, 300.0, 400.0])),
question: Some("what?"),
on_stage: None,
};
let wire = options_value(&request);
assert_eq!(wire["square_pad"], true);
assert_eq!(
region_from_wire(wire.get("region")),
Some(Region::Box([10.0, 20.5, 300.0, 400.0]))
);
for region in [
None,
Some(Region::Point([3.0, 4.0])),
Some(Region::Color(FrameColor::Green)),
] {
let wire = json!({ "region": region_to_wire(region) });
assert_eq!(region_from_wire(wire.get("region")), region, "{region:?}");
}
let decode = decode_overrides_from_wire(&wire);
assert_eq!(decode.max_length, Some(4096));
assert_eq!(decode.temperature, Some(0.5));
assert_eq!(decode.no_repeat_ngram, None);
assert_eq!(decode.ngram_window, Some(512));
assert_eq!(decode.max_page_tokens, Some(0));
let preprocess = preprocess_overrides_from_wire(&wire);
assert_eq!(preprocess.base_size, Some(1024));
assert_eq!(preprocess.image_size, None);
assert_eq!(preprocess.gundam, Some(true));
assert_eq!(wire.get("format").and_then(Value::as_bool), Some(true));
assert_eq!(wire.get("question").and_then(Value::as_str), Some("what?"));
}
#[test]
fn load_fingerprint_ignores_forward_read_options() {
let base = json!({
"decode": {"max_length": 100},
"preprocess": {"gundam": null},
"format": false,
"question": null,
});
let mut format_flipped = base.clone();
format_flipped["format"] = json!(true);
assert_eq!(load_fingerprint(&base), load_fingerprint(&format_flipped));
let mut decode_changed = base.clone();
decode_changed["decode"]["max_length"] = json!(200);
assert_ne!(load_fingerprint(&base), load_fingerprint(&decode_changed));
}
#[test]
fn daemon_refuses_bad_token() {
use std::io::{BufRead, BufReader};
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let mut resident = None;
handle_connection(
stream,
Path::new("/nonexistent/model.focrq"),
"right-token",
0,
&mut resident,
);
});
let mut stream = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
let request = json!({
"protocol": PROTOCOL,
"op": "ocr",
"token": "wrong-token",
"version": env!("CARGO_PKG_VERSION"),
"model_root": "/nonexistent/model.focrq",
"image": "/tmp/x.png",
"options": {},
"env": {},
});
stream.write_all(format!("{request}\n").as_bytes()).unwrap();
let mut reply = String::new();
BufReader::new(&mut stream).read_line(&mut reply).unwrap();
let value: Value = serde_json::from_str(&reply).unwrap();
assert_eq!(value.get("ok").and_then(Value::as_bool), Some(false));
assert_eq!(value.get("kind").and_then(Value::as_str), Some("auth"));
handle.join().unwrap();
}
#[test]
fn daemon_refuses_mismatched_env_fingerprint() {
use std::io::{BufRead, BufReader};
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let mut resident = None;
handle_connection(
stream,
Path::new("/nonexistent/model.focrq"),
"tok",
0,
&mut resident,
);
});
let mut stream = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
let request = json!({
"protocol": PROTOCOL,
"op": "ocr",
"token": "tok",
"version": env!("CARGO_PKG_VERSION"),
"model_root": "/nonexistent/model.focrq",
"image": "/tmp/x.png",
"options": {},
"env": {"FOCR_DECODE_INT8": "1", "__focr_test_unlikely": "x"},
});
stream.write_all(format!("{request}\n").as_bytes()).unwrap();
let mut reply = String::new();
BufReader::new(&mut stream).read_line(&mut reply).unwrap();
let value: Value = serde_json::from_str(&reply).unwrap();
assert_eq!(value.get("ok").and_then(Value::as_bool), Some(false));
assert_eq!(value.get("kind").and_then(Value::as_str), Some("env"));
handle.join().unwrap();
}
fn budget_report() -> DecodeReport {
use crate::native_engine::{DocumentStats, FinishReason, PageBodies, PageFinish};
DecodeReport {
ids: vec![7, 8, 9],
finish_reason: FinishReason::Budget,
cap: None,
pages: Some(PageBodies {
expected: 1,
bodies: 1,
}),
max_length: 8192,
max_positions: 32768,
decodes: 1,
length_stops: 1,
runaway_stops: 0,
document: Some(DocumentStats {
page_finish: vec![PageFinish::Budget],
page_tokens: vec![3],
leading_text: true,
..DocumentStats::default()
}),
admission_warnings: Vec::new(),
}
}
#[test]
fn a_decode_report_crosses_the_wire_with_all_the_run_reports() {
use crate::native_engine::{FinishReason, decode_warnings, partial_output};
let inline = budget_report();
let reply = json!({ "ok": true, "markdown": "m", "raw": "r", "report": inline });
let served = reply_from_wire(reply.to_string().as_bytes())
.unwrap()
.expect("a whole reply");
let report = served.report.expect("the report");
assert_eq!(
report,
DecodeReport {
ids: Vec::new(),
..budget_report()
}
);
assert_eq!(report.run_finish_reason(), FinishReason::Budget);
assert_eq!(decode_warnings(&report), decode_warnings(&inline));
let model = ModelIdentity {
model_id: "unlimited-ocr-2",
source_sha256: None,
};
let exit = |report: &DecodeReport| {
partial_output(report, &decode_warnings(report), &model).map(|e| e.exit_code())
};
assert_eq!(exit(&report), Some(5));
}
#[test]
fn a_report_that_cannot_be_read_runs_inline() {
let served = |report: Value| {
let reply = json!({ "ok": true, "markdown": "m", "raw": "r", "report": report });
reply_from_wire(reply.to_string().as_bytes()).unwrap()
};
let none = served(Value::Null).expect("a reply without a report");
assert!(none.report.is_none());
let mut wire = serde_json::to_value(budget_report()).unwrap();
wire["finish_reason"] = json!("sideways");
assert!(served(wire).is_none());
assert!(
served(json!({ "finish_reason": "stop" })).is_none(),
"half a report"
);
}
#[test]
fn the_default_wait_lasts_as_long_as_the_daemon_stage_can() {
let started = Instant::now();
let now = started + Duration::from_secs(5);
let floor = DEFAULT_CLIENT_READ_TIMEOUT;
let first = Some(started + floor);
assert_eq!(
ReplyWait::Fixed(floor).after_stage(started, now, None, first),
first,
"a fixed wait ignores the daemon's stages"
);
let staged = ReplyWait::Staged(floor);
assert_eq!(
staged.after_stage(started, now, Some(Duration::from_secs(1)), first),
first,
"a short stage keeps the floor"
);
let budget = Duration::from_secs(750);
let bound = budget + crate::native_engine::document_budget_grace(budget) + REPLY_SLACK;
assert_eq!(
staged.after_stage(started, now, Some(budget), first),
Some(now + bound)
);
assert_eq!(
staged.after_stage(started, now, None, first),
None,
"a stage without a budget has no deadline, as inline"
);
assert!(
staged
.after_stage(started, now, Some(Duration::MAX), first)
.is_some(),
"an absurd budget is clamped, not an overflow panic"
);
}
#[test]
fn a_stage_holds_the_default_wait_past_its_floor() {
const BUDGETED: &[u8] = b"{\"budget_ms\":100,\"kind\":\"stage\",\"name\":\"forward\"}\n";
const UNBUDGETED: &[u8] = b"{\"kind\":\"stage\",\"name\":\"forward\"}\n";
const REPLY: &[u8] = b"{\"ok\":true,\"raw\":\"r\"}\n";
let daemon = |stage: &'static [u8]| {
move |mut stream: TcpStream| {
stream.write_all(ACK).unwrap();
stream.write_all(stage).unwrap();
std::thread::sleep(SHORT * 3);
let _ = stream.write_all(REPLY);
}
};
for stage in [BUDGETED, UNBUDGETED] {
let mut stream = client_of(daemon(stage));
let reply = read(&mut stream, (LONG, ReplyWait::Staged(SHORT)));
assert_eq!(reply.as_deref(), Some(&REPLY[..REPLY.len() - 1]));
}
let mut stream = client_of(daemon(BUDGETED));
assert_eq!(read(&mut stream, (LONG, ReplyWait::Fixed(SHORT))), None);
}
}