use crate::build::build_internal;
use crate::cmd::{cfg_spinner, run_stage};
use crate::install::Tools;
use crate::parse::{Opts, ServeOpts};
use crate::thread::{spawn_thread, ThreadHandle};
use crate::{errors::*, order_reload};
use console::{style, Emoji};
use indicatif::{MultiProgress, ProgressBar};
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
static BUILDING_SERVER: Emoji<'_, '_> = Emoji("📡", "");
static SERVING: Emoji<'_, '_> = Emoji("🛰️ ", "");
macro_rules! handle_exit_code {
($code:expr) => {{
let (stdout, stderr, code) = $code;
if code != 0 {
return ::std::result::Result::Ok(code);
}
(stdout, stderr)
}};
}
fn build_server(
dir: PathBuf,
spinners: &MultiProgress,
num_steps: u8,
exec: Arc<Mutex<String>>,
is_release: bool,
tools: &Tools,
global_opts: &Opts,
) -> Result<
ThreadHandle<impl FnOnce() -> Result<i32, ExecutionError>, Result<i32, ExecutionError>>,
ExecutionError,
> {
let tools = tools.clone();
let Opts {
cargo_engine_args, ..
} = global_opts.clone();
let sb_msg = format!(
"{} {} Building server",
style(format!("[{}/{}]", num_steps - 1, num_steps))
.bold()
.dim(),
BUILDING_SERVER
);
let sb_spinner = spinners.insert((num_steps - 1).into(), ProgressBar::new_spinner());
let sb_spinner = cfg_spinner(sb_spinner, &sb_msg);
let sb_target = dir;
let sb_thread = spawn_thread(
move || {
let (stdout, _stderr) = handle_exit_code!(run_stage(
vec![&format!(
"{} build --message-format json {} {}",
tools.cargo_engine,
if is_release { "--release" } else { "" },
cargo_engine_args
)],
&sb_target,
&sb_spinner,
&sb_msg,
vec![
("CARGO_TARGET_DIR", "dist/target_engine"),
("RUSTFLAGS", "--cfg=engine"),
("CARGO_TERM_COLOR", "always")
]
)?);
let msgs: Vec<&str> = stdout.trim().split('\n').collect();
let msg = msgs.get(msgs.len() - 2);
let msg = match msg {
Some(msg) => serde_json::from_str::<serde_json::Value>(msg)
.map_err(|err| ExecutionError::GetServerExecutableFailed { source: err })?,
None => return Err(ExecutionError::ServerExecutableMsgNotFound),
};
let server_exec_path = msg.get("executable");
let server_exec_path = match server_exec_path {
Some(server_exec_path) => match server_exec_path.as_str() {
Some(server_exec_path) => server_exec_path,
None => {
return Err(ExecutionError::ParseServerExecutableFailed {
err: "expected 'executable' field to be string".to_string(),
})
}
},
None => return Err(ExecutionError::ParseServerExecutableFailed {
err: "expected 'executable' field in JSON map in second-last message, not present"
.to_string(),
}),
};
let mut exec_val = exec.lock().unwrap();
*exec_val = server_exec_path.to_string();
Ok(0)
},
global_opts.sequential,
);
Ok(sb_thread)
}
fn run_server(
exec: Arc<Mutex<String>>,
dir: PathBuf,
num_steps: u8,
) -> Result<i32, ExecutionError> {
let exec_val = exec.lock().unwrap();
if exec_val.is_empty() {
return Err(ExecutionError::ParseServerExecutableFailed {
err: "mutex value empty, implies uncaught thread termination (please report this as a bug)"
.to_string()
});
}
let server_exec_path = (*exec_val).to_string();
let child = Command::new(&server_exec_path)
.current_dir(&dir)
.env("PERSEUS_ENGINE_OPERATION", "serve")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| ExecutionError::CmdExecFailed {
cmd: server_exec_path,
source: err,
})?;
let host = env::var("PERSEUS_HOST").unwrap_or_else(|_| "localhost".to_string());
let port = env::var("PERSEUS_PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse::<u16>()
.map_err(|err| ExecutionError::PortNotNumber { source: err })?;
println!(
" {} {} Your app is now live on <http://{host}:{port}>! To change this, re-run this command with different settings for `--host` and `--port`.",
style(format!("[{}/{}]", num_steps, num_steps)).bold().dim(),
SERVING,
host=host,
port=port
);
let output = child.wait_with_output().unwrap();
let exit_code = match output.status.code() {
Some(exit_code) => exit_code, None if output.status.success() => 0,
None => 1,
};
if !output.stderr.is_empty() && exit_code != 0 {
std::io::stderr().write_all(&output.stdout).unwrap();
std::io::stderr().write_all(&output.stderr).unwrap();
return Ok(1);
}
Ok(0)
}
pub fn serve(
dir: PathBuf,
opts: &ServeOpts,
tools: &Tools,
global_opts: &Opts,
spinners: &MultiProgress,
silent_no_run: bool,
) -> Result<(i32, Option<String>), ExecutionError> {
env::set_var("PERSEUS_HOST", &opts.host);
env::set_var("PERSEUS_PORT", opts.port.to_string());
let did_build = !opts.no_build;
let should_run = !opts.no_run;
let num_steps = if did_build { 4 } else { 2 };
let exec = Arc::new(Mutex::new(String::new()));
let sb_thread = build_server(
dir.clone(),
&spinners,
num_steps,
Arc::clone(&exec),
opts.release,
tools,
global_opts,
)?;
if did_build {
let (sg_thread, wb_thread) = build_internal(
dir.clone(),
&spinners,
num_steps,
opts.release,
tools,
global_opts,
)?;
let sg_res = sg_thread
.join()
.map_err(|_| ExecutionError::ThreadWaitFailed)??;
let wb_res = wb_thread
.join()
.map_err(|_| ExecutionError::ThreadWaitFailed)??;
if sg_res != 0 {
return Ok((sg_res, None));
} else if wb_res != 0 {
return Ok((wb_res, None));
}
}
let sb_res = sb_thread
.join()
.map_err(|_| ExecutionError::ThreadWaitFailed)??;
if sb_res != 0 {
return Ok((sb_res, None));
}
order_reload(
global_opts.reload_server_host.to_string(),
global_opts.reload_server_port,
);
if should_run {
let exit_code = run_server(Arc::clone(&exec), dir, num_steps)?;
Ok((exit_code, None))
} else {
let exec_str = (*exec.lock().unwrap()).to_string();
if !silent_no_run {
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);
}
Ok((0, Some(exec_str)))
}
}