mirror of
https://github.com/Llewellynvdm/starship.git
synced 2024-05-28 06:00:54 +00:00
318 lines
9.3 KiB
Rust
318 lines
9.3 KiB
Rust
//! The on-disk caching functionality for Starship.
|
|
//!
|
|
//! This module contains the caching mechanism allowing Starship to reuse the
|
|
//! output of previously run commands when possible.
|
|
//!
|
|
//! The cache stores the output of commands, and the metadata of the binaries
|
|
//! being called at the time the command is run. When the binary's metadata
|
|
//! changes, the cache clears all the values of the commands calling that binary.
|
|
//!
|
|
//! The goals of this library are to be quick to cache outputs, quick to retreive
|
|
//! cached values, compatible with version-managed tools, and easy to troubleshoot.
|
|
|
|
pub mod errors;
|
|
|
|
pub use errors::Error;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
collections::HashMap,
|
|
convert::TryFrom,
|
|
fs::{self, OpenOptions},
|
|
io::Read,
|
|
path::{Path, PathBuf},
|
|
time::UNIX_EPOCH,
|
|
};
|
|
|
|
type FullCommand = String;
|
|
type BinaryPath = PathBuf;
|
|
|
|
const CURRENT_VERSION: u8 = 1;
|
|
|
|
/// An instance of the binary output cache
|
|
pub struct Cache {
|
|
/// The path of the cache file the cache serializes to
|
|
path: PathBuf,
|
|
/// Whether the cache has been changed and requires writing to disk
|
|
changed: bool,
|
|
/// The cache's internal state
|
|
contents: CacheContents,
|
|
}
|
|
|
|
impl Cache {
|
|
/// Create or parse a cache file at the given path
|
|
pub fn create_or_parse<P: AsRef<Path>>(cache_dir: P) -> Result<Self, Error> {
|
|
let cache_dir = cache_dir.as_ref();
|
|
fs::create_dir_all(&cache_dir)
|
|
.map_err(Error::CreateCacheDir)?;
|
|
|
|
let cache_file = cache_dir.join("bin-cache");
|
|
let mut file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.open(&cache_file)
|
|
.map_err(Error::OpenFile)?;
|
|
let mut contents = String::new();
|
|
|
|
// Clear the cache if it is not valid UTF-8
|
|
file.read_to_string(&mut contents).unwrap_or_default();
|
|
|
|
// Clear the cache if it unable to be parsed
|
|
let mut cache: CacheContents = toml::from_str(&contents).unwrap_or_default();
|
|
|
|
// Clear the cache if it is not using the current version
|
|
if cache.version != CURRENT_VERSION {
|
|
cache = CacheContents::default();
|
|
}
|
|
|
|
Ok(Self {
|
|
path: cache_file,
|
|
changed: false,
|
|
contents: cache,
|
|
})
|
|
}
|
|
|
|
/// Get the output of the given command if it has been previously cached
|
|
pub fn get(&mut self, binary_path: &Path, command: &str) -> Option<&CachedOutput> {
|
|
let bin = self.contents.binaries.get(binary_path)?;
|
|
|
|
let current_metadata = BinaryMetadata::try_from(binary_path).ok()?;
|
|
let is_stale = current_metadata != bin.metadata;
|
|
if is_stale {
|
|
return None;
|
|
};
|
|
|
|
bin.commands.get(command)
|
|
}
|
|
|
|
/// Set the cached output of the given command
|
|
pub fn set<O: Into<CachedOutput>>(&mut self, binary_path: &Path, command: &str, output: O) {
|
|
let current_metadata = match BinaryMetadata::try_from(binary_path) {
|
|
Ok(metadata) => metadata,
|
|
// Skip caching if unable to read binary metadata
|
|
Err(_e) => return,
|
|
};
|
|
let mut bin = self
|
|
.contents
|
|
.binaries
|
|
.entry(binary_path.to_path_buf())
|
|
.or_insert(BinaryCache {
|
|
metadata: current_metadata.clone(),
|
|
commands: HashMap::new(),
|
|
});
|
|
|
|
let is_stale = current_metadata != bin.metadata;
|
|
if is_stale {
|
|
bin.metadata = current_metadata;
|
|
bin.commands.clear();
|
|
};
|
|
|
|
bin.commands.insert(command.to_owned(), output.into());
|
|
self.changed = true;
|
|
}
|
|
|
|
/// Write any cache updates to disk
|
|
pub fn write(&self) -> Result<(), Error> {
|
|
if !self.changed {
|
|
return Ok(());
|
|
};
|
|
|
|
let contents = toml::to_string(&self.contents).map_err(Error::SerializeCache)?;
|
|
fs::write(&self.path, contents).map_err(Error::WriteFile)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct CacheContents {
|
|
/// The version of the cache file
|
|
version: u8,
|
|
/// A mapping of binaries' paths and their caches
|
|
binaries: HashMap<BinaryPath, BinaryCache>,
|
|
}
|
|
|
|
impl Default for CacheContents {
|
|
fn default() -> Self {
|
|
Self {
|
|
version: CURRENT_VERSION,
|
|
binaries: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct BinaryCache {
|
|
/// The metadata of the binary at the time it was last called
|
|
/// If the binary's metadata changes, its cached data is cleared
|
|
metadata: BinaryMetadata,
|
|
/// A mapping of commands and their cached outputs
|
|
commands: HashMap<FullCommand, CachedOutput>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
|
|
pub struct CachedOutput {
|
|
pub stdout: String,
|
|
pub stderr: String,
|
|
pub status: Option<i32>,
|
|
}
|
|
|
|
impl CachedOutput {
|
|
pub fn success(&self) -> bool {
|
|
self.status == Some(0)
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
|
struct BinaryMetadata {
|
|
size: u64,
|
|
is_dir: bool,
|
|
is_file: bool,
|
|
readonly: bool,
|
|
c_time: u64,
|
|
m_time: u64,
|
|
}
|
|
|
|
impl TryFrom<&Path> for BinaryMetadata {
|
|
type Error = crate::Error;
|
|
|
|
fn try_from(path: &Path) -> Result<Self, Error> {
|
|
let metadata = fs::metadata(path).map_err(Error::ReadMetadata)?;
|
|
|
|
// If ctime or mtime are not provided, store `0` in their place
|
|
let c_time = match metadata.created() {
|
|
Err(_e) => 0,
|
|
Ok(t) => t
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|t| t.as_secs())
|
|
.unwrap_or(0),
|
|
};
|
|
|
|
let m_time = match metadata.modified() {
|
|
Err(_e) => 0,
|
|
Ok(t) => t
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|t| t.as_secs())
|
|
.unwrap_or(0),
|
|
};
|
|
|
|
Ok(Self {
|
|
size: metadata.len(),
|
|
is_dir: metadata.is_dir(),
|
|
is_file: metadata.is_file(),
|
|
readonly: metadata.permissions().readonly(),
|
|
c_time,
|
|
m_time,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::{fs::File, io::Write};
|
|
use tempfile::tempdir;
|
|
|
|
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
|
|
|
|
// Convenience
|
|
impl From<&str> for CachedOutput {
|
|
fn from(stdout: &str) -> Self {
|
|
Self {
|
|
stdout: stdout.to_owned(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_cache_file_is_created() -> Result {
|
|
let dir = tempdir()?;
|
|
let cache_path = Path::join(dir.path(), "bin-cache");
|
|
let cache = Cache::create_or_parse(&cache_path)?;
|
|
cache.write()?;
|
|
|
|
assert!(Path::exists(&cache_path));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn retreive_from_populated_cache() -> Result {
|
|
let dir = tempdir()?;
|
|
let cache_path = dir.path().join("bin-cache");
|
|
let mut cache = Cache::create_or_parse(&cache_path)?;
|
|
|
|
// Create "node" binary
|
|
let bin_path = dir.path().join("node");
|
|
File::create(&bin_path)?;
|
|
|
|
// Populate cache with "node" output
|
|
let version = "v14.16.0";
|
|
cache.set(&bin_path, "node --version", version);
|
|
cache.write()?;
|
|
|
|
// Retreive cached output
|
|
let mut new_cache = Cache::create_or_parse(&cache_path)?;
|
|
let actual = new_cache.get(&bin_path, "node --version").unwrap();
|
|
|
|
let expected = CachedOutput::from(version);
|
|
assert_eq!(&expected, actual);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn overrites_stale_cache() -> Result {
|
|
let dir = tempdir()?;
|
|
let cache_path = dir.path().join("bin-cache");
|
|
let mut cache = Cache::create_or_parse(&cache_path)?;
|
|
|
|
// Create "node" binary
|
|
let bin_path = dir.path().join("node");
|
|
File::create(&bin_path)?;
|
|
|
|
// Populate cache with "node" output
|
|
let expected = "v14.16.0";
|
|
cache.set(&bin_path, "node -v", expected);
|
|
cache.set(&bin_path, "node --help", expected);
|
|
cache.set(&bin_path, "node --version", expected);
|
|
cache.write()?;
|
|
|
|
// Update "node" binary
|
|
File::create(&bin_path)?.write(b"updated")?;
|
|
|
|
// Retreive cached output
|
|
let mut new_cache = Cache::create_or_parse(&cache_path)?;
|
|
|
|
// Set a cached value again
|
|
new_cache.set(&bin_path, "node -v", "v15.0.0");
|
|
|
|
// The other, previously cached values, should be cleared as stale
|
|
assert_eq!(new_cache.get(&bin_path, "node --version"), None);
|
|
assert_eq!(new_cache.get(&bin_path, "node --help"), None);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn doesnt_retreive_stale_cache() -> Result {
|
|
let dir = tempdir()?;
|
|
let cache_path = dir.path().join("bin-cache");
|
|
let mut cache = Cache::create_or_parse(&cache_path)?;
|
|
|
|
// Create "node" binary
|
|
let bin_path = dir.path().join("node");
|
|
File::create(&bin_path)?;
|
|
|
|
// Populate cache with "node" output
|
|
cache.set(&bin_path, "node --version", "v14.16.0");
|
|
cache.write()?;
|
|
|
|
// Update "node" binary
|
|
File::create(&bin_path)?.write(b"updated")?;
|
|
|
|
let mut new_cache = Cache::create_or_parse(&cache_path)?;
|
|
let actual = new_cache.get(&bin_path, "node --version");
|
|
|
|
assert_eq!(None, actual);
|
|
Ok(())
|
|
}
|
|
}
|