1use crate::{CommandResult, ResolvedContext, ShellExecutor, ShellType};
4use rez_next_common::RezCoreError;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::process::Stdio;
9use tokio::process::Command as AsyncCommand;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExecutionConfig {
14 pub shell_type: ShellType,
16 pub working_directory: Option<PathBuf>,
18 pub inherit_parent_env: bool,
20 pub additional_env_vars: HashMap<String, String>,
22 pub timeout_seconds: u64,
24 pub capture_output: bool,
26}
27
28impl Default for ExecutionConfig {
29 fn default() -> Self {
30 Self {
31 shell_type: ShellType::detect(),
32 working_directory: None,
33 inherit_parent_env: true,
34 additional_env_vars: HashMap::new(),
35 timeout_seconds: 300, capture_output: true,
37 }
38 }
39}
40
41#[derive(Debug)]
43pub struct ContextExecutor {
44 context: ResolvedContext,
46 config: ExecutionConfig,
48 shell_executor: ShellExecutor,
50}
51
52impl ContextExecutor {
53 pub fn new(context: ResolvedContext) -> Self {
55 let config = ExecutionConfig::default();
56 Self::with_config(context, config)
57 }
58
59 pub fn with_config(context: ResolvedContext, config: ExecutionConfig) -> Self {
61 let mut environment = context.environment_vars.clone();
62
63 environment.extend(config.additional_env_vars.clone());
65
66 let shell_executor = ShellExecutor::with_shell(config.shell_type.clone())
67 .with_environment(environment)
68 .with_timeout(config.timeout_seconds);
69
70 let shell_executor = if let Some(ref wd) = config.working_directory {
71 shell_executor.with_working_directory(wd.clone())
72 } else {
73 shell_executor
74 };
75
76 Self {
77 context,
78 config,
79 shell_executor,
80 }
81 }
82
83 pub async fn execute(&self, command: &str) -> Result<CommandResult, RezCoreError> {
85 self.shell_executor.execute(command).await
86 }
87
88 pub async fn execute_background(&self, command: &str) -> Result<u32, RezCoreError> {
90 self.shell_executor.execute_background(command).await
91 }
92
93 pub async fn execute_batch(
95 &self,
96 commands: &[String],
97 ) -> Result<Vec<CommandResult>, RezCoreError> {
98 self.shell_executor.execute_batch(commands).await
99 }
100
101 pub async fn execute_script(
103 &self,
104 script_path: &PathBuf,
105 ) -> Result<CommandResult, RezCoreError> {
106 self.shell_executor.execute_script(script_path).await
107 }
108
109 pub async fn start_interactive_shell(&self) -> Result<(), RezCoreError> {
111 self.shell_executor.start_interactive_shell().await
112 }
113
114 pub async fn spawn_process(
116 &self,
117 program: &str,
118 args: &[String],
119 ) -> Result<SpawnedProcess, RezCoreError> {
120 let mut cmd = AsyncCommand::new(program);
121 cmd.args(args);
122
123 if let Some(ref wd) = self.config.working_directory {
125 cmd.current_dir(wd);
126 }
127
128 if self.config.inherit_parent_env {
130 for (key, value) in &self.context.environment_vars {
132 cmd.env(key, value);
133 }
134 } else {
135 cmd.env_clear();
137 for (key, value) in &self.context.environment_vars {
138 cmd.env(key, value);
139 }
140 }
141
142 for (key, value) in &self.config.additional_env_vars {
144 cmd.env(key, value);
145 }
146
147 if self.config.capture_output {
149 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
150 } else {
151 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
152 }
153
154 let child = cmd.spawn().map_err(|e| {
155 RezCoreError::ExecutionError(format!("Failed to spawn process {}: {}", program, e))
156 })?;
157
158 Ok(SpawnedProcess {
159 child,
160 program: program.to_string(),
161 args: args.to_vec(),
162 start_time: std::time::Instant::now(),
163 })
164 }
165
166 pub async fn command_exists(&self, command: &str) -> bool {
168 self.shell_executor.command_exists(command).await
169 }
170
171 pub fn get_available_tools(&self) -> Vec<String> {
173 self.context.get_all_tools()
174 }
175
176 pub fn get_context(&self) -> &ResolvedContext {
178 &self.context
179 }
180
181 pub fn get_config(&self) -> &ExecutionConfig {
183 &self.config
184 }
185
186 pub fn set_config(&mut self, config: ExecutionConfig) {
188 self.config = config;
189 let mut environment = self.context.environment_vars.clone();
191 environment.extend(self.config.additional_env_vars.clone());
192
193 self.shell_executor = ShellExecutor::with_shell(self.config.shell_type.clone())
194 .with_environment(environment)
195 .with_timeout(self.config.timeout_seconds);
196
197 if let Some(ref wd) = self.config.working_directory {
198 self.shell_executor = self
199 .shell_executor
200 .clone()
201 .with_working_directory(wd.clone());
202 }
203 }
204
205 pub async fn execute_package_command(
207 &self,
208 package_name: &str,
209 command: &str,
210 ) -> Result<CommandResult, RezCoreError> {
211 if !self.context.contains_package(package_name) {
213 return Err(RezCoreError::ExecutionError(format!(
214 "Package {} not found in context",
215 package_name
216 )));
217 }
218
219 let package_env_var = format!("{}_ROOT", package_name.to_uppercase());
221 let package_root = self.context.environment_vars.get(&package_env_var);
222
223 if package_root.is_none() {
224 return Err(RezCoreError::ExecutionError(format!(
225 "Package root not found for {}",
226 package_name
227 )));
228 }
229
230 self.execute(command).await
232 }
233
234 pub fn get_execution_stats(&self) -> ExecutionStats {
236 ExecutionStats {
237 context_id: self.context.id.clone(),
238 package_count: self.context.resolved_packages.len(),
239 env_var_count: self.context.environment_vars.len(),
240 tool_count: self.get_available_tools().len(),
241 shell_type: self.config.shell_type.clone(),
242 working_directory: self.config.working_directory.clone(),
243 }
244 }
245}
246
247pub struct SpawnedProcess {
249 child: tokio::process::Child,
251 program: String,
253 args: Vec<String>,
255 start_time: std::time::Instant,
257}
258
259impl SpawnedProcess {
260 pub fn id(&self) -> Option<u32> {
262 self.child.id()
263 }
264
265 pub async fn wait(mut self) -> Result<ProcessResult, RezCoreError> {
267 let output = self.child.wait_with_output().await.map_err(|e| {
268 RezCoreError::ExecutionError(format!("Failed to wait for process: {}", e))
269 })?;
270
271 let execution_time_ms = self.start_time.elapsed().as_millis() as u64;
272
273 Ok(ProcessResult {
274 program: self.program,
275 args: self.args,
276 exit_code: output.status.code().unwrap_or(-1),
277 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
278 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
279 execution_time_ms,
280 })
281 }
282
283 pub async fn kill(&mut self) -> Result<(), RezCoreError> {
285 self.child
286 .kill()
287 .await
288 .map_err(|e| RezCoreError::ExecutionError(format!("Failed to kill process: {}", e)))
289 }
290
291 pub fn try_kill(&mut self) -> Result<(), RezCoreError> {
293 self.child
294 .start_kill()
295 .map_err(|e| RezCoreError::ExecutionError(format!("Failed to kill process: {}", e)))
296 }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ProcessResult {
302 pub program: String,
304 pub args: Vec<String>,
306 pub exit_code: i32,
308 pub stdout: String,
310 pub stderr: String,
312 pub execution_time_ms: u64,
314}
315
316impl ProcessResult {
317 pub fn is_success(&self) -> bool {
319 self.exit_code == 0
320 }
321
322 pub fn combined_output(&self) -> String {
324 if self.stderr.is_empty() {
325 self.stdout.clone()
326 } else if self.stdout.is_empty() {
327 self.stderr.clone()
328 } else {
329 format!("{}\n{}", self.stdout, self.stderr)
330 }
331 }
332
333 pub fn command_line(&self) -> String {
335 if self.args.is_empty() {
336 self.program.clone()
337 } else {
338 format!("{} {}", self.program, self.args.join(" "))
339 }
340 }
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct ExecutionStats {
346 pub context_id: String,
348 pub package_count: usize,
350 pub env_var_count: usize,
352 pub tool_count: usize,
354 pub shell_type: ShellType,
356 pub working_directory: Option<PathBuf>,
358}
359
360#[derive(Debug)]
362pub struct ContextExecutionBuilder {
363 context: ResolvedContext,
364 config: ExecutionConfig,
365}
366
367impl ContextExecutionBuilder {
368 pub fn new(context: ResolvedContext) -> Self {
370 Self {
371 context,
372 config: ExecutionConfig::default(),
373 }
374 }
375
376 pub fn with_shell(mut self, shell_type: ShellType) -> Self {
378 self.config.shell_type = shell_type;
379 self
380 }
381
382 pub fn with_working_directory(mut self, working_directory: PathBuf) -> Self {
384 self.config.working_directory = Some(working_directory);
385 self
386 }
387
388 pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
390 self.config.timeout_seconds = timeout_seconds;
391 self
392 }
393
394 pub fn with_env_var(mut self, name: String, value: String) -> Self {
396 self.config.additional_env_vars.insert(name, value);
397 self
398 }
399
400 pub fn with_capture_output(mut self, capture: bool) -> Self {
402 self.config.capture_output = capture;
403 self
404 }
405
406 pub fn build(self) -> ContextExecutor {
408 ContextExecutor::with_config(self.context, self.config)
409 }
410}