Skip to content

Commit

Permalink
feat: ✨ added serving systems to cli
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Sep 1, 2021
1 parent 66dc282 commit 335ff5d
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 20 deletions.
3 changes: 2 additions & 1 deletion packages/perseus-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ error-chain = "0.12"
cargo_toml = "0.9"
indicatif = "0.16"
console = "0.14"
notify = "4.0"
serde = "1"
serde_json = "1"

[lib]
name = "lib"
Expand Down
15 changes: 8 additions & 7 deletions packages/perseus-cli/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ static FINALIZING: Emoji<'_, '_> = Emoji("📦", "");
/// Returns the exit code if it's non-zero.
macro_rules! handle_exit_code {
($code:expr) => {
let code = $code;
let (_, _, code) = $code;
if code != 0 {
return Ok(code);
}
};
}

/// Actually builds the user's code, program arguments having been interpreted.
fn build_internal(dir: PathBuf) -> Result<i32> {
/// Actually builds the user's code, program arguments having been interpreted. This needs to know how many steps there are in total
/// because the serving logic also uses it.
pub fn build_internal(dir: PathBuf, num_steps: u8) -> Result<i32> {
let mut target = dir;
target.extend([".perseus"]);

Expand All @@ -31,7 +32,7 @@ fn build_internal(dir: PathBuf) -> Result<i32> {
&target,
format!(
"{} {} Generating your app",
style("[1/3]").bold().dim(),
style(format!("[1/{}]", num_steps)).bold().dim(),
GENERATING
)
)?);
Expand All @@ -46,7 +47,7 @@ fn build_internal(dir: PathBuf) -> Result<i32> {
&target,
format!(
"{} {} Building your app to WASM",
style("[2/3]").bold().dim(),
style(format!("[2/{}]", num_steps)).bold().dim(),
BUILDING
)
)?);
Expand All @@ -58,7 +59,7 @@ fn build_internal(dir: PathBuf) -> Result<i32> {
&target,
format!(
"{} {} Finalizing bundle",
style("[3/3]").bold().dim(),
style(format!("[3/{}]", num_steps)).bold().dim(),
FINALIZING
)
)?);
Expand All @@ -76,7 +77,7 @@ pub fn build(dir: PathBuf, prog_args: &[String]) -> Result<i32> {
if should_watch == Some(&"-w".to_string()) || should_watch == Some(&"--watch".to_string()) {
todo!("watching not yet supported, try a tool like 'entr'");
}
let exit_code = build_internal(dir.clone())?;
let exit_code = build_internal(dir.clone(), 3)?;

Ok(exit_code)
}
25 changes: 17 additions & 8 deletions packages/perseus-cli/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ use console::Emoji;
use crate::errors::*;

// Some useful emojis
static SUCCESS: Emoji<'_, '_> = Emoji("✅", "success!");
static FAILURE: Emoji<'_, '_> = Emoji("❌", "failed!");
pub static SUCCESS: Emoji<'_, '_> = Emoji("✅", "success!");
pub static FAILURE: Emoji<'_, '_> = Emoji("❌", "failed!");

/// Runs the given command conveniently, returning the exit code. Notably, this parses the given command by separating it on spaces.
pub fn run_cmd(raw_cmd: String, dir: &Path, pre_dump: impl Fn()) -> Result<i32> {
/// Returns the command's output and the exit code.
pub fn run_cmd(raw_cmd: String, dir: &Path, pre_dump: impl Fn()) -> Result<(String, String, i32)> {
let mut cmd_args: Vec<&str> = raw_cmd.split(' ').collect();
let cmd = cmd_args.remove(0);

Expand All @@ -33,10 +34,16 @@ pub fn run_cmd(raw_cmd: String, dir: &Path, pre_dump: impl Fn()) -> Result<i32>
std::io::stderr().write_all(&output.stderr).unwrap();
}

Ok(exit_code)
Ok((
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
exit_code
))
}

pub fn run_stage(cmds: Vec<&str>, target: &Path, message: String) -> Result<i32> {
/// Runs a series of commands and provides a nice spinner with a custom message. Returns the last command's output and an appropriate exit
/// code (0 if everything worked, otherwise the exit code of the one that failed).
pub fn run_stage(cmds: Vec<&str>, target: &Path, message: String) -> Result<(String, String, i32)> {
// Tell the user about the stage with a nice progress bar
let spinner = ProgressBar::new_spinner();
spinner.set_style(
Expand All @@ -47,20 +54,22 @@ pub fn run_stage(cmds: Vec<&str>, target: &Path, message: String) -> Result<i32>
// Tick the spinner every 50 milliseconds
spinner.enable_steady_tick(50);

let mut last_output = (String::new(), String::new());
// Run the commands
for cmd in cmds {
// We make sure all commands run in the target directory ('.perseus/' itself)
let exit_code = run_cmd(cmd.to_string(), target, || {
let (stdout, stderr, exit_code) = run_cmd(cmd.to_string(), target, || {
// We're done, we'll write a more permanent version of the message
spinner.finish_with_message(format!(
"{}...{}",
message,
FAILURE
))
})?;
last_output = (stdout, stderr);
// If we have a non-zero exit code, we should NOT continue (stderr has been written to the console already)
if exit_code != 0 {
return Ok(1);
return Ok((last_output.0, last_output.1, 1));
}
}

Expand All @@ -71,5 +80,5 @@ pub fn run_stage(cmds: Vec<&str>, target: &Path, message: String) -> Result<i32>
SUCCESS
));

Ok(0)
Ok((last_output.0, last_output.1, 0))
}
15 changes: 15 additions & 0 deletions packages/perseus-cli/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ error_chain! {
description("watching files failed")
display("Couldn't watch '{}' for changes. Error was: '{}'.", path, err)
}
/// For when the next line of the stdout of a command is `None` when it shouldn't have been.
NextStdoutLineNone {
description("next stdout line was None, expected Some(_)")
display("Executing a command failed because it seemed to stop reporting prmeaturely. If this error persists, you should file a bug report (particularly if you've just upgraded Rust).")
}
/// For when getting the path to the built executable for the server from the JSON build output failed.
GetServerExecutableFailed(err: String) {
description("getting server executable path failed")
display("Couldn't get the path to the server executable from `cargo build`. If this problem persists, please report it as a bug (especially if you just updated cargo). Error was: '{}'.", err)
}
/// For when getting the path to the built executable for the server from the JSON build output failed.
PortNotNumber(err: String) {
description("port in PORT environment variable couldn't be parsed as number")
display("Couldn't parse 'PORT' environment variable as a number, please check that you've provided the correct value. Error was: '{}'.", err)
}
}
}

Expand Down
124 changes: 120 additions & 4 deletions packages/perseus-cli/src/serve.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,125 @@
use std::path::PathBuf;
use console::{style, Emoji};
use std::env;
use std::io::Write;
use std::process::{Command, Stdio};
use crate::build::build_internal;
use crate::cmd::run_stage;
use crate::errors::*;

/// Serves the user's app. If no arguments are provided, this will build in watch mode and serve. If `-p/--prod` is specified, we'll
/// build for development, and if `--no-build` is specified, we won't build at all (useful for pseudo-production serving).
/// General message though: do NOT use the CLI for production serving!
// Emojis for stages
static BUILDING_SERVER: Emoji<'_, '_> = Emoji("📡", "");
static SERVING: Emoji<'_, '_> = Emoji("🛰️ ", "");

/// Returns the exit code if it's non-zero.
macro_rules! handle_exit_code {
($code:expr) => {
{
let (stdout, stderr, code) = $code;
if code != 0 {
return Ok(code);
}
(stdout, stderr)
}
};
}

/// Actually serves the user's app, program arguments having been interpreted. This needs to know if we've built as part of this process
/// so it can show an accurate progress count.
fn serve_internal(dir: PathBuf, did_build: bool) -> Result<i32> {
let num_steps = match did_build {
true => 5,
false => 2
};
let mut target = dir;
// All the serving work can be done in the `server` subcrate after building is finished
target.extend([".perseus", "server"]);

// Build the server runner
// We use the JSON message format so we can get extra info about the generated executable
let (stdout, _stderr) = handle_exit_code!(run_stage(
vec![
"cargo build --message-format json"
],
&target,
format!(
"{} {} Building server",
style(format!("[{}/{}]", num_steps - 1, num_steps)).bold().dim(),
BUILDING_SERVER
)
)?);
let msgs: Vec<&str> = stdout.trim().split('\n').collect();
// If we got to here, the exit code was 0 and everything should've worked
// The last message will just tell us that the build finished, the second-last one will tell us the executable path
let msg = msgs.get(msgs.len() - 2);
let msg = match msg {
// We'll parse it as a Serde `Value`, we don't need to know everything that's in there
Some(msg) => serde_json::from_str::<serde_json::Value>(msg).map_err(|err| ErrorKind::GetServerExecutableFailed(err.to_string()))?,
None => bail!(ErrorKind::GetServerExecutableFailed("expected second-last message, none existed (too few messages)".to_string()))
};
let server_exec_path = msg.get("executable");
let server_exec_path = match server_exec_path {
// We'll parse it as a Serde `Value`, we don't need to know everything that's in there
Some(server_exec_path) => {
match server_exec_path.as_str() {
Some(server_exec_path) => server_exec_path,
None => bail!(ErrorKind::GetServerExecutableFailed("expected 'executable' field to be string".to_string()))
}
},
None => bail!(ErrorKind::GetServerExecutableFailed("expected 'executable' field in JSON map in second-last message, not present".to_string()))
};

// Manually run the generated binary (invoking in the right directory context for good measure if it ever needs it in future)
let child = Command::new(server_exec_path)
.current_dir(target)
// We should be able to access outputs in case there's an error
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| ErrorKind::CmdExecFailed(server_exec_path.to_string(), err.to_string()))?;
// Figure out what host/port the app will be live on
let host = env::var("HOST").unwrap_or_else(|_| "localhost".to_string());
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string()).parse::<u16>().map_err(|err| ErrorKind::PortNotNumber(err.to_string()))?;
// Give the user a nice informational message
println!(
" {} {} Your app is now live on http://{host}:{port}! To change this, re-run this command with different settings of the HOST/PORT environment variables.",
style(format!("[{}/{}]", num_steps, num_steps)).bold().dim(),
SERVING,
host=host,
port=port
);

// Wait on the child process to finish (which it shouldn't unless there's an error), then perform error handling
let output = child.wait_with_output().unwrap();
let exit_code = match output.status.code() {
Some(exit_code) => exit_code, // If we have an exit code, use it
None if output.status.success() => 0, // If we don't, but we know the command succeeded, return 0 (success code)
None => 1, // If we don't know an exit code but we know that the command failed, return 1 (general error code)
};
// Print `stderr` only if there's something therein and the exit code is non-zero
if !output.stderr.is_empty() && exit_code != 0 {
// We don't print any failure message other than the actual error right now (see if people want something else?)
std::io::stderr().write_all(&output.stderr).unwrap();
return Ok(1);
}

Ok(0)
}

/// Builds the subcrates to get a directory that we can serve. Returns an exit code.
pub fn serve(dir: PathBuf, prog_args: &[String]) -> Result<i32> {
todo!("serve command")
// TODO support watching files
let mut did_build = false;
// Only build if the user hasn't set `--no-build`, handling non-zero exit codes
if !prog_args.contains(&"--no-build".to_string()) {
did_build = true;
let build_exit_code = build_internal(dir.clone(), 4)?;
if build_exit_code != 0 {
return Ok(build_exit_code);
}
}
// Now actually serve the user's data
let exit_code = serve_internal(dir.clone(), did_build)?;

Ok(exit_code)
}

0 comments on commit 335ff5d

Please sign in to comment.