Skip to main content

fission_command_server/
lib.rs

1use anyhow::{bail, Context, Result};
2use std::net::TcpListener;
3use std::path::Path;
4use std::process::Command;
5
6pub fn check(project_dir: &Path, release: bool) -> Result<()> {
7    ensure_server_entry_configured(project_dir)?;
8    artifacts(project_dir, release, true).context("failed to build server browser artifacts")?;
9    run_server_builder(project_dir, release, "check", &[])
10}
11
12pub fn build(project_dir: &Path, release: bool) -> Result<()> {
13    ensure_server_entry_configured(project_dir)?;
14    artifacts(project_dir, release, true).context("failed to build server browser artifacts")?;
15    build_server_binary(project_dir, release)
16}
17
18pub fn routes(project_dir: &Path) -> Result<()> {
19    ensure_server_entry_configured(project_dir)?;
20    run_server_builder(project_dir, false, "routes", &[])
21}
22
23pub fn serve(project_dir: &Path, release: bool, host: String, port: u16) -> Result<()> {
24    ensure_server_entry_configured(project_dir)?;
25    ensure_server_address_available(&host, port)?;
26    artifacts(project_dir, release, true).context("failed to build server browser artifacts")?;
27    let port = port.to_string();
28    run_server_builder(
29        project_dir,
30        release,
31        "serve",
32        &["--host", host.as_str(), "--port", port.as_str()],
33    )
34}
35
36fn ensure_server_address_available(host: &str, port: u16) -> Result<()> {
37    let address = format!("{host}:{port}");
38    let listener = TcpListener::bind(&address).with_context(|| {
39        format!(
40            "server address {address} is already in use; stop the existing process or choose another port with --port"
41        )
42    })?;
43    drop(listener);
44    Ok(())
45}
46
47pub fn artifacts(project_dir: &Path, release: bool, compile: bool) -> Result<()> {
48    ensure_server_entry_configured(project_dir)?;
49    let package_name = package_name(project_dir)?;
50    let features = package_features(project_dir)?;
51    let mut args = vec!["--package-name", package_name.as_str()];
52    if features.iter().any(|feature| feature == "browser") {
53        args.push("--package-no-default-features");
54        args.push("--package-feature");
55        args.push("browser");
56    }
57    if !compile {
58        args.push("--no-compile");
59    }
60    run_server_builder(project_dir, release, "artifacts", &args)
61}
62
63fn ensure_server_entry_configured(project_dir: &Path) -> Result<()> {
64    let path = project_dir.join("fission.toml");
65    let data = std::fs::read_to_string(&path)
66        .with_context(|| format!("failed to read {}", path.display()))?;
67    let value: toml::Value =
68        toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?;
69    if value
70        .get("server")
71        .and_then(|server| server.get("entry"))
72        .and_then(|entry| entry.as_str())
73        .is_some()
74    {
75        Ok(())
76    } else {
77        bail!("fission.toml is missing [server].entry")
78    }
79}
80
81fn package_name(project_dir: &Path) -> Result<String> {
82    let path = project_dir.join("Cargo.toml");
83    let data = std::fs::read_to_string(&path)
84        .with_context(|| format!("failed to read {}", path.display()))?;
85    let value: toml::Value =
86        toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?;
87    value
88        .get("package")
89        .and_then(|package| package.get("name"))
90        .and_then(|name| name.as_str())
91        .map(ToString::to_string)
92        .ok_or_else(|| anyhow::anyhow!("{} is missing [package].name", path.display()))
93}
94
95fn package_features(project_dir: &Path) -> Result<Vec<String>> {
96    let path = project_dir.join("Cargo.toml");
97    let data = std::fs::read_to_string(&path)
98        .with_context(|| format!("failed to read {}", path.display()))?;
99    let value: toml::Value =
100        toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?;
101    Ok(value
102        .get("features")
103        .and_then(|features| features.as_table())
104        .map(|features| features.keys().cloned().collect())
105        .unwrap_or_default())
106}
107
108fn run_server_builder(
109    project_dir: &Path,
110    release: bool,
111    command_name: &str,
112    extra_args: &[&str],
113) -> Result<()> {
114    let manifest_path = project_dir.join("Cargo.toml");
115    if !manifest_path.exists() {
116        bail!(
117            "server entry is configured but {} is missing",
118            manifest_path.display()
119        );
120    }
121    let manifest_path = manifest_path
122        .canonicalize()
123        .with_context(|| format!("failed to resolve {}", manifest_path.display()))?;
124    let mut command = Command::new("cargo");
125    command.current_dir(project_dir);
126    command
127        .arg("run")
128        .arg("--manifest-path")
129        .arg(&manifest_path);
130    if release {
131        command.arg("--release");
132    }
133    command.arg("--").arg(command_name);
134    for arg in extra_args {
135        command.arg(arg);
136    }
137    let status = command.status().context("failed to run server app")?;
138    if !status.success() {
139        bail!("server app failed with {status}");
140    }
141    Ok(())
142}
143
144fn build_server_binary(project_dir: &Path, release: bool) -> Result<()> {
145    let manifest_path = project_dir.join("Cargo.toml");
146    if !manifest_path.exists() {
147        bail!(
148            "server entry is configured but {} is missing",
149            manifest_path.display()
150        );
151    }
152    let manifest_path = manifest_path
153        .canonicalize()
154        .with_context(|| format!("failed to resolve {}", manifest_path.display()))?;
155    let mut command = Command::new("cargo");
156    command.current_dir(project_dir);
157    command
158        .arg("build")
159        .arg("--manifest-path")
160        .arg(&manifest_path);
161    if release {
162        command.arg("--release");
163    }
164    let status = command.status().context("failed to build server app")?;
165    if !status.success() {
166        bail!("server app build failed with {status}");
167    }
168    Ok(())
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use std::fs;
175
176    fn temp_project(name: &str) -> std::path::PathBuf {
177        let dir = std::env::temp_dir().join(format!("{name}-{}", std::process::id()));
178        let _ = fs::remove_dir_all(&dir);
179        fs::create_dir_all(&dir).unwrap();
180        dir
181    }
182
183    #[test]
184    fn server_entry_configuration_is_required() {
185        let dir = temp_project("fission-server-config-missing");
186        fs::write(dir.join("fission.toml"), "[app]\nname = \"Test\"\n").unwrap();
187
188        let error = ensure_server_entry_configured(&dir).unwrap_err();
189        assert!(error.to_string().contains("[server].entry"));
190
191        let _ = fs::remove_dir_all(&dir);
192    }
193
194    #[test]
195    fn reads_package_name_and_browser_feature_for_artifact_shims() {
196        let dir = temp_project("fission-server-config-package");
197        fs::write(
198            dir.join("Cargo.toml"),
199            r#"[package]
200name = "server-app"
201version = "0.1.0"
202edition = "2021"
203
204[features]
205default = ["server"]
206server = []
207browser = []
208"#,
209        )
210        .unwrap();
211
212        assert_eq!(package_name(&dir).unwrap(), "server-app");
213        assert!(package_features(&dir)
214            .unwrap()
215            .iter()
216            .any(|feature| feature == "browser"));
217
218        let _ = fs::remove_dir_all(&dir);
219    }
220
221    #[test]
222    fn serve_preflight_reports_busy_port_before_building_artifacts() {
223        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
224        let port = listener.local_addr().unwrap().port();
225
226        let error = ensure_server_address_available("127.0.0.1", port).unwrap_err();
227        assert!(error.to_string().contains("already in use"));
228    }
229}