cli/actions/
start_executor.rs1use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::{Child, Command, Stdio};
6use std::thread;
7
8use crate::Result;
9use crate::build_executor::{
10 BuildExecutionPlan, BuildWatchState, BuildWatchTickResult, create_build_execution_plan,
11 create_build_watch_state, execute_build_plan, execute_build_watch_tick,
12};
13use crate::error::CliError;
14use crate::runners::RunnerCommand;
15use crate::start_action::StartActionPlan;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct StartExecutionPlan {
19 pub command: RunnerCommand,
20 pub warnings: Vec<String>,
21 pub watch: Option<StartWatchExecutionPlan>,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct StartWatchExecutionPlan {
26 pub build_plan: BuildExecutionPlan,
27 pub kill_previous_process_on_success: bool,
28}
29
30pub trait StartChildProcess {
31 fn kill(&mut self) -> Result<()>;
32}
33
34pub trait StartProcessSpawner {
35 type Child: StartChildProcess;
36
37 fn spawn(&mut self, command: &RunnerCommand) -> Result<Self::Child>;
38}
39
40#[derive(Debug, Default)]
41pub struct OsStartProcessSpawner;
42
43#[derive(Debug)]
44pub struct OsStartChildProcess {
45 child: Child,
46}
47
48pub fn create_start_execution_plan(plan: &StartActionPlan) -> Result<StartExecutionPlan> {
49 let mut warnings = Vec::new();
50 if plan.process_plan.debug_flag.is_some() {
51 warnings.push("`--debug` is ignored for Rust cargo runs".to_string());
52 }
53 if plan.process_plan.requested_exec.is_some() {
54 warnings.push("`--exec` is ignored for Rust cargo runs".to_string());
55 }
56 if !plan.process_plan.shell {
57 warnings.push("`--no-shell` is ignored for Rust cargo runs".to_string());
58 }
59
60 let watch = if plan.build_plan.watch_mode {
61 let build_plan = create_build_execution_plan(&plan.build_plan)?;
62 warnings.extend(build_plan.warnings.clone());
63 Some(StartWatchExecutionPlan {
64 build_plan,
65 kill_previous_process_on_success: plan
66 .process_plan
67 .restart
68 .kill_previous_process_on_success,
69 })
70 } else {
71 None
72 };
73
74 let mut command = plan.process_plan.source_root_command.clone();
75 command.env = load_env_files(command.cwd.as_deref(), &plan.process_plan.env_file)?;
76
77 Ok(StartExecutionPlan {
78 command,
79 warnings,
80 watch,
81 })
82}
83
84pub fn execute_start_plan(plan: &StartExecutionPlan) -> Result<()> {
85 if plan.watch.is_some() {
86 let mut spawner = OsStartProcessSpawner;
87 return execute_start_watch_plan_with(plan, &mut spawner);
88 }
89
90 plan.command.execute().map(|_| ())
91}
92
93pub fn execute_start_watch_plan_with<S>(plan: &StartExecutionPlan, spawner: &mut S) -> Result<()>
94where
95 S: StartProcessSpawner,
96{
97 let watch = plan.watch.as_ref().ok_or_else(|| {
98 CliError::UnsupportedCommand(
99 "`nest start --watch` requires a watch execution plan".to_string(),
100 )
101 })?;
102 let build_watch = watch.build_plan.watch.as_ref().ok_or_else(|| {
103 CliError::UnsupportedCommand(
104 "`nest start --watch` requires a build watch execution plan".to_string(),
105 )
106 })?;
107
108 execute_build_plan(&watch.build_plan)?;
109 let mut child = spawner.spawn(&plan.command)?;
110 let mut state = create_build_watch_state(build_watch)?;
111
112 loop {
113 thread::sleep(build_watch.poll_interval);
114 start_watch_tick(plan, &mut state, &mut child, spawner)?;
115 }
116}
117
118pub fn start_watch_tick<S>(
119 plan: &StartExecutionPlan,
120 state: &mut BuildWatchState,
121 child: &mut S::Child,
122 spawner: &mut S,
123) -> Result<Vec<BuildWatchTickResult>>
124where
125 S: StartProcessSpawner,
126{
127 let watch = plan.watch.as_ref().ok_or_else(|| {
128 CliError::UnsupportedCommand(
129 "`nest start --watch` requires a watch execution plan".to_string(),
130 )
131 })?;
132
133 let results = execute_build_watch_tick(&watch.build_plan, state)?;
134 if results.iter().any(|result| result.changed) {
135 let mut next_child = spawner.spawn(&plan.command)?;
136 if watch.kill_previous_process_on_success {
137 child.kill()?;
138 }
139 std::mem::swap(child, &mut next_child);
140 }
141
142 Ok(results)
143}
144
145impl StartProcessSpawner for OsStartProcessSpawner {
146 type Child = OsStartChildProcess;
147
148 fn spawn(&mut self, command: &RunnerCommand) -> Result<Self::Child> {
149 let mut process = command_for_spawn(command);
150 if let Some(cwd) = &command.cwd {
151 process.current_dir(cwd);
152 }
153 process.envs(command.env.iter().map(|(key, value)| (key, value)));
154 process
155 .stdin(Stdio::inherit())
156 .stdout(Stdio::inherit())
157 .stderr(Stdio::inherit());
158
159 let child = process.spawn().map_err(|error| CliError::RunnerFailed {
160 command: command.raw_full_command(),
161 reason: format!("failed to spawn process: {error}"),
162 })?;
163
164 Ok(OsStartChildProcess { child })
165 }
166}
167
168impl StartChildProcess for OsStartChildProcess {
169 fn kill(&mut self) -> Result<()> {
170 self.child.kill()?;
171 self.child.wait()?;
172 Ok(())
173 }
174}
175
176fn command_for_spawn(command: &RunnerCommand) -> Command {
177 if command.shell {
178 return shell_command(&command_line_for_execution(command));
179 }
180
181 let mut process = Command::new(&command.binary);
182 process.args(&command.prefix_args).arg(&command.command);
183 process.envs(command.env.iter().map(|(key, value)| (key, value)));
184 process
185}
186
187pub fn load_env_files(cwd: Option<&Path>, env_files: &[String]) -> Result<Vec<(String, String)>> {
188 let cwd = cwd.unwrap_or_else(|| Path::new("."));
189 let mut values = Vec::new();
190
191 for env_file in env_files {
192 let path = resolve_env_file(cwd, env_file);
193 let content = fs::read_to_string(&path).map_err(|error| {
194 CliError::InvalidConfiguration(format!(
195 "Failed to read env file `{}`: {error}",
196 path.display()
197 ))
198 })?;
199 values.extend(parse_env_file(&content)?);
200 }
201
202 Ok(values)
203}
204
205fn resolve_env_file(cwd: &Path, env_file: &str) -> PathBuf {
206 let path = Path::new(env_file);
207 if path.is_absolute() {
208 path.to_path_buf()
209 } else {
210 cwd.join(path)
211 }
212}
213
214pub fn parse_env_file(content: &str) -> Result<Vec<(String, String)>> {
215 let mut values = Vec::new();
216
217 for (line_number, raw_line) in content.lines().enumerate() {
218 let line = raw_line.trim();
219 if line.is_empty() || line.starts_with('#') {
220 continue;
221 }
222 let line = line.strip_prefix("export ").unwrap_or(line);
223 let Some((key, value)) = line.split_once('=') else {
224 return Err(CliError::InvalidConfiguration(format!(
225 "Invalid env file line {}: missing `=`",
226 line_number + 1
227 )));
228 };
229 let key = key.trim();
230 if key.is_empty() || key.contains(char::is_whitespace) {
231 return Err(CliError::InvalidConfiguration(format!(
232 "Invalid env file line {}: invalid key `{key}`",
233 line_number + 1
234 )));
235 }
236 values.push((key.to_string(), parse_env_value(value.trim())));
237 }
238
239 Ok(values)
240}
241
242fn parse_env_value(value: &str) -> String {
243 let value = strip_inline_comment(value).trim();
244 if (value.starts_with('"') && value.ends_with('"'))
245 || (value.starts_with('\'') && value.ends_with('\''))
246 {
247 value[1..value.len() - 1].to_string()
248 } else {
249 value.to_string()
250 }
251}
252
253fn strip_inline_comment(value: &str) -> &str {
254 let mut in_single = false;
255 let mut in_double = false;
256 let mut previous = None;
257
258 for (index, character) in value.char_indices() {
259 match character {
260 '\'' if !in_double => in_single = !in_single,
261 '"' if !in_single => in_double = !in_double,
262 '#' if !in_single
263 && !in_double
264 && previous.is_none_or(|previous: char| previous.is_whitespace()) =>
265 {
266 return &value[..index];
267 }
268 _ => {}
269 }
270 previous = Some(character);
271 }
272 value
273}
274
275fn command_line_for_execution(command: &RunnerCommand) -> String {
276 let mut parts = Vec::with_capacity(2 + command.prefix_args.len());
277 parts.push(quote_shell_part(&command.binary));
278 parts.extend(command.prefix_args.iter().map(|arg| quote_shell_part(arg)));
279 parts.push(command.command.clone());
280 parts.join(" ")
281}
282
283fn shell_command(command_line: &str) -> Command {
284 #[cfg(windows)]
285 {
286 let mut command =
287 Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()));
288 command.arg("/C").arg(command_line);
289 command
290 }
291
292 #[cfg(not(windows))]
293 {
294 let mut command = Command::new("sh");
295 command.arg("-c").arg(command_line);
296 command
297 }
298}
299
300fn quote_shell_part(part: &str) -> String {
301 if part.is_empty()
302 || part.starts_with('"')
303 || part.starts_with('\'')
304 || !part.chars().any(char::is_whitespace)
305 {
306 return part.to_owned();
307 }
308
309 #[cfg(windows)]
310 {
311 format!("\"{}\"", part.replace('"', "\\\""))
312 }
313
314 #[cfg(not(windows))]
315 {
316 format!("'{}'", part.replace('\'', "'\\''"))
317 }
318}