use std::collections::HashMap;
use std::io::ErrorKind;
use std::net::TcpListener;
use std::path::Path;
use anyhow::{Context, Result, bail};
use crate::cli::ServeArgs;
use crate::{cargo, project};
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 3000;
pub fn run(args: &ServeArgs) -> Result<()> {
let root = project::find_root()?;
let (host, port) = resolve_address(args, &root)?;
ensure_port_available(&host, port)?;
println!("Starting development server: http://{host}:{port}");
println!("Press Ctrl+C to stop the server.\n");
let port_value = port.to_string();
let mut cargo_args = vec!["run"];
if args.release {
cargo_args.push("--release");
}
let envs = [
("APP_HOST", host.as_str()),
("APP_PORT", port_value.as_str()),
];
let status = cargo::run(&root, &cargo_args, &envs)?;
match status.code() {
Some(0) | None => Ok(()), Some(code) => bail!("development server exited with status {code}"),
}
}
fn resolve_address(args: &ServeArgs, root: &Path) -> Result<(String, u16)> {
let dotenv = read_dotenv(&root.join(".env"))?;
let lookup = |key: &str| std::env::var(key).ok().or_else(|| dotenv.get(key).cloned());
let host = args
.host
.clone()
.or_else(|| lookup("APP_HOST"))
.unwrap_or_else(|| DEFAULT_HOST.to_owned());
let port = match (args.port, lookup("APP_PORT")) {
(Some(port), _) => port,
(None, Some(value)) => value
.trim()
.parse()
.with_context(|| format!("APP_PORT={value:?} is not a valid port"))?,
(None, None) => DEFAULT_PORT,
};
Ok((host, port))
}
fn read_dotenv(path: &Path) -> Result<HashMap<String, String>> {
let iter = match dotenvy::from_path_iter(path) {
Ok(iter) => iter,
Err(err) if err.not_found() => return Ok(HashMap::new()),
Err(err) => return Err(err).with_context(|| format!("failed to read {}", path.display())),
};
iter.collect::<Result<_, _>>()
.with_context(|| format!("failed to parse {}", path.display()))
}
fn ensure_port_available(host: &str, port: u16) -> Result<()> {
match TcpListener::bind((host, port)) {
Ok(_) => Ok(()),
Err(err) if err.kind() == ErrorKind::AddrInUse => {
bail!("port {port} is already in use on {host}; choose another with --port")
}
Err(err) => Err(err).with_context(|| format!("cannot bind to {host}:{port}")),
}
}