exiftool_rs_wrapper/
process.rs1use crate::error::{Error, Result};
4use std::io::{BufRead, BufReader, BufWriter, Write};
5use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
6use std::time::Duration;
7use tracing::{debug, info, warn};
8
9pub struct ExifToolInner {
11 process: Child,
12 stdin: BufWriter<ChildStdin>,
13 stdout: BufReader<ChildStdout>,
14}
15
16impl std::fmt::Debug for ExifToolInner {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 f.debug_struct("ExifToolInner")
19 .field("process", &self.process.id())
20 .finish()
21 }
22}
23
24impl ExifToolInner {
25 pub fn new() -> Result<Self> {
27 info!("Starting ExifTool process with -stay_open mode");
28
29 let mut process = Command::new("exiftool")
30 .arg("-stay_open")
31 .arg("True")
32 .arg("-@")
33 .arg("-")
34 .stdin(Stdio::piped())
35 .stdout(Stdio::piped())
36 .stderr(Stdio::piped())
37 .spawn()
38 .map_err(|e| {
39 if e.kind() == std::io::ErrorKind::NotFound {
40 Error::ExifToolNotFound
41 } else {
42 e.into()
43 }
44 })?;
45
46 let stdin = process
47 .stdin
48 .take()
49 .ok_or_else(|| Error::process("Failed to capture stdin"))?;
50
51 let stdout = process
52 .stdout
53 .take()
54 .ok_or_else(|| Error::process("Failed to capture stdout"))?;
55
56 let mut inner = Self {
57 process,
58 stdin: BufWriter::new(stdin),
59 stdout: BufReader::new(stdout),
60 };
61
62 inner.verify_process()?;
64
65 info!("ExifTool process started successfully");
66 Ok(inner)
67 }
68
69 fn verify_process(&mut self) -> Result<()> {
71 debug!("Verifying ExifTool process");
72
73 self.send_line("-ver")?;
75 self.send_line("-execute")?;
76 self.stdin.flush()?;
77
78 let response = self.read_response()?;
80 debug!("ExifTool version: {}", response.text().trim());
81
82 Ok(())
83 }
84
85 pub fn send_line(&mut self, line: &str) -> Result<()> {
87 debug!("Sending command: {}", line);
88 writeln!(self.stdin, "{}", line)?;
89 Ok(())
90 }
91
92 pub fn execute(&mut self, args: &[String]) -> Result<Response> {
94 debug!("Executing command with {} args", args.len());
95
96 for arg in args {
98 self.send_line(arg)?;
99 }
100
101 self.send_line("-execute")?;
103 self.stdin.flush()?;
104
105 self.read_response()
107 }
108
109 pub fn read_response(&mut self) -> Result<Response> {
111 let mut lines = Vec::new();
112 let mut buffer = String::new();
113
114 loop {
115 buffer.clear();
116 let bytes_read = self.stdout.read_line(&mut buffer)?;
117
118 if bytes_read == 0 {
119 return Err(Error::process("Unexpected EOF from ExifTool process"));
121 }
122
123 let trimmed = buffer.trim();
124 debug!("Received line: {}", trimmed);
125
126 if trimmed == "{ready}" {
127 break;
128 }
129
130 lines.push(buffer.clone());
131 }
132
133 Ok(Response::new(lines))
134 }
135
136 pub fn execute_batch(&mut self, commands: &[Vec<String>]) -> Result<Vec<Response>> {
138 debug!("Executing batch of {} commands", commands.len());
139
140 let mut responses = Vec::with_capacity(commands.len());
141
142 for args in commands {
143 let response = self.execute(args)?;
144 responses.push(response);
145 }
146
147 Ok(responses)
148 }
149
150 pub fn flush(&mut self) -> Result<()> {
152 self.stdin.flush().map_err(|e| e.into())
153 }
154
155 pub fn close(&mut self) -> Result<()> {
157 info!("Closing ExifTool process");
158
159 let _ = self.send_line("-stay_open");
161 let _ = self.send_line("False");
162 let _ = self.send_line("-execute");
163 let _ = self.stdin.flush();
164
165 match self.wait_with_timeout(Duration::from_secs(5)) {
167 Ok(Some(status)) => {
168 if let Some(code) = status.code() {
169 if code != 0 {
170 warn!("ExifTool exited with code: {}", code);
171 } else {
172 info!("ExifTool process exited cleanly");
173 }
174 }
175 }
176 Ok(None) => {
177 warn!("ExifTool did not exit gracefully, forcing kill");
178 let _ = self.process.kill();
179 }
180 Err(e) => {
181 warn!("Error waiting for ExifTool: {}", e);
182 let _ = self.process.kill();
183 }
184 }
185
186 Ok(())
187 }
188
189 fn wait_with_timeout(&mut self, timeout: Duration) -> Result<Option<std::process::ExitStatus>> {
191 use std::thread;
192
193 let start = std::time::Instant::now();
194
195 loop {
196 match self.process.try_wait()? {
197 Some(status) => return Ok(Some(status)),
198 None => {
199 if start.elapsed() >= timeout {
200 return Ok(None);
201 }
202 thread::sleep(Duration::from_millis(10));
203 }
204 }
205 }
206 }
207}
208
209impl Drop for ExifToolInner {
210 fn drop(&mut self) {
211 if let Err(e) = self.close() {
212 warn!("Error closing ExifTool process: {}", e);
213 }
214 }
215}
216
217#[derive(Debug, Clone)]
219pub struct Response {
220 lines: Vec<String>,
221}
222
223impl Response {
224 pub fn new(lines: Vec<String>) -> Self {
226 Self { lines }
227 }
228
229 pub fn lines(&self) -> &[String] {
231 &self.lines
232 }
233
234 pub fn text(&self) -> String {
236 self.lines.join("")
237 }
238
239 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
241 let text = self.text();
242 serde_json::from_str(&text).map_err(|e| e.into())
243 }
244
245 pub fn is_error(&self) -> bool {
247 self.lines
248 .iter()
249 .any(|line| line.contains("Error:") || line.contains("Warning:"))
250 }
251
252 pub fn error_message(&self) -> Option<String> {
254 self.lines
255 .iter()
256 .find(|line| line.contains("Error:"))
257 .cloned()
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_response() {
267 let lines = vec!["Line 1".to_string(), "Line 2".to_string()];
268 let response = Response::new(lines);
269
270 assert_eq!(response.lines().len(), 2);
271 assert_eq!(response.text(), "Line 1Line 2");
272 assert!(!response.is_error());
273 }
274
275 #[test]
276 fn test_response_error() {
277 let lines = vec!["Error: Something went wrong".to_string()];
278 let response = Response::new(lines);
279
280 assert!(response.is_error());
281 assert!(response.error_message().is_some());
282 }
283
284 #[test]
285 fn test_response_json() {
286 let lines = vec![r#"{"key": "value"}"#.to_string()];
287 let response = Response::new(lines);
288
289 #[derive(Debug, serde::Deserialize, PartialEq)]
290 struct TestData {
291 key: String,
292 }
293
294 let data: TestData = response.json().unwrap();
295 assert_eq!(data.key, "value");
296 }
297}