robomotion 0.1.3

Official Rust SDK for building Robomotion RPA packages
Documentation
//! Debug attach/detach functionality for development.

use crate::runtime::Result;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use sysinfo::System;

/// Attached gRPC address.
static ATTACHED_TO: OnceCell<RwLock<String>> = OnceCell::new();

fn get_attached() -> &'static RwLock<String> {
    ATTACHED_TO.get_or_init(|| RwLock::new(String::new()))
}

/// Attach configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachConfig {
    pub protocol: String,
    pub addr: String,
    pub pid: i32,
    pub namespace: String,
}

/// Attach timeout (30 seconds).
const ATTACH_TIMEOUT: Duration = Duration::from_secs(30);

/// Socket state for netstat.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocketState {
    Listening,
    Established,
    Other,
}

/// Socket table entry.
#[derive(Debug, Clone)]
pub struct SockTabEntry {
    pub local_address: String,
    pub robot_name: String,
}

/// Attach to a running robot for debugging.
pub async fn attach(namespace: &str, listen_addr: &str) -> Result<()> {
    let grpc_addr = get_rpc_addr().await?;

    if grpc_addr.is_empty() {
        return Err(crate::runtime::RobomotionError::Variable(
            "Empty gRPC address".to_string(),
        ));
    }

    let config = AttachConfig {
        protocol: "grpc".to_string(),
        addr: listen_addr.to_string(),
        pid: std::process::id() as i32,
        namespace: namespace.to_string(),
    };

    let config_data = serde_json::to_vec(&config)?;

    tracing::info!("Attaching to {}", grpc_addr);
    *get_attached().write() = grpc_addr.clone();

    // Connect to the robot's debug service
    // TODO: Implement actual gRPC call to Debug.Attach
    // let channel = tonic::transport::Channel::from_shared(grpc_addr)?
    //     .connect()
    //     .await?;
    // let mut client = proto::debug_client::DebugClient::new(channel);
    // client.attach(proto::AttachRequest { config: config_data }).await?;

    Ok(())
}

/// Detach from the robot.
pub async fn detach(namespace: &str) -> Result<()> {
    let attached = get_attached().read().clone();

    if attached.is_empty() {
        return Err(crate::runtime::RobomotionError::Variable(
            "Not attached to any robot".to_string(),
        ));
    }

    // TODO: Implement actual gRPC call to Debug.Detach
    // let channel = tonic::transport::Channel::from_shared(attached)?
    //     .connect()
    //     .await?;
    // let mut client = proto::debug_client::DebugClient::new(channel);
    // client.detach(proto::DetachRequest { namespace: namespace.to_string() }).await?;

    Ok(())
}

/// Get the gRPC address of the running robot.
async fn get_rpc_addr() -> Result<String> {
    let tabs = get_netstat_ports(SocketState::Listening, "robomotion-runner").await?;
    let tabs = filter_tabs(tabs).await?;

    match tabs.len() {
        0 => Ok(String::new()),
        1 => Ok(tabs[0].local_address.clone()),
        _ => select_tab(&tabs).await,
    }
}

/// Get netstat ports for a process.
async fn get_netstat_ports(
    state: SocketState,
    process_name: &str,
) -> Result<Vec<SockTabEntry>> {
    let mut entries = Vec::new();

    // Use sysinfo to find the process
    let mut system = System::new_all();
    system.refresh_all();

    for (_pid, process) in system.processes() {
        let proc_name = process.name().to_string_lossy();
        if proc_name.contains(process_name) {
            // For now, we'll scan common ports
            // A full implementation would parse /proc/net/tcp on Linux
            for port in 40000..50000 {
                let addr = format!("127.0.0.1:{}", port);
                if let Ok(_) = std::net::TcpStream::connect_timeout(
                    &addr.parse().unwrap(),
                    Duration::from_millis(100),
                ) {
                    entries.push(SockTabEntry {
                        local_address: addr,
                        robot_name: String::new(),
                    });
                    break;
                }
            }
        }
    }

    Ok(entries)
}

/// Filter tabs by checking if they respond to RobotName request.
async fn filter_tabs(tabs: Vec<SockTabEntry>) -> Result<Vec<SockTabEntry>> {
    let mut filtered = Vec::new();

    for mut tab in tabs {
        if let Ok(name) = get_robot_name(&tab.local_address).await {
            tab.robot_name = name;
            filtered.push(tab);
        }
    }

    Ok(filtered)
}

/// Get the robot name from a running robot.
async fn get_robot_name(addr: &str) -> Result<String> {
    // TODO: Implement actual gRPC call to Runner.RobotName
    // let channel = tonic::transport::Channel::from_shared(format!("http://{}", addr))?
    //     .connect()
    //     .await?;
    // let mut client = proto::runner_client::RunnerClient::new(channel);
    // let response = client.robot_name(proto::Null {}).await?;
    // Ok(response.into_inner().robot_name)

    Ok("Unknown Robot".to_string())
}

/// Interactive selection of a robot when multiple are running.
async fn select_tab(tabs: &[SockTabEntry]) -> Result<String> {
    let count = tabs.len();

    println!("\nFound {} robots running on the machine:", count);
    for (i, tab) in tabs.iter().enumerate() {
        println!("{}) {}", i + 1, tab.robot_name);
    }

    print!("Please select a robot to attach (1-{}): ", count);

    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;

    let selected: usize = input.trim().parse().unwrap_or(0);
    if selected > 0 && selected <= count {
        Ok(tabs[selected - 1].local_address.clone())
    } else {
        Err(crate::runtime::RobomotionError::Variable(
            "Invalid selection".to_string(),
        ))
    }
}