fission_command_server/
lib.rs1use 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}