1pub mod downloader;
2pub mod finder;
3pub mod platform;
4
5use std::io::{BufReader, Read, Write};
6use std::path::PathBuf;
7use std::process::{Child, Command, ExitStatus, Stdio};
8use std::thread;
9use std::time::Duration;
10
11pub struct P4Cli {
32 bin_path: PathBuf,
33 _cache: Option<tempfile::TempDir>,
35}
36
37impl P4Cli {
38 pub fn new() -> std::io::Result<Self> {
40 if let Some(path) = finder::find_system_p4() {
42 return Ok(Self {
43 bin_path: path,
44 _cache: None,
45 });
46 }
47 let (bin_path, cache) = downloader::download_p4()?;
49 Ok(Self {
50 bin_path,
51 _cache: Some(cache),
52 })
53 }
54
55 pub fn run<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Output> {
57 self.command().args(args).run()
58 }
59
60 pub fn stream<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Stream> {
62 self.command().args(args).stream()
63 }
64
65 pub fn command(&self) -> P4Command<'_> {
84 P4Command::new(self)
85 }
86}
87
88pub struct P4Output {
94 exit_code: i32,
95 timed_out: bool,
96 stdout: Vec<u8>,
97 stderr: Vec<u8>,
98}
99
100impl P4Output {
101 pub fn exit_code(&self) -> i32 {
102 self.exit_code
103 }
104
105 pub fn timed_out(&self) -> bool {
106 self.timed_out
107 }
108
109 pub fn success(&self) -> bool {
110 !self.timed_out && self.exit_code == 0
111 }
112
113 pub fn stdout(&self) -> &[u8] {
114 &self.stdout
115 }
116
117 pub fn stderr(&self) -> &[u8] {
118 &self.stderr
119 }
120
121 pub fn stdout_str(&self) -> std::io::Result<&str> {
122 std::str::from_utf8(&self.stdout).map_err(std::io::Error::other)
123 }
124
125 pub fn stderr_str(&self) -> std::io::Result<&str> {
126 std::str::from_utf8(&self.stderr).map_err(std::io::Error::other)
127 }
128
129 pub fn stdout_lines(&self) -> std::io::Result<Vec<&str>> {
130 let s = self.stdout_str()?;
131 if s.is_empty() {
132 Ok(Vec::new())
133 } else {
134 Ok(s.lines().collect())
135 }
136 }
137
138 pub fn stderr_lines(&self) -> std::io::Result<Vec<&str>> {
139 let s = self.stderr_str()?;
140 if s.is_empty() {
141 Ok(Vec::new())
142 } else {
143 Ok(s.lines().collect())
144 }
145 }
146}
147
148impl std::fmt::Debug for P4Output {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("P4Output")
151 .field("exit_code", &self.exit_code)
152 .field("timed_out", &self.timed_out)
153 .field("stdout_len", &self.stdout.len())
154 .field("stderr_len", &self.stderr.len())
155 .finish()
156 }
157}
158
159pub enum P4StreamEvent {
164 Stdout(Vec<u8>),
165 Stderr(Vec<u8>),
166 Exit(i32),
167}
168
169impl P4StreamEvent {
170 pub fn as_utf8(&self) -> Option<&str> {
171 match self {
172 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
173 std::str::from_utf8(data).ok()
174 }
175 P4StreamEvent::Exit(_) => None,
176 }
177 }
178}
179
180impl std::fmt::Display for P4StreamEvent {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 match self {
183 P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
184 if let Ok(text) = std::str::from_utf8(data) {
185 write!(f, "{text}")
186 } else {
187 write!(f, "<{} bytes>", data.len())
188 }
189 }
190 P4StreamEvent::Exit(code) => write!(f, "(exit {code})"),
191 }
192 }
193}
194
195pub struct P4Stream {
220 rx: std::sync::mpsc::Receiver<std::io::Result<P4StreamEvent>>,
221 child: Option<Child>,
222 exhausted: bool,
223}
224
225impl std::fmt::Debug for P4Stream {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("P4Stream")
228 .field("exhausted", &self.exhausted)
229 .finish()
230 }
231}
232
233impl Iterator for P4Stream {
234 type Item = std::io::Result<P4StreamEvent>;
235
236 fn next(&mut self) -> Option<Self::Item> {
237 if self.exhausted {
238 return None;
239 }
240 match self.rx.recv() {
241 Ok(item) => Some(item),
242 Err(_) => {
243 self.exhausted = true;
244 match self.child.take() {
245 Some(mut c) => match c.wait() {
246 Ok(status) => Some(Ok(P4StreamEvent::Exit(status.code().unwrap_or(-1)))),
247 Err(e) => Some(Err(e)),
248 },
249 None => None,
250 }
251 }
252 }
253 }
254}
255
256impl Drop for P4Stream {
257 fn drop(&mut self) {
258 if let Some(ref mut child) = self.child {
259 let _ = child.kill();
260 let _ = child.wait();
261 }
262 }
263}
264
265pub struct P4Command<'a> {
270 cli: &'a P4Cli,
271 args: Vec<std::ffi::OsString>,
272 timeout: Option<Duration>,
273 cwd: Option<PathBuf>,
274 envs: Vec<(std::ffi::OsString, std::ffi::OsString)>,
275 stdin_data: Option<Vec<u8>>,
276}
277
278impl<'a> P4Command<'a> {
279 fn new(cli: &'a P4Cli) -> Self {
280 Self {
281 cli,
282 args: Vec::new(),
283 timeout: None,
284 cwd: None,
285 envs: Vec::new(),
286 stdin_data: None,
287 }
288 }
289
290 pub fn arg(&mut self, arg: impl AsRef<std::ffi::OsStr>) -> &mut Self {
291 self.args.push(arg.as_ref().to_os_string());
292 self
293 }
294
295 pub fn args(&mut self, args: &[impl AsRef<std::ffi::OsStr>]) -> &mut Self {
296 self.args
297 .extend(args.iter().map(|a| a.as_ref().to_os_string()));
298 self
299 }
300
301 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
304 self.timeout = Some(timeout);
305 self
306 }
307
308 pub fn cwd(&mut self, path: impl Into<PathBuf>) -> &mut Self {
309 self.cwd = Some(path.into());
310 self
311 }
312
313 pub fn env(
314 &mut self,
315 key: impl Into<std::ffi::OsString>,
316 val: impl Into<std::ffi::OsString>,
317 ) -> &mut Self {
318 self.envs.push((key.into(), val.into()));
319 self
320 }
321
322 pub fn stdin(&mut self, data: impl Into<Vec<u8>>) -> &mut Self {
323 self.stdin_data = Some(data.into());
324 self
325 }
326
327 pub fn run(&mut self) -> std::io::Result<P4Output> {
328 let mut cmd = Command::new(&self.cli.bin_path);
329 cmd.args(&self.args)
330 .stdout(Stdio::piped())
331 .stderr(Stdio::piped());
332 if self.stdin_data.is_some() {
333 cmd.stdin(Stdio::piped());
334 } else {
335 cmd.stdin(Stdio::null());
336 }
337 if let Some(ref cwd) = self.cwd {
338 cmd.current_dir(cwd);
339 }
340 for (k, v) in &self.envs {
341 cmd.env(k, v);
342 }
343
344 let mut child = cmd.spawn()?;
345
346 let stdin_handle = self.stdin_data.take().and_then(|data| {
347 child.stdin.take().map(|mut stdin| {
348 thread::spawn(move || match stdin.write_all(&data) {
349 Err(e) => Err(e),
350 Ok(_) => Ok(()),
351 })
352 })
353 });
354
355 let stdout = child
356 .stdout
357 .take()
358 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
359 let stderr = child
360 .stderr
361 .take()
362 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
363
364 let stdout_handle = thread::spawn(move || {
365 let mut buf = Vec::new();
366 BufReader::new(stdout).read_to_end(&mut buf)?;
367 Ok::<_, std::io::Error>(buf)
368 });
369 let stderr_handle = thread::spawn(move || {
370 let mut buf = Vec::new();
371 BufReader::new(stderr).read_to_end(&mut buf)?;
372 Ok::<_, std::io::Error>(buf)
373 });
374
375 let (exit_status, timed_out) = wait_process(&mut child, self.timeout)?;
376
377 if let Some(handle) = stdin_handle {
378 handle
379 .join()
380 .map_err(|_| std::io::Error::other("stdin thread panicked"))?
381 .map_err(|e| std::io::Error::other(format!("stdin write failed: {e}")))?;
382 }
383 let stdout_buf = stdout_handle
384 .join()
385 .map_err(|_| std::io::Error::other("stdout reader thread panicked"))?
386 .map_err(|e| std::io::Error::other(format!("stdout read failed: {e}")))?;
387 let stderr_buf = stderr_handle
388 .join()
389 .map_err(|_| std::io::Error::other("stderr reader thread panicked"))?
390 .map_err(|e| std::io::Error::other(format!("stderr read failed: {e}")))?;
391
392 Ok(P4Output {
393 exit_code: exit_status.code().unwrap_or(-1),
394 timed_out,
395 stdout: stdout_buf,
396 stderr: stderr_buf,
397 })
398 }
399
400 pub fn stream(&mut self) -> std::io::Result<P4Stream> {
403 if self.timeout.is_some() {
404 return Err(std::io::Error::other(
405 "timeout is not supported on stream(); use run() instead",
406 ));
407 }
408 let mut cmd = Command::new(&self.cli.bin_path);
409 cmd.args(&self.args)
410 .stdout(Stdio::piped())
411 .stderr(Stdio::piped());
412 if self.stdin_data.is_some() {
413 cmd.stdin(Stdio::piped());
414 } else {
415 cmd.stdin(Stdio::null());
416 }
417 if let Some(ref cwd) = self.cwd {
418 cmd.current_dir(cwd);
419 }
420 for (k, v) in &self.envs {
421 cmd.env(k, v);
422 }
423
424 let mut child = cmd.spawn()?;
425 if let Some(data) = self.stdin_data.take()
426 && let Some(mut stdin) = child.stdin.take()
427 {
428 thread::spawn(move || {
429 let _ = stdin.write_all(&data);
430 });
431 }
432
433 let mut stdout = child
434 .stdout
435 .take()
436 .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
437 let mut stderr = child
438 .stderr
439 .take()
440 .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
441
442 let (tx, rx) = std::sync::mpsc::sync_channel(64);
443
444 let tx_out = tx.clone();
445 thread::spawn(move || {
446 let mut buf = vec![0u8; 65536];
447 loop {
448 let n = match stdout.read(&mut buf) {
449 Ok(0) => break,
450 Ok(n) => n,
451 Err(e) => {
452 let _ = tx_out.send(Err(e));
453 break;
454 }
455 };
456 if tx_out
457 .send(Ok(P4StreamEvent::Stdout(buf[..n].to_vec())))
458 .is_err()
459 {
460 break;
461 }
462 }
463 });
464
465 let tx_err = tx.clone();
466 thread::spawn(move || {
467 let mut buf = vec![0u8; 65536];
468 loop {
469 let n = match stderr.read(&mut buf) {
470 Ok(0) => break,
471 Ok(n) => n,
472 Err(e) => {
473 let _ = tx_err.send(Err(e));
474 break;
475 }
476 };
477 if tx_err
478 .send(Ok(P4StreamEvent::Stderr(buf[..n].to_vec())))
479 .is_err()
480 {
481 break;
482 }
483 }
484 });
485
486 Ok(P4Stream {
487 rx,
488 child: Some(child),
489 exhausted: false,
490 })
491 }
492}
493
494fn wait_process(
499 child: &mut Child,
500 timeout: Option<Duration>,
501) -> std::io::Result<(ExitStatus, bool)> {
502 match timeout {
503 None => Ok((child.wait()?, false)),
504 Some(t) => wait_with_timeout(child, t),
505 }
506}
507
508fn wait_with_timeout(child: &mut Child, timeout: Duration) -> std::io::Result<(ExitStatus, bool)> {
509 let start = std::time::Instant::now();
510 loop {
511 if let Some(status) = child.try_wait()? {
512 return Ok((status, false));
513 }
514 if start.elapsed() >= timeout {
515 child.kill()?;
516 return Ok((child.wait()?, true));
517 }
518 thread::sleep(Duration::from_millis(50));
519 }
520}