use libloading::Library;
use notify_debouncer_mini::{notify::*,new_debouncer,DebounceEventResult, Debouncer};
use std::{
env, fs,
path::{Path, PathBuf},
sync::{
mpsc::{channel, Receiver, Sender},
Arc,
},
thread,
time::Duration,
};
pub use libloading::Symbol;
use tempfile::TempDir;
mod error;
pub use self::error::Error;
pub type Result<T> = std::result::Result<T, Error>;
pub struct Lib {
pub lib: Library,
pub loaded_path: PathBuf,
pub original_path: Option<PathBuf>,
}
pub struct DynamicReload {
libs: Vec<Arc<Lib>>,
watcher: Option<Debouncer<RecommendedWatcher>>,
shadow_dir: Option<TempDir>,
search_paths: Vec<PathBuf>,
watch_recv: Receiver<DebounceEventResult>,
}
pub enum Search {
Default,
Backwards,
}
pub enum UpdateState {
Before,
After,
ReloadFailed(Error),
}
#[derive(PartialEq)]
pub enum PlatformName {
No,
Yes,
}
impl<'a> DynamicReload {
pub fn new(
search_paths: Option<Vec<&'a str>>,
shadow_dir: Option<&'a str>,
_search: Search,
debounce_duration: Duration,
) -> DynamicReload {
let (tx, rx) = channel();
DynamicReload {
libs: Vec::new(),
watcher: Self::get_watcher(tx, debounce_duration),
shadow_dir: Self::get_temp_dir(shadow_dir),
watch_recv: rx,
search_paths: Self::get_search_paths(search_paths),
}
}
pub unsafe fn add_library(
&mut self,
name: &str,
name_format: PlatformName,
) -> Result<Arc<Lib>> {
match Self::try_load_library(self, name, name_format) {
Ok(lib) => {
if let Some(w) = self.watcher.as_mut() {
if let Some(path) = lib.original_path.as_ref() {
let parent = path.as_path().parent().unwrap();
let parent_buf = if cfg!(windows) {
parent.to_path_buf().canonicalize().unwrap()
} else {
parent.to_path_buf()
};
let _ = w.watcher().watch(&parent_buf, RecursiveMode::NonRecursive);
}
}
self.libs.push(lib.clone());
Ok(lib)
}
Err(e) => Err(e),
}
}
pub unsafe fn update<F, T>(&mut self, update_call: &F, data: &mut T)
where
F: Fn(&mut T, UpdateState, Option<&Arc<Lib>>),
{
while let Ok(evt) = self.watch_recv.try_recv() {
match evt {
Ok(events) => {
for event in events {
Self::reload_libs(self, &event.path, update_call, data);
}
}
_ => (),
}
}
}
unsafe fn reload_libs<F, T>(&mut self, file_path: &Path, update_call: &F, data: &mut T)
where
F: Fn(&mut T, UpdateState, Option<&Arc<Lib>>),
{
let len = self.libs.len();
for i in (0..len).rev() {
if Self::should_reload(file_path, &self.libs[i]) {
Self::reload_lib(self, i, file_path, update_call, data);
}
}
}
unsafe fn reload_lib<F, T>(
&mut self,
index: usize,
file_path: &Path,
update_call: &F,
data: &mut T,
) where
F: Fn(&mut T, UpdateState, Option<&Arc<Lib>>),
{
update_call(data, UpdateState::Before, Some(&self.libs[index]));
self.remove_lib(index);
match Self::load_library(self, file_path) {
Ok(lib) => {
self.libs.push(lib.clone());
update_call(data, UpdateState::After, Some(&lib));
}
Err(err) => {
update_call(data, UpdateState::ReloadFailed(err), None);
}
}
}
unsafe fn try_load_library(&self, name: &str, name_format: PlatformName) -> Result<Arc<Lib>> {
match Self::search_dirs(self, name, name_format) {
Some(path) => Self::load_library(self, &path),
None => Err(Error::Find(name.into())),
}
}
unsafe fn load_library(&self, full_path: &Path) -> Result<Arc<Lib>> {
let path;
let original_path;
if let Some(sd) = self.shadow_dir.as_ref() {
path = Self::format_filename(sd.path(), full_path);
Self::try_copy(full_path, &path)?;
original_path = Some(full_path.to_path_buf());
} else {
original_path = None;
path = full_path.to_path_buf();
}
Self::init_library(original_path, path)
}
unsafe fn init_library(org_path: Option<PathBuf>, path: PathBuf) -> Result<Arc<Lib>> {
match Library::new(&path) {
Ok(l) => Ok(Arc::new(Lib {
original_path: org_path,
loaded_path: path,
lib: l,
})),
Err(e) => Err(Error::Load(e)),
}
}
fn should_reload(reload_path: &Path, lib: &Lib) -> bool {
if let Some(p) = lib.original_path.as_ref() {
if reload_path.file_name() == p.file_name() {
return true;
}
}
false
}
fn search_dirs(&self, name: &str, name_format: PlatformName) -> Option<PathBuf> {
let lib_name = Self::get_library_name(name, name_format);
if let Some(path) = Self::search_current_dir(&lib_name) {
return Some(path);
}
if let Some(path) = Self::search_relative_paths(self, &lib_name) {
return Some(path);
}
Self::search_backwards_from_exe(&lib_name)
}
fn search_current_dir(name: &String) -> Option<PathBuf> {
Self::is_file(&Path::new(name).to_path_buf())
}
fn search_relative_paths(&self, name: &String) -> Option<PathBuf> {
for p in self.search_paths.iter() {
let path = Path::new(p).join(name);
if let Some(file) = Self::is_file(&path) {
return Some(file);
}
}
None
}
fn get_parent_dir(path: &Path) -> Option<PathBuf> {
path.parent().map(|p| p.to_path_buf())
}
fn search_backwards_from_file(path: &Path, lib_name: &String) -> Option<PathBuf> {
match Self::get_parent_dir(path) {
Some(p) => {
let new_path = Path::new(&p).join(lib_name);
if Self::is_file(&new_path).is_some() {
return Some(new_path);
}
Self::search_backwards_from_file(&p, lib_name)
}
_ => None,
}
}
fn search_backwards_from_exe(lib_name: &String) -> Option<PathBuf> {
let exe_path = env::current_exe().unwrap_or_default();
Self::search_backwards_from_file(&exe_path, lib_name)
}
fn get_temp_dir(shadow_dir: Option<&str>) -> Option<TempDir> {
match shadow_dir {
Some(dir) => match TempDir::new_in(dir) {
Ok(td) => {
if !Path::exists(td.path()) {
println!("Unable to create tempdir in {}", dir);
None
} else {
Some(td)
}
}
Err(_er) => {
None
}
},
_ => None,
}
}
fn is_file(path: &PathBuf) -> Option<PathBuf> {
match fs::metadata(path) {
Ok(md) => {
if md.is_file() {
Some(path.clone())
} else {
None
}
}
_ => None,
}
}
fn try_copy(src: &Path, dest: &Path) -> Result<()> {
for _ in 0..10 {
if let Ok(file) = fs::metadata(src) {
let len = file.len();
if len > 0 {
if fs::copy(src, dest).is_ok() {
return Ok(());
}
}
}
thread::sleep(Duration::from_millis(100));
}
Err(Error::CopyTimeOut(src.to_path_buf(), dest.to_path_buf()))
}
fn get_watcher(
tx: Sender<DebounceEventResult>,
debounce_duration: Duration,
) -> Option<Debouncer<RecommendedWatcher>> {
match new_debouncer(debounce_duration, None, tx) {
Ok(watcher) => Some(watcher),
Err(e) => {
println!(
"Unable to create file watcher, no dynamic reloading will be done, \
error: {:?}",
e
);
None
}
}
}
fn get_search_paths(search_paths: Option<Vec<&str>>) -> Vec<PathBuf> {
match search_paths {
Some(paths) => paths
.iter()
.map(|p| {
let path_buf = Path::new(p).to_path_buf();
path_buf.canonicalize().unwrap_or(path_buf)
})
.collect(),
None => Vec::new(),
}
}
fn get_library_name(name: &str, name_format: PlatformName) -> String {
if name_format == PlatformName::Yes {
Self::get_dynamiclib_name(name)
} else {
name.to_string()
}
}
fn remove_lib(&mut self, idx: usize) {
#[cfg(feature = "no-unload")]
std::mem::forget(self.libs.swap_remove(idx));
#[cfg(not(feature = "no-unload"))]
self.libs.swap_remove(idx);
}
#[cfg(not(feature = "no-timestamps"))]
fn format_filename(shadow_dir: &Path, full_path: &Path) -> PathBuf {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards");
let filename = full_path.file_name().unwrap();
shadow_dir.join(format!("{}_{}", ts.as_millis(), filename.to_str().unwrap()))
}
#[cfg(feature = "no-timestamps")]
fn format_filename(shadow_dir: &Path, full_path: &PathBuf) -> PathBuf {
shadow_dir.join(full_path.file_name().unwrap())
}
#[cfg(target_os = "windows")]
fn get_dynamiclib_name(name: &str) -> String {
format!("{}.dll", name)
}
#[cfg(target_os = "macos")]
fn get_dynamiclib_name(name: &str) -> String {
format!("lib{}.dylib", name)
}
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
fn get_dynamiclib_name(name: &str) -> String {
format!("lib{}.so", name)
}
}
impl PartialEq for Lib {
fn eq(&self, other: &Lib) -> bool {
self.original_path == other.original_path
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
#[derive(Debug, Default)]
struct TestNotifyCallback {
update_call_done: bool,
after_update_done: bool,
fail_update_done: bool,
}
impl TestNotifyCallback {
fn update_call(&mut self, state: UpdateState, _lib: Option<&Arc<Lib>>) {
match state {
UpdateState::Before => self.update_call_done = true,
UpdateState::After => self.after_update_done = true,
UpdateState::ReloadFailed(_) => self.fail_update_done = true,
}
println!("Update state {:?}", self);
}
}
fn get_test_shared_lib() -> PathBuf {
let exe_path = env::current_exe().unwrap();
let lib_path = exe_path.parent().unwrap().parent().unwrap();
let lib_name = "test_shared";
Path::new(&lib_path).join(DynamicReload::get_dynamiclib_name(lib_name))
}
#[test]
fn test_search_paths_none() {
assert_eq!(DynamicReload::get_search_paths(None).len(), 0);
}
#[test]
fn test_search_paths_some() {
assert_eq!(
DynamicReload::get_search_paths(Some(vec!["test", "test"])).len(),
2
);
}
#[test]
fn test_get_watcher() {
let (tx, _) = channel();
assert!(DynamicReload::get_watcher(tx, Duration::from_secs(2)).is_some());
}
#[test]
fn test_get_temp_dir_fail() {
assert!(DynamicReload::get_temp_dir(Some("_no_such_dir")).is_none());
}
#[test]
fn test_get_temp_dir_none() {
assert!(DynamicReload::get_temp_dir(None).is_none());
}
#[test]
fn test_get_temp_dir_ok() {
assert!(DynamicReload::get_temp_dir(Some("")).is_some());
}
#[test]
fn test_is_file_fail() {
assert!(
DynamicReload::is_file(&Path::new("haz_no_file_with_this_name").to_path_buf())
.is_none()
);
}
#[test]
fn test_is_file_ok() {
assert!(DynamicReload::is_file(&env::current_exe().unwrap()).is_some());
}
#[test]
#[cfg(target_os = "macos")]
fn test_get_library_name_mac() {
assert_eq!(
DynamicReload::get_library_name("foobar", PlatformName::Yes),
"libfoobar.dylib"
);
}
#[test]
fn test_get_library_name() {
assert_eq!(
DynamicReload::get_library_name("foobar", PlatformName::No),
"foobar"
);
}
#[test]
fn test_search_backwards_from_file_ok() {
assert!(DynamicReload::search_backwards_from_exe(&"Cargo.toml".to_string()).is_some());
}
#[test]
fn test_search_backwards_from_file_fail() {
assert!(DynamicReload::search_backwards_from_exe(&"_no_such_file".to_string()).is_none());
}
#[test]
fn test_add_library_fail() {
let mut dr = DynamicReload::new(None, None, Search::Default, Duration::from_secs(2));
unsafe {
assert!(dr
.add_library("wont_find_this_lib", PlatformName::No)
.is_err());
}
}
#[test]
fn test_add_shared_lib_ok() {
let mut dr = DynamicReload::new(None, None, Search::Default, Duration::from_secs(2));
unsafe {
assert!(dr.add_library("test_shared", PlatformName::Yes).is_ok());
}
}
#[test]
fn test_add_shared_lib_search_paths() {
let mut dr = DynamicReload::new(
Some(vec!["../..", "../test"]),
None,
Search::Default,
Duration::from_secs(2),
);
unsafe {
assert!(dr.add_library("test_shared", PlatformName::Yes).is_ok());
}
}
#[test]
fn test_add_shared_lib_fail_load() {
let mut dr = DynamicReload::new(None, None, Search::Default, Duration::from_secs(2));
unsafe {
assert!(dr.add_library("Cargo.toml", PlatformName::No).is_err());
}
}
#[test]
fn test_add_shared_shadow_dir_ok() {
let dr = DynamicReload::new(
None,
Some("target/debug"),
Search::Default,
Duration::from_secs(2),
);
assert!(dr.shadow_dir.is_some());
}
#[test]
fn test_add_shared_string_arg_ok() {
let shadow_dir_string = "target/debug".to_owned();
let dr = DynamicReload::new(
None,
Some(&shadow_dir_string),
Search::Default,
Duration::from_secs(2),
);
assert!(dr.shadow_dir.is_some());
}
#[test]
fn test_add_shared_lib_search_paths_strings() {
let path1 = "../..".to_owned();
let path2 = "../test".to_owned();
let mut dr = DynamicReload::new(
Some(vec![&path1, &path2]),
None,
Search::Default,
Duration::from_secs(2),
);
unsafe {
assert!(dr.add_library("test_shared", PlatformName::Yes).is_ok());
}
}
#[test]
fn test_add_shared_update() {
let mut notify_callback = TestNotifyCallback::default();
let target_path = get_test_shared_lib();
let mut dest_path = Path::new(&target_path).to_path_buf();
let mut dr = DynamicReload::new(
None,
Some("target/debug"),
Search::Default,
Duration::from_secs(1),
);
dest_path.set_file_name("test_file");
fs::copy(&target_path, &dest_path).unwrap();
unsafe {
assert!(dr.add_library("test_shared", PlatformName::Yes).is_ok());
}
for i in 0..10 {
unsafe {
dr.update(&TestNotifyCallback::update_call, &mut notify_callback);
}
if i == 2 {
fs::copy(&dest_path, &target_path).unwrap();
}
thread::sleep(Duration::from_millis(200));
}
assert!(notify_callback.update_call_done);
assert!(notify_callback.after_update_done);
}
#[test]
fn test_add_shared_update_fail_after() {
let mut notify_callback = TestNotifyCallback::default();
let target_path = get_test_shared_lib();
let test_file = DynamicReload::get_dynamiclib_name("test_file_2");
let mut dest_path = Path::new(&target_path).to_path_buf();
let mut dr = DynamicReload::new(
Some(vec!["target/debug"]),
Some("target/debug"),
Search::Default,
Duration::from_secs(1),
);
assert!(dr.shadow_dir.is_some());
dest_path.set_file_name(&test_file);
DynamicReload::try_copy(&target_path, &dest_path).unwrap();
thread::sleep(Duration::from_millis(2000));
unsafe {
assert!(dr.add_library(&test_file, PlatformName::No).is_ok());
}
for i in 0..10 {
println!("update {}", i);
unsafe {
dr.update(&TestNotifyCallback::update_call, &mut notify_callback);
}
if i == 2 {
fs::copy("Cargo.toml", &dest_path).unwrap();
}
thread::sleep(Duration::from_millis(200));
}
assert_eq!(notify_callback.update_call_done, true);
assert_eq!(notify_callback.after_update_done, false);
assert_eq!(notify_callback.fail_update_done, true);
}
#[test]
fn test_lib_equals_true() {
let mut dr = DynamicReload::new(None, None, Search::Default, Duration::from_secs(2));
let lib = unsafe { dr.add_library("test_shared", PlatformName::Yes).unwrap() };
let lib2 = lib.clone();
assert!(lib == lib2);
}
#[test]
fn test_lib_equals_false() {
let mut dr = DynamicReload::new(
Some(vec!["target/debug"]),
Some("target/debug"),
Search::Default,
Duration::from_secs(2),
);
let target_path = get_test_shared_lib();
let test_file = DynamicReload::get_dynamiclib_name("test_file_2");
let mut dest_path = Path::new(&target_path).to_path_buf();
dest_path.set_file_name(&test_file);
let _ = DynamicReload::try_copy(&target_path, &dest_path);
thread::sleep(Duration::from_millis(100));
let lib0 = unsafe { dr.add_library(&test_file, PlatformName::No).unwrap() };
let lib1 = unsafe { dr.add_library("test_shared", PlatformName::Yes).unwrap() };
assert!(lib0 != lib1);
}
}