1use bytes::Bytes;
2use cargo::core::compiler::{CompileMode, Executor, UserIntent};
3use cargo::core::{PackageId, Shell, Target, Verbosity, Workspace};
4use cargo::ops::{compile_with_exec, CompileOptions};
5use cargo::util::errors::CargoResult;
6use cargo::util::{homedir, GlobalContext};
7use cargo_util::ProcessBuilder;
8use http::response::Builder as ResponseBuilder;
9use http::{header, StatusCode};
10use http_body_util::combinators::BoxBody;
11use http_body_util::{BodyExt, Full};
12use hyper::server::conn::http1;
13use hyper::service::service_fn;
14use hyper::{Request, Response};
15use hyper_staticfile::Body;
16use hyper_staticfile::Static;
17use hyper_util::rt::tokio::TokioIo;
18use std::path::PathBuf;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::Arc;
21use tokio::net::TcpListener;
22
23type DynBody = BoxBody<Bytes, std::io::Error>;
29
30fn full_body(bytes: impl Into<Bytes>) -> DynBody {
32 Full::new(bytes.into())
33 .map_err(|e| -> std::io::Error { match e {} })
34 .boxed()
35}
36
37const RELOAD_SCRIPT: &str = r#"<script>
40(function() {
41 var lastId = null;
42 function poll() {
43 fetch('/_buildid')
44 .then(function(r) { return r.text(); })
45 .then(function(id) {
46 if (lastId !== null && id !== lastId) { location.reload(); }
47 lastId = id;
48 })
49 .catch(function() {})
50 .finally(function() { setTimeout(poll, 1000); });
51 }
52 poll();
53})();
54</script>
55"#;
56
57#[allow(dead_code)]
59pub async fn run_cargo_doc(args: &Vec<String>) -> std::process::ExitStatus {
60 let mut cmd = tokio::process::Command::new("cargo");
63 cmd.arg("doc").args(args);
64 let stdcmd = cmd.as_std();
65 log::info!(
66 "Running {} {}",
67 stdcmd.get_program().to_string_lossy(),
68 stdcmd
69 .get_args()
70 .map(|s| s.to_string_lossy().to_string())
71 .collect::<Vec<String>>()
72 .join(" ")
73 );
74 let mut child = tokio::process::Command::new("cargo")
75 .arg("doc")
76 .args(args)
77 .spawn()
78 .expect("failed to run `cargo doc`");
79 child.wait().await.expect("failed to wait")
80}
81
82#[allow(dead_code)]
86pub async fn handle_crate_request<B>(
87 req: Request<B>,
88 static_: Static,
89 crate_name: String,
90) -> Result<Response<Body>, std::io::Error> {
91 let target = if let Some(query) = req.uri().query() {
92 format!("/{crate_name}/?{query}")
93 } else {
94 format!("/{crate_name}/")
95 };
96 match req.uri().path() {
97 "/" => Ok(ResponseBuilder::new()
98 .status(StatusCode::FOUND)
99 .header(header::LOCATION, target)
100 .body(Body::Empty)
101 .expect("unable to build response")),
102 _ => static_.clone().serve(req).await,
103 }
104}
105
106#[allow(dead_code)]
108pub async fn serve_rust_doc(addr: &std::net::SocketAddr) -> Result<(), anyhow::Error> {
109 Ok(serve_rustbook(addr).await?)
110}
111
112#[allow(dead_code)]
114pub fn get_crate_info(manifest_path: &PathBuf) -> Result<(String, PathBuf), anyhow::Error> {
115 let mut shell = Shell::default();
116 shell.set_verbosity(Verbosity::Quiet);
117 let cwd = std::env::current_dir()?;
118 let cargo_home_dir = homedir(&cwd).expect("Errror locating homedir");
119 let config = GlobalContext::new(shell, cwd, cargo_home_dir);
120 let workspace = Workspace::new(manifest_path, &config).expect("Error making workspace");
121
122 let mut compile_opts = CompileOptions::new(
123 &config,
124 UserIntent::Doc {
125 deps: true,
126 json: false,
127 },
128 )
129 .expect("Making CompileOptions");
130
131 compile_opts.spec = cargo::ops::Packages::Default;
136
137 #[derive(Copy, Clone)]
142 struct DefaultExecutor;
143
144 impl Executor for DefaultExecutor {
145 fn exec(
146 &self,
147 _cmd: &ProcessBuilder,
148 _id: PackageId,
149 _target: &Target,
150 _mode: CompileMode,
151 _on_stdout_line: &mut dyn FnMut(&str) -> CargoResult<()>,
152 _on_stderr_line: &mut dyn FnMut(&str) -> CargoResult<()>,
153 ) -> CargoResult<()> {
154 Ok(())
156 }
157 }
158
159 let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
160 let compilation = compile_with_exec(&workspace, &compile_opts, &exec)?;
161 let root_crate_names = &compilation.root_crate_names;
162 let crate_doc_dir = workspace.target_dir().join("doc").into_path_unlocked();
163 let crate_name = root_crate_names
164 .get(0)
165 .ok_or_else(|| anyhow::anyhow!("no crates with documentation"))?;
166 Ok((crate_name.to_string(), crate_doc_dir))
167}
168
169#[allow(dead_code)]
171pub async fn serve_crate_doc(
172 manifest_path: &PathBuf,
173 addr: &std::net::SocketAddr,
174) -> Result<(), anyhow::Error> {
175 let (crate_name, crate_doc_dir) = get_crate_info(manifest_path)?;
176 let crate_doc_dir = Static::new(crate_doc_dir.clone());
177 let crate_name = crate_name.clone();
178 let handler =
179 service_fn(move |req| handle_crate_request(req, crate_doc_dir.clone(), crate_name.clone()));
180
181 let listener = TcpListener::bind(addr)
182 .await
183 .expect("Failed to create TCP listener");
184
185 loop {
186 let (tcp, _) = listener.accept().await?;
187 let io = TokioIo::new(tcp);
188 let service = handler.clone();
189 tokio::task::spawn(async move {
190 if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
191 println!("Failed to serve connection: {:?}", err);
192 }
193 });
194 }
195}
196
197pub fn find_rustdoc() -> Option<PathBuf> {
201 let output = std::process::Command::new("rustup")
202 .arg("which")
203 .arg("rustdoc")
204 .output()
205 .ok()?;
206 if output.status.success() {
207 Some(PathBuf::from(String::from_utf8(output.stdout).ok()?))
208 } else {
209 None
210 }
211 .and_then(|rustdoc| {
212 Some(
213 rustdoc
214 .parent()?
215 .parent()?
216 .join("share")
217 .join("doc")
218 .join("rust")
219 .join("html"),
220 )
221 })
222}
223
224#[allow(dead_code)]
228pub async fn handle_request<B>(
229 req: Request<B>,
230 static_: Static,
231) -> Result<Response<Body>, std::io::Error> {
232 static_.clone().serve(req).await
233}
234
235#[allow(dead_code)]
237pub async fn serve_rustbook(addr: &std::net::SocketAddr) -> Result<(), anyhow::Error> {
238 let rustdoc_dir = find_rustdoc().expect("Error locating rustdoc");
239 Ok(serve_dir(&rustdoc_dir, addr).await?)
240}
241
242#[allow(dead_code)]
244pub async fn serve_dir(dir: &PathBuf, addr: &std::net::SocketAddr) -> Result<(), anyhow::Error> {
245 let dir = Static::new(dir.clone());
246 let handler = service_fn(move |req| handle_request(req, dir.clone()));
247
248 let listener = TcpListener::bind(addr)
249 .await
250 .expect("Failed to create TCP listener");
251
252 loop {
253 let (tcp, _) = listener.accept().await?;
254 let io = TokioIo::new(tcp);
255 let service = handler.clone();
256 tokio::task::spawn(async move {
257 if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
258 println!("Failed to serve connection: {:?}", err);
259 }
260 });
261 }
262}
263
264#[allow(dead_code)]
270pub async fn handle_crate_request_watch<B>(
271 req: Request<B>,
272 static_: Static,
273 crate_name: String,
274 build_id: Arc<AtomicU64>,
275) -> Result<Response<DynBody>, std::io::Error> {
276 if req.uri().path() == "/_buildid" {
278 let id = build_id.load(Ordering::Relaxed).to_string();
279 return Ok(ResponseBuilder::new()
280 .status(StatusCode::OK)
281 .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
282 .header(header::CACHE_CONTROL, "no-cache, no-store")
283 .body(full_body(id))
284 .expect("unable to build response"));
285 }
286
287 let target = if let Some(query) = req.uri().query() {
288 format!("/{crate_name}/?{query}")
289 } else {
290 format!("/{crate_name}/")
291 };
292 if req.uri().path() == "/" {
293 return Ok(ResponseBuilder::new()
294 .status(StatusCode::FOUND)
295 .header(header::LOCATION, target)
296 .body(full_body(Bytes::new()))
297 .expect("unable to build response"));
298 }
299
300 let response = static_.clone().serve(req).await?;
301
302 let status = response.status();
303
304 if status == StatusCode::NOT_FOUND {
309 let fallback = format!(
310 r#"<!DOCTYPE html><html><head><meta charset="utf-8"><title>Rebuilding…</title></head><body><p>Regenerating documentation…</p>{}</body></html>"#,
311 RELOAD_SCRIPT
312 );
313 let fallback_bytes = Bytes::from(fallback);
314 return Ok(ResponseBuilder::new()
315 .status(StatusCode::OK)
316 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
317 .header(header::CACHE_CONTROL, "no-cache, no-store")
318 .body(full_body(fallback_bytes))
319 .expect("unable to build response"));
320 }
321
322 let is_html = response
324 .headers()
325 .get(header::CONTENT_TYPE)
326 .and_then(|v| v.to_str().ok())
327 .map(|s| s.contains("text/html"))
328 .unwrap_or(false);
329
330 if is_html {
331 let (mut parts, body) = response.into_parts();
332 let bytes = body
333 .collect()
334 .await
335 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
336 .to_bytes();
337 let html = String::from_utf8_lossy(&bytes);
338 let modified: String = if html.contains("</body>") {
339 html.replacen("</body>", RELOAD_SCRIPT, 1) + "</body>"
340 } else {
341 html.into_owned() + RELOAD_SCRIPT
342 };
343 let modified_bytes = Bytes::from(modified);
344 parts.headers.insert(
345 header::CONTENT_LENGTH,
346 modified_bytes.len().into(),
347 );
348 return Ok(Response::from_parts(parts, full_body(modified_bytes)));
349 }
350
351 Ok(response.map(|body| body.boxed()))
352}
353
354#[allow(dead_code)]
359pub async fn serve_crate_doc_watch(
360 manifest_path: &PathBuf,
361 addr: &std::net::SocketAddr,
362 build_id: Arc<AtomicU64>,
363) -> Result<(), anyhow::Error> {
364 let (crate_name, crate_doc_dir) = get_crate_info(manifest_path)?;
365 let crate_doc_dir = Static::new(crate_doc_dir.clone());
366 let crate_name = crate_name.clone();
367 let handler = service_fn(move |req| {
368 handle_crate_request_watch(
369 req,
370 crate_doc_dir.clone(),
371 crate_name.clone(),
372 Arc::clone(&build_id),
373 )
374 });
375
376 let listener = TcpListener::bind(addr)
377 .await
378 .expect("Failed to create TCP listener");
379
380 loop {
381 let (tcp, _) = listener.accept().await?;
382 let io = TokioIo::new(tcp);
383 let service = handler.clone();
384 tokio::task::spawn(async move {
385 if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
386 log::error!("Failed to serve connection: {:?}", err);
387 }
388 });
389 }
390}
391
392#[allow(dead_code)]
397pub async fn handle_all_books_request<B>(
398 req: Request<B>,
399 static_: Static,
400 index_html: Arc<String>,
401) -> Result<Response<DynBody>, std::io::Error> {
402 match req.uri().path() {
403 "/" | "/index.html" => Ok(ResponseBuilder::new()
404 .status(StatusCode::OK)
405 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
406 .body(full_body(index_html.as_str().to_owned()))
407 .expect("unable to build response")),
408 _ => Ok(static_.clone().serve(req).await?.map(|body| body.boxed())),
409 }
410}
411
412#[allow(dead_code)]
417pub async fn serve_rustbook_with_index(
418 addr: &std::net::SocketAddr,
419 index_html: String,
420) -> Result<(), anyhow::Error> {
421 let rustdoc_dir = find_rustdoc().expect("Error locating rustdoc");
422 let dir = Static::new(rustdoc_dir);
423 let index_html = Arc::new(index_html);
424 let handler = service_fn(move |req| {
425 handle_all_books_request(req, dir.clone(), Arc::clone(&index_html))
426 });
427
428 let listener = TcpListener::bind(addr)
429 .await
430 .expect("Failed to create TCP listener");
431
432 loop {
433 let (tcp, _) = listener.accept().await?;
434 let io = TokioIo::new(tcp);
435 let service = handler.clone();
436 tokio::task::spawn(async move {
437 if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
438 log::error!("Failed to serve connection: {:?}", err);
439 }
440 });
441 }
442}