perseus_cli/
serve.rs

1use crate::build::build_internal;
2use crate::cmd::{cfg_spinner, run_stage};
3use crate::install::Tools;
4use crate::parse::{Opts, ServeOpts};
5use crate::thread::{spawn_thread, ThreadHandle};
6use crate::{errors::*, order_reload};
7use console::{style, Emoji};
8use indicatif::{MultiProgress, ProgressBar};
9use std::env;
10use std::io::Write;
11use std::path::PathBuf;
12use std::process::{Command, Stdio};
13use std::sync::{Arc, Mutex};
14
15// Emojis for stages
16static BUILDING_SERVER: Emoji<'_, '_> = Emoji("📡", "");
17static SERVING: Emoji<'_, '_> = Emoji("🛰️ ", "");
18
19/// Returns the exit code if it's non-zero.
20macro_rules! handle_exit_code {
21    ($code:expr) => {{
22        let (stdout, stderr, code) = $code;
23        if code != 0 {
24            return ::std::result::Result::Ok(code);
25        }
26        (stdout, stderr)
27    }};
28}
29
30/// Builds the server for the app, program arguments having been interpreted.
31/// This needs to know if we've built as part of this process so it can show an
32/// accurate progress count. This also takes a `MultiProgress` so it can be used
33/// truly atomically (which will have build spinners already on it if
34/// necessary). This also takes a `Mutex<String>` to inform the caller of the
35/// path of the server executable.
36fn build_server(
37    dir: PathBuf,
38    spinners: &MultiProgress,
39    num_steps: u8,
40    exec: Arc<Mutex<String>>,
41    is_release: bool,
42    tools: &Tools,
43    global_opts: &Opts,
44) -> Result<
45    ThreadHandle<impl FnOnce() -> Result<i32, ExecutionError>, Result<i32, ExecutionError>>,
46    ExecutionError,
47> {
48    let tools = tools.clone();
49    let Opts {
50        cargo_engine_args, ..
51    } = global_opts.clone();
52
53    // Server building message
54    let sb_msg = format!(
55        "{} {} Building server",
56        style(format!("[{}/{}]", num_steps - 1, num_steps))
57            .bold()
58            .dim(),
59        BUILDING_SERVER
60    );
61
62    // We'll parallelize the building of the server with any build commands that are
63    // currently running We deliberately insert the spinner at the end of the
64    // list
65    let sb_spinner = spinners.insert((num_steps - 1).into(), ProgressBar::new_spinner());
66    let sb_spinner = cfg_spinner(sb_spinner, &sb_msg);
67    let sb_target = dir;
68    let sb_thread = spawn_thread(
69        move || {
70            let (stdout, _stderr) = handle_exit_code!(run_stage(
71                vec![&format!(
72                    // This sets Cargo to tell us everything, including the executable path to the
73                    // server
74                    "{} build --message-format json {} {}",
75                    tools.cargo_engine,
76                    if is_release { "--release" } else { "" },
77                    cargo_engine_args
78                )],
79                &sb_target,
80                &sb_spinner,
81                &sb_msg,
82                vec![
83                    ("CARGO_TARGET_DIR", "dist/target_engine"),
84                    ("RUSTFLAGS", "--cfg=engine"),
85                    ("CARGO_TERM_COLOR", "always")
86                ],
87                // These are JSON logs, never print them (they're duplicated by the build logs
88                // anyway, we're compiling the same thing)
89                false,
90            )?);
91
92            let msgs: Vec<&str> = stdout.trim().split('\n').collect();
93            // If we got to here, the exit code was 0 and everything should've worked
94            // The last message will just tell us that the build finished, the second-last
95            // one will tell us the executable path
96            let msg = msgs.get(msgs.len() - 2);
97            let msg = match msg {
98                // We'll parse it as a Serde `Value`, we don't need to know everything that's in
99                // there
100                Some(msg) => serde_json::from_str::<serde_json::Value>(msg)
101                    .map_err(|err| ExecutionError::GetServerExecutableFailed { source: err })?,
102                None => return Err(ExecutionError::ServerExecutableMsgNotFound),
103            };
104            let server_exec_path = msg.get("executable");
105            let server_exec_path = match server_exec_path {
106            // We'll parse it as a Serde `Value`, we don't need to know everything that's in there
107            Some(server_exec_path) => match server_exec_path.as_str() {
108                Some(server_exec_path) => server_exec_path,
109                None => {
110                    return Err(ExecutionError::ParseServerExecutableFailed {
111                        err: "expected 'executable' field to be string".to_string(),
112                    })
113                }
114            },
115            None => return Err(ExecutionError::ParseServerExecutableFailed {
116                err: "expected 'executable' field in JSON map in second-last message, not present"
117                    .to_string(),
118            }),
119        };
120
121            // And now the main thread needs to know about this
122            let mut exec_val = exec.lock().unwrap();
123            *exec_val = server_exec_path.to_string();
124
125            Ok(0)
126        },
127        global_opts.sequential,
128    );
129
130    Ok(sb_thread)
131}
132
133/// Runs the server at the given path, handling any errors therewith. This will
134/// likely be a black hole until the user manually terminates the process.
135///
136/// This function is not used by the testing process.
137fn run_server(
138    exec: Arc<Mutex<String>>,
139    dir: PathBuf,
140    num_steps: u8,
141    verbose: bool,
142) -> Result<i32, ExecutionError> {
143    // First off, handle any issues with the executable path
144    let exec_val = exec.lock().unwrap();
145    if exec_val.is_empty() {
146        return Err(ExecutionError::ParseServerExecutableFailed {
147            err: "mutex value empty, implies uncaught thread termination (please report this as a bug)"
148                .to_string()
149    });
150    }
151    let server_exec_path = (*exec_val).to_string();
152
153    // Manually run the generated binary (invoking in the right directory context
154    // for good measure if it ever needs it in future)
155    let child = Command::new(&server_exec_path)
156        .current_dir(&dir)
157        // This needs to be provided in development, but not in production
158        .env("PERSEUS_ENGINE_OPERATION", "serve")
159        // We should be able to access outputs in case there's an error
160        .stdout(if verbose {
161            Stdio::inherit()
162        } else {
163            Stdio::piped()
164        })
165        .stderr(if verbose {
166            Stdio::inherit()
167        } else {
168            Stdio::piped()
169        })
170        .spawn()
171        .map_err(|err| ExecutionError::CmdExecFailed {
172            cmd: server_exec_path,
173            source: err,
174        })?;
175    // Figure out what host/port the app will be live on (these have been set by the
176    // system)
177    let host = env::var("PERSEUS_HOST").unwrap_or_else(|_| "localhost".to_string());
178    let port = env::var("PERSEUS_PORT")
179        .unwrap_or_else(|_| "8080".to_string())
180        .parse::<u16>()
181        .map_err(|err| ExecutionError::PortNotNumber { source: err })?;
182    // Give the user a nice informational message
183    println!(
184        "  {} {} Your app is now live on <http://{host}:{port}>! To change this, re-run this command with different settings for `--host` and `--port`.",
185        style(format!("[{}/{}]", num_steps, num_steps)).bold().dim(),
186        SERVING,
187        host=host,
188        port=port
189    );
190
191    // Wait on the child process to finish (which it shouldn't unless there's an
192    // error), then perform error handling
193    let output = child.wait_with_output().unwrap();
194    let exit_code = match output.status.code() {
195        Some(exit_code) => exit_code,         // If we have an exit code, use it
196        None if output.status.success() => 0, /* If we don't, but we know the command succeeded, */
197        // return 0 (success code)
198        None => 1, /* If we don't know an exit code but we know that the command failed, return 1
199                    * (general error code) */
200    };
201    // Print `stderr` and stdout` only if there's something therein and the exit
202    // code is non-zero
203    if !output.stderr.is_empty() && exit_code != 0 {
204        // We don't print any failure message other than the actual error right now (see
205        // if people want something else?)
206        std::io::stderr().write_all(&output.stdout).unwrap();
207        std::io::stderr().write_all(&output.stderr).unwrap();
208        return Ok(1);
209    }
210
211    Ok(0)
212}
213
214/// Builds the subcrates to get a directory that we can serve and then serves
215/// it. If possible, this will return the path to the server executable so that
216/// it can be used in deployment.
217pub fn serve(
218    dir: PathBuf,
219    opts: &ServeOpts,
220    tools: &Tools,
221    global_opts: &Opts,
222    spinners: &MultiProgress,
223    silent_no_run: bool,
224) -> Result<(i32, Option<String>), ExecutionError> {
225    // Set the environment variables for the host and port
226    // NOTE Another part of this code depends on setting these in this way
227    env::set_var("PERSEUS_HOST", &opts.host);
228    env::set_var("PERSEUS_PORT", opts.port.to_string());
229
230    let did_build = !opts.no_build;
231    let should_run = !opts.no_run;
232
233    // Weird naming here, but this is right
234    let num_steps = if did_build { 4 } else { 2 };
235
236    // We need to have a way of knowing what the executable path to the server is
237    let exec = Arc::new(Mutex::new(String::new()));
238    // We can begin building the server in a thread without having to deal with the
239    // rest of the build stage yet
240    let sb_thread = build_server(
241        dir.clone(),
242        spinners,
243        num_steps,
244        Arc::clone(&exec),
245        opts.release,
246        tools,
247        global_opts,
248    )?;
249    // Only build if the user hasn't set `--no-build`, handling non-zero exit codes
250    if did_build {
251        let (sg_thread, wb_thread) = build_internal(
252            dir.clone(),
253            spinners,
254            num_steps,
255            opts.release,
256            tools,
257            global_opts,
258        )?;
259        let sg_res = sg_thread
260            .join()
261            .map_err(|_| ExecutionError::ThreadWaitFailed)??;
262        let wb_res = wb_thread
263            .join()
264            .map_err(|_| ExecutionError::ThreadWaitFailed)??;
265        if sg_res != 0 {
266            return Ok((sg_res, None));
267        } else if wb_res != 0 {
268            return Ok((wb_res, None));
269        }
270    }
271    // Handle errors from the server building
272    let sb_res = sb_thread
273        .join()
274        .map_err(|_| ExecutionError::ThreadWaitFailed)??;
275    if sb_res != 0 {
276        return Ok((sb_res, None));
277    }
278
279    // Order any connected browsers to reload
280    order_reload(
281        global_opts.reload_server_host.to_string(),
282        global_opts.reload_server_port,
283    );
284
285    // Now actually run that executable path if we should
286    if should_run {
287        let exit_code = run_server(Arc::clone(&exec), dir, num_steps, global_opts.verbose)?;
288        Ok((exit_code, None))
289    } else {
290        // The user doesn't want to run the server, so we'll give them the executable
291        // path instead
292        let exec_str = (*exec.lock().unwrap()).to_string();
293        // Only tell the user about this if they've told us not to run (since deployment
294        // and testing both implicitly do this)
295        if !silent_no_run {
296            println!("Not running server because `--no-run` was provided. You can run it manually by running the following executable from the root of the project.\n{}", &exec_str);
297        }
298        Ok((0, Some(exec_str)))
299    }
300}