1use std::collections::HashMap;
8use std::path::Path;
9use std::process::Stdio;
10use tokio::process::Command;
11
12#[cfg(windows)]
13pub(crate) fn shell_escape(arg: &str) -> String {
14 if arg.is_empty() {
15 return "\"\"".to_string();
16 }
17 if arg.chars().all(|c| {
18 c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | '\\' | ':' | '=' | '@')
19 }) {
20 return arg.to_string();
21 }
22 format!("\"{}\"", arg.replace('"', "\\\""))
23}
24
25#[cfg(not(windows))]
26pub(crate) fn shell_escape(arg: &str) -> String {
27 if arg.is_empty() {
28 return "''".to_string();
29 }
30 if arg
31 .chars()
32 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':' | '=' | '@'))
33 {
34 return arg.to_string();
35 }
36 format!("'{}'", arg.replace('\'', "'\\''"))
37}
38
39pub fn wrap_command_with_toolchain(
40 cmd_str: &str,
41 toolchain: Option<&HashMap<String, String>>,
42) -> String {
43 let Some(toolchain) = toolchain else {
44 return cmd_str.to_string();
45 };
46 if toolchain.is_empty() {
47 return cmd_str.to_string();
48 }
49
50 #[cfg(not(unix))]
51 {
52 let _ = toolchain;
53 cmd_str.to_string()
54 }
55
56 #[cfg(unix)]
57 {
58 let mut prefixes: Vec<String> = Vec::new();
59 for (lang, ver) in toolchain {
60 let l = lang.to_ascii_lowercase();
61 match l.as_str() {
62 "node" | "nodejs" => {
63 prefixes.push(format!(
64 "(export NVM_DIR=\"$HOME/.nvm\"; [ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\" && nvm use {} >/dev/null 2>&1) || (which fnm >/dev/null 2>&1 && eval \"$(fnm env)\" && fnm use {} >/dev/null 2>&1) || true",
65 ver, ver
66 ));
67 }
68 "go" | "golang" => {
69 prefixes.push(format!(
70 "(which goenv >/dev/null 2>&1 && export GOENV_VERSION={} && eval \"$(goenv init -)\") || true",
71 ver
72 ));
73 }
74 "python" | "pyenv" => {
75 prefixes.push(
76 "(which pyenv >/dev/null 2>&1 && eval \"$(pyenv init -)\") || true"
77 .to_string(),
78 );
79 }
80 _ => {}
81 }
82 }
83
84 if prefixes.is_empty() {
85 cmd_str.to_string()
86 } else {
87 format!("{} && {}", prefixes.join(" && "), cmd_str)
88 }
89 }
90}
91
92pub fn apply_toolchain_env(cmd: &mut Command, toolchain: Option<&HashMap<String, String>>) {
93 if let Some(tc) = toolchain {
94 for (lang, ver) in tc {
95 let l = lang.to_ascii_lowercase();
96 match l.as_str() {
97 "rust" | "rustup" => {
98 cmd.env("RUSTUP_TOOLCHAIN", ver);
99 }
100 "python" | "pyenv" => {
101 cmd.env("PYENV_VERSION", ver);
102 }
103 "node" | "nodejs" => {
104 cmd.env("NODE_VERSION", ver);
105 }
106 _ => {}
107 }
108 let env_key = format!("FARHAND_TOOLCHAIN_{}", l.to_ascii_uppercase());
109 cmd.env(env_key, ver);
110 }
111 }
112}
113
114pub fn apply_toolchain_pty(
115 cmd_builder: &mut portable_pty::CommandBuilder,
116 toolchain: Option<&HashMap<String, String>>,
117) {
118 if let Some(tc) = toolchain {
119 for (lang, ver) in tc {
120 let l = lang.to_ascii_lowercase();
121 match l.as_str() {
122 "rust" | "rustup" => {
123 cmd_builder.env("RUSTUP_TOOLCHAIN", ver);
124 }
125 "python" | "pyenv" => {
126 cmd_builder.env("PYENV_VERSION", ver);
127 }
128 "node" | "nodejs" => {
129 cmd_builder.env("NODE_VERSION", ver);
130 }
131 _ => {}
132 }
133 let env_key = format!("FARHAND_TOOLCHAIN_{}", l.to_ascii_uppercase());
134 cmd_builder.env(env_key, ver);
135 }
136 }
137}
138
139pub fn parse_custom_shell(shell: &str) -> Option<Vec<String>> {
143 let tokens: Vec<String> = shell.split_whitespace().map(str::to_string).collect();
144 if tokens.is_empty() {
145 None
146 } else {
147 Some(tokens)
148 }
149}
150
151pub fn build_shell_command(
152 cwd: &Path,
153 argv: &[String],
154 custom_shell: Option<&str>,
155 toolchain: Option<&HashMap<String, String>>,
156) -> Command {
157 let joined_cmd = argv
158 .iter()
159 .map(|a| shell_escape(a))
160 .collect::<Vec<_>>()
161 .join(" ");
162 let wrapped_cmd = wrap_command_with_toolchain(&joined_cmd, toolchain);
163
164 let mut cmd = if let Some(shell_override) = custom_shell {
165 let shell_tokens = parse_custom_shell(shell_override)
166 .unwrap_or_else(|| vec!["/bin/sh".to_string(), "-c".to_string()]);
167 let mut c = Command::new(&shell_tokens[0]);
168 for part in &shell_tokens[1..] {
169 c.arg(part);
170 }
171 c.arg(&wrapped_cmd);
172 c
173 } else if cfg!(windows) {
174 let mut c = Command::new("cmd.exe");
175 #[cfg(windows)]
176 c.raw_arg(format!("/C \"{}\"", wrapped_cmd));
177 #[cfg(not(windows))]
178 c.arg("/C").arg(&wrapped_cmd);
179 c
180 } else {
181 let mut c = Command::new("/bin/sh");
182 c.arg("-c").arg(&wrapped_cmd);
183 c
184 };
185
186 cmd.current_dir(cwd);
187 cmd.stdout(Stdio::piped());
188 cmd.stderr(Stdio::piped());
189
190 #[cfg(unix)]
191 cmd.process_group(0);
192
193 cmd
194}
195
196pub fn build_raw_shell_command(
197 cwd: &Path,
198 raw_cmd: &str,
199 custom_shell: Option<&str>,
200 toolchain: Option<&HashMap<String, String>>,
201) -> Command {
202 let wrapped_cmd = wrap_command_with_toolchain(raw_cmd, toolchain);
203 let mut cmd = if let Some(shell_override) = custom_shell {
204 let shell_tokens = parse_custom_shell(shell_override)
205 .unwrap_or_else(|| vec!["/bin/sh".to_string(), "-c".to_string()]);
206 let mut c = Command::new(&shell_tokens[0]);
207 for part in &shell_tokens[1..] {
208 c.arg(part);
209 }
210 c.arg(&wrapped_cmd);
211 c
212 } else if cfg!(windows) {
213 let mut c = Command::new("cmd.exe");
214 #[cfg(windows)]
215 c.raw_arg(format!("/C \"{}\"", wrapped_cmd));
216 #[cfg(not(windows))]
217 c.arg("/C").arg(&wrapped_cmd);
218 c
219 } else {
220 let mut c = Command::new("/bin/sh");
221 c.arg("-c").arg(&wrapped_cmd);
222 c
223 };
224
225 cmd.current_dir(cwd);
226 cmd.stdout(Stdio::piped());
227 cmd.stderr(Stdio::piped());
228
229 #[cfg(unix)]
230 cmd.process_group(0);
231
232 cmd
233}
234
235#[allow(unsafe_code)] pub async fn kill_process_group(child: &mut tokio::process::Child) {
244 #[cfg(unix)]
245 if let Some(pid) = child.id() {
246 unsafe {
251 libc::kill(-(pid as i32), libc::SIGTERM);
252 }
253 tokio::select! {
254 _ = child.wait() => {}
255 _ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {
256 unsafe {
259 libc::kill(-(pid as i32), libc::SIGKILL);
260 }
261 }
262 }
263 }
264 #[cfg(windows)]
265 if let Some(pid) = child.id() {
266 let _ = tokio::process::Command::new("taskkill")
267 .args(["/F", "/T", "/PID", &pid.to_string()])
268 .output()
269 .await;
270 let _ = child.kill().await;
271 }
272 #[cfg(not(any(unix, windows)))]
273 {
274 let _ = child.kill().await;
275 }
276}
277
278pub fn resolve_shell_executable(requested: &str) -> String {
279 if requested != "$SHELL" && !requested.is_empty() {
280 if Path::new(requested).is_file() {
281 return requested.to_string();
282 }
283 if !requested.contains('/') && !requested.contains('\\') {
284 for dir in &["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"] {
285 let candidate = Path::new(dir).join(requested);
286 if candidate.is_file() {
287 return candidate.to_string_lossy().to_string();
288 }
289 }
290 }
291 }
292
293 if let Ok(sh) = std::env::var("SHELL") {
294 if Path::new(&sh).is_file() {
295 return sh;
296 }
297 }
298 for candidate in &[
299 "/bin/zsh",
300 "/bin/bash",
301 "/usr/bin/zsh",
302 "/usr/bin/bash",
303 "/bin/sh",
304 ] {
305 if Path::new(candidate).is_file() {
306 return candidate.to_string();
307 }
308 }
309 #[cfg(windows)]
310 {
311 "powershell.exe".to_string()
312 }
313 #[cfg(not(windows))]
314 {
315 "/bin/sh".to_string()
316 }
317}