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
131pub fn init(
136 package: &'static str,
137 package_source_dir: &'static str,
138 features: &'static [&'static str],
139) -> Arc<Mutex<Option<LoadedLib>>> {
140 let lib_storage = Arc::new(Mutex::new(Some(LoadedLib::compile_load(package, features))));
141
142 let (reload_send, reload_recv) = mpsc::channel();
144
145 let mut watcher = recommended_watcher(move |res| event_fn(res, &reload_send)).unwrap();
147
148 let watch_dir = Search::Kids(1)
150 .for_folder(package_source_dir)
151 .unwrap_or_else(|_| {
152 panic!(
153 "Could not find the {} crate directory relative to the current directory",
154 package_source_dir
155 )
156 });
157
158 watcher.watch(&watch_dir, RecursiveMode::Recursive).unwrap();
159
160 let lib_storage_clone = Arc::clone(&lib_storage);
164 std::thread::Builder::new()
165 .name(format!("{}_hotreload_watcher", package))
166 .spawn(move || {
167 let mut modified_paths = std::collections::HashSet::new();
168 while let Ok(path) = reload_recv.recv() {
169 modified_paths.insert(path);
170 while let Ok(path) = reload_recv.recv_timeout(Duration::from_millis(300)) {
172 modified_paths.insert(path);
173 }
174
175 info!(
176 ?modified_paths,
177 "Hot reloading {} because files in `{}` modified.", package, package_source_dir
178 );
179
180 hotreload(package, &lib_storage_clone, features);
181 }
182 })
183 .unwrap();
184
185 std::mem::forget(watcher);
187
188 lib_storage
189}
190
191fn compiled_file(dyn_package: &str) -> String { dyn_lib_file(dyn_package, None) }
192
193fn active_file(dyn_package: &str, reload_count: u64) -> String {
194 dyn_lib_file(dyn_package, Some(reload_count))
195}
196
197fn dyn_lib_file(dyn_package: &str, active: Option<u64>) -> String {
198 if let Some(count) = active {
199 format!(
200 "{}{}_active{}{}",
201 DLL_PREFIX,
202 dyn_package.replace('-', "_"),
203 count,
204 DLL_SUFFIX
205 )
206 } else {
207 format!(
208 "{}{}{}",
209 DLL_PREFIX,
210 dyn_package.replace('-', "_"),
211 DLL_SUFFIX
212 )
213 }
214}
215
216fn event_fn(res: notify::Result<notify::Event>, sender: &mpsc::Sender<String>) {
221 match res {
222 Ok(event) => {
223 if let EventKind::Modify(_) = event.kind {
224 event
225 .paths
226 .iter()
227 .filter(|p| p.extension().map(|e| e == "rs").unwrap_or(false))
228 .map(|p| p.to_string_lossy().into_owned())
229 .for_each(|p| { let _ = sender.send(p); });
231 }
232 },
233 Err(e) => error!(?e, "hotreload watcher error."),
234 }
235}
236
237fn hotreload(dyn_package: &str, loaded_lib: &Mutex<Option<LoadedLib>>, features: &[&str]) {
242 if compile(dyn_package, features) {
244 let mut lock = loaded_lib.lock().unwrap();
245
246 let loaded_lib = lock.take().unwrap();
248 loaded_lib.lib.close().unwrap();
249 let new_count = loaded_lib.reload_count + 1;
250 copy(&loaded_lib.lib_path, dyn_package, new_count);
251
252 *lock = Some(LoadedLib::load(dyn_package, new_count));
254
255 info!("Updated {}.", dyn_package);
256 }
257}
258
259fn compile(dyn_package: &str, features: &[&str]) -> bool {
263 let mut features_arg = format!("{}/be-dyn-lib", dyn_package);
264
265 for feature in features {
266 features_arg.push(',');
267 features_arg.push_str(dyn_package);
268 features_arg.push('/');
269 features_arg.push_str(feature);
270 }
271 let output = Command::new("cargo")
272 .stderr(Stdio::inherit())
273 .stdout(Stdio::inherit())
274 .arg("rustc")
275 .arg("--package")
276 .arg(dyn_package)
277 .arg("--features")
278 .arg(features_arg)
279 .arg("-Z")
280 .arg("unstable-options")
281 .arg("--crate-type")
282 .arg("dylib")
283 .output()
284 .unwrap();
285
286 output.status.success()
287}
288
289fn copy(lib_path: &Path, dyn_package: &str, reload_count: u64) {
294 let lib_compiled_path = lib_path.with_file_name(compiled_file(dyn_package));
296 let lib_output_path = lib_path.with_file_name(active_file(dyn_package, reload_count));
297 let old_lib_output_path = reload_count
298 .checked_sub(1)
299 .map(|old_count| lib_path.with_file_name(active_file(dyn_package, old_count)));
300
301 debug!(?lib_compiled_path, ?lib_output_path, "Moving.");
303
304 if let Some(old) = old_lib_output_path {
306 std::fs::remove_file(old).expect("Failed to delete old library");
307 }
308
309 std::fs::copy(&lib_compiled_path, &lib_output_path).unwrap_or_else(|err| {
312 panic!(
313 "Failed to rename dynamic library from {:?} to {:?}. {:?}",
314 lib_compiled_path, lib_output_path, err
315 )
316 });
317}