Skip to main content

craken_cli/
hot_reload.rs

1use std::{path::Path, time::Duration};
2
3use anyhow::Result;
4use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
5use tokio::sync::mpsc;
6
7/// Run the development server with automatic hot-reload.
8///
9/// # Behaviour
10///
11/// 1. Spawns `cargo run -- serve --addr <addr>` as a child process.
12/// 2. Watches `./src` for `Create`, `Modify`, or `Remove` events via
13///    the [`notify`] crate.
14/// 3. On change: kills the running child, waits for it to exit, drains
15///    any burst of additional events (300 ms debounce), and re-spawns.
16/// 4. If the child exits on its own (e.g. compile error), the loop waits
17///    for the next file-change event before attempting to restart, avoiding
18///    a tight CPU-burning restart loop.
19///
20/// # Limitations
21///
22/// Because Rust is AOT-compiled, "hot reload" here means recompilation and
23/// process restart — not in-process code swapping. Use `RUST_LOG=info` for
24/// structured log output during development.
25pub async fn run_dev(addr: &str) -> Result<()> {
26    tracing::info!("Craken dev server — hot-reload enabled");
27    tracing::info!("Addr: {addr}");
28
29    // Bridge notify's sync callback into an async tokio channel.
30    let (tx, mut rx) = mpsc::channel::<()>(64);
31
32    let tx_for_watcher = tx.clone();
33    let mut watcher = RecommendedWatcher::new(
34        move |result: notify::Result<notify::Event>| {
35            if let Ok(event) = result {
36                // Only react to meaningful filesystem mutations.
37                if matches!(
38                    event.kind,
39                    EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
40                ) {
41                    // `blocking_send` is safe here: notify callbacks run on a
42                    // plain OS thread, outside the tokio runtime.
43                    let _ = tx_for_watcher.blocking_send(());
44                }
45            }
46        },
47        // Poll every 500 ms as a fallback on platforms where inotify is
48        // unavailable or has limitations (e.g. network filesystems).
49        Config::default().with_poll_interval(Duration::from_millis(500)),
50    )?;
51
52    let src = Path::new("src");
53    if src.exists() {
54        watcher.watch(src, RecursiveMode::Recursive)?;
55        tracing::info!("Watching: src/");
56    } else {
57        tracing::warn!("src/ not found — file watching disabled");
58    }
59
60    loop {
61        tracing::info!("Spawning: cargo run -- serve {addr}");
62
63        let mut child = tokio::process::Command::new("cargo")
64            .args(["run", "--", "serve", addr])
65            .spawn()
66            .map_err(|e| anyhow::anyhow!("Failed to spawn cargo: {e}"))?;
67
68        tokio::select! {
69            // ── Branch 1: child process exited on its own ──────────────────
70            status = child.wait() => {
71                match status {
72                    Ok(s) if s.success() => tracing::info!("Server exited cleanly"),
73                    Ok(s)  => tracing::warn!("Server exited: {s}"),
74                    Err(e) => tracing::error!("Server wait error: {e}"),
75                }
76                // Wait for a file change before re-spawning to avoid hammering
77                // the filesystem with rebuild attempts on a compile error.
78                tracing::info!("Waiting for file change…");
79                rx.recv().await;
80                // Debounce burst events.
81                while rx.try_recv().is_ok() {}
82                tokio::time::sleep(Duration::from_millis(300)).await;
83            }
84
85            // ── Branch 2: file change detected ────────────────────────────
86            Some(()) = rx.recv() => {
87                tracing::info!("File change detected — restarting server");
88                let _ = child.kill().await;
89                let _ = child.wait().await;
90                // Drain any burst of change events before re-spawning.
91                while rx.try_recv().is_ok() {}
92                tokio::time::sleep(Duration::from_millis(300)).await;
93            }
94        }
95    }
96}