#![forbid(unsafe_code)]
#![deny(
missing_docs,
unstable_features,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unused_import_braces,
unused_qualifications,
bad_style,
const_err,
dead_code,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
unconditional_recursion,
unused,
unused_allocation,
unused_comparisons,
unused_parens,
while_true,
missing_debug_implementations,
missing_docs,
trivial_casts,
trivial_numeric_casts,
unused_extern_crates,
unused_import_braces,
unused_qualifications
)]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[macro_use]
extern crate log;
use kqueue2::{Ident::*, *};
use std::{
collections::HashMap,
env,
fmt::Display,
fs::{metadata, File},
io::{prelude::*, BufReader, SeekFrom},
path::Path,
process::exit,
};
use chrono::Local;
use colored::Colorize;
use fern::Dispatch;
use log::LevelFilter;
use walkdir::WalkDir;
type FileAndPosition = HashMap<String, u64>;
const STDOUT_DEV: &str = "/dev/stdout";
const MIN_DIR_DEPTH: usize = 1;
const MAX_DIR_DEPTH: usize = 3;
const HEADER_AFTER_BYTES: u64 = 256;
fn fatal<S: Display>(fmt: S) -> ! {
error!("FATAL ERROR: {}", fmt.to_string().red());
exit(1)
}
fn walkdir_recursive(mut kqueue_watcher: &mut Watcher, file_path: &Path) {
WalkDir::new(&file_path)
.follow_links(true)
.min_depth(MIN_DIR_DEPTH)
.max_depth(MAX_DIR_DEPTH)
.into_iter()
.filter_map(|element| element.ok())
.for_each(|element| watch_file(&mut kqueue_watcher, element.path()));
}
fn main() {
let loglevel = match env::var("DEBUG") {
Ok(_) => LevelFilter::Debug,
Err(_) => LevelFilter::Info,
};
Dispatch::new()
.format(move |out, message, _record| {
out.finish(format_args!(
"{}: {}",
Local::now().to_rfc3339().black(),
message
))
})
.level(loglevel)
.chain(File::open(STDOUT_DEV).unwrap_or_else(|_| {
fatal(format!(
"{}: STDOUT device {} is not available! Something is terribly wrong here!",
"FATAL ERROR".red(),
STDOUT_DEV.yellow()
))
}))
.apply()
.unwrap_or_else(|err| {
fatal(format!(
"{}: Couldn't initialize Log-Watcher. Details: {}",
"FATAL ERROR".red(),
err.to_string().yellow()
));
});
let mut watched_file_states = FileAndPosition::new();
let mut kqueue_watcher = Watcher::new()
.unwrap_or_else(|e| fatal(format!("Could not create kq watcher: {}", e)));
let paths_to_watch: Vec<String> = env::args()
.skip(1) .collect();
debug!("Watching paths: {}", paths_to_watch.join(", "));
if paths_to_watch.is_empty() {
fatal(
"No paths specified as arguments! You have to specify at least a single directory/file to watch!",
);
}
{
paths_to_watch.into_iter().for_each(|a_path| {
let file_path = Path::new(&a_path);
watch_file(&mut kqueue_watcher, &file_path);
walkdir_recursive(&mut kqueue_watcher, &file_path);
});
}
if kqueue_watcher.watch().is_ok() {
while let Some(an_event) = kqueue_watcher.iter().next() {
match an_event.ident {
Filename(_file_descriptor, abs_file_name) => {
let file_path = Path::new(&abs_file_name);
match metadata(file_path) {
Ok(metadata) => {
if metadata.is_dir() {
debug!("{}: {}", "+DirLoad".magenta(), abs_file_name.cyan());
walkdir_recursive(&mut kqueue_watcher, file_path);
kqueue_watcher.watch().unwrap_or_default();
} else {
debug!("{}: {}", "+New".magenta(), abs_file_name.cyan());
watch_file(&mut kqueue_watcher, file_path);
kqueue_watcher.watch().unwrap_or_default();
handle_file_event(&mut watched_file_states, &abs_file_name);
}
}
Err(error_cause) => {
debug!("{}: {}", "-Watch".magenta(), abs_file_name.cyan());
kqueue_watcher
.remove_filename(file_path, EventFilter::EVFILT_VNODE)
.unwrap_or_else(|error| {
error!(
"Could not remove watch on file: {:?}. Error cause: {}",
abs_file_name.cyan(),
error.to_string().red()
)
});
if file_path.exists() {
walkdir_recursive(&mut kqueue_watcher, file_path);
kqueue_watcher.watch().unwrap_or_default();
} else {
error!(
"Dropped watch on file/dir: {}. Error cause: {}",
format!("{:?}", &file_path).red(),
format!("{}", &error_cause).red()
);
}
}
};
}
event => warn!("Unknown event: {:?}", event),
}
}
}
}
fn watch_file(kqueue_watcher: &mut Watcher, file: &Path) {
debug!("{}: {}", "+Watch".magenta(), format!("{:?}", file).cyan());
kqueue_watcher
.add_filename(
&file,
EventFilter::EVFILT_VNODE,
NOTE_WRITE | NOTE_LINK | NOTE_RENAME | NOTE_DELETE, )
.unwrap_or_else(|error_cause| {
error!(
"Could not watch file {:?}. Error cause: {}",
file,
error_cause.to_string().red()
)
});
}
fn handle_file_event(states: &mut FileAndPosition, file_path: &str) {
let file_entry_in_hashmap = states.iter().find(|hashmap| *hashmap.0 == file_path);
match file_entry_in_hashmap {
Some((watched_file, file_position)) => {
debug!(
"{}: {} {}",
"+EventHandle".magenta(),
watched_file.cyan(),
format!("@{}", file_position).black()
);
let file_size = match metadata(&watched_file) {
Ok(file_metadata) => file_metadata.len(),
Err(_) => 0,
};
if *file_position + HEADER_AFTER_BYTES < file_size || *file_position == 0 {
println!();
println!(); info!("{}", watched_file.blue());
}
if *file_position < file_size {
let content = seek_file_to_position_and_read(&watched_file, *file_position);
println!("{}", content.join("\n"));
states.insert(file_path.to_string(), file_size);
}
}
None => {
states.insert(file_path.to_string(), 0);
}
}
}
fn seek_file_to_position_and_read(file_to_watch: &str, file_position: u64) -> Vec<String> {
match File::open(&file_to_watch) {
Ok(some_file) => {
let mut cursor = BufReader::new(some_file);
cursor
.seek(SeekFrom::Start(file_position))
.unwrap_or_else(|_| 0);
cursor.lines().filter_map(|line| line.ok()).collect()
}
Err(error_cause) => {
error!(
"Couldn't open file: {}. Error cause: {}",
file_to_watch.yellow(),
error_cause.to_string().red()
);
vec![]
}
}
}