use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use crate::Result;
use crate::build_executor::{
BuildExecutionPlan, BuildWatchState, BuildWatchTickResult, create_build_execution_plan,
create_build_watch_state, execute_build_plan, execute_build_watch_tick,
};
use crate::error::CliError;
use crate::runners::RunnerCommand;
use crate::start_action::StartActionPlan;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StartExecutionPlan {
pub command: RunnerCommand,
pub warnings: Vec<String>,
pub watch: Option<StartWatchExecutionPlan>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StartWatchExecutionPlan {
pub build_plan: BuildExecutionPlan,
pub kill_previous_process_on_success: bool,
}
pub trait StartChildProcess {
fn kill(&mut self) -> Result<()>;
}
pub trait StartProcessSpawner {
type Child: StartChildProcess;
fn spawn(&mut self, command: &RunnerCommand) -> Result<Self::Child>;
}
#[derive(Debug, Default)]
pub struct OsStartProcessSpawner;
#[derive(Debug)]
pub struct OsStartChildProcess {
child: Child,
}
pub fn create_start_execution_plan(plan: &StartActionPlan) -> Result<StartExecutionPlan> {
let mut warnings = Vec::new();
if plan.process_plan.debug_flag.is_some() {
warnings.push("`--debug` is ignored for Rust cargo runs".to_string());
}
if plan.process_plan.requested_exec.is_some() {
warnings.push("`--exec` is ignored for Rust cargo runs".to_string());
}
if !plan.process_plan.shell {
warnings.push("`--no-shell` is ignored for Rust cargo runs".to_string());
}
let watch = if plan.build_plan.watch_mode {
let build_plan = create_build_execution_plan(&plan.build_plan)?;
warnings.extend(build_plan.warnings.clone());
Some(StartWatchExecutionPlan {
build_plan,
kill_previous_process_on_success: plan
.process_plan
.restart
.kill_previous_process_on_success,
})
} else {
None
};
let mut command = plan.process_plan.source_root_command.clone();
command.env = load_env_files(command.cwd.as_deref(), &plan.process_plan.env_file)?;
Ok(StartExecutionPlan {
command,
warnings,
watch,
})
}
pub fn execute_start_plan(plan: &StartExecutionPlan) -> Result<()> {
if plan.watch.is_some() {
let mut spawner = OsStartProcessSpawner;
return execute_start_watch_plan_with(plan, &mut spawner);
}
plan.command.execute().map(|_| ())
}
pub fn execute_start_watch_plan_with<S>(plan: &StartExecutionPlan, spawner: &mut S) -> Result<()>
where
S: StartProcessSpawner,
{
let watch = plan.watch.as_ref().ok_or_else(|| {
CliError::UnsupportedCommand(
"`nest start --watch` requires a watch execution plan".to_string(),
)
})?;
let build_watch = watch.build_plan.watch.as_ref().ok_or_else(|| {
CliError::UnsupportedCommand(
"`nest start --watch` requires a build watch execution plan".to_string(),
)
})?;
execute_build_plan(&watch.build_plan)?;
let mut child = spawner.spawn(&plan.command)?;
let mut state = create_build_watch_state(build_watch)?;
loop {
thread::sleep(build_watch.poll_interval);
start_watch_tick(plan, &mut state, &mut child, spawner)?;
}
}
pub fn start_watch_tick<S>(
plan: &StartExecutionPlan,
state: &mut BuildWatchState,
child: &mut S::Child,
spawner: &mut S,
) -> Result<Vec<BuildWatchTickResult>>
where
S: StartProcessSpawner,
{
let watch = plan.watch.as_ref().ok_or_else(|| {
CliError::UnsupportedCommand(
"`nest start --watch` requires a watch execution plan".to_string(),
)
})?;
let results = execute_build_watch_tick(&watch.build_plan, state)?;
if results.iter().any(|result| result.changed) {
let mut next_child = spawner.spawn(&plan.command)?;
if watch.kill_previous_process_on_success {
child.kill()?;
}
std::mem::swap(child, &mut next_child);
}
Ok(results)
}
impl StartProcessSpawner for OsStartProcessSpawner {
type Child = OsStartChildProcess;
fn spawn(&mut self, command: &RunnerCommand) -> Result<Self::Child> {
let mut process = command_for_spawn(command);
if let Some(cwd) = &command.cwd {
process.current_dir(cwd);
}
process.envs(command.env.iter().map(|(key, value)| (key, value)));
process
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let child = process.spawn().map_err(|error| CliError::RunnerFailed {
command: command.raw_full_command(),
reason: format!("failed to spawn process: {error}"),
})?;
Ok(OsStartChildProcess { child })
}
}
impl StartChildProcess for OsStartChildProcess {
fn kill(&mut self) -> Result<()> {
self.child.kill()?;
self.child.wait()?;
Ok(())
}
}
fn command_for_spawn(command: &RunnerCommand) -> Command {
if command.shell {
return shell_command(&command_line_for_execution(command));
}
let mut process = Command::new(&command.binary);
process.args(&command.prefix_args).arg(&command.command);
process.envs(command.env.iter().map(|(key, value)| (key, value)));
process
}
pub fn load_env_files(cwd: Option<&Path>, env_files: &[String]) -> Result<Vec<(String, String)>> {
let cwd = cwd.unwrap_or_else(|| Path::new("."));
let mut values = Vec::new();
for env_file in env_files {
let path = resolve_env_file(cwd, env_file);
let content = fs::read_to_string(&path).map_err(|error| {
CliError::InvalidConfiguration(format!(
"Failed to read env file `{}`: {error}",
path.display()
))
})?;
values.extend(parse_env_file(&content)?);
}
Ok(values)
}
fn resolve_env_file(cwd: &Path, env_file: &str) -> PathBuf {
let path = Path::new(env_file);
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
}
pub fn parse_env_file(content: &str) -> Result<Vec<(String, String)>> {
let mut values = Vec::new();
for (line_number, raw_line) in content.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let line = line.strip_prefix("export ").unwrap_or(line);
let Some((key, value)) = line.split_once('=') else {
return Err(CliError::InvalidConfiguration(format!(
"Invalid env file line {}: missing `=`",
line_number + 1
)));
};
let key = key.trim();
if key.is_empty() || key.contains(char::is_whitespace) {
return Err(CliError::InvalidConfiguration(format!(
"Invalid env file line {}: invalid key `{key}`",
line_number + 1
)));
}
values.push((key.to_string(), parse_env_value(value.trim())));
}
Ok(values)
}
fn parse_env_value(value: &str) -> String {
let value = strip_inline_comment(value).trim();
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
value[1..value.len() - 1].to_string()
} else {
value.to_string()
}
}
fn strip_inline_comment(value: &str) -> &str {
let mut in_single = false;
let mut in_double = false;
let mut previous = None;
for (index, character) in value.char_indices() {
match character {
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'#' if !in_single
&& !in_double
&& previous.is_none_or(|previous: char| previous.is_whitespace()) =>
{
return &value[..index];
}
_ => {}
}
previous = Some(character);
}
value
}
fn command_line_for_execution(command: &RunnerCommand) -> String {
let mut parts = Vec::with_capacity(2 + command.prefix_args.len());
parts.push(quote_shell_part(&command.binary));
parts.extend(command.prefix_args.iter().map(|arg| quote_shell_part(arg)));
parts.push(command.command.clone());
parts.join(" ")
}
fn shell_command(command_line: &str) -> Command {
#[cfg(windows)]
{
let mut command =
Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()));
command.arg("/C").arg(command_line);
command
}
#[cfg(not(windows))]
{
let mut command = Command::new("sh");
command.arg("-c").arg(command_line);
command
}
}
fn quote_shell_part(part: &str) -> String {
if part.is_empty()
|| part.starts_with('"')
|| part.starts_with('\'')
|| !part.chars().any(char::is_whitespace)
{
return part.to_owned();
}
#[cfg(windows)]
{
format!("\"{}\"", part.replace('"', "\\\""))
}
#[cfg(not(windows))]
{
format!("'{}'", part.replace('\'', "'\\''"))
}
}