use bevy_brp_mcp_macros::ParamStruct;
use bevy_brp_mcp_macros::ResultStruct;
use bevy_brp_mcp_macros::ToolFn;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use sysinfo::Process;
use sysinfo::ProcessesToUpdate;
use sysinfo::System;
use super::constants::CARGO_BIN_FLAG;
use super::constants::CARGO_COMMAND_NAME;
use super::constants::CARGO_EXAMPLE_FLAG;
use super::constants::CARGO_RUN_SUBCOMMAND;
use super::constants::EXAMPLES_PATH_SEGMENT;
use super::constants::GENERIC_PROCESS_NAMES;
use super::constants::MCP_BINARY_NAME;
use super::constants::STATUS_MAX_RETRIES;
use super::constants::STATUS_POLL_INTERVAL;
use super::constants::TARGET_DEBUG_PATH;
use super::constants::TARGET_RELEASE_PATH;
use super::process;
use crate::brp_tools;
use crate::brp_tools::Port;
use crate::brp_tools::ResponseStatus;
use crate::error::Error;
use crate::error::Result;
use crate::tool::BrpMethod;
use crate::tool::HandlerContext;
use crate::tool::HandlerResult;
use crate::tool::ToolFn;
use crate::tool::ToolResult;
#[derive(Clone, Deserialize, Serialize, JsonSchema, ParamStruct)]
pub struct StatusParams {
pub app_name: String,
#[serde(default)]
pub port: Port,
}
#[derive(Debug, Clone, Serialize, Deserialize, ResultStruct)]
pub struct StatusResult {
#[to_metadata]
app_name: String,
#[to_metadata]
pid: u32,
#[to_metadata]
port: u16,
#[to_message(
message_template = "Process '{app_name}' (PID: {pid}) is running with BRP enabled on port {port}"
)]
message_template: String,
}
#[derive(ToolFn)]
#[tool_fn(params = "StatusParams", output = "StatusResult")]
pub struct Status;
async fn handle_impl(params: StatusParams) -> Result<StatusResult> {
check_brp_for_app(¶ms.app_name, params.port).await
}
#[derive(Debug, Clone, Serialize, Deserialize, ResultStruct)]
struct ProcessNotFoundError {
#[to_error_info]
app_name: String,
#[to_error_info(skip_if_none)]
similar_app_name: Option<String>,
#[serde(rename = "brp_responding_on_port")]
#[to_error_info]
brp_port_status: BrpPortStatus,
#[to_error_info]
port: u16,
#[to_message]
message_template: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(from = "bool", into = "bool")]
enum BrpPortStatus {
NotResponding,
Responding,
}
impl From<bool> for BrpPortStatus {
fn from(value: bool) -> Self {
if value {
Self::Responding
} else {
Self::NotResponding
}
}
}
impl From<BrpPortStatus> for bool {
fn from(value: BrpPortStatus) -> Self { matches!(value, BrpPortStatus::Responding) }
}
impl BrpPortStatus {
const fn is_responding(self) -> bool { matches!(self, Self::Responding) }
}
#[derive(Debug, Clone, Serialize, Deserialize, ResultStruct)]
struct BrpNotRespondingError {
#[to_error_info]
app_name: String,
#[to_error_info]
pid: u32,
#[to_error_info]
port: u16,
#[to_message(
message_template = "Process '{app_name}' (PID: {pid}) is running but not responding to BRP on port {port}. Make sure RemotePlugin is added to your Bevy app."
)]
message_template: String,
}
fn process_matches_app_substring(process: &Process, target_app: &str) -> bool {
let normalized_target = process::normalize_process_name(target_app);
let process_name = process.name().to_string_lossy();
let normalized_process_name = process::normalize_process_name(&process_name);
if normalized_process_name.contains(&normalized_target) {
return true;
}
if let Some(command_path) = process.cmd().first() {
let command_string = command_path.to_string_lossy();
let normalized_command = process::normalize_process_name(&command_string);
if !GENERIC_PROCESS_NAMES.contains(&normalized_process_name.as_str())
&& normalized_command.contains(&normalized_target)
{
return true;
}
}
false
}
fn is_bevy_brp_mcp(process: &Process) -> bool {
let process_name = process.name().to_string_lossy();
process_name == MCP_BINARY_NAME
}
fn extract_app_name(process: &Process) -> String {
let process_name = process.name().to_string_lossy();
if process_name == CARGO_COMMAND_NAME {
let command_line_arguments: Vec<String> = process
.cmd()
.iter()
.map(|argument| argument.to_string_lossy().to_string())
.collect();
if command_line_arguments
.iter()
.any(|argument| argument == CARGO_RUN_SUBCOMMAND)
{
if let Some(binary_position) = command_line_arguments
.iter()
.position(|argument| argument == CARGO_BIN_FLAG)
&& let Some(binary_name) = command_line_arguments.get(binary_position + 1)
{
return binary_name.clone();
}
if let Some(example_position) = command_line_arguments
.iter()
.position(|argument| argument == CARGO_EXAMPLE_FLAG)
&& let Some(example_name) = command_line_arguments.get(example_position + 1)
{
return example_name.clone();
}
}
}
if process_name.contains(TARGET_DEBUG_PATH) || process_name.contains(TARGET_RELEASE_PATH) {
return process::normalize_process_name(&process_name);
}
if let Some(command_path) = process.cmd().first() {
let command_string = command_path.to_string_lossy();
if command_string.contains(TARGET_DEBUG_PATH)
|| command_string.contains(TARGET_RELEASE_PATH)
|| command_string.contains(EXAMPLES_PATH_SEGMENT)
{
return process::normalize_process_name(&command_string);
}
}
process::normalize_process_name(&process_name)
}
async fn check_brp_for_app(app_name: &str, port: Port) -> Result<StatusResult> {
let brp_port_status = check_brp_on_port(port).await?;
let mut system = System::new_all();
system.refresh_processes(ProcessesToUpdate::All, true);
if let Some(process_id) = process::get_pid_for_port(port) {
return resolve_pid_on_port(&system, app_name, port, brp_port_status, process_id);
}
if let Some(process_id) = find_exact_match_pid(&system, app_name) {
Err(Error::Structured {
result: Box::new(BrpNotRespondingError::new(
app_name.to_string(),
process_id,
port.0,
)),
})?;
}
build_missing_process_result(
app_name,
collect_similar_app_names(&system, app_name)
.first()
.cloned(),
brp_port_status,
port,
)
}
fn resolve_pid_on_port(
system: &System,
app_name: &str,
port: Port,
brp_port_status: BrpPortStatus,
process_id: u32,
) -> Result<StatusResult> {
if let Some(process) = system.process(sysinfo::Pid::from_u32(process_id))
&& process::process_matches_name_exact(process, app_name)
{
if brp_port_status.is_responding() {
return Ok(StatusResult::new(app_name.to_string(), process_id, port.0));
}
Err(Error::Structured {
result: Box::new(BrpNotRespondingError::new(
app_name.to_string(),
process_id,
port.0,
)),
})?;
}
build_missing_process_result(app_name, None, brp_port_status, port)
}
fn build_missing_process_result(
app_name: &str,
similar_app_name: Option<String>,
brp_port_status: BrpPortStatus,
port: Port,
) -> Result<StatusResult> {
let process_not_found_error = ProcessNotFoundError::new(
app_name.to_string(),
similar_app_name.clone(),
brp_port_status,
port.0,
)
.with_message_template(missing_process_message(
app_name,
similar_app_name.as_deref(),
brp_port_status,
port,
));
Err(Error::Structured {
result: Box::new(process_not_found_error),
})?
}
fn missing_process_message(
app_name: &str,
similar_app_name: Option<&str>,
brp_port_status: BrpPortStatus,
port: Port,
) -> String {
match (similar_app_name, brp_port_status) {
(Some(suggestion), BrpPortStatus::Responding) => {
format!(
"Process '{app_name}' not found. Did you mean: {suggestion}? (BRP is responding on port {})",
port.0
)
},
(Some(suggestion), BrpPortStatus::NotResponding) => {
format!("Process '{app_name}' not found. Did you mean: {suggestion}?")
},
(None, BrpPortStatus::Responding) => {
format!(
"Process '{app_name}' not found. BRP is responding on port {} - another process may be using it.",
port.0
)
},
(None, BrpPortStatus::NotResponding) => {
format!(
"Process '{app_name}' not found and BRP is not responding on port {}.",
port.0
)
},
}
}
fn find_exact_match_pid(system: &System, app_name: &str) -> Option<u32> {
system.processes().values().find_map(|process| {
(!matches!(process.status(), sysinfo::ProcessStatus::Zombie)
&& process::process_matches_name_exact(process, app_name))
.then(|| process.pid().as_u32())
})
}
fn collect_similar_app_names(system: &System, app_name: &str) -> Vec<String> {
system
.processes()
.values()
.filter(|process| {
!matches!(process.status(), sysinfo::ProcessStatus::Zombie)
&& process_matches_app_substring(process, app_name)
&& !is_bevy_brp_mcp(process)
})
.map(extract_app_name)
.collect()
}
async fn check_brp_on_port(port: Port) -> Result<BrpPortStatus> {
for _ in 0..STATUS_MAX_RETRIES {
let client = brp_tools::BrpClient::new(BrpMethod::WorldListComponents, port, None);
match client.execute_raw().await {
Ok(ResponseStatus::Success(_)) => {
return Ok(BrpPortStatus::Responding);
},
Ok(ResponseStatus::Error(_)) | Err(_) => {
tokio::time::sleep(STATUS_POLL_INTERVAL).await;
},
}
}
Ok(BrpPortStatus::NotResponding)
}