jocker_lib/
start.rs

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    // We take all captures, replace them by their associated env value, then build a new string
144    // keeping the characters outside of placeholders, using captures' ranges.
145    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}