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
15static BUILDING_SERVER: Emoji<'_, '_> = Emoji("📡", "");
17static SERVING: Emoji<'_, '_> = Emoji("🛰️ ", "");
18
19macro_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
30fn 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 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 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 "{} 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 false,
90 )?);
91
92 let msgs: Vec<&str> = stdout.trim().split('\n').collect();
93 let msg = msgs.get(msgs.len() - 2);
97 let msg = match msg {
98 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 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 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
133fn run_server(
138 exec: Arc<Mutex<String>>,
139 dir: PathBuf,
140 num_steps: u8,
141 verbose: bool,
142) -> Result<i32, ExecutionError> {
143 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 let child = Command::new(&server_exec_path)
156 .current_dir(&dir)
157 .env("PERSEUS_ENGINE_OPERATION", "serve")
159 .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 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 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 let output = child.wait_with_output().unwrap();
194 let exit_code = match output.status.code() {
195 Some(exit_code) => exit_code, None if output.status.success() => 0, None => 1, };
201 if !output.stderr.is_empty() && exit_code != 0 {
204 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
214pub 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 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 let num_steps = if did_build { 4 } else { 2 };
235
236 let exec = Arc::new(Mutex::new(String::new()));
238 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 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 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_reload(
281 global_opts.reload_server_host.to_string(),
282 global_opts.reload_server_port,
283 );
284
285 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 let exec_str = (*exec.lock().unwrap()).to_string();
293 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}