Skip to main content

regain_core/
worker.rs

1use crate::{Diagnostic, Failure, invalid, process::ProcessGuard};
2use anyhow::{Context, Result, ensure};
3use serde_json::{Value, json};
4use std::{path::PathBuf, process::Stdio, time::Duration};
5use tokio::{
6    io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
7    process::{Child, ChildStdin, ChildStdout, Command},
8};
9use tokio_util::sync::CancellationToken;
10
11#[derive(Clone)]
12pub struct Runtime {
13    pub directory: PathBuf,
14    pub sdk: PathBuf,
15    pub simulate: bool,
16    pub sdk_simulation: Option<Value>,
17}
18impl Runtime {
19    pub async fn usb_command(&self, args: &[&str], seconds: u64) -> Result<String> {
20        ensure!(
21            !self.simulate,
22            "Simulation must not launch USB recovery helpers"
23        );
24        let mut command = Command::new(
25            self.directory
26                .join(format!("regain-device{}", std::env::consts::EXE_SUFFIX)),
27        );
28        command
29            .args(args)
30            .stdin(Stdio::null())
31            .stdout(Stdio::piped())
32            .stderr(Stdio::piped())
33            .kill_on_drop(true);
34        #[cfg(windows)]
35        command.creation_flags(0x08000000);
36        let output = tokio::time::timeout(Duration::from_secs(seconds), command.output())
37            .await
38            .context("USB helper deadline exceeded")??;
39        ensure!(
40            output.status.success(),
41            "USB helper failed ({}; Windows may require administrator approval): {}",
42            output.status,
43            String::from_utf8_lossy(&output.stderr).trim()
44        );
45        Ok(String::from_utf8(output.stdout)?.trim().to_owned())
46    }
47    pub async fn spawn(&self, direct: bool, log: Diagnostic) -> Result<Worker> {
48        let path = self
49            .directory
50            .join(format!("regain-device{}", std::env::consts::EXE_SUFFIX));
51        let mut command = Command::new(&path);
52        command
53            .args([
54                "zwo",
55                if direct {
56                    "camera-direct"
57                } else {
58                    "camera-sdk"
59                },
60            ])
61            .stdin(Stdio::piped())
62            .stdout(Stdio::piped())
63            .stderr(Stdio::piped())
64            .kill_on_drop(true)
65            .current_dir(&self.directory);
66        #[cfg(windows)]
67        command.creation_flags(0x08000000);
68        if direct {
69            command.arg("--serve");
70        }
71        if self.simulate {
72            command.arg("--simulate");
73        } else if !direct {
74            command.arg("--sdk").arg(&self.sdk);
75        }
76        let mut child = command
77            .spawn()
78            .with_context(|| format!("Start {}", path.display()))?;
79        let pid = child.id().context("Worker has no process ID")?;
80        let guard = ProcessGuard::attach(pid)?;
81        let stderr = child.stderr.take().context("Worker stderr unavailable")?;
82        let diagnostic = tokio::spawn(async move {
83            let mut lines = BufReader::new(stderr).lines();
84            while let Ok(Some(line)) = lines.next_line().await {
85                if let Some(text) = line.strip_prefix("REGAIN_DIAGNOSTIC ")
86                    && let Ok(record) = serde_json::from_str::<Value>(text)
87                {
88                    log(
89                        record["level"].as_str().unwrap_or("info"),
90                        record["event"].as_str().unwrap_or("worker"),
91                        record["message"].as_str().unwrap_or(&line),
92                    );
93                } else {
94                    log("info", "worker", &line);
95                }
96            }
97        });
98        let mut worker = Worker {
99            input: child.stdin.take().context("Worker stdin unavailable")?,
100            output: child.stdout.take().context("Worker stdout unavailable")?,
101            child,
102            _guard: guard,
103            diagnostic,
104            id: 0,
105        };
106        if self.simulate
107            && !direct
108            && let Some(settings) = &self.sdk_simulation
109        {
110            worker
111                .call(
112                    "simulation",
113                    settings.clone(),
114                    5.,
115                    &CancellationToken::new(),
116                )
117                .await?;
118        }
119        Ok(worker)
120    }
121    /// SDK discovery is active hardware probing, even in a short-lived worker:
122    /// ASIGetCameraProperty can internally open cameras owned by other processes.
123    /// Cache results and schedule SDK discovery before imaging, not as live polling.
124    /// Worker isolation bounds user-space calls, not side effects on shared USB.
125    pub async fn list(
126        &self,
127        direct: bool,
128        log: Diagnostic,
129        token: &CancellationToken,
130    ) -> Result<Value> {
131        let mut worker = self.spawn(direct, log).await?;
132        let result = worker
133            .call("list", Value::Null, 20., token)
134            .await
135            .map(|r| r.0);
136        worker.kill().await;
137        result
138    }
139}
140pub struct Worker {
141    child: Child,
142    input: ChildStdin,
143    output: ChildStdout,
144    _guard: ProcessGuard,
145    diagnostic: tokio::task::JoinHandle<()>,
146    id: u64,
147}
148impl Worker {
149    pub fn pid(&self) -> Option<u32> {
150        self.child.id()
151    }
152    pub async fn kill(&mut self) {
153        let _ = self.child.kill().await;
154        let _ = self.child.wait().await;
155    }
156    pub async fn call(
157        &mut self,
158        method: &str,
159        params: Value,
160        seconds: f64,
161        token: &CancellationToken,
162    ) -> Result<(Value, Vec<u8>)> {
163        let result = tokio::select! {
164            biased;
165            _=token.cancelled()=>Err(Failure::Cancelled.into()),
166            result=tokio::time::timeout(Duration::from_secs_f64(seconds),self.exchange(method,params))=>result.unwrap_or_else(|_|Err(anyhow::anyhow!("Worker {method} timed out after {seconds} seconds"))),
167        };
168        if result.is_err()
169            && !matches!(
170                result
171                    .as_ref()
172                    .err()
173                    .and_then(|e| e.downcast_ref::<Failure>()),
174                Some(Failure::Worker { .. })
175            )
176        {
177            self.kill().await;
178        }
179        result
180    }
181    async fn exchange(&mut self, method: &str, params: Value) -> Result<(Value, Vec<u8>)> {
182        self.id += 1;
183        let bytes =
184            serde_json::to_vec(&json!({"version":1,"id":self.id,"method":method,"params":params}))?;
185        ensure!(
186            bytes.len() <= 65536,
187            Failure::Invalid("Command too large".into())
188        );
189        self.input.write_u32_le(bytes.len() as u32).await?;
190        self.input.write_all(&bytes).await?;
191        self.input.flush().await?;
192        let length = self.output.read_u32_le().await? as usize;
193        ensure!(
194            (1..=65536).contains(&length),
195            Failure::Invalid("Invalid worker response length".into())
196        );
197        let mut header = vec![0; length];
198        self.output.read_exact(&mut header).await?;
199        let reply: Value =
200            serde_json::from_slice(&header).map_err(|_| invalid("Malformed worker JSON"))?;
201        ensure!(
202            reply["id"] == self.id && reply["version"] == 1,
203            Failure::Invalid("Stale worker response".into())
204        );
205        let count = reply["binaryLength"]
206            .as_u64()
207            .ok_or_else(|| invalid("Missing frame length"))?;
208        ensure!(
209            count <= 512 * 1024 * 1024 && (method == "download" || count == 0),
210            Failure::Invalid("Invalid worker image length".into())
211        );
212        if reply["ok"] == false {
213            ensure!(
214                count == 0,
215                Failure::Invalid("Error response contains pixels".into())
216            );
217            return Err(Failure::Worker {
218                message: reply["error"].as_str().unwrap_or("Worker error").into(),
219                code: reply["sdkCode"].as_i64().map(|v| v as i32),
220            }
221            .into());
222        }
223        ensure!(
224            reply["ok"] == true,
225            Failure::Invalid("Missing worker status".into())
226        );
227        let mut pixels = vec![0; count as usize];
228        self.output.read_exact(&mut pixels).await?;
229        Ok((reply["result"].clone(), pixels))
230    }
231}
232impl Drop for Worker {
233    fn drop(&mut self) {
234        self.diagnostic.abort();
235    }
236}