robotrt-cli 0.1.0-beta.1

RobotRT modular robotics runtime and middleware components.
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};

use crate::constants::DEFAULT_ORCHESTRATION_CONFIG_PATH;
use crate::helpers::{has_flag, option_value};

mod cli;
mod command;
mod config;
mod execution;
mod planner;
mod runtime_state;

use self::cli::parse_cli_options;
use self::config::{default_orchestration_config, load_resolved_config, validate_config};
use self::execution::{
    resolve_max_parallel, run_oneshot_parallel, run_oneshot_task, spawn_service_task,
};
use self::planner::{build_task_index, filtered_ordered_tasks, topological_order};
use self::runtime_state::{
    load_runtime_state, load_runtime_state_or_default, now_unix_ms, process_alive, sanitize_name,
    save_runtime_state, terminate_process,
};

const ORCHESTRATION_CONFIG_API_VERSION: &str = "robotrt.orchestrate.v1";
const ORCHESTRATION_RUNTIME_STATE_API_VERSION: &str = "robotrt.orchestrate.runtime.v1";
const DEFAULT_RUNTIME_STATE_FILE: &str = "artifacts/orchestrate/runtime-state.json";
const DEFAULT_RUN_LOG_DIR: &str = "artifacts/orchestrate/logs";

#[derive(Clone, Debug, Serialize, Deserialize)]
struct OrchestrationConfig {
    api_version: String,
    #[serde(default)]
    project: String,
    #[serde(default = "default_max_parallel")]
    max_parallel: usize,
    #[serde(default)]
    profiles: BTreeMap<String, OrchestrationProfile>,
    tasks: Vec<OrchestrationTask>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct OrchestrationTask {
    name: String,
    #[serde(default)]
    command: Vec<String>,
    #[serde(default)]
    depends_on: Vec<String>,
    #[serde(default)]
    cwd: Option<PathBuf>,
    #[serde(default)]
    env: BTreeMap<String, String>,
    #[serde(default)]
    continue_on_error: bool,
    #[serde(default = "default_enabled")]
    enabled: bool,
    #[serde(default)]
    group: Option<String>,
    #[serde(default)]
    run_mode: TaskRunMode,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
struct OrchestrationProfile {
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    max_parallel: Option<usize>,
    #[serde(default)]
    env: BTreeMap<String, String>,
    #[serde(default)]
    tasks: BTreeMap<String, TaskOverride>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
struct TaskOverride {
    #[serde(default)]
    command: Option<Vec<String>>,
    #[serde(default)]
    depends_on: Option<Vec<String>>,
    #[serde(default)]
    cwd: Option<PathBuf>,
    #[serde(default)]
    env: BTreeMap<String, String>,
    #[serde(default)]
    continue_on_error: Option<bool>,
    #[serde(default)]
    enabled: Option<bool>,
    #[serde(default)]
    group: Option<String>,
    #[serde(default)]
    run_mode: Option<TaskRunMode>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
struct OrchestrationOverlay {
    #[serde(default)]
    project: Option<String>,
    #[serde(default)]
    max_parallel: Option<usize>,
    #[serde(default)]
    env: BTreeMap<String, String>,
    #[serde(default)]
    tasks: BTreeMap<String, TaskOverride>,
    #[serde(default)]
    add_tasks: Vec<OrchestrationTask>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum TaskRunMode {
    #[default]
    Oneshot,
    Service,
}

#[derive(Debug, Serialize)]
struct TaskRunResult {
    name: String,
    status: String,
    exit_code: Option<i32>,
    elapsed_ms: u64,
    log_file: Option<PathBuf>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct RuntimeState {
    api_version: String,
    config_file: PathBuf,
    profile: Option<String>,
    updated_at_unix_ms: u64,
    processes: Vec<ServiceProcessRecord>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct ServiceProcessRecord {
    task: String,
    group: Option<String>,
    pid: u32,
    command: Vec<String>,
    cwd: Option<PathBuf>,
    log_file: PathBuf,
    started_at_unix_ms: u64,
}

#[derive(Debug)]
struct RunningTask {
    child: Child,
    started_at: Instant,
    log_file: PathBuf,
}

#[derive(Default)]
struct CliOrchestrateOptions {
    config_path: PathBuf,
    profile: Option<String>,
    overlay_files: Vec<PathBuf>,
    json: bool,
    target_task: Option<String>,
    group: Option<String>,
    dry_run: bool,
    continue_on_error: bool,
    max_parallel_override: Option<usize>,
    state_file: PathBuf,
    prune: bool,
}

fn default_max_parallel() -> usize {
    1
}

fn default_enabled() -> bool {
    true
}

pub fn orchestrate_cmd(args: &[String]) -> Result<(), String> {
    command::orchestrate_cmd(args)
}