Files
vibe-kanban/backend/src/command_runner.rs

292 lines
8.2 KiB
Rust
Raw Normal View History

Alex/refactor command runner (#323) * feat: implement CommandRunner and integrate with executors refactor: replace command_group::AsyncGroupChild with command_runner::CommandProcess in executor and process_service Migrate traits and claude to commandrunner Migrate gemini to command_runner Migrate sst_opencode Migrate ccr Migrate amp Migrate charm opencode Migrate cleanup_script Migrate executor (vibe-kanban 28b4ede6) Ive added an abstract command runner to enable local and remote execution later. I already migrated the amp executor, please go ahead and replace migrate process handling with the new command runner @backend/src/command_runner.rs . If there are any missing functions ask me about them. Migrate backend/src/executors/echo.rs to be compatible. Migrate executor (vibe-kanban 9dc48bc8) Ive added an abstract command runner to enable local and remote execution later. I already migrated the amp executor, please go ahead and replace migrate process handling with the new command runner @backend/src/command_runner.rs . If there are any missing functions ask me about them. Migrate @backend/src/executors/dev_server.rs to be compatible. Migrate executor (vibe-kanban d3ac2aa5) Ive added an abstract command runner to enable local and remote execution later. I already migrated the amp executor, please go ahead and replace migrate process handling with the new command runner @backend/src/command_runner.rs . If there are any missing functions ask me about them. Migrate backend/src/executors/setup_script.rs to be compatible. Fmt + lint * Refactor CommandRunner initialization to use new() method for improved environment handling * Add basic cloud runner and test scripts Enhance cloud runner and command runner for true streaming support - Refactor process management in cloud runner to use ProcessEntry struct for better handling of stdout and stderr streams. - Implement true chunk-based streaming for command output via HTTP in command runner. - Update test_remote to verify streaming functionality with real-time output capture. Clippy and fmt Refactor CommandStream and CommandProcess to remove dead code and improve stream handling Refactor cloud runner and command runner to improve API response handling and streamline process status management Change stream setup to be async * Revert "Change stream setup to be async" This reverts commit 79b5cde12aefafe9e669b93167036c8c6adf9145. Revert "Refactor cloud runner and command runner to improve API response handling and streamline process status management" This reverts commit 3cc03ff82424bd715a6f20f3124bd7bf80bc2d72. Revert "Refactor CommandStream and CommandProcess to remove dead code and improve stream handling" This reverts commit dcab0fcd9622416b7881af4add513b371894e408. * refactor: remove unused imports and update command execution to use CommandProcess * refactor: clean up CommandRunner and CommandProcess by removing dead code and updating initialization logic * Fix improts * refactor commandexecutors into local and remote * refactor: update stream methods to be asynchronous across command execution components * refactor: update command runner references; remove remote test binary; remove debug script * Remove unused stdout alias * Clippy * refactor: consolidate CommandExitStatus implementations for local and remote processes * refactor: replace CreateCommandRequest with CommandRunnerArgs in command execution * refactor: optimize stream creation by using concurrent HTTP requests
2025-07-24 11:44:57 +01:00
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncRead;
use crate::models::Environment;
mod local;
mod remote;
pub use local::LocalCommandExecutor;
pub use remote::RemoteCommandExecutor;
// Core trait that defines the interface for command execution
#[async_trait]
pub trait CommandExecutor: Send + Sync {
/// Start a process and return a handle to it
async fn start(
&self,
request: &CommandRunnerArgs,
) -> Result<Box<dyn ProcessHandle>, CommandError>;
}
// Trait for managing running processes
#[async_trait]
pub trait ProcessHandle: Send + Sync {
/// Check if the process is still running, return exit status if finished
async fn try_wait(&mut self) -> Result<Option<CommandExitStatus>, CommandError>;
/// Wait for the process to complete and return exit status
async fn wait(&mut self) -> Result<CommandExitStatus, CommandError>;
/// Kill the process
async fn kill(&mut self) -> Result<(), CommandError>;
/// Get streams for stdout and stderr
async fn stream(&mut self) -> Result<CommandStream, CommandError>;
/// Get process identifier (for debugging/logging)
fn process_id(&self) -> String;
/// Check current status (alias for try_wait for backward compatibility)
async fn status(&mut self) -> Result<Option<CommandExitStatus>, CommandError> {
self.try_wait().await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandRunnerArgs {
pub command: String,
pub args: Vec<String>,
pub working_dir: Option<String>,
pub env_vars: Vec<(String, String)>,
pub stdin: Option<String>,
}
pub struct CommandRunner {
executor: Box<dyn CommandExecutor>,
command: Option<String>,
args: Vec<String>,
working_dir: Option<String>,
env_vars: Vec<(String, String)>,
stdin: Option<String>,
}
impl Default for CommandRunner {
fn default() -> Self {
Self::new()
}
}
pub struct CommandProcess {
handle: Box<dyn ProcessHandle>,
}
impl std::fmt::Debug for CommandProcess {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CommandProcess")
.field("process_id", &self.handle.process_id())
.finish()
}
}
#[derive(Debug)]
pub enum CommandError {
SpawnFailed {
command: String,
error: std::io::Error,
},
StatusCheckFailed {
error: std::io::Error,
},
KillFailed {
error: std::io::Error,
},
ProcessNotStarted,
NoCommandSet,
IoError {
error: std::io::Error,
},
}
impl From<std::io::Error> for CommandError {
fn from(error: std::io::Error) -> Self {
CommandError::IoError { error }
}
}
impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CommandError::SpawnFailed { command, error } => {
write!(f, "Failed to spawn command '{}': {}", command, error)
}
CommandError::StatusCheckFailed { error } => {
write!(f, "Failed to check command status: {}", error)
}
CommandError::KillFailed { error } => {
write!(f, "Failed to kill command: {}", error)
}
CommandError::ProcessNotStarted => {
write!(f, "Process has not been started yet")
}
CommandError::NoCommandSet => {
write!(f, "No command has been set")
}
CommandError::IoError { error } => {
write!(f, "Failed to spawn command: {}", error)
}
}
}
}
impl std::error::Error for CommandError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandExitStatus {
/// Exit code (0 for success on most platforms)
code: Option<i32>,
/// Whether the process exited successfully
success: bool,
/// Unix signal that terminated the process (Unix only)
#[cfg(unix)]
signal: Option<i32>,
/// Optional remote process identifier for cloud execution
remote_process_id: Option<String>,
/// Optional session identifier for remote execution tracking
remote_session_id: Option<String>,
}
impl CommandExitStatus {
/// Returns true if the process exited successfully
pub fn success(&self) -> bool {
self.success
}
/// Returns the exit code of the process, if available
pub fn code(&self) -> Option<i32> {
self.code
}
}
pub struct CommandStream {
pub stdout: Option<Box<dyn AsyncRead + Unpin + Send>>,
pub stderr: Option<Box<dyn AsyncRead + Unpin + Send>>,
}
impl CommandRunner {
pub fn new() -> Self {
let env = std::env::var("ENVIRONMENT").unwrap_or_else(|_| "local".to_string());
let mode = env.parse().unwrap_or(Environment::Local);
match mode {
Environment::Cloud => CommandRunner {
executor: Box::new(RemoteCommandExecutor::new()),
command: None,
args: Vec::new(),
working_dir: None,
env_vars: Vec::new(),
stdin: None,
},
Environment::Local => CommandRunner {
executor: Box::new(LocalCommandExecutor::new()),
command: None,
args: Vec::new(),
working_dir: None,
env_vars: Vec::new(),
stdin: None,
},
}
}
pub fn command(&mut self, cmd: &str) -> &mut Self {
self.command = Some(cmd.to_string());
self
}
pub fn get_program(&self) -> &str {
self.command.as_deref().unwrap_or("")
}
pub fn get_args(&self) -> &[String] {
&self.args
}
pub fn get_current_dir(&self) -> Option<&str> {
self.working_dir.as_deref()
}
pub fn arg(&mut self, arg: &str) -> &mut Self {
self.args.push(arg.to_string());
self
}
pub fn stdin(&mut self, prompt: &str) -> &mut Self {
self.stdin = Some(prompt.to_string());
self
}
pub fn working_dir(&mut self, dir: &str) -> &mut Self {
self.working_dir = Some(dir.to_string());
self
}
pub fn env(&mut self, key: &str, val: &str) -> &mut Self {
self.env_vars.push((key.to_string(), val.to_string()));
self
}
/// Convert the current CommandRunner state to a CreateCommandRequest
pub fn to_args(&self) -> Option<CommandRunnerArgs> {
Some(CommandRunnerArgs {
command: self.command.clone()?,
args: self.args.clone(),
working_dir: self.working_dir.clone(),
env_vars: self.env_vars.clone(),
stdin: self.stdin.clone(),
})
}
/// Create a CommandRunner from a CreateCommandRequest, respecting the environment
#[allow(dead_code)]
pub fn from_args(request: CommandRunnerArgs) -> Self {
let mut runner = Self::new();
runner.command(&request.command);
for arg in &request.args {
runner.arg(arg);
}
if let Some(dir) = &request.working_dir {
runner.working_dir(dir);
}
for (key, value) in &request.env_vars {
runner.env(key, value);
}
if let Some(stdin) = &request.stdin {
runner.stdin(stdin);
}
runner
}
pub async fn start(&self) -> Result<CommandProcess, CommandError> {
let request = self.to_args().ok_or(CommandError::NoCommandSet)?;
let handle = self.executor.start(&request).await?;
Ok(CommandProcess { handle })
}
}
impl CommandProcess {
#[allow(dead_code)]
pub async fn status(&mut self) -> Result<Option<CommandExitStatus>, CommandError> {
self.handle.status().await
}
pub async fn try_wait(&mut self) -> Result<Option<CommandExitStatus>, CommandError> {
self.handle.try_wait().await
}
pub async fn kill(&mut self) -> Result<(), CommandError> {
self.handle.kill().await
}
pub async fn stream(&mut self) -> Result<CommandStream, CommandError> {
self.handle.stream().await
}
#[allow(dead_code)]
pub async fn wait(&mut self) -> Result<CommandExitStatus, CommandError> {
self.handle.wait().await
}
}