punktf_lib/profile/
hook.rs1use std::io::{BufRead as _, BufReader};
4use std::path::Path;
5use std::process::{Command, Stdio};
6
7use color_eyre::eyre::Result;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11#[derive(Error, Debug)]
13pub enum HookError {
14 #[error("IO Error")]
16 IoError(#[from] std::io::Error),
17
18 #[error("Process failed with status `{0}`")]
20 ExitStatusError(std::process::ExitStatus),
21}
22
23impl From<std::process::ExitStatus> for HookError {
24 fn from(value: std::process::ExitStatus) -> Self {
25 Self::ExitStatusError(value)
26 }
27}
28
29trait ExitOk {
33 type Error;
35
36 fn exit_ok(self) -> Result<(), Self::Error>;
38}
39
40impl ExitOk for std::process::ExitStatus {
41 type Error = HookError;
42
43 fn exit_ok(self) -> Result<(), <Self as ExitOk>::Error> {
44 if self.success() {
45 Ok(())
46 } else {
47 Err(self.into())
48 }
49 }
50}
51
52#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct Hook(String);
56
57impl Hook {
58 pub fn new<S: Into<String>>(command: S) -> Self {
60 Self(command.into())
61 }
62
63 pub fn command(&self) -> &str {
65 &self.0
66 }
67
68 pub fn execute(&self, cwd: &Path) -> Result<()> {
70 let mut child = self
71 .prepare_command()?
72 .current_dir(cwd)
73 .stdout(Stdio::piped())
74 .stderr(Stdio::piped())
75 .spawn()?;
76
77 let stdout = child.stdout.take().expect("Failed to get stdout from hook");
80
81 for line in BufReader::new(stdout).lines() {
82 match line {
83 Ok(line) => log::info!("hook::stdout > {}", line),
84 Err(err) => {
85 let _ = child.kill();
88 return Err(err.into());
89 }
90 }
91 }
92
93 let stderr = child.stderr.take().expect("Failed to get stderr from hook");
96
97 for line in BufReader::new(stderr).lines() {
98 match line {
99 Ok(line) => log::error!("hook::stderr > {}", line),
100 Err(err) => {
101 let _ = child.kill();
104 return Err(err.into());
105 }
106 }
107 }
108
109 child
110 .wait_with_output()?
111 .status
112 .exit_ok()
113 .map_err(Into::into)
114 }
115
116 fn prepare_command(&self) -> Result<Command> {
118 cfg_if::cfg_if! {
119 if #[cfg(target_family = "windows")] {
120 let mut cmd = Command::new("cmd");
121 cmd.args(["/C", &self.0]);
122 Ok(cmd)
123 } else if #[cfg(target_family = "unix")] {
124 let mut cmd = Command::new("sh");
125 cmd.args(["-c", &self.0]);
126 Ok(cmd)
127 } else {
128 Err(std::io::Error::new(std::io::ErrorKind::Other, "Hooks are only supported on Windows and Unix-based systems"))
129 }
130 }
131 }
132}