use std::collections::BTreeMap;
use std::sync::mpsc::{channel, RecvTimeoutError};
use std::sync::{Mutex, Arc};
use std::time::{Instant, Duration};
use std::thread;
use std::net;
use std::hash::Hash;
use std::time::{SystemTime, UNIX_EPOCH};
use std::path::{Path, PathBuf};
use notify::{
RecommendedWatcher,
Watcher,
RecursiveMode,
DebouncedEvent
};
use handlebars::Handlebars;
use percent_encoding::percent_decode;
use hyper::StatusCode;
use cargo_shim::{
Profile,
TargetKind,
CargoTarget
};
use build::{BuildArgs, Project, PathKind, ShouldTriggerRebuild};
use http_utils::{
SimpleServer,
response_from_data,
response_from_status,
response_from_file
};
use deployment::{Deployment, ArtifactKind};
use error::Error;
fn auto_reload_code( hash: u32 ) -> String {
const TEMPLATE: &'static str = r##"
window.addEventListener( "load", function() {
var current_build_hash = {{{current_build_hash}}};
function try_reload() {
var req = new XMLHttpRequest();
req.addEventListener( "load" , function() {
if( req.responseText != current_build_hash ) {
window.location.reload( true );
}
});
req.addEventListener( "loadend", function() {
setTimeout( try_reload, 500 );
});
req.open( "GET", "/__cargo-web__/build_hash" );
req.send();
}
try_reload();
});
"##;
let handlebars = Handlebars::new();
let mut template_data = BTreeMap::new();
template_data.insert( "current_build_hash", hash );
handlebars.render_template( TEMPLATE, &template_data ).unwrap()
}
fn hash< T: Hash >( value: T ) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
value.hash( &mut hasher );
hasher.finish()
}
#[derive(Clone)]
struct Counter {
seed: u64,
value: u64
}
impl Counter {
fn new() -> Self {
let timestamp = SystemTime::now().duration_since( UNIX_EPOCH ).unwrap();
let seed = hash( timestamp.as_secs() ) ^ hash( timestamp.subsec_nanos() );
Counter {
seed,
value: 0
}
}
fn next( &self ) -> Self {
Counter {
seed: self.seed,
value: self.value + 1
}
}
fn get_hash( &self ) -> u32 {
hash( self.seed + self.value ) as u32
}
}
impl From< PathKind > for RecursiveMode {
fn from( mode: PathKind ) -> Self {
match mode {
PathKind::File => RecursiveMode::NonRecursive,
PathKind::Directory => RecursiveMode::Recursive
}
}
}
struct LastBuild {
counter: Counter,
deployment: Deployment,
project: Project,
target: CargoTarget
}
fn select_target( project: &Project ) -> Result< CargoTarget, Error > {
let target = {
let targets = project.target_or_select( |target| {
target.kind == TargetKind::Bin ||
(target.kind == TargetKind::CDyLib && project.backend().is_native_wasm())
})?;
if targets.is_empty() {
return Err(
Error::ConfigurationError( format!( "cannot start a webserver for a crate which is a library!" ) )
);
}
targets[ 0 ].clone()
};
Ok( target )
}
impl LastBuild {
fn new( project: Project, target: CargoTarget, counter: Counter ) -> Result< Self, Error > {
let config = project.aggregate_configuration( Profile::Main )?;
let result = project.build( &config, &target )?;
let deployment = Deployment::new( project.package(), &target, &result )?;
Ok( LastBuild {
counter,
deployment,
project,
target
})
}
fn get_build_hash( &self ) -> u32 {
self.counter.get_hash()
}
}
fn should_rebuild( paths_to_watch: &[(PathBuf, PathKind, ShouldTriggerRebuild)], path: &Path ) -> bool {
paths_to_watch.iter()
.filter( |&&(_, _, should_rebuild)| should_rebuild == ShouldTriggerRebuild::Yes )
.any( |&(ref root, _, _)| path.starts_with( root ) )
}
fn watch_paths( watcher: &mut RecommendedWatcher, paths_to_watch: &[(PathBuf, PathKind, ShouldTriggerRebuild)] ) {
for &(ref path, ref mode, _) in paths_to_watch {
match watcher.watch( &path, (*mode).into() ) {
Ok( _ ) => trace!( "Watching {:?} ({:?})", path, mode ),
Err( error ) => trace!( "Failed to watch {:?} ({:?}): {:?}", path, mode, error )
}
}
}
fn monitor_for_changes_and_rebuild(
last_build: Arc< Mutex< LastBuild > >
) -> Arc< Mutex< RecommendedWatcher > > {
let event_timeout = Duration::from_millis( 500 );
let (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new( tx, event_timeout ).unwrap();
let last_paths_to_watch = {
let last_build = last_build.lock().unwrap();
let paths_to_watch = last_build.project.paths_to_watch( &last_build.target );
debug!( "Found paths to watch: {:#?}", paths_to_watch );
watch_paths( &mut watcher, &paths_to_watch );
paths_to_watch.clone()
};
let watcher = Arc::new( Mutex::new( watcher ) );
let weak_watcher = Arc::downgrade( &watcher );
thread::spawn( move || {
let rx = rx;
let mut last_paths_to_watch = last_paths_to_watch;
fn event_triggers_rebuild( event: DebouncedEvent, paths_to_watch: &Vec< ( PathBuf, PathKind, ShouldTriggerRebuild ) > ) -> bool {
match event {
DebouncedEvent::Create( ref path ) => should_rebuild( paths_to_watch, path ),
DebouncedEvent::Remove( ref path ) => should_rebuild( paths_to_watch, path ),
DebouncedEvent::Rename( ref old_path, ref new_path ) => {
should_rebuild( paths_to_watch, old_path ) ||
should_rebuild( paths_to_watch, new_path )
},
DebouncedEvent::Write( ref path ) => should_rebuild( paths_to_watch, path ),
_ => false
}
}
fn record_inconsequential_change(last_build: &Arc< Mutex< LastBuild > >) {
trace!( "Nothing of consequence changed; bumping build counter without rebuilding" );
let mut last_build = last_build.lock().unwrap();
let counter = last_build.counter.next();
last_build.counter = counter;
}
'outer: while let Ok( event ) = rx.recv() {
trace!( "Watch event: {:?}", event );
if !event_triggers_rebuild( event, &last_paths_to_watch ) {
record_inconsequential_change( &last_build );
continue;
}
trace!( "Starting build in {}.{:0>3}s if no more changes detected", event_timeout.as_secs(), event_timeout.subsec_nanos() / 1_000_000 );
let mut deadline = Instant::now() + event_timeout;
while Instant::now() < deadline {
match rx.recv_timeout( deadline - Instant::now() ) {
Ok( event ) => {
trace!( "Watch event: {:?}", event );
if !event_triggers_rebuild( event, &last_paths_to_watch ) {
record_inconsequential_change( &last_build );
continue;
}
trace!( "Noticed follow-up change; waiting additional {}.{:0>3}s for more", event_timeout.as_secs(), event_timeout.subsec_nanos() / 1_000_000 );
deadline = Instant::now() + event_timeout;
}
Err( RecvTimeoutError::Timeout ) => {
trace!( "Timed out waiting for follow-up changes; proceeding to build" );
break;
}
Err( RecvTimeoutError::Disconnected ) => {
break 'outer;
}
}
}
eprintln!( "==== Triggering `cargo build` ====" );
let (counter, build_args) = {
let last_build = last_build.lock().unwrap();
let counter = last_build.counter.next();
let build_args = last_build.project.build_args().clone();
(counter, build_args)
};
if let Ok( project ) = build_args.load_project() {
if let Ok( target ) = select_target( &project ) {
let new_paths_to_watch = project.paths_to_watch( &target );
if new_paths_to_watch != last_paths_to_watch {
debug!( "Paths to watch have changed; new paths to watch: {:#?}", new_paths_to_watch );
if let Some( watcher ) = weak_watcher.upgrade() {
let mut watcher = watcher.lock().expect( "watcher was poisoned" );
for (path, _, _) in last_paths_to_watch {
let _ = watcher.unwatch( path );
}
watch_paths( &mut watcher, &new_paths_to_watch );
}
last_paths_to_watch = new_paths_to_watch;
}
if let Ok( new_build ) = LastBuild::new( project, target, counter ) {
*last_build.lock().unwrap() = new_build;
}
}
}
}
});
watcher
}
pub fn command_start(
build_args: BuildArgs,
host: net::IpAddr,
port: u16,
open: bool,
auto_reload: bool
) -> Result<(), Error> {
let project = build_args.load_project()?;
let last_build = {
let target = select_target( &project )?;
LastBuild::new( project, target, Counter::new() )?
};
let last_build = Arc::new( Mutex::new( last_build ) );
let (target, js_url) = {
let last_build = last_build.lock().unwrap();
let target = last_build.target.clone();
let js_url = last_build.deployment.js_url().to_owned();
(target, js_url)
};
let _watcher = monitor_for_changes_and_rebuild( last_build.clone() );
let target_name = target.name.clone();
let address = net::SocketAddr::new(host, port);
let server = SimpleServer::new(&address, move |request| {
let path = request.uri().path();
let path = percent_decode( path.as_bytes() ).decode_utf8().unwrap();
let last_build = last_build.lock().unwrap();
if path == "/__cargo-web__/build_hash" {
let data = format!( "{}", last_build.get_build_hash() );
return response_from_data(&"application/text".parse().unwrap(), data.into_bytes());
}
if path == "/js/app.js" {
eprintln!( "!!!!!!!!!!!!!!!!!!!!!" );
eprintln!( "WARNING: `/js/app.js` is deprecated; you should update your HTML file to use `/{}.js` instead!", target_name );
eprintln!( "!!!!!!!!!!!!!!!!!!!!!" );
}
debug!( "Received a request for {:?}", path );
if let Some( mut artifact ) = last_build.deployment.get_by_url(&path) {
if auto_reload && (path == "/" || path == "/index.html") {
let result = artifact.map_text( |text| {
let injected_code = auto_reload_code( last_build.get_build_hash() );
text.replace( "<head>", &format!( "<head><script>{}</script>", injected_code ) )
});
artifact = match result {
Ok( artifact ) => artifact,
Err( error ) => {
warn!( "Cannot read {:?}: {:?}", path, error );
return response_from_status(StatusCode::INTERNAL_SERVER_ERROR);
}
}
}
match artifact.kind {
ArtifactKind::Data( data ) => {
return response_from_data(&artifact.mime_type, data);
},
ArtifactKind::File( fp ) => {
return response_from_file(&artifact.mime_type, fp);
}
}
} else {
response_from_status(StatusCode::NOT_FOUND)
}
});
eprintln!( "" );
eprintln!( "If you need to serve any extra files put them in the 'static' directory" );
eprintln!( "in the root of your crate; they will be served alongside your application." );
match target.kind {
TargetKind::Example => eprintln!( "You can also put a '{}-static' directory in your 'examples' directory.", target.name ),
TargetKind::Bin | TargetKind::CDyLib => eprintln!( "You can also put a 'static' directory in your 'src' directory." ),
_ => unreachable!()
};
eprintln!( "" );
eprintln!( "Your application is being served at '/{}'. It will be automatically", js_url );
eprintln!( "rebuilt if you make any changes in your code." );
eprintln!( "" );
eprintln!( "You can access the web server at `http://{}`.", &address );
if open {
thread::spawn( move || {
let start = Instant::now();
let check_url = format!( "http://{}/__cargo-web__/build_hash", &address );
let mut response = None;
while start.elapsed() < Duration::from_secs( 10 ) && response.is_none() {
thread::sleep( Duration::from_millis( 100 ) );
response = ::reqwest::get( &check_url ).ok();
}
::open::that( &format!( "http://{}", &address ) ).expect( "Failed to open browser" );
});
}
server.run();
Ok(())
}