Skip to content

Commit

Permalink
Added --clear-cache flag and automatic clean-up.
Browse files Browse the repository at this point in the history
After running a script, it will now go through the cache and kill
everything at least 1 week old.

--clear-cache, if given, will cause it to go through and kill *everything*
before attempting to load the input.

This also expands the surface area of platform-specific code.
See issue #1.
  • Loading branch information
DanielKeep committed May 31, 2015
1 parent 211f459 commit 4442bef
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 6 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ flate2 = "0.2.7"
log = "0.3.1"
rustc-serialize = "0.3.14"
shaman = "0.1.0"
time = "0.1.25"
toml = "0.1.20"

[dependencies.clap]
Expand Down
9 changes: 9 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,12 @@ pub const DEFLATE_PATH_LEN_MAX: usize = 20;
When generating a package's unique ID, how many hex nibbles of the script digest should be used *at most*?
*/
pub const CONTENT_DIGEST_LEN_MAX: usize = 20;

/**
How old can stuff in the cache be before we automatically clear it out?
Measured in milliseconds.
*/
// It's been *one week* since you looked at me,
// cocked your head to the side and said "I'm angry."
pub const MAX_CACHE_AGE_MS: u64 = 1*7*24*60*60*1000;
99 changes: 94 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ As such, `cargo-script` does two major things:
2. It caches the generated and compiled packages, regenerating them only if the script or its metadata have changed.
*/
#![allow(deprecated)] // for file metadata
#![feature(collections)]
#![feature(fs_time)]
#![feature(metadata_ext)]
#![feature(path_ext)]

extern crate clap;
Expand Down Expand Up @@ -53,6 +52,7 @@ struct Args {
count: bool,

build_only: bool,
clear_cache: bool,
debug: bool,
dep: Vec<String>,
force: bool,
Expand Down Expand Up @@ -104,6 +104,10 @@ fn parse_args() -> Args {
.long("loop")
.conflicts_with_all(csas!["expr"])
)
.arg(Arg::with_name("clear_cache")
.help("Clears out the script cache.")
.long("clear-cache")
)
.arg(Arg::with_name("count")
.help("Invoke the loop closure with two arguments: line, and line number.")
.long("count")
Expand Down Expand Up @@ -142,6 +146,7 @@ fn parse_args() -> Args {
count: m.is_present("count"),

build_only: m.is_present("build_only"),
clear_cache: m.is_present("clear_cache"),
debug: m.is_present("debug"),
dep: m.values_of("dep").unwrap_or(vec![]).into_iter()
.map(Into::into).collect(),
Expand Down Expand Up @@ -173,6 +178,16 @@ fn try_main() -> Result<i32> {
let args = parse_args();
info!("Arguments: {:?}", args);

/*
If we've been asked to clear the cache, do that *now*. There are two reasons:
1. Do it *before* we call `cache_action_for` such that this flag *also* acts as a synonym for `--force`.
2. Do it *before* we start trying to read the input so that, later on, we can make `<script>` optional, but still supply `--clear-cache`.
*/
if args.clear_cache {
try!(clean_cache(0));
}

// Take the arguments and work out what our input is going to be. Primarily, this gives us the content, a user-friendly name, and a cache-friendly ID.
// These three are just storage for the borrows we'll actually use.
let script_name: String;
Expand All @@ -190,7 +205,7 @@ fn try_main() -> Result<i32> {
let mut body = String::new();
try!(file.read_to_string(&mut body));

let mtime = file.metadata().map(|md| md.modified()).unwrap_or(0);
let mtime = platform::file_last_modified(&file);

script_path = try!(std::env::current_dir()).join(path);
content = body;
Expand Down Expand Up @@ -277,6 +292,18 @@ fn try_main() -> Result<i32> {
try!(compile(&input, &meta, &pkg_path));
}

// Once we're done, clean out old packages from the cache. There's no point if we've already done a full clear, though.
let _defer_clear = {
// To get around partially moved args problems.
let cc = args.clear_cache;
util::Defer::<_, MainError>::defer(move || {
if !cc {
try!(clean_cache(consts::MAX_CACHE_AGE_MS));
}
Ok(())
})
};

if args.build_only {
return Ok(0);
}
Expand All @@ -288,6 +315,60 @@ fn try_main() -> Result<i32> {
.map(|st| st.code().unwrap_or(1))))
}

/**
Clean up the cache folder.
Looks for all folders whose metadata says they were created at least `max_age` in the past and kills them dead.
*/
fn clean_cache(max_age: u64) -> Result<()> {
info!("cleaning cache with max_age: {:?}", max_age);

let cutoff = platform::current_time() - max_age;
info!("cutoff: {:>20?} ms", cutoff);

let cache_dir = try!(get_cache_path());
for child in try!(fs::read_dir(cache_dir)) {
let child = try!(child);
let path = child.path();
if path.is_file() { continue }

info!("checking: {:?}", path);

let remove_dir = || {
/*
Ok, so *why* aren't we using `modified in the package metadata? The point of *that* is to track what we know about the input. The problem here is that `--expr` and `--loop` don't *have* modification times; they just *are*.
Now, `PackageMetadata` *could* be modified to store, say, the moment in time the input was compiled, but then we couldn't use that field for metadata matching when decided whether or not a *file* input should be recompiled.
So, instead, we're just going to go by the timestamp on the metadata file *itself*.
*/
let meta_mtime = {
let meta_path = get_pkg_metadata_path(&path);
let meta_file = match fs::File::open(&meta_path) {
Ok(file) => file,
Err(..) => {
info!("couldn't open metadata for {:?}", path);
return true
}
};
platform::file_last_modified(&meta_file)
};
info!("meta_mtime: {:>20?} ms", meta_mtime);

(meta_mtime <= cutoff)
};

if remove_dir() {
info!("removing {:?}", path);
if let Err(err) = fs::remove_dir_all(&path) {
error!("failed to remove {:?} from cache: {}", path, err);
}
}
}
info!("done cleaning cache.");
Ok(())
}

/**
Compile a package from the input.
Expand Down Expand Up @@ -652,7 +733,7 @@ Load the package metadata, given the path to the package's cache folder.
*/
fn get_pkg_metadata<P>(pkg_path: P) -> Result<PackageMetadata>
where P: AsRef<Path> {
let meta_path = pkg_path.as_ref().join(consts::METADATA_FILE);
let meta_path = get_pkg_metadata_path(pkg_path);
debug!("meta_path: {:?}", meta_path);
let mut meta_file = try!(fs::File::open(&meta_path));

Expand All @@ -667,12 +748,20 @@ where P: AsRef<Path> {
Ok(meta)
}

/**
Work out the path to a package's metadata file.
*/
fn get_pkg_metadata_path<P>(pkg_path: P) -> PathBuf
where P: AsRef<Path> {
pkg_path.as_ref().join(consts::METADATA_FILE)
}

/**
Save the package metadata, given the path to the package's cache folder.
*/
fn write_pkg_metadata<P>(pkg_path: P, meta: &PackageMetadata) -> Result<()>
where P: AsRef<Path> {
let meta_path = pkg_path.as_ref().join(consts::METADATA_FILE);
let meta_path = get_pkg_metadata_path(pkg_path);
debug!("meta_path: {:?}", meta_path);
let mut meta_file = try!(fs::File::create(&meta_path));
let meta_str = try!(rustc_serialize::json::encode(meta)
Expand Down
40 changes: 39 additions & 1 deletion src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This module is for platform-specific stuff.
*/

pub use self::inner::get_cache_dir_for;
pub use self::inner::{current_time, file_last_modified, get_cache_dir_for};

#[cfg(windows)]
pub mod inner {
Expand All @@ -15,12 +15,50 @@ pub mod inner {

use std::ffi::OsString;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::mem;
use std::os::windows::ffi::OsStringExt;
use self::uuid::FOLDERID_LocalAppData;
use error::MainError;

/**
Gets the last-modified time of a file, in milliseconds since the UNIX epoch.
*/
pub fn file_last_modified(file: &fs::File) -> u64 {
use ::std::os::windows::fs::MetadataExt;

const MS_BETWEEN_1601_1970: u64 = 11_644_473_600_000;

let mtime_100ns_1601_utc = file.metadata()
.map(|md| md.last_write_time())
.unwrap_or(0);
let mtime_ms_1601_utc = mtime_100ns_1601_utc / (1000*10);

// This can obviously underflow... but since files created prior to 1970 are going to be *somewhat rare*, I'm just going to saturate to zero.
let mtime_ms_1970_utc = mtime_ms_1601_utc.saturating_sub(MS_BETWEEN_1601_1970);
mtime_ms_1970_utc
}

/**
Gets the current system time, in milliseconds since the UNIX epoch.
*/
pub fn current_time() -> u64 {
extern crate time;

/*
This is kinda dicey, since *ideally* both this function and `file_last_modified` would be using the same underlying APIs. They are not, insofar as I know.
*/
let mut now_1970_utc = time::now_utc().to_timespec();
if now_1970_utc.sec < 0 || now_1970_utc.nsec < 0 {
// Fuck it.
now_1970_utc = time::Timespec::new(0, 0);
}
let now_ms_1970_utc = (now_1970_utc.sec as u64 * 1000)
+ (now_1970_utc.nsec as u64 / 1_000_000);
now_ms_1970_utc
}

/**
Get a directory suitable for storing user- and machine-specific data which may or may not be persisted across sessions.
Expand Down

0 comments on commit 4442bef

Please sign in to comment.