wavecraft_dev_server/
session.rs1use anyhow::Result;
7use std::path::PathBuf;
8use std::sync::Arc;
9use tokio::sync::{mpsc, watch};
10
11use crate::host::DevServerHost;
12use crate::reload::guard::BuildGuard;
13use crate::reload::rebuild::{RebuildCallbacks, RebuildPipeline};
14use crate::reload::watcher::{FileWatcher, WatchEvent};
15use crate::ws::WsServer;
16
17#[cfg(feature = "audio")]
18use crate::audio::server::AudioHandle;
19
20fn log_rust_file_change(paths: &[PathBuf]) {
21 let timestamp = chrono::Local::now().format("%H:%M:%S");
22 if paths.len() == 1 {
23 println!(
24 "[{}] File changed: {}",
25 timestamp,
26 paths[0].file_name().unwrap_or_default().to_string_lossy()
27 );
28 } else {
29 println!("[{}] {} files changed", timestamp, paths.len());
30 }
31}
32
33fn report_rebuild_result(result: Result<Result<()>, tokio::task::JoinError>) {
34 match result {
35 Ok(Ok(())) => {
36 }
38 Ok(Err(e)) => {
39 eprintln!(" {} Rebuild failed: {:#}", console::style("✗").red(), e);
40 }
41 Err(join_err) => {
42 eprintln!(
43 " {} Hot-reload pipeline panicked: {}\n {} Continuing to watch for changes...",
44 console::style("✗").red(),
45 join_err,
46 console::style("→").cyan()
47 );
48 }
49 }
50}
51
52async fn handle_rust_files_changed(paths: Vec<PathBuf>, pipeline: &Arc<RebuildPipeline>) {
53 log_rust_file_change(&paths);
54
55 let pipeline = Arc::clone(pipeline);
57 let result = tokio::spawn(async move { pipeline.handle_change().await }).await;
58 report_rebuild_result(result);
59}
60
61pub struct DevSession {
68 #[allow(dead_code)] watcher: FileWatcher,
71 #[allow(dead_code)] _pipeline_handle: tokio::task::JoinHandle<()>,
74 #[allow(dead_code)] ws_server: Arc<WsServer<Arc<DevServerHost>>>,
77 #[allow(dead_code)]
79 _shutdown_rx: watch::Receiver<bool>,
80 #[cfg(feature = "audio")]
82 #[allow(dead_code)] _audio_handle: Option<AudioHandle>,
84}
85
86impl DevSession {
87 #[allow(clippy::too_many_arguments)]
102 pub fn new(
103 engine_dir: PathBuf,
104 host: Arc<DevServerHost>,
105 ws_server: Arc<WsServer<Arc<DevServerHost>>>,
106 shutdown_rx: watch::Receiver<bool>,
107 callbacks: RebuildCallbacks,
108 #[cfg(feature = "audio")] audio_handle: Option<AudioHandle>,
109 ) -> Result<Self> {
110 let (watch_tx, mut watch_rx) = mpsc::unbounded_channel::<WatchEvent>();
112
113 let watcher = FileWatcher::new(
115 &engine_dir,
116 &callbacks.additional_watch_paths,
117 watch_tx,
118 shutdown_rx.clone(),
119 )?;
120
121 let guard = Arc::new(BuildGuard::new());
123 let pipeline = Arc::new(RebuildPipeline::new(
124 guard,
125 engine_dir,
126 Arc::clone(&host),
127 Arc::clone(&ws_server),
128 shutdown_rx.clone(),
129 callbacks,
130 #[cfg(feature = "audio")]
131 None, ));
133
134 let shutdown_rx_for_task = shutdown_rx.clone();
136 let pipeline_handle = tokio::spawn(async move {
137 let mut shutdown_rx = shutdown_rx_for_task.clone();
138 loop {
139 if *shutdown_rx.borrow() {
140 break;
141 }
142
143 tokio::select! {
144 _ = shutdown_rx.changed() => {
145 break;
146 }
147 maybe_event = watch_rx.recv() => {
148 let event = match maybe_event {
149 Some(event) => event,
150 None => break,
151 };
152
153 match event {
154 WatchEvent::RustFilesChanged(paths) => {
155 handle_rust_files_changed(paths, &pipeline).await;
156 }
157 }
158 }
159 }
160 }
161 });
162
163 Ok(Self {
164 watcher,
165 _pipeline_handle: pipeline_handle,
166 ws_server,
167 _shutdown_rx: shutdown_rx,
168 #[cfg(feature = "audio")]
169 _audio_handle: audio_handle,
170 })
171 }
172}