1use libloading::Library;
2use notify::{EventKind, RecursiveMode, Watcher, recommended_watcher};
3use std::{
4 process::{Command, Stdio},
5 sync::{Mutex, mpsc},
6 time::Duration,
7};
8
9use find_folder::Search;
10use std::{
11 env,
12 env::consts::{DLL_PREFIX, DLL_SUFFIX},
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16use tracing::{debug, error, info};
17
18pub use libloading::Symbol;
20
21pub struct LoadedLib {
29 pub lib: Library,
31 lib_path: PathBuf,
33 reload_count: u64,
36}
37
38impl LoadedLib {
39 fn compile_load(dyn_package: &str, features: &[&str]) -> Self {
44 let reload_count = 0; #[cfg(target_os = "macos")]
47 error!("The hot reloading feature does not work on macos.");
48
49 if !compile(dyn_package, features) {
51 panic!("{} compile failed.", dyn_package);
52 } else {
53 info!("{} compile succeeded.", dyn_package);
54 }
55
56 copy(
57 &LoadedLib::determine_path(dyn_package, reload_count),
58 dyn_package,
59 reload_count,
60 );
61
62 Self::load(dyn_package, reload_count)
63 }
64
65 fn load(dyn_package: &str, reload_count: u64) -> Self {
71 let lib_path = LoadedLib::determine_path(dyn_package, reload_count);
72
73 let lib = match unsafe { Library::new(lib_path.clone()) } {
75 Ok(lib) => lib,
76 Err(e) => panic!(
77 "Tried to load dynamic library from {:?}, but it could not be found. A potential \
78 reason is we may require a special case for your OS so we can find it. {:?}",
79 lib_path, e
80 ),
81 };
82
83 Self {
84 lib,
85 lib_path,
86 reload_count,
87 }
88 }
89
90 fn determine_path(dyn_package: &str, reload_count: u64) -> PathBuf {
93 let current_exe = env::current_exe();
94
95 let mut lib_path = match current_exe {
98 Ok(mut path) => {
99 path.pop();
101
102 let dir = Search::ParentsThenKids(1, 1)
104 .of(path)
105 .for_folder("debug")
106 .expect(
107 "Could not find the debug build directory relative to the current \
108 executable.",
109 );
110
111 debug!(?dir, "Found the debug build directory.");
112 dir
113 },
114 Err(e) => {
115 panic!(
116 "Could not determine the path of the current executable, this is needed to \
117 hot-reload the dynamic library. {:?}",
118 e
119 );
120 },
121 };
122
123 lib_path.push(active_file(dyn_package, reload_count));
126
127 lib_path
128 }
129
130 pub fn reload_count(&self) -> u64 { self.reload_count }
131}
132
133pub fn init(
138 package: &'static str,
139 package_source_dir: &'static str,
140 features: &'static [&'static str],
141) -> Arc<Mutex<Option<LoadedLib>>> {
142 let lib_storage = Arc::new(Mutex::new(Some(LoadedLib::compile_load(package, features))));
143
144 let (reload_send, reload_recv) = mpsc::channel();
146
147 let mut watcher = recommended_watcher(move |res| event_fn(res, &reload_send)).unwrap();
149
150 let watch_dir = Search::Kids(1)
152 .for_folder(package_source_dir)
153 .unwrap_or_else(|_| {
154 panic!(
155 "Could not find the {} crate directory relative to the current directory",
156 package_source_dir
157 )
158 });
159
160 watcher.watch(&watch_dir, RecursiveMode::Recursive).unwrap();
161
162 let lib_storage_clone = Arc::clone(&lib_storage);
166 std::thread::Builder::new()
167 .name(format!("{}_hotreload_watcher", package))
168 .spawn(move || {
169 let mut modified_paths = std::collections::HashSet::new();
170 while let Ok(path) = reload_recv.recv() {
171 modified_paths.insert(path);
172 while let Ok(path) = reload_recv.recv_timeout(Duration::from_millis(300)) {
174 modified_paths.insert(path);
175 }
176
177 info!(
178 ?modified_paths,
179 "Hot reloading {} because files in `{}` modified.", package, package_source_dir
180 );
181
182 hotreload(package, &lib_storage_clone, features);
183 }
184 })
185 .unwrap();
186
187 std::mem::forget(watcher);
189
190 lib_storage
191}
192
193fn compiled_file(dyn_package: &str) -> String { dyn_lib_file(dyn_package, None) }
194
195fn active_file(dyn_package: &str, reload_count: u64) -> String {
196 dyn_lib_file(dyn_package, Some(reload_count))
197}
198
199fn dyn_lib_file(dyn_package: &str, active: Option<u64>) -> String {
200 if let Some(count) = active {
201 format!(
202 "{}{}_active{}{}",
203 DLL_PREFIX,
204 dyn_package.replace('-', "_"),
205 count,
206 DLL_SUFFIX
207 )
208 } else {
209 format!(
210 "{}{}{}",
211 DLL_PREFIX,
212 dyn_package.replace('-', "_"),
213 DLL_SUFFIX
214 )
215 }
216}
217
218fn event_fn(res: notify::Result<notify::Event>, sender: &mpsc::Sender<String>) {
223 match res {
224 Ok(event) => {
225 if let EventKind::Modify(_) = event.kind {
226 event
227 .paths
228 .iter()
229 .filter(|p| p.extension().map(|e| e == "rs").unwrap_or(false))
230 .map(|p| p.to_string_lossy().into_owned())
231 .for_each(|p| { let _ = sender.send(p); });
233 }
234 },
235 Err(e) => error!(?e, "hotreload watcher error."),
236 }
237}
238
239fn hotreload(dyn_package: &str, loaded_lib: &Mutex<Option<LoadedLib>>, features: &[&str]) {
244 if compile(dyn_package, features) {
246 let mut lock = loaded_lib.lock().unwrap();
247
248 let loaded_lib = lock.take().unwrap();
250 loaded_lib.lib.close().unwrap();
251 let new_count = loaded_lib.reload_count + 1;
252 copy(&loaded_lib.lib_path, dyn_package, new_count);
253
254 *lock = Some(LoadedLib::load(dyn_package, new_count));
256
257 info!("Updated {}.", dyn_package);
258 }
259}
260
261fn compile(dyn_package: &str, features: &[&str]) -> bool {
265 let mut features_arg = format!("{}/be-dyn-lib", dyn_package);
266
267 for feature in features {
268 features_arg.push(',');
269 features_arg.push_str(dyn_package);
270 features_arg.push('/');
271 features_arg.push_str(feature);
272 }
273 let output = Command::new("cargo")
274 .stderr(Stdio::inherit())
275 .stdout(Stdio::inherit())
276 .arg("rustc")
277 .arg("--package")
278 .arg(dyn_package)
279 .arg("--features")
280 .arg(features_arg)
281 .arg("-Z")
282 .arg("unstable-options")
283 .arg("--crate-type")
284 .arg("dylib")
285 .output()
286 .unwrap();
287
288 output.status.success()
289}
290
291fn copy(lib_path: &Path, dyn_package: &str, reload_count: u64) {
296 let lib_compiled_path = lib_path.with_file_name(compiled_file(dyn_package));
298 let lib_output_path = lib_path.with_file_name(active_file(dyn_package, reload_count));
299 let old_lib_output_path = reload_count
300 .checked_sub(1)
301 .map(|old_count| lib_path.with_file_name(active_file(dyn_package, old_count)));
302
303 debug!(?lib_compiled_path, ?lib_output_path, "Moving.");
305
306 if let Some(old) = old_lib_output_path {
308 std::fs::remove_file(old).expect("Failed to delete old library");
309 }
310
311 std::fs::copy(&lib_compiled_path, &lib_output_path).unwrap_or_else(|err| {
314 panic!(
315 "Failed to rename dynamic library from {:?} to {:?}. {:?}",
316 lib_compiled_path, lib_output_path, err
317 )
318 });
319}