rez_next_context/
execution.rs

1//! Context execution and command spawning
2
3use 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/// Context execution configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExecutionConfig {
14    /// Shell type to use for execution
15    pub shell_type: ShellType,
16    /// Working directory
17    pub working_directory: Option<PathBuf>,
18    /// Whether to inherit parent environment
19    pub inherit_parent_env: bool,
20    /// Additional environment variables
21    pub additional_env_vars: HashMap<String, String>,
22    /// Execution timeout in seconds
23    pub timeout_seconds: u64,
24    /// Whether to capture output
25    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, // 5 minutes
36            capture_output: true,
37        }
38    }
39}
40
41/// Context executor for running commands in resolved contexts
42#[derive(Debug)]
43pub struct ContextExecutor {
44    /// The resolved context to execute in
45    context: ResolvedContext,
46    /// Execution configuration
47    config: ExecutionConfig,
48    /// Shell executor
49    shell_executor: ShellExecutor,
50}
51
52impl ContextExecutor {
53    /// Create a new context executor
54    pub fn new(context: ResolvedContext) -> Self {
55        let config = ExecutionConfig::default();
56        Self::with_config(context, config)
57    }
58
59    /// Create a context executor with custom configuration
60    pub fn with_config(context: ResolvedContext, config: ExecutionConfig) -> Self {
61        let mut environment = context.environment_vars.clone();
62
63        // Add additional environment variables
64        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    /// Execute a command in the context
84    pub async fn execute(&self, command: &str) -> Result<CommandResult, RezCoreError> {
85        self.shell_executor.execute(command).await
86    }
87
88    /// Execute a command in the background
89    pub async fn execute_background(&self, command: &str) -> Result<u32, RezCoreError> {
90        self.shell_executor.execute_background(command).await
91    }
92
93    /// Execute multiple commands in sequence
94    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    /// Execute a script file in the context
102    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    /// Start an interactive shell in the context
110    pub async fn start_interactive_shell(&self) -> Result<(), RezCoreError> {
111        self.shell_executor.start_interactive_shell().await
112    }
113
114    /// Spawn a new process in the context
115    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        // Set working directory
124        if let Some(ref wd) = self.config.working_directory {
125            cmd.current_dir(wd);
126        }
127
128        // Set environment variables
129        if self.config.inherit_parent_env {
130            // Inherit parent environment and override with context environment
131            for (key, value) in &self.context.environment_vars {
132                cmd.env(key, value);
133            }
134        } else {
135            // Clear environment and set only context environment
136            cmd.env_clear();
137            for (key, value) in &self.context.environment_vars {
138                cmd.env(key, value);
139            }
140        }
141
142        // Add additional environment variables
143        for (key, value) in &self.config.additional_env_vars {
144            cmd.env(key, value);
145        }
146
147        // Configure stdio
148        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    /// Check if a command/tool is available in the context
167    pub async fn command_exists(&self, command: &str) -> bool {
168        self.shell_executor.command_exists(command).await
169    }
170
171    /// Get all available tools in the context
172    pub fn get_available_tools(&self) -> Vec<String> {
173        self.context.get_all_tools()
174    }
175
176    /// Get context information
177    pub fn get_context(&self) -> &ResolvedContext {
178        &self.context
179    }
180
181    /// Get execution configuration
182    pub fn get_config(&self) -> &ExecutionConfig {
183        &self.config
184    }
185
186    /// Update execution configuration
187    pub fn set_config(&mut self, config: ExecutionConfig) {
188        self.config = config;
189        // Recreate shell executor with new config
190        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    /// Execute a package-specific command
206    pub async fn execute_package_command(
207        &self,
208        package_name: &str,
209        command: &str,
210    ) -> Result<CommandResult, RezCoreError> {
211        // Check if package exists in context
212        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        // Get package-specific environment
220        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        // Execute the command (could be enhanced to look in package-specific paths)
231        self.execute(command).await
232    }
233
234    /// Get execution statistics
235    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
247/// Spawned process handle
248pub struct SpawnedProcess {
249    /// The child process
250    child: tokio::process::Child,
251    /// Program name
252    program: String,
253    /// Program arguments
254    args: Vec<String>,
255    /// Start time
256    start_time: std::time::Instant,
257}
258
259impl SpawnedProcess {
260    /// Get the process ID
261    pub fn id(&self) -> Option<u32> {
262        self.child.id()
263    }
264
265    /// Wait for the process to complete
266    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    /// Kill the process
284    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    /// Try to kill the process
292    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/// Process execution result
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ProcessResult {
302    /// Program name
303    pub program: String,
304    /// Program arguments
305    pub args: Vec<String>,
306    /// Exit code
307    pub exit_code: i32,
308    /// Standard output
309    pub stdout: String,
310    /// Standard error
311    pub stderr: String,
312    /// Execution time in milliseconds
313    pub execution_time_ms: u64,
314}
315
316impl ProcessResult {
317    /// Check if the process was successful
318    pub fn is_success(&self) -> bool {
319        self.exit_code == 0
320    }
321
322    /// Get combined output
323    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    /// Get command line representation
334    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/// Execution statistics
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct ExecutionStats {
346    /// Context ID
347    pub context_id: String,
348    /// Number of packages in context
349    pub package_count: usize,
350    /// Number of environment variables
351    pub env_var_count: usize,
352    /// Number of available tools
353    pub tool_count: usize,
354    /// Shell type being used
355    pub shell_type: ShellType,
356    /// Working directory
357    pub working_directory: Option<PathBuf>,
358}
359
360/// Context execution builder for fluent API
361#[derive(Debug)]
362pub struct ContextExecutionBuilder {
363    context: ResolvedContext,
364    config: ExecutionConfig,
365}
366
367impl ContextExecutionBuilder {
368    /// Create a new execution builder
369    pub fn new(context: ResolvedContext) -> Self {
370        Self {
371            context,
372            config: ExecutionConfig::default(),
373        }
374    }
375
376    /// Set shell type
377    pub fn with_shell(mut self, shell_type: ShellType) -> Self {
378        self.config.shell_type = shell_type;
379        self
380    }
381
382    /// Set working directory
383    pub fn with_working_directory(mut self, working_directory: PathBuf) -> Self {
384        self.config.working_directory = Some(working_directory);
385        self
386    }
387
388    /// Set timeout
389    pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
390        self.config.timeout_seconds = timeout_seconds;
391        self
392    }
393
394    /// Add environment variable
395    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    /// Set whether to capture output
401    pub fn with_capture_output(mut self, capture: bool) -> Self {
402        self.config.capture_output = capture;
403        self
404    }
405
406    /// Build the context executor
407    pub fn build(self) -> ContextExecutor {
408        ContextExecutor::with_config(self.context, self.config)
409    }
410}