Skip to content

Commit 72275a6

Browse files
committed
refactor: introduce command utilities for cross-platform compatibility
This commit adds a new module for command utilities that standardizes the creation of command instances across different platforms. It replaces direct calls to `Command` with utility functions to prevent console windows from appearing on Windows. The changes are reflected in various modules, enhancing the overall cross-platform functionality and maintainability of the codebase.
1 parent 4383d45 commit 72275a6

File tree

12 files changed

+229
-46
lines changed

12 files changed

+229
-46
lines changed

cc/src/claude_binary.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ use std::cmp::Ordering;
55
/// Shared module for detecting Claude Code binary installations
66
/// Supports NVM installations, aliased paths, and version-based selection
77
use std::path::PathBuf;
8-
use std::process::Command;
98
use tauri::Manager;
109

10+
use crate::command_utils::create_command;
11+
1112
/// Type of Claude installation
1213
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1314
pub enum InstallationType {
@@ -170,7 +171,7 @@ fn discover_system_installations() -> Vec<ClaudeInstallation> {
170171
fn try_which_command() -> Option<ClaudeInstallation> {
171172
debug!("Trying 'which claude' to find binary...");
172173

173-
match Command::new("which").arg("claude").output() {
174+
match create_command("which").arg("claude").output() {
174175
Ok(output) if output.status.success() => {
175176
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
176177

@@ -214,7 +215,7 @@ fn try_which_command() -> Option<ClaudeInstallation> {
214215
fn try_which_command() -> Option<ClaudeInstallation> {
215216
debug!("Trying 'where claude' to find binary...");
216217

217-
match Command::new("where").arg("claude").output() {
218+
match create_command("where").arg("claude").output() {
218219
Ok(output) if output.status.success() => {
219220
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
220221

@@ -412,7 +413,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
412413
}
413414

414415
// Also check if claude is available in PATH (without full path)
415-
if let Ok(output) = Command::new("claude").arg("--version").output() {
416+
if let Ok(output) = create_command("claude").arg("--version").output() {
416417
if output.status.success() {
417418
debug!("claude is available in PATH");
418419
let version = extract_version_from_output(&output.stdout);
@@ -481,7 +482,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
481482
}
482483

483484
// Also check if claude is available in PATH (without full path)
484-
if let Ok(output) = Command::new("claude.exe").arg("--version").output() {
485+
if let Ok(output) = create_command("claude.exe").arg("--version").output() {
485486
if output.status.success() {
486487
debug!("claude.exe is available in PATH");
487488
let version = extract_version_from_output(&output.stdout);
@@ -500,7 +501,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
500501

501502
/// Get Claude version by running --version command
502503
fn get_claude_version(path: &str) -> Result<Option<String>, String> {
503-
match Command::new(path).arg("--version").output() {
504+
match create_command(path).arg("--version").output() {
504505
Ok(output) => {
505506
if output.status.success() {
506507
Ok(extract_version_from_output(&output.stdout))
@@ -618,8 +619,8 @@ fn compare_versions(a: &str, b: &str) -> Ordering {
618619

619620
/// Helper function to create a Command with proper environment variables
620621
/// This ensures commands like Claude can find Node.js and other dependencies
621-
pub fn create_command_with_env(program: &str) -> Command {
622-
let mut cmd = Command::new(program);
622+
pub fn create_command_with_env(program: &str) -> std::process::Command {
623+
let mut cmd = create_command(program);
623624

624625
info!("Creating command for: {}", program);
625626

cc/src/command_utils.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//! Common command utilities to prevent console windows from appearing on Windows
2+
3+
use std::process::{Command as StdCommand, Stdio};
4+
use tokio::process::Command as TokioCommand;
5+
6+
/// Create a std Command that won't show a window on Windows
7+
///
8+
/// On Windows, this sets the CREATE_NO_WINDOW flag to prevent a console window
9+
/// from appearing when running external processes.
10+
#[cfg(windows)]
11+
pub fn create_command(program: &str) -> StdCommand {
12+
use std::os::windows::process::CommandExt;
13+
14+
const CREATE_NO_WINDOW: u32 = 0x08000000;
15+
16+
let mut cmd = StdCommand::new(program);
17+
cmd.creation_flags(CREATE_NO_WINDOW);
18+
cmd
19+
}
20+
21+
#[cfg(not(windows))]
22+
pub fn create_command(program: &str) -> StdCommand {
23+
StdCommand::new(program)
24+
}
25+
26+
/// Create a std Command with stdin set to null to prevent console windows
27+
///
28+
/// This is useful for commands that don't need stdin and should run silently.
29+
#[cfg(windows)]
30+
pub fn create_silent_command(program: &str) -> StdCommand {
31+
use std::os::windows::process::CommandExt;
32+
33+
const CREATE_NO_WINDOW: u32 = 0x08000000;
34+
35+
let mut cmd = StdCommand::new(program);
36+
cmd.creation_flags(CREATE_NO_WINDOW);
37+
cmd.stdin(Stdio::null());
38+
cmd
39+
}
40+
41+
#[cfg(not(windows))]
42+
pub fn create_silent_command(program: &str) -> StdCommand {
43+
let mut cmd = StdCommand::new(program);
44+
cmd.stdin(Stdio::null());
45+
cmd
46+
}
47+
48+
/// Create a tokio Command that won't show a window on Windows
49+
///
50+
/// On Windows, this sets the CREATE_NO_WINDOW flag to prevent a console window
51+
/// from appearing when running external processes.
52+
#[cfg(windows)]
53+
pub fn create_tokio_command(program: &str) -> TokioCommand {
54+
use std::os::windows::process::CommandExt;
55+
56+
const CREATE_NO_WINDOW: u32 = 0x08000000;
57+
58+
let mut cmd = TokioCommand::new(program);
59+
cmd.creation_flags(CREATE_NO_WINDOW);
60+
cmd
61+
}
62+
63+
#[cfg(not(windows))]
64+
pub fn create_tokio_command(program: &str) -> TokioCommand {
65+
TokioCommand::new(program)
66+
}
67+
68+
/// Create a tokio Command with stdin set to null to prevent console windows
69+
///
70+
/// This is useful for async commands that don't need stdin and should run silently.
71+
#[cfg(windows)]
72+
pub fn create_tokio_silent_command(program: &str) -> TokioCommand {
73+
use std::os::windows::process::CommandExt;
74+
75+
const CREATE_NO_WINDOW: u32 = 0x08000000;
76+
77+
let mut cmd = TokioCommand::new(program);
78+
cmd.creation_flags(CREATE_NO_WINDOW);
79+
cmd.stdin(Stdio::null());
80+
cmd
81+
}
82+
83+
#[cfg(not(windows))]
84+
pub fn create_tokio_silent_command(program: &str) -> TokioCommand {
85+
let mut cmd = TokioCommand::new(program);
86+
cmd.stdin(Stdio::null());
87+
cmd
88+
}

cc/src/commands/agents.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use std::sync::Mutex;
1212
use tauri::{AppHandle, Emitter, Manager, State};
1313
// Sidecar support removed; using system binary execution only
1414
use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader};
15-
use tokio::process::Command;
15+
16+
use crate::command_utils::{create_command, create_tokio_command};
1617

1718
/// Finds the full path to the claude binary
1819
/// This is necessary because macOS apps have a limited PATH environment
@@ -789,7 +790,7 @@ fn create_agent_system_command(
789790
claude_path: &str,
790791
args: Vec<String>,
791792
project_path: &str,
792-
) -> Command {
793+
) -> tokio::process::Command {
793794
let mut cmd = create_command_with_env(claude_path);
794795

795796
// Add all arguments
@@ -1038,7 +1039,7 @@ async fn spawn_agent_system(
10381039
"🔍 Process likely stuck waiting for input, attempting to kill PID: {}",
10391040
pid
10401041
);
1041-
let kill_result = std::process::Command::new("kill")
1042+
let kill_result = create_command("kill")
10421043
.arg("-TERM")
10431044
.arg(pid.to_string())
10441045
.output();
@@ -1049,7 +1050,7 @@ async fn spawn_agent_system(
10491050
}
10501051
Ok(_) => {
10511052
warn!("🔍 Failed to kill process with TERM, trying KILL");
1052-
let _ = std::process::Command::new("kill")
1053+
let _ = create_command("kill")
10531054
.arg("-KILL")
10541055
.arg(pid.to_string())
10551056
.output();
@@ -1295,7 +1296,7 @@ pub async fn cleanup_finished_processes(db: State<'_, AgentDb>) -> Result<Vec<i6
12951296
// Check if the process is still running
12961297
let is_running = if cfg!(target_os = "windows") {
12971298
// On Windows, use tasklist to check if process exists
1298-
match std::process::Command::new("tasklist")
1299+
match create_command("tasklist")
12991300
.args(["/FI", &format!("PID eq {}", pid)])
13001301
.args(["/FO", "CSV"])
13011302
.output()
@@ -1308,7 +1309,7 @@ pub async fn cleanup_finished_processes(db: State<'_, AgentDb>) -> Result<Vec<i6
13081309
}
13091310
} else {
13101311
// On Unix-like systems, use kill -0 to check if process exists
1311-
match std::process::Command::new("kill")
1312+
match create_command("kill")
13121313
.args(["-0", &pid.to_string()])
13131314
.output()
13141315
{
@@ -1643,12 +1644,12 @@ pub async fn list_claude_installations(
16431644

16441645
/// Helper function to create a tokio Command with proper environment variables
16451646
/// This ensures commands like Claude can find Node.js and other dependencies
1646-
fn create_command_with_env(program: &str) -> Command {
1647+
fn create_command_with_env(program: &str) -> tokio::process::Command {
16471648
// Convert std::process::Command to tokio::process::Command
16481649
let _std_cmd = crate::claude_binary::create_command_with_env(program);
16491650

16501651
// Create a new tokio Command from the program path
1651-
let mut tokio_cmd = Command::new(program);
1652+
let mut tokio_cmd = create_tokio_command(program);
16521653

16531654
// Copy over all environment variables from the std::process::Command
16541655
// This is a workaround since we can't directly convert between the two types

cc/src/commands/claude.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use std::process::Stdio;
77
use std::sync::Arc;
88
use std::time::{SystemTime, UNIX_EPOCH};
99
use tauri::{AppHandle, Emitter, Manager};
10-
use tokio::process::{Child, Command};
10+
use tokio::process::Child;
1111
use tokio::sync::Mutex;
1212
use crate::claude_discovery;
13+
use crate::command_utils::{create_command, create_tokio_command};
1314

1415
/// Global state to track current Claude process
1516
pub struct ClaudeProcessState {
@@ -234,12 +235,12 @@ fn extract_first_user_message(jsonl_path: &PathBuf) -> (Option<String>, Option<S
234235

235236
/// Helper function to create a tokio Command with proper environment variables
236237
/// This ensures commands like Claude can find Node.js and other dependencies
237-
fn create_command_with_env(program: &str) -> Command {
238+
fn create_command_with_env(program: &str) -> tokio::process::Command {
238239
// Convert std::process::Command to tokio::process::Command
239240
let _std_cmd = crate::claude_binary::create_command_with_env(program);
240241

241242
// Create a new tokio Command from the program path
242-
let mut tokio_cmd = Command::new(program);
243+
let mut tokio_cmd = create_tokio_command(program);
243244

244245
// Copy over all environment variables
245246
for (key, value) in std::env::vars() {
@@ -293,7 +294,7 @@ fn create_command_with_env(program: &str) -> Command {
293294
}
294295

295296
/// Creates a system binary command with the given arguments
296-
fn create_system_command(claude_path: &str, args: Vec<String>, project_path: &str) -> Command {
297+
fn create_system_command(claude_path: &str, args: Vec<String>, project_path: &str) -> tokio::process::Command {
297298
let mut cmd = create_command_with_env(claude_path);
298299

299300
// Add all arguments
@@ -603,7 +604,7 @@ pub async fn open_new_session(app: AppHandle, path: Option<String>) -> Result<St
603604

604605
#[cfg(debug_assertions)]
605606
{
606-
let mut cmd = std::process::Command::new(claude_path);
607+
let mut cmd = create_command(&claude_path);
607608

608609
// If a path is provided, use it; otherwise use current directory
609610
if let Some(project_path) = path {
@@ -681,7 +682,7 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
681682

682683
#[cfg(debug_assertions)]
683684
{
684-
let output = std::process::Command::new(claude_path)
685+
let output = create_command(&claude_path)
685686
.arg("--version")
686687
.output();
687688

@@ -1095,11 +1096,11 @@ pub async fn cancel_claude_execution(
10951096
if let Some(pid) = pid {
10961097
log::info!("Attempting system kill as last resort for PID: {}", pid);
10971098
let kill_result = if cfg!(target_os = "windows") {
1098-
std::process::Command::new("taskkill")
1099+
create_command("taskkill")
10991100
.args(["/F", "/PID", &pid.to_string()])
11001101
.output()
11011102
} else {
1102-
std::process::Command::new("kill")
1103+
create_command("kill")
11031104
.args(["-KILL", &pid.to_string()])
11041105
.output()
11051106
};
@@ -1176,7 +1177,7 @@ pub async fn get_claude_session_output(
11761177
/// Helper function to spawn Claude process and handle streaming
11771178
async fn spawn_claude_process(
11781179
app: AppHandle,
1179-
mut cmd: Command,
1180+
mut cmd: tokio::process::Command,
11801181
prompt: String,
11811182
model: String,
11821183
project_path: String,
@@ -2167,7 +2168,7 @@ pub async fn validate_hook_command(command: String) -> Result<serde_json::Value,
21672168
log::info!("Validating hook command syntax");
21682169

21692170
// Validate syntax without executing
2170-
let mut cmd = std::process::Command::new("bash");
2171+
let mut cmd = create_command("bash");
21712172
cmd.arg("-n") // Syntax check only
21722173
.arg("-c")
21732174
.arg(&command);

cc/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod claude_binary;
22
pub mod claude_discovery;
3+
pub mod command_utils;
34
pub mod commands;
45
pub mod checkpoint;
56
pub mod process;

cc/src/process/registry.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::collections::HashMap;
44
use std::sync::{Arc, Mutex};
55
use tokio::process::Child;
66

7+
use crate::command_utils::create_command;
8+
79
/// Type of process being tracked
810
#[derive(Debug, Clone, Serialize, Deserialize)]
911
pub enum ProcessType {
@@ -366,12 +368,12 @@ impl ProcessRegistry {
366368
info!("Attempting to kill process {} by PID {}", run_id, pid);
367369

368370
let kill_result = if cfg!(target_os = "windows") {
369-
std::process::Command::new("taskkill")
371+
create_command("taskkill")
370372
.args(["/F", "/PID", &pid.to_string()])
371373
.output()
372374
} else {
373375
// First try SIGTERM
374-
let term_result = std::process::Command::new("kill")
376+
let term_result = create_command("kill")
375377
.args(["-TERM", &pid.to_string()])
376378
.output();
377379

@@ -382,7 +384,7 @@ impl ProcessRegistry {
382384
std::thread::sleep(std::time::Duration::from_secs(2));
383385

384386
// Check if still running
385-
let check_result = std::process::Command::new("kill")
387+
let check_result = create_command("kill")
386388
.args(["-0", &pid.to_string()])
387389
.output();
388390

@@ -393,7 +395,7 @@ impl ProcessRegistry {
393395
"Process {} still running after SIGTERM, sending SIGKILL",
394396
pid
395397
);
396-
std::process::Command::new("kill")
398+
create_command("kill")
397399
.args(["-KILL", &pid.to_string()])
398400
.output()
399401
} else {
@@ -406,7 +408,7 @@ impl ProcessRegistry {
406408
_ => {
407409
// SIGTERM failed, try SIGKILL directly
408410
warn!("SIGTERM failed for PID {}, trying SIGKILL", pid);
409-
std::process::Command::new("kill")
411+
create_command("kill")
410412
.args(["-KILL", &pid.to_string()])
411413
.output()
412414
}

0 commit comments

Comments
 (0)