extern crate libc;
extern crate notify;
extern crate libloading;
extern crate tempdir;
use libloading::Library;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc::{channel, Receiver, Sender};
use notify::{RecommendedWatcher, Watcher, Event};
use tempdir::TempDir;
use std::time::Duration;
use std::thread;
use std::fs;
use std::env;
pub use libloading::Symbol;
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<'a> {
libs: Vec<Arc<Lib>>,
watcher: Option<RecommendedWatcher>,
shadow_dir: Option<TempDir>,
search_paths: Vec<&'a str>,
watch_recv: Receiver<Event>,
}
pub enum Search {
Default,
Backwards,
}
pub enum UpdateState {
Before,
After,
ReloadFailed(Error),
}
#[derive(PartialEq)]
pub enum PlatformName {
No,
Yes,
}
impl<'a> DynamicReload<'a> {
pub fn new(search_paths: Option<Vec<&'a str>>,
shadow_dir: Option<&'a str>,
_search: Search)
-> DynamicReload<'a> {
let (tx, rx) = channel();
DynamicReload {
libs: Vec::new(),
watcher: Self::get_watcher(tx),
shadow_dir: Self::get_temp_dir(shadow_dir),
watch_recv: rx,
search_paths: Self::get_search_paths(search_paths),
}
}
pub 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 _ = w.watch(path);
}
}
self.libs.push(lib.clone());
Ok(lib)
}
Err(e) => Err(e),
}
}
pub fn update<F, T>(&mut self, ref update_call: F, data: &mut T) where F: Fn(&mut T, UpdateState, Option<&Arc<Lib>>)
{
match self.watch_recv.try_recv() {
Ok(file) => {
Self::reload_libs(self,
file.path.as_ref().unwrap(),
update_call,
data)
}
_ => (),
}
}
fn reload_libs<F, T>(&mut self,
file_path: &PathBuf,
ref 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);
}
}
}
fn reload_lib<F, T>(&mut self,
index: usize,
file_path: &PathBuf,
ref 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.libs.swap_remove(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);
}
}
}
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())),
}
}
fn load_library(&self, full_path: &PathBuf) -> Result<Arc<Lib>> {
let path;
let original_path;
if let Some(sd) = self.shadow_dir.as_ref() {
path = sd.path().join(full_path.file_name().unwrap());
try!(Self::try_copy(&full_path, &path));
original_path = Some(full_path.clone());
} else {
original_path = None;
path = full_path.clone();
}
Self::init_library(original_path, path)
}
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: &PathBuf, lib: &Lib) -> bool {
if let Some(p) = lib.original_path.as_ref() {
if reload_path.to_str().unwrap().contains(p.to_str().unwrap()) {
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: &PathBuf) -> Option<PathBuf> {
match path.parent() {
Some(p) => Some(p.to_path_buf()),
_ => None,
}
}
fn search_backwards_from_file(path: &PathBuf, 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(PathBuf::new());
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, "shadow_libs") {
Ok(td) => Some(td),
Err(er) => {
println!("Unable to create tempdir {}", 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 {
return match fs::copy(&src, &dest) {
Ok(_) => Ok(()),
Err(e) => Err(Error::Copy(e, src.to_path_buf(), dest.to_path_buf()))
};
}
}
thread::sleep(Duration::from_millis(100));
}
Err(Error::CopyTimeOut(src.to_path_buf(), dest.to_path_buf()))
}
fn get_watcher(tx: Sender<Event>) -> Option<RecommendedWatcher> {
match Watcher::new(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<&str> {
match search_paths {
Some(paths) => paths.clone(),
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()
}
}
#[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
}
fn ne(&self, other: &Lib) -> bool {
self.original_path != other.original_path
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc::channel;
use std::path::{Path, PathBuf};
use std::env;
use std::thread;
use std::time::Duration;
use std::sync::Arc;
use std::fs;
#[derive(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,
}
}
}
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).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);
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);
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);
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);
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);
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);
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);
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);
dest_path.set_file_name("test_file");
fs::copy(&target_path, &dest_path).unwrap();
assert!(dr.add_library("test_shared", PlatformName::Yes).is_ok());
for i in 0..10 {
dr.update(TestNotifyCallback::update_call, &mut notify_callback);
if i == 2 {
fs::copy(&dest_path, &target_path).unwrap();
}
thread::sleep(Duration::from_millis(50));
}
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);
assert!(dr.shadow_dir.is_some());
dest_path.set_file_name(&test_file);
let _ = DynamicReload::try_copy(&target_path, &dest_path);
thread::sleep(Duration::from_millis(100));
assert!(dr.add_library(&test_file, PlatformName::No).is_ok());
for i in 0..10 {
dr.update(TestNotifyCallback::update_call, &mut notify_callback);
if i == 2 {
fs::copy("Cargo.toml", &dest_path).unwrap();
}
thread::sleep(Duration::from_millis(50));
}
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);
let lib = 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);
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 = dr.add_library(&test_file, PlatformName::No).unwrap();
let lib1 = dr.add_library("test_shared", PlatformName::Yes).unwrap();
assert!(lib0 != lib1);
}
}