robomotion 0.1.3

Official Rust SDK for building Robomotion RPA packages
Documentation
//! Node registry and startup functions.

use crate::runtime::{
    generate_spec_file, MessageHandler, NodeSpec, RegisteredNodeSpec, Result,
};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use serde_json::Value;
use std::collections::HashMap;

/// Type alias for node creator functions.
pub type NodeCreator = Box<dyn Fn() -> Box<dyn MessageHandler> + Send + Sync>;

/// Type alias for spec generator functions.
pub type SpecGenerator = Box<dyn Fn() -> RegisteredNodeSpec + Send + Sync>;

/// Global node registry.
static REGISTRY: Lazy<RwLock<Registry>> = Lazy::new(|| RwLock::new(Registry::new()));

/// Node registry.
struct Registry {
    creators: HashMap<String, NodeCreator>,
    specs: HashMap<String, SpecGenerator>,
}

impl Registry {
    fn new() -> Self {
        Self {
            creators: HashMap::new(),
            specs: HashMap::new(),
        }
    }
}

/// Register a node type.
pub fn register_node<N>()
where
    N: MessageHandler + NodeSpec + Default + serde::de::DeserializeOwned + 'static,
{
    let node_id = N::node_id().to_string();

    let creator: NodeCreator = Box::new(|| Box::new(N::default()) as Box<dyn MessageHandler>);

    let spec_gen: SpecGenerator = Box::new(|| RegisteredNodeSpec {
        id: N::node_id().to_string(),
        name: N::node_name().to_string(),
        icon: N::node_icon().to_string(),
        color: N::node_color().to_string(),
        inputs: N::inputs(),
        outputs: N::outputs(),
        tool: N::tool_spec(),
        properties: N::properties(),
    });

    let mut registry = REGISTRY.write();
    registry.creators.insert(node_id.clone(), creator);
    registry.specs.insert(node_id, spec_gen);
}

/// Get all registered node specifications.
pub fn registered_node_specs() -> Vec<RegisteredNodeSpec> {
    let registry = REGISTRY.read();
    registry.specs.values().map(|gen| gen()).collect()
}

/// Get a registered node creator.
pub fn get_node_creator(node_id: &str) -> Option<Box<dyn MessageHandler>> {
    let registry = REGISTRY.read();
    registry.creators.get(node_id).map(|creator| creator())
}

/// Macro to register multiple nodes.
#[macro_export]
macro_rules! register_nodes {
    ($($node:ty),* $(,)?) => {
        $(
            $crate::runtime::register_node::<$node>();
        )*
    };
}

pub use register_nodes;

/// Read the config.json file.
pub fn read_config_file() -> Value {
    let paths = ["config.json", "../config.json"];

    for path in &paths {
        if let Ok(content) = std::fs::read_to_string(path) {
            if let Ok(config) = serde_json::from_str(&content) {
                return config;
            }
        }
    }

    tracing::error!("Failed to read config.json");
    Value::Null
}

/// Start the plugin runtime.
pub async fn start() {
    // Parse command line arguments
    let args: Vec<String> = std::env::args().collect();
    let config = read_config_file();

    if args.len() > 1 {
        match args[1].as_str() {
            "-s" => {
                // Generate spec file
                let name = config["name"].as_str().unwrap_or("unknown");
                let version = config["version"].as_str().unwrap_or("0.0.0");
                let spec = generate_spec_file(name, version);
                println!("{}", spec);
                return;
            }
            "-a" => {
                // Attach mode - start debug server
                let namespace = config["namespace"].as_str().unwrap_or("");
                tracing::info!("Starting in attach mode for namespace: {}", namespace);
                // Start the plugin server
                if let Err(e) = run_plugin_server(true, namespace).await {
                    tracing::error!("Plugin server error: {}", e);
                }
                return;
            }
            _ => {}
        }
    }

    // Normal mode - start plugin server
    if let Err(e) = run_plugin_server(false, "").await {
        tracing::error!("Plugin server error: {}", e);
    }
}

/// Run the plugin gRPC server.
async fn run_plugin_server(attach_mode: bool, namespace: &str) -> Result<()> {
    use tokio::signal;
    use tokio::sync::oneshot;

    // Initialize logger
    tracing_subscriber::fmt()
        .with_env_filter("robomotion=info")
        .with_writer(std::io::stderr)
        .init();

    // Set up the magic cookie (required for go-plugin compatibility)
    std::env::set_var("robomotion_plugin", "6e80b1a2cf26c5935ed7b6e5be77fe218d5f358d");

    let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();

    // Handle shutdown signals
    tokio::spawn(async move {
        let _ = signal::ctrl_c().await;
        let _ = shutdown_tx.send(());
    });

    // Start gRPC server
    // TODO: Implement actual gRPC server using tonic

    if attach_mode {
        // In attach mode, connect to the local robot
        // TODO: Implement attach logic
        tracing::info!("Attach mode started for namespace: {}", namespace);
    }

    // Wait for shutdown
    let _ = shutdown_rx.await;

    tracing::info!("Shutting down...");
    Ok(())
}