use crate::BuildOptions;
use crate::{file_system::WatchableFileSystemAPI, file_system_stdlib, server, site::Site};
use anyhow::Result;
use console::{style, Term};
use indicatif::ProgressBar;
use rsa::pkcs8::der::Writer;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::{mpsc, Arc, RwLock};
use std::time::{Duration, Instant};
use std::{process::exit, sync::atomic::Ordering, thread};
use tracing::warn;
#[derive(Debug, Default)]
pub enum DevServerMode {
#[default]
NoServe,
NoServeStreamLogs,
Serve(Option<u16>),
}
#[derive(Debug, Default)]
pub struct UploadsConfig<'a> {
pub url: Option<&'a str>,
pub prefix: Option<&'a str>,
}
impl<'a> UploadsConfig<'a> {
pub fn prefix(prefix: &'a str) -> Self {
UploadsConfig {
url: None,
prefix: Some(prefix),
}
}
pub fn new(url: &'a str, prefix: &'a str) -> Self {
UploadsConfig {
url: Some(url),
prefix: Some(prefix),
}
}
}
pub fn watch(
root_dir: PathBuf,
uploads_config: UploadsConfig,
mode: DevServerMode,
change_sender: Option<mpsc::Sender<Vec<PathBuf>>>,
watch_paths: Option<Vec<String>>,
quit: Arc<AtomicBool>,
) -> Result<crate::binary::ExitStatus> {
let mut term = Term::stdout();
let is_interactive =
term.features().is_attended() && !matches!(mode, DevServerMode::NoServeStreamLogs);
let mut fs = file_system_stdlib::NativeFileSystem::new(&root_dir);
let mut site = Site::load(&fs, uploads_config.prefix)?;
if let Some(uploads_url) = uploads_config.url {
site.modify_manifest(&mut fs, |manifest| {
manifest.uploads_url = Some(uploads_url.to_string());
})?;
}
site.sync_static_files(&mut fs)?;
let (tx, rx) = mpsc::channel();
let initial_build = site.build(&mut fs, BuildOptions::default());
let mut init_message = format!("Watching site: {}", site);
let change_queue = Arc::new(RwLock::new(vec![]));
let queue_changes = change_sender.is_some();
let watcher_queue = change_queue.clone();
let mut merged_watch_paths = watch_paths.unwrap_or_default();
merged_watch_paths.append(&mut site.manifest.watched_paths());
let kill_watcher = fs.watch(fs.root.to_owned(), merged_watch_paths, move |paths| {
for path in paths {
if queue_changes {
watcher_queue.write().unwrap().push(path.clone());
}
if let Err(e) = tx.send(path.clone()) {
warn!("Failed sending change event to builder: {}", e);
}
}
})?;
let path = root_dir.join(&site.manifest.build_dir);
if let DevServerMode::Serve(port) = mode {
let mut sb = server::ServerBuilder::new(&path, Some("404.html"));
if let Some(port) = port {
sb.port(port);
}
let server = sb.build();
init_message += &format!("Serving {}\n", path.display());
init_message += &format!("See http://{}\n", server.addr());
init_message += "Hit CTRL-C to stop\n";
thread::spawn(move || {
server.serve().unwrap();
});
}
let quit_clone = quit.clone();
ctrlc::set_handler(move || {
quit_clone.store(true, Ordering::SeqCst);
exit(0);
})?;
let mut last_build = Instant::now();
let mut changed = false;
let mut object_definitions_changed = false;
let mut static_files_changed = false;
term.write(init_message.as_bytes())?;
if let Err(e) = initial_build {
let bar = ProgressBar::new_spinner();
bar.finish_with_message(format!("Initial build failed: {}", e));
}
loop {
match rx.recv_timeout(Duration::from_millis(50)) {
Ok(path) => {
let changed_file = path.strip_prefix(&root_dir).unwrap();
if changed_file == site.manifest.object_definition_file {
object_definitions_changed = true;
}
if changed_file.starts_with(&site.manifest.static_dir) {
static_files_changed = true;
}
site.invalidate_file(changed_file);
changed = true;
}
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => {
panic!("Build Channel Disconnected.")
}
}
if quit.load(Ordering::SeqCst) {
kill_watcher();
exit(0);
}
if changed && Instant::now() - last_build > Duration::from_millis(200) {
last_build = Instant::now();
changed = false;
let bar = if is_interactive {
let bar = ProgressBar::new_spinner();
bar.enable_steady_tick(Duration::from_millis(100));
if is_interactive {
term.clear_screen()?;
}
term.write(init_message.as_bytes())?;
bar.set_message("Rebuilding...");
Some(bar)
} else {
println!("Rebuilding...");
None
};
let mut fs = file_system_stdlib::NativeFileSystem::new(&root_dir);
let mut reload_error = None;
let mut site_reloaded = false;
if object_definitions_changed {
object_definitions_changed = false;
match Site::load(&fs, uploads_config.prefix) {
Ok(mut reloaded) => {
if let Some(uploads_url) = uploads_config.url {
reloaded.manifest.uploads_url = Some(uploads_url.to_string());
}
site = reloaded;
site_reloaded = true;
}
Err(e) => reload_error = Some(e),
}
}
let output = if let Some(e) = reload_error {
format!("{} {}", style("Reload failed:").red(), style(e).red())
} else {
if static_files_changed || site_reloaded {
static_files_changed = false;
site.sync_static_files(&mut fs).unwrap();
}
if let Err(e) = site.build(&mut fs, BuildOptions::default()) {
format!("{} {}", style("Build failed:").red(), style(e).red())
} else {
format!(
"{} {:?}",
style("Rebuilt in").green(),
style(Instant::now() - last_build).green()
)
}
};
if let Some(bar) = bar {
bar.finish_with_message(output);
if let Some(cs) = &change_sender {
let mut guard = change_queue.write().unwrap();
let q = std::mem::take(&mut *guard);
if let Err(e) = cs.send(q) {
warn!("Failed sending change event to host: {}", e);
}
}
} else {
println!("{}", output);
}
}
}
}