1use std::io::{self, BufRead, BufReader, Write};
12use std::path::{Path, PathBuf};
13use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
14
15use serde_json::Value;
16use xil_core::pyjson::{dumps, Style};
17
18pub struct Worker {
20 child: Child,
21 stdin: Option<ChildStdin>,
22 stdout: BufReader<ChildStdout>,
23}
24
25impl Worker {
26 pub fn spawn(python: &Path, script: &Path, args: &[String]) -> io::Result<Worker> {
29 let mut child = Command::new(python)
30 .arg(script)
31 .args(args)
32 .stdin(Stdio::piped())
33 .stdout(Stdio::piped())
34 .stderr(Stdio::inherit())
35 .spawn()?;
36 let stdin = child.stdin.take();
37 let stdout = BufReader::new(child.stdout.take().expect("piped stdout"));
38 Ok(Worker {
39 child,
40 stdin,
41 stdout,
42 })
43 }
44
45 pub fn read_line(&mut self) -> io::Result<String> {
48 let mut buf = Vec::new();
49 self.stdout.read_until(b'\n', &mut buf)?;
50 Ok(String::from_utf8_lossy(&buf).into_owned())
51 }
52
53 pub fn wait_ready(&mut self, mut on_noise: impl FnMut(&str)) -> io::Result<Option<Value>> {
57 loop {
58 let raw = self.read_line()?;
59 if raw.is_empty() {
60 return Ok(None);
61 }
62 let line = raw.trim();
63 if line.is_empty() {
64 continue;
65 }
66 match serde_json::from_str::<Value>(line) {
67 Ok(msg) if is_truthy(msg.get("ready")) => return Ok(Some(msg)),
68 _ => on_noise(line),
69 }
70 }
71 }
72
73 pub fn send(&mut self, request: &Value) -> io::Result<()> {
75 let stdin = self
76 .stdin
77 .as_mut()
78 .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "worker stdin closed"))?;
79 let mut line = dumps(request, Style::COMPACT);
80 line.push('\n');
81 stdin.write_all(line.as_bytes())?;
82 stdin.flush()
83 }
84
85 pub fn close(mut self) -> io::Result<()> {
87 drop(self.stdin.take());
88 self.child.wait().map(|_| ())
89 }
90}
91
92pub fn is_truthy(v: Option<&Value>) -> bool {
94 match v {
95 None | Some(Value::Null) | Some(Value::Bool(false)) => false,
96 Some(Value::Number(n)) => n.as_f64() != Some(0.0),
97 Some(Value::String(s)) => !s.is_empty(),
98 Some(Value::Array(a)) => !a.is_empty(),
99 Some(Value::Object(o)) => !o.is_empty(),
100 Some(Value::Bool(true)) => true,
101 }
102}
103
104pub fn worker_script(package_dir: &Path, name: &str) -> PathBuf {
107 package_dir.join(name)
108}
109
110#[cfg(all(test, unix))]
111mod tests {
112 use super::*;
113 use std::os::unix::fs::PermissionsExt;
114
115 fn fake_python(dir: &Path) -> PathBuf {
118 let p = dir.join("fake-python");
119 std::fs::write(
120 &p,
121 "#!/bin/sh\necho 'loading weights...'\necho '{\"ready\": true, \"device\": \"cpu\"}'\nwhile read line; do echo '{\"done\": true}'; done\n",
122 )
123 .unwrap();
124 std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
125 p
126 }
127
128 #[test]
129 fn handshake_request_close() {
130 let tmp = tempfile::tempdir().unwrap();
131 let py = fake_python(tmp.path());
132 let mut w = Worker::spawn(&py, Path::new("worker.py"), &["cpu".into()]).unwrap();
133 let mut noise = Vec::new();
134 let ready = w
135 .wait_ready(|l| noise.push(l.to_string()))
136 .unwrap()
137 .unwrap();
138 assert_eq!(ready["device"], "cpu");
139 assert_eq!(noise, vec!["loading weights..."]);
140 w.send(&serde_json::json!({"prompt": "door"})).unwrap();
141 let resp: Value = serde_json::from_str(&w.read_line().unwrap()).unwrap();
142 assert_eq!(resp["done"], true);
143 w.close().unwrap();
144 }
145}