Skip to content

Instantly share code, notes, and snippets.

@sczizzo
Last active March 16, 2016 21:56
Show Gist options
  • Save sczizzo/88ed750b25ce0f16e18b to your computer and use it in GitHub Desktop.
Save sczizzo/88ed750b25ce0f16e18b to your computer and use it in GitHub Desktop.
Watch files, in Rust
extern crate rustc_serialize;
extern crate docopt;
extern crate glob;
use std::time::Duration;
use std::thread::sleep;
use std::os::unix::fs::MetadataExt;
use std::os::unix::raw::*;
use std::collections::HashMap;
use std::collections::HashSet;
use docopt::Docopt;
use glob::glob;
use std::fs;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const USAGE: &'static str = "
Julio.
Usage:
julio [--interval=<seconds>] <includes>...
julio (-h | --help)
julio (-v | --version)
Options:
--interval=<seconds> Interval between loops, in seconds [default: 1].
-h --help Show this screen.
-v --version Show version.
";
#[derive(Debug, RustcDecodable)]
struct Args {
flag_version: bool,
flag_interval: u64,
arg_includes: Vec<String>
}
#[derive(Debug, RustcDecodable, Clone, PartialEq, PartialOrd)]
struct Inode {
ino: ino_t,
dev: dev_t,
rdev: dev_t
}
#[derive(Debug, RustcDecodable, Clone, PartialEq, PartialOrd)]
struct Stat {
inode: Inode,
size: off_t,
mtime: time_t
}
fn stat(file: &String) -> Option<Stat> {
match fs::metadata(file) {
Ok(md) => {
if md.is_dir() { return None }
let inode = Inode {
ino: md.ino(),
dev: md.dev(),
rdev: md.rdev()
};
let stat = Stat {
inode: inode,
size: md.size(),
mtime: md.mtime()
};
Some(stat)
},
Err(_) => None
}
}
fn was_replaced(old: &Option<&Stat>, new: &Option<Stat>) -> bool {
if let &Some(ref new_stat) = new {
if let &Some(old_stat) = old {
return new_stat.inode != old_stat.inode
}
}
false
}
fn was_appended(old: &Option<&Stat>, new: &Option<Stat>) -> bool {
if let &Some(ref new_stat) = new {
if let &Some(old_stat) = old {
return new_stat.size > old_stat.size
}
return new_stat.size > 0
}
false
}
fn was_truncated(old: &Option<&Stat>, new: &Option<Stat>) -> bool {
if let &Some(ref new_stat) = new {
if let &Some(old_stat) = old {
return new_stat.size < old_stat.size
}
}
false
}
fn watch(includes: &Vec<String>, known: &mut HashSet<String>, created: &mut HashSet<String>, deleted: &mut HashSet<String>) {
let mut now_known: HashSet<String> = HashSet::new();
for include in includes.iter() {
if let Ok(entries) = glob(include) {
for entry in entries {
if let Ok(path) = entry {
if let Ok(full_path) = fs::canonicalize(path) {
now_known.insert(full_path.to_string_lossy().into_owned());
}
}
}
}
}
*created = now_known.difference(known).cloned().collect();
*deleted = known.difference(&now_known).cloned().collect();
*known = now_known;
}
fn report(known: &HashSet<String>, created: &HashSet<String>, deleted: &HashSet<String>, stats: &mut HashMap<String,Stat>) {
for delete in deleted.iter() {
println!("deleted:{:?} stat:{:?}", delete, stats.get(delete));
}
for create in created.iter() {
println!("created:{:?} stat:{:?}", create, stats.get(create));
}
let old_stats = stats.clone();
stats.clear();
for file in known.iter() {
let old_stat = old_stats.get(file);
let new_stat = stat(file);
if was_appended(&old_stat, &new_stat) {
println!("appended:{:?} stat:{:?}", file, new_stat);
} else if was_replaced(&old_stat, &new_stat) {
println!("replaced:{:?} stat:{:?}", file, new_stat);
} else if was_truncated(&old_stat, &new_stat) {
println!("truncated:{:?} stat:{:?}", file, new_stat);
}
if let Some(actual_stat) = new_stat {
stats.insert(file.clone(), actual_stat);
}
}
}
fn tail(interval: u64, includes: &Vec<String>) {
let delay = Duration::new(interval,0);
let mut known = HashSet::new();
let mut created = HashSet::new();
let mut deleted = HashSet::new();
let mut stats = HashMap::new();
loop {
watch(includes, &mut known, &mut created, &mut deleted);
report(&known, &created, &deleted, &mut stats);
sleep(delay);
}
}
fn main() {
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.decode())
.unwrap_or_else(|e| e.exit());
if args.flag_version {
println!("{}", VERSION);
} else {
let interval = if args.flag_interval < 1 {
println!("WARNING: Invalid <interval> provided, falling back to default...");
1
} else { args.flag_interval };
tail(interval, &args.arg_includes);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment