systemprompt-agent 0.6.1

Agent-to-Agent (A2A) protocol for systemprompt.io AI governance: streaming, JSON-RPC models, task lifecycle, .well-known discovery, and governed agent orchestration.
Documentation
//! Cross-platform process / port probing helpers.

use anyhow::{Context, Result};
use std::process::Command;
use systemprompt_models::CliPaths;

#[derive(Debug, Clone)]
pub struct ProcessInfo {
    pub pid: u32,
    pub command: String,
}

#[cfg(unix)]
pub fn find_process_using_port(port: u16) -> Result<Option<u32>> {
    let output = Command::new("lsof")
        .arg("-ti")
        .arg(format!(":{port}"))
        .output()
        .with_context(|| format!("failed to run `lsof -ti :{port}` for port {port}"))?;

    if !output.status.success() {
        return Ok(None);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let pid_str = stdout.trim();

    if pid_str.is_empty() {
        return Ok(None);
    }

    let pid = pid_str
        .parse::<u32>()
        .context("Failed to parse PID from lsof output")?;

    Ok(Some(pid))
}

#[cfg(windows)]
pub fn find_process_using_port(port: u16) -> Result<Option<u32>> {
    let output = Command::new("netstat")
        .args(["-ano", "-p", "TCP"])
        .output()
        .with_context(|| format!("failed to run `netstat -ano -p TCP` for port {port}"))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let port_pattern = format!(":{port} ");
    let port_pattern_tab = format!(":{port}\t");

    for line in stdout.lines() {
        if line.contains(&port_pattern) || line.contains(&port_pattern_tab) {
            if let Some(pid_str) = line.split_whitespace().last() {
                if let Ok(pid) = pid_str.parse::<u32>() {
                    return Ok(Some(pid));
                }
            }
        }
    }

    Ok(None)
}

#[cfg(unix)]
pub fn get_process_info(pid: u32) -> Result<Option<ProcessInfo>> {
    let output = Command::new("ps")
        .arg("-p")
        .arg(pid.to_string())
        .arg("-o")
        .arg("pid,comm,args")
        .output()
        .with_context(|| format!("failed to run `ps -p {pid} -o pid,comm,args`"))?;

    if !output.status.success() {
        return Ok(None);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let lines: Vec<&str> = stdout.lines().collect();

    if lines.len() < 2 {
        return Ok(None);
    }

    let line = lines[1].trim();
    if line.is_empty() {
        return Ok(None);
    }

    let parts: Vec<&str> = line.splitn(3, char::is_whitespace).collect();
    if parts.len() < 3 {
        return Ok(None);
    }

    let command_line = parts[2].trim();

    Ok(Some(ProcessInfo {
        pid,
        command: command_line.to_string(),
    }))
}

#[cfg(windows)]
pub fn get_process_info(pid: u32) -> Result<Option<ProcessInfo>> {
    let output = Command::new("tasklist")
        .args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"])
        .output()
        .with_context(|| format!("failed to run `tasklist /FI PID eq {pid}`"))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let line = stdout.trim();

    if line.is_empty() || line.contains("INFO: No tasks") {
        return Ok(None);
    }

    let parts: Vec<&str> = line.split(',').collect();
    if parts.is_empty() {
        return Ok(None);
    }

    let command = parts[0].trim_matches('"').to_string();

    Ok(Some(ProcessInfo { pid, command }))
}

pub fn is_agent_process(pid: u32) -> Result<bool, String> {
    match get_process_info(pid) {
        Ok(Some(info)) => {
            let is_agent = info.command.contains("systemprompt")
                && (info.command.contains(CliPaths::agent_run_cmd_pattern())
                    || info.command.contains("agent-worker"));
            Ok(is_agent)
        },
        Ok(None) => Err(format!("No process info found for PID {}", pid)),
        Err(e) => Err(format!("Failed to get process info for PID {}: {}", pid, e)),
    }
}