use std::collections::HashMap;
use std::marker::PhantomData;
use std::path::PathBuf;
use std::process::Command;
use std::time::Instant;
use bevy_brp_mcp_macros::ResultStruct;
use serde::Deserialize;
use serde::Serialize;
use super::build;
use super::build::BuildState;
use super::build_freshness;
use super::build_freshness::FreshnessCheckResult;
use crate::app_tools::instance_count::InstanceCount;
use crate::app_tools::launch_params::SearchOrder;
use crate::app_tools::targets::BevyTarget;
use crate::app_tools::targets::TargetType;
use crate::brp_tools::Port;
use crate::error::Result;
#[derive(Clone)]
pub(super) struct App;
#[derive(Clone)]
pub(super) struct Example;
#[derive(Clone)]
pub(super) struct LaunchConfig<T> {
target: String,
profile: String,
package: Option<String>,
port: Port,
instance_count: InstanceCount,
env: Option<HashMap<String, String>>,
arguments: Option<Vec<String>>,
phantom_data: PhantomData<T>,
}
impl<T> LaunchConfig<T> {
const fn new(
target: String,
profile: String,
package: Option<String>,
port: Port,
instance_count: InstanceCount,
env: Option<HashMap<String, String>>,
arguments: Option<Vec<String>>,
) -> Self {
Self {
target,
profile,
package,
port,
instance_count,
env,
arguments,
phantom_data: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LaunchedInstance {
pub pid: u32,
pub log_file: String,
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, ResultStruct)]
pub struct LaunchResult {
#[serde(rename = "target_name")]
#[to_metadata(skip_if_none)]
target: Option<String>,
#[to_result]
instances: Vec<LaunchedInstance>,
#[to_metadata(skip_if_none)]
working_directory: Option<String>,
#[to_metadata(skip_if_none)]
profile: Option<String>,
#[to_metadata(skip_if_none)]
binary_path: Option<String>,
#[serde(rename = "launch_duration_ms")]
#[to_metadata(skip_if_none)]
duration_ms: Option<u128>,
#[serde(rename = "launch_timestamp")]
#[to_metadata(skip_if_none)]
timestamp: Option<String>,
#[to_metadata(skip_if_none)]
workspace: Option<String>,
#[serde(rename = "package_name")]
#[to_metadata(skip_if_none)]
package: Option<String>,
#[to_metadata(skip_if_none)]
launched_as: Option<String>,
#[to_metadata(skip_if_none)]
duplicate_paths: Option<Vec<String>>,
#[to_message]
message_template: Option<String>,
}
pub struct LaunchParams {
pub target: String,
pub profile: String,
pub path: Option<String>,
pub package: Option<String>,
pub port: Port,
pub instance_count: InstanceCount,
pub env: Option<HashMap<String, String>>,
pub search_order: SearchOrder,
pub args: Option<Vec<String>>,
}
pub(super) trait LaunchConfigTrait: Clone {
const TARGET_TYPE: TargetType;
fn target(&self) -> &str;
fn profile(&self) -> &str;
fn package(&self) -> Option<&str>;
fn port(&self) -> Port;
fn instance_count(&self) -> InstanceCount;
fn set_port(&mut self, port: Port);
fn build_command(&self, target: &BevyTarget) -> Command;
fn extra_log_info(&self, target: &BevyTarget) -> Option<String>;
fn ensure_built(&self, target: &BevyTarget) -> Result<BuildState> {
if Self::TARGET_TYPE == TargetType::App {
let freshness = build_freshness::check_target_freshness(target, self.profile());
match &freshness {
FreshnessCheckResult::Fresh => {},
FreshnessCheckResult::Stale(reason) => tracing::debug!(
"Lock-free freshness check marked {} '{}' stale: {reason}",
Self::TARGET_TYPE,
self.target(),
),
FreshnessCheckResult::Unknown(reason) => tracing::debug!(
"Lock-free freshness check was inconclusive for {} '{}': {reason}",
Self::TARGET_TYPE,
self.target(),
),
}
if matches!(freshness, FreshnessCheckResult::Fresh) {
return Ok(BuildState::Fresh);
}
}
let manifest_dir = build::validate_manifest_directory(&target.manifest)?;
build::run_cargo_build(
self.target(),
Self::TARGET_TYPE,
self.profile(),
manifest_dir,
)
}
}
pub(super) fn build_launch_result<T: LaunchConfigTrait>(
all_pids: Vec<u32>,
all_log_files: Vec<PathBuf>,
all_ports: Vec<u16>,
config: &T,
target: &BevyTarget,
launch_start: Instant,
) -> LaunchResult {
let launch_duration = launch_start.elapsed();
let instances: Vec<LaunchedInstance> = all_pids
.into_iter()
.zip(all_log_files.iter())
.zip(all_ports.iter())
.map(|((process_id, log_file), port)| LaunchedInstance {
pid: process_id,
log_file: log_file.display().to_string(),
port: *port,
})
.collect();
let workspace = target
.workspace_root
.file_name()
.and_then(|name| name.to_str())
.map(String::from);
let port_range = if all_ports.len() == 1 {
all_ports[0].to_string()
} else {
format!("{}-{}", all_ports[0], all_ports[all_ports.len() - 1])
};
let instance_count = all_ports.len();
let message = format!(
"Successfully launched {instance_count} instance(s) of {} on ports {port_range}",
config.target()
);
LaunchResult {
target: Some(config.target().to_string()),
instances,
working_directory: std::env::current_dir()
.ok()
.map(|dir| dir.display().to_string()),
profile: Some(config.profile().to_string()),
duration_ms: Some(launch_duration.as_millis()),
timestamp: Some(chrono::Utc::now().to_rfc3339()),
workspace,
package: if T::TARGET_TYPE == TargetType::Example {
Some(target.package_name.clone())
} else {
None
},
binary_path: if T::TARGET_TYPE == TargetType::App {
Some(
target
.get_binary_path(config.profile())
.display()
.to_string(),
)
} else {
None
},
launched_as: Some(T::TARGET_TYPE.to_string()),
duplicate_paths: None,
message_template: Some(message),
}
}
impl From<&LaunchParams> for LaunchConfig<App> {
fn from(params: &LaunchParams) -> Self {
Self::new(
params.target.clone(),
params.profile.clone(),
params.package.clone(),
params.port,
params.instance_count,
params.env.clone(),
params.args.clone(),
)
}
}
impl LaunchConfigTrait for LaunchConfig<App> {
const TARGET_TYPE: TargetType = TargetType::App;
fn target(&self) -> &str { &self.target }
fn profile(&self) -> &str { &self.profile }
fn package(&self) -> Option<&str> { self.package.as_deref() }
fn port(&self) -> Port { self.port }
fn instance_count(&self) -> InstanceCount { self.instance_count }
fn set_port(&mut self, port: Port) { self.port = port; }
fn build_command(&self, target: &BevyTarget) -> Command {
build::build_app_command(
&target.get_binary_path(self.profile()),
Some(self.port),
self.env.as_ref(),
self.arguments.as_deref(),
)
}
fn extra_log_info(&self, _: &BevyTarget) -> Option<String> { None }
}
impl From<&LaunchParams> for LaunchConfig<Example> {
fn from(params: &LaunchParams) -> Self {
Self::new(
params.target.clone(),
params.profile.clone(),
params.package.clone(),
params.port,
params.instance_count,
params.env.clone(),
params.args.clone(),
)
}
}
impl LaunchConfigTrait for LaunchConfig<Example> {
const TARGET_TYPE: TargetType = TargetType::Example;
fn target(&self) -> &str { &self.target }
fn profile(&self) -> &str { &self.profile }
fn package(&self) -> Option<&str> { self.package.as_deref() }
fn port(&self) -> Port { self.port }
fn instance_count(&self) -> InstanceCount { self.instance_count }
fn set_port(&mut self, port: Port) { self.port = port; }
fn build_command(&self, _: &BevyTarget) -> Command {
build::build_cargo_example_command(
&self.target,
self.profile(),
Some(self.port),
self.env.as_ref(),
self.arguments.as_deref(),
)
}
fn extra_log_info(&self, target: &BevyTarget) -> Option<String> {
Some(format!("Package: {}", target.package_name))
}
}