1use std::{collections::HashMap, sync::Arc};
2
3use dotenvy::dotenv_iter;
4use log::debug;
5use once_cell::sync::OnceCell;
6use regex::Regex;
7
8use crate::{
9 command::{cargo::Cargo, util::CommandLogger},
10 common::{Exec, Process, ProcessState},
11 error::{Error, InnerError, Result},
12 state::State,
13};
14
15#[derive(Debug, Default, PartialEq)]
16pub struct StartArgs {
17 pub processes: Vec<String>,
18}
19
20pub struct Start {
21 args: StartArgs,
22 state: Arc<State>,
23}
24
25impl Start {
26 pub fn new(args: StartArgs, state: Arc<State>) -> Self {
27 Start { args, state }
28 }
29
30 async fn build(&self, processes: &[Process]) -> Result<()> {
31 let binaries: Vec<&str> = processes.iter().map(|p| p.binary()).collect();
32 let cargo_args: Vec<&str> = processes
33 .iter()
34 .flat_map(|p| p.cargo_args())
35 .map(String::as_str)
36 .collect();
37 match Cargo::build(
38 self.state.get_target_dir(),
39 binaries.as_slice(),
40 cargo_args.as_slice(),
41 )
42 .await
43 {
44 Ok(mut build_process) => {
45 build_process.log_to_console().await?;
46 let build_exit_status = build_process.wait().await?;
47
48 if !build_exit_status.success() {
49 return Err(Error::new(InnerError::Start(format!(
50 "Build produced exit code {}",
51 build_exit_status
52 ))));
53 }
54 }
55 Err(e) => {
56 println!("Error while building crates: {e}");
57 for process in processes {
58 self.state
59 .set_state(process.name(), ProcessState::Stopped)
60 .await?;
61 }
62 }
63 }
64 Ok(())
65 }
66
67 pub async fn run(&self) -> Result<()> {
68 let processes = self.state.filter_processes(&self.args.processes).await?;
69 for process in &processes {
70 self.state
71 .set_state(process.name(), ProcessState::Building)
72 .await?;
73 }
74 self.build(processes.as_slice()).await?;
75 for process in processes {
76 let process_name = process.name().to_string();
77 if let Err(e) = self.run_process(process).await {
78 println!("Error while starting process {process_name}: {e}")
79 }
80 }
81 Ok(())
82 }
83
84 async fn run_process(&self, process: Process) -> Result<()> {
85 if process.state != ProcessState::Stopped && process.state != ProcessState::Building {
86 println!("Process is already started: {}", process.name());
87 return Ok(());
88 }
89 let process_name = process.name().to_string();
90 println!("Starting process {process_name} ...");
91 let mut env: HashMap<String, String> = HashMap::new();
92 if let Ok(dotenv) = dotenv_iter() {
93 for (key, val) in dotenv.flatten() {
94 debug!("process {}, .env variable {} = {}", process_name, key, val);
95 env.insert(key, val);
96 }
97 }
98 for (key, val) in process.env.iter() {
99 env.insert(key.to_string(), val.to_string());
100 }
101 let env = env;
102
103 let mut command = vec![];
104 command.push(format!("./target/debug/{}", process.binary()));
105 for arg in process.args() {
106 command.push(envsubst(arg, &env));
107 }
108
109 let pid = self
110 .state
111 .scheduler()
112 .start(
113 process_name.clone(),
114 command.join(" "),
115 self.state.get_target_dir().to_path_buf(),
116 env,
117 )
118 .await?;
119 self.state
120 .set_state(process.name(), ProcessState::Running)
121 .await?;
122 self.state.set_pid(process.name(), Some(pid)).await?;
123 println!("Process {process_name} started");
124 Ok(())
125 }
126}
127
128impl Exec<()> for Start {
129 async fn exec(&self) -> Result<()> {
130 self.run().await?;
131
132 Ok(())
133 }
134}
135
136static ENVSUBST_REGEX: OnceCell<Regex> = OnceCell::new();
137
138pub fn envsubst(value: &str, env: &HashMap<String, String>) -> String {
139 let re = ENVSUBST_REGEX.get_or_init(|| Regex::new(r"\$\{([a-zA-Z0-9-_:/.\[\]]*)}").unwrap());
140
141 let mut last_range_end = 0;
142 let mut ret = "".to_string();
143 for capture in re.captures_iter(value) {
146 let (_, [name]) = capture.extract();
147 let range = capture
148 .get(0)
149 .expect("Cannot happen as i == 0 is guaranteed to return Some")
150 .range();
151 if range.start != 0 {
152 ret.push_str(&value[last_range_end..range.start]);
153 }
154 last_range_end = range.end;
155 let split: Vec<&str> = name.split(":-").collect();
156 let var_name = split.first().map(|s| s.to_string()).unwrap_or_default();
157 let default = split.get(1).map(|s| s.to_string());
158 let var_value = env
159 .get(&var_name)
160 .map(|s| s.to_string())
161 .or(default)
162 .unwrap_or_default();
163 ret.push_str(&var_value);
164 }
165 if last_range_end != value.len() {
166 ret.push_str(&value[last_range_end..value.len()]);
167 }
168 ret
169}
170
171#[cfg(test)]
172mod tests {
173 use std::collections::HashMap;
174
175 use crate::start::envsubst;
176
177 #[test]
178 fn test_envsubst() {
179 let mut env = HashMap::new();
180 assert_eq!(&envsubst("${FOO:-baz}", &env), "baz");
181 env.insert("FOO".to_string(), "BAR".to_string());
182 assert_eq!(&envsubst("FOO", &env), "FOO");
183 assert_eq!(&envsubst("${FOO}", &env), "BAR");
184 assert_eq!(&envsubst("${FOO:-baz}", &env), "BAR");
185 }
186}