1 #[cfg(feature = "watch")]
3 use crate::{get_book_dir, open}
;
4 use clap
::{App, Arg, ArgMatches, SubCommand}
;
6 use iron
::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set}
;
15 // Create clap subcommand arguments
16 pub fn make_subcommand
<'a
, 'b
>() -> App
<'a
, 'b
> {
17 SubCommand
::with_name("serve")
18 .about("Serves a book at http://localhost:3000, and rebuilds it on changes")
20 "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
21 Relative paths are interpreted relative to the book's root directory.{n}\
22 If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
25 "[dir] 'Root directory for the book{n}\
26 (Defaults to the Current Directory when omitted)'",
29 Arg
::with_name("hostname")
33 .default_value("localhost")
35 .help("Hostname to listen on for HTTP connections"),
38 Arg
::with_name("port")
42 .default_value("3000")
44 .help("Port to use for HTTP connections"),
47 Arg
::with_name("websocket-hostname")
48 .long("websocket-hostname")
52 "Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
56 Arg
::with_name("websocket-port")
58 .long("websocket-port")
60 .default_value("3001")
62 .help("Port to use for WebSockets livereload connections"),
64 .arg_from_usage("-o, --open 'Opens the book server in a web browser'")
67 // Watch command implementation
68 pub fn execute(args
: &ArgMatches
) -> Result
<()> {
69 let book_dir
= get_book_dir(args
);
70 let mut book
= MDBook
::load(&book_dir
)?
;
72 let port
= args
.value_of("port").unwrap();
73 let ws_port
= args
.value_of("websocket-port").unwrap();
74 let hostname
= args
.value_of("hostname").unwrap();
75 let public_address
= args
.value_of("websocket-hostname").unwrap_or(hostname
);
76 let open_browser
= args
.is_present("open");
78 let address
= format
!("{}:{}", hostname
, port
);
79 let ws_address
= format
!("{}:{}", hostname
, ws_port
);
81 let livereload_url
= format
!("ws://{}:{}", public_address
, ws_port
);
83 .set("output.html.livereload-url", &livereload_url
)?
;
85 if let Some(dest_dir
) = args
.value_of("dest-dir") {
86 book
.config
.build
.build_dir
= dest_dir
.into();
91 let mut chain
= Chain
::new(staticfile
::Static
::new(book
.build_dir_for("html")));
92 chain
.link_after(NoCache
);
93 chain
.link_after(ErrorRecover
);
94 let _iron
= Iron
::new(chain
)
96 .chain_err(|| "Unable to launch the server")?
;
99 ws
::WebSocket
::new(|_
| |_
| Ok(())).chain_err(|| "Unable to start the websocket")?
;
101 let broadcaster
= ws_server
.broadcaster();
103 std
::thread
::spawn(move || {
104 ws_server
.listen(&*ws_address
).unwrap();
107 let serving_url
= format
!("http://{}", address
);
108 info
!("Serving on: {}", serving_url
);
114 #[cfg(feature = "watch")]
115 watch
::trigger_on_change(&book
, move |paths
, book_dir
| {
116 info
!("Files changed: {:?}", paths
);
117 info
!("Building book...");
119 // FIXME: This area is really ugly because we need to re-set livereload :(
121 let result
= MDBook
::load(&book_dir
)
124 .set("output.html.livereload-url", &livereload_url
)?
;
127 .and_then(|b
| b
.build());
129 if let Err(e
) = result
{
130 error
!("Unable to load the book");
131 utils
::log_backtrace(&e
);
133 let _
= broadcaster
.send("reload");
140 impl AfterMiddleware
for NoCache
{
141 fn after(&self, _
: &mut Request
, mut res
: Response
) -> IronResult
<Response
> {
142 res
.headers
.set(headers
::CacheControl(vec
![
143 headers
::CacheDirective
::NoStore
,
144 headers
::CacheDirective
::MaxAge(0u32),
151 impl AfterMiddleware
for ErrorRecover
{
152 fn catch(&self, _
: &mut Request
, err
: IronError
) -> IronResult
<Response
> {
153 match err
.response
.status
{
154 // each error will result in 404 response
155 Some(_
) => Ok(err
.response
.set(status
::NotFound
)),