1use std::io::{BufReader, Read, Write};
2use std::path::PathBuf;
3use std::process::{Child, Command, ExitStatus, Stdio};
4use std::thread;
5use std::time::Duration;
6
7pub struct P4Cli {
20 bin_path: PathBuf,
21 _temp_dir: PathBuf,
22}
23
24pub struct P4Output {
26 exit_code: i32,
27 stdout: Vec<u8>,
28 stderr: Vec<u8>,
29}
30
31impl P4Output {
32 pub fn exit_code(&self) -> i32 {
33 self.exit_code
34 }
35
36 pub fn success(&self) -> bool {
37 self.exit_code == 0
38 }
39
40 pub fn stdout(&self) -> &[u8] {
41 &self.stdout
42 }
43
44 pub fn stderr(&self) -> &[u8] {
45 &self.stderr
46 }
47
48 pub fn stdout_str(&self) -> std::io::Result<&str> {
49 std::str::from_utf8(&self.stdout).map_err(std::io::Error::other)
50 }
51
52 pub fn stderr_str(&self) -> std::io::Result<&str> {
53 std::str::from_utf8(&self.stderr).map_err(std::io::Error::other)
54 }
55
56 pub fn stdout_lines(&self) -> std::io::Result<Vec<&str>> {
57 let s = self.stdout_str()?;
58 if s.is_empty() {
59 Ok(Vec::new())
60 } else {
61 Ok(s.lines().collect())
62 }
63 }
64
65 pub fn stderr_lines(&self) -> std::io::Result<Vec<&str>> {
66 let s = self.stderr_str()?;
67 if s.is_empty() {
68 Ok(Vec::new())
69 } else {
70 Ok(s.lines().collect())
71 }
72 }
73}
74
75impl std::fmt::Debug for P4Output {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("P4Output")
78 .field("exit_code", &self.exit_code)
79 .field("stdout_len", &self.stdout.len())
80 .field("stderr_len", &self.stderr.len())
81 .finish()
82 }
83}
84
85pub enum P4StreamEvent {
91 Stdout(Vec<u8>),
92 Stderr(Vec<u8>),
93 Exit(i32),
94}
95
96impl P4StreamEvent {
97 pub fn as_utf8(&self) -> Option<&str> {
99 match self {
100 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
101 std::str::from_utf8(data).ok()
102 }
103 P4StreamEvent::Exit(_) => None,
104 }
105 }
106}
107
108impl std::fmt::Display for P4StreamEvent {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
112 if let Ok(text) = std::str::from_utf8(data) {
113 write!(f, "{text}")
114 } else {
115 write!(f, "<{} bytes>", data.len())
116 }
117 }
118 P4StreamEvent::Exit(code) => write!(f, "(exit {code})"),
119 }
120 }
121}
122
123pub struct P4Stream {
150 rx: std::sync::mpsc::Receiver<std::io::Result<P4StreamEvent>>,
151 child: Option<Child>,
152 #[allow(dead_code)]
153 handles: Vec<thread::JoinHandle<()>>,
154 exhausted: bool,
155}
156
157impl Iterator for P4Stream {
158 type Item = std::io::Result<P4StreamEvent>;
159
160 fn next(&mut self) -> Option<Self::Item> {
161 if self.exhausted {
162 return None;
163 }
164 match self.rx.recv() {
165 Ok(item) => Some(item),
166 Err(_) => {
167 self.exhausted = true;
168 let code = self
169 .child
170 .take()
171 .and_then(|mut c| c.wait().ok())
172 .and_then(|s| s.code())
173 .unwrap_or(-1);
174 Some(Ok(P4StreamEvent::Exit(code)))
175 }
176 }
177 }
178}
179
180impl Drop for P4Stream {
181 fn drop(&mut self) {
182 if let Some(ref mut child) = self.child {
183 let _ = child.kill();
184 let _ = child.wait();
185 }
186 }
187}
188
189fn write_p4_cli_to_disk() -> std::io::Result<(PathBuf, PathBuf)> {
194 let zst_data = get_p4_cli_zst();
195 let binary_data = decompress_zst(&zst_data)?;
196
197 let base = std::env::temp_dir().join("p4cli-20251");
198 std::fs::create_dir_all(&base)?;
199
200 let dir = base.join(format!(
201 "{}_{}",
202 std::process::id(),
203 std::time::SystemTime::now()
204 .duration_since(std::time::UNIX_EPOCH)
205 .map(|d| d.as_nanos())
206 .unwrap_or(0)
207 ));
208 std::fs::create_dir(&dir)?;
209
210 let bin_path = dir.join("p4_binary");
211 let tmp_path = dir.join(".tmp");
212
213 {
214 let mut file = std::fs::File::create(&tmp_path)?;
215 file.write_all(&binary_data)?;
216 file.sync_all()?;
217 }
218 std::fs::rename(&tmp_path, &bin_path)?;
219 set_executable_perms(&bin_path)?;
220
221 Ok((bin_path, dir))
222}
223
224fn decompress_zst(zst_data: &[u8]) -> std::io::Result<Vec<u8>> {
225 let mut decoder = zstd::stream::Decoder::new(zst_data)?;
226 let mut buf = Vec::new();
227 std::io::copy(&mut decoder, &mut buf)?;
228 Ok(buf)
229}
230
231#[cfg(unix)]
232fn set_executable_perms(path: &std::path::Path) -> std::io::Result<()> {
233 use std::os::unix::fs::PermissionsExt;
234 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
235}
236
237#[cfg(not(unix))]
238fn set_executable_perms(_path: &std::path::Path) -> std::io::Result<()> {
239 Ok(())
240}
241
242fn get_p4_cli_zst() -> Vec<u8> {
243 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
244 {
245 use p4cli_20251_win_x64::get_p4_cli_zst;
246 get_p4_cli_zst()
247 }
248
249 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
250 {
251 use p4cli_20251_mac_arm64::get_p4_cli_zst;
252 get_p4_cli_zst()
253 }
254
255 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
256 {
257 use p4cli_20251_mac_x64::get_p4_cli_zst;
258 get_p4_cli_zst()
259 }
260
261 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
262 {
263 use p4cli_20251_linux_x64::get_p4_cli_zst;
264 get_p4_cli_zst()
265 }
266
267 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
268 {
269 use p4cli_20251_linux_arm64::get_p4_cli_zst;
270 get_p4_cli_zst()
271 }
272
273 #[cfg(not(any(
274 all(target_os = "windows", target_arch = "x86_64"),
275 all(target_os = "macos", target_arch = "aarch64"),
276 all(target_os = "macos", target_arch = "x86_64"),
277 all(target_os = "linux", target_arch = "x86_64"),
278 all(target_os = "linux", target_arch = "aarch64")
279 )))]
280 {
281 compile_error!(format!(
282 "Unsupported platform: {}-{}",
283 std::env::consts::OS,
284 std::env::consts::ARCH
285 ));
286 Vec::new()
287 }
288}
289
290pub struct P4Command<'a> {
296 cli: &'a P4Cli,
297 args: Vec<std::ffi::OsString>,
298 timeout: Option<Duration>,
299 cwd: Option<PathBuf>,
300 envs: Vec<(std::ffi::OsString, std::ffi::OsString)>,
301 stdin_data: Option<Vec<u8>>,
302}
303
304impl<'a> P4Command<'a> {
305 fn new(cli: &'a P4Cli) -> Self {
306 Self {
307 cli,
308 args: Vec::new(),
309 timeout: None,
310 cwd: None,
311 envs: Vec::new(),
312 stdin_data: None,
313 }
314 }
315
316 pub fn arg(&mut self, arg: impl AsRef<std::ffi::OsStr>) -> &mut Self {
317 self.args.push(arg.as_ref().to_os_string());
318 self
319 }
320
321 pub fn args(&mut self, args: &[impl AsRef<std::ffi::OsStr>]) -> &mut Self {
322 self.args
323 .extend(args.iter().map(|a| a.as_ref().to_os_string()));
324 self
325 }
326
327 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
329 self.timeout = Some(timeout);
330 self
331 }
332
333 pub fn cwd(&mut self, path: impl Into<PathBuf>) -> &mut Self {
334 self.cwd = Some(path.into());
335 self
336 }
337
338 pub fn env(
339 &mut self,
340 key: impl Into<std::ffi::OsString>,
341 val: impl Into<std::ffi::OsString>,
342 ) -> &mut Self {
343 self.envs.push((key.into(), val.into()));
344 self
345 }
346
347 pub fn stdin(&mut self, data: impl Into<Vec<u8>>) -> &mut Self {
348 self.stdin_data = Some(data.into());
349 self
350 }
351
352 pub fn run(&mut self) -> std::io::Result<P4Output> {
354 let mut cmd = Command::new(&self.cli.bin_path);
355 cmd.args(&self.args)
356 .stdout(Stdio::piped())
357 .stderr(Stdio::piped());
358
359 if self.stdin_data.is_some() {
360 cmd.stdin(Stdio::piped());
361 } else {
362 cmd.stdin(Stdio::null());
363 }
364
365 if let Some(ref cwd) = self.cwd {
366 cmd.current_dir(cwd);
367 }
368 for (k, v) in &self.envs {
369 cmd.env(k, v);
370 }
371
372 let mut child = cmd.spawn()?;
373
374 if let Some(data) = self.stdin_data.take()
375 && let Some(mut stdin) = child.stdin.take()
376 {
377 thread::spawn(move || {
378 let _ = stdin.write_all(&data);
379 });
380 }
381
382 let stdout = child
383 .stdout
384 .take()
385 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
386 let stderr = child
387 .stderr
388 .take()
389 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
390
391 let stdout_handle = thread::spawn(move || {
392 let mut buf = Vec::new();
393 BufReader::new(stdout).read_to_end(&mut buf)?;
394 Ok::<_, std::io::Error>(buf)
395 });
396
397 let stderr_handle = thread::spawn(move || {
398 let mut buf = Vec::new();
399 BufReader::new(stderr).read_to_end(&mut buf)?;
400 Ok::<_, std::io::Error>(buf)
401 });
402
403 let exit_status = wait_process(&mut child, self.timeout)?;
404
405 let stdout_buf = stdout_handle
406 .join()
407 .map_err(|_| std::io::Error::other("stdout reader thread panicked"))?
408 .map_err(|e| std::io::Error::other(format!("stdout read failed: {e}")))?;
409 let stderr_buf = stderr_handle
410 .join()
411 .map_err(|_| std::io::Error::other("stderr reader thread panicked"))?
412 .map_err(|e| std::io::Error::other(format!("stderr read failed: {e}")))?;
413
414 Ok(P4Output {
415 exit_code: exit_status.code().unwrap_or(-1),
416 stdout: stdout_buf,
417 stderr: stderr_buf,
418 })
419 }
420
421 pub fn stream(&mut self) -> std::io::Result<P4Stream> {
425 let mut cmd = Command::new(&self.cli.bin_path);
426 cmd.args(&self.args)
427 .stdout(Stdio::piped())
428 .stderr(Stdio::piped());
429
430 if self.stdin_data.is_some() {
431 cmd.stdin(Stdio::piped());
432 } else {
433 cmd.stdin(Stdio::null());
434 }
435 if let Some(ref cwd) = self.cwd {
436 cmd.current_dir(cwd);
437 }
438 for (k, v) in &self.envs {
439 cmd.env(k, v);
440 }
441
442 let mut child = cmd.spawn()?;
443
444 if let Some(data) = self.stdin_data.take()
445 && let Some(mut stdin) = child.stdin.take()
446 {
447 thread::spawn(move || {
448 let _ = stdin.write_all(&data);
449 });
450 }
451
452 let mut stdout = child
453 .stdout
454 .take()
455 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
456 let mut stderr = child
457 .stderr
458 .take()
459 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
460
461 let (tx, rx) = std::sync::mpsc::channel();
462 let mut handles = Vec::new();
463
464 let tx_out = tx.clone();
465 handles.push(thread::spawn(move || {
466 let mut buf = vec![0u8; 65536];
467 loop {
468 let n = match stdout.read(&mut buf) {
469 Ok(0) => break,
470 Ok(n) => n,
471 Err(e) => {
472 let _ = tx_out.send(Err(e));
473 break;
474 }
475 };
476 if tx_out
477 .send(Ok(P4StreamEvent::Stdout(buf[..n].to_vec())))
478 .is_err()
479 {
480 break;
481 }
482 }
483 }));
484
485 let tx_err = tx.clone();
486 handles.push(thread::spawn(move || {
487 let mut buf = vec![0u8; 65536];
488 loop {
489 let n = match stderr.read(&mut buf) {
490 Ok(0) => break,
491 Ok(n) => n,
492 Err(e) => {
493 let _ = tx_err.send(Err(e));
494 break;
495 }
496 };
497 if tx_err
498 .send(Ok(P4StreamEvent::Stderr(buf[..n].to_vec())))
499 .is_err()
500 {
501 break;
502 }
503 }
504 }));
505
506 Ok(P4Stream {
507 rx,
508 child: Some(child),
509 handles,
510 exhausted: false,
511 })
512 }
513}
514
515fn wait_process(child: &mut Child, timeout: Option<Duration>) -> std::io::Result<ExitStatus> {
520 match timeout {
521 None => child.wait(),
522 Some(t) => wait_with_timeout(child, t),
523 }
524}
525
526fn wait_with_timeout(child: &mut Child, timeout: Duration) -> std::io::Result<ExitStatus> {
527 let start = std::time::Instant::now();
528 loop {
529 if let Some(status) = child.try_wait()? {
530 return Ok(status);
531 }
532 if start.elapsed() >= timeout {
533 child.kill()?;
534 return child.wait();
535 }
536 thread::sleep(Duration::from_millis(50));
537 }
538}
539
540impl P4Cli {
545 pub fn new() -> std::io::Result<Self> {
547 let (bin_path, temp_dir) = write_p4_cli_to_disk()?;
548 Ok(Self {
549 bin_path,
550 _temp_dir: temp_dir,
551 })
552 }
553
554 pub fn run<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Output> {
556 self.command().args(args).run()
557 }
558
559 pub fn stream<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Stream> {
561 self.command().args(args).stream()
562 }
563
564 pub fn command(&self) -> P4Command<'_> {
583 P4Command::new(self)
584 }
585}
586
587impl Drop for P4Cli {
588 fn drop(&mut self) {
589 let _ = std::fs::remove_dir_all(&self._temp_dir);
590 }
591}