use std::collections::HashMap;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
pub trait ParkStore: Send + Sync + 'static {
fn save(&self, key: u64, snapshot: &[u8]) -> io::Result<()>;
fn take(&self, key: u64) -> io::Result<Vec<u8>>;
fn remove(&self, key: u64);
}
#[derive(Debug, Default)]
pub struct MemoryStore {
snapshots: Mutex<HashMap<u64, Vec<u8>>>,
}
impl MemoryStore {
pub fn new() -> Self {
Self::default()
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<u64, Vec<u8>>> {
self.snapshots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn len_bytes(&self) -> usize {
self.lock().values().map(Vec::len).sum()
}
}
impl ParkStore for MemoryStore {
fn save(&self, key: u64, snapshot: &[u8]) -> io::Result<()> {
self.lock().insert(key, snapshot.to_vec());
Ok(())
}
fn take(&self, key: u64) -> io::Result<Vec<u8>> {
self.lock()
.remove(&key)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no parked snapshot"))
}
fn remove(&self, key: u64) {
self.lock().remove(&key);
}
}
#[derive(Debug, Clone)]
pub struct DirStore {
dir: PathBuf,
}
impl DirStore {
pub fn new(dir: impl Into<PathBuf>) -> io::Result<Self> {
let dir = dir.into();
std::fs::create_dir_all(&dir)?;
Ok(Self { dir })
}
pub fn dir(&self) -> &Path {
&self.dir
}
fn path(&self, key: u64) -> PathBuf {
self.dir
.join(format!("terminal-{}-{key}.snapshot", std::process::id()))
}
}
impl ParkStore for DirStore {
fn save(&self, key: u64, snapshot: &[u8]) -> io::Result<()> {
use std::io::Write as _;
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.mode(0o600);
}
let mut file = options.open(self.path(key))?;
file.write_all(snapshot)?;
file.sync_data()
}
fn take(&self, key: u64) -> io::Result<Vec<u8>> {
let path = self.path(key);
let snapshot = std::fs::read(&path)?;
let _ = std::fs::remove_file(path);
Ok(snapshot)
}
fn remove(&self, key: u64) {
let _ = std::fs::remove_file(self.path(key));
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct ParkOptions {
pub(crate) enabled: bool,
pub(crate) idle: Duration,
pub(crate) store: Arc<dyn ParkStore>,
}
impl Default for ParkOptions {
fn default() -> Self {
Self {
enabled: true,
idle: Duration::from_secs(60),
store: Arc::new(MemoryStore::default()),
}
}
}
impl ParkOptions {
#[must_use]
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub fn with_idle(mut self, idle: Duration) -> Self {
self.idle = idle;
self
}
#[must_use]
pub fn with_store(mut self, store: impl ParkStore) -> Self {
self.store = Arc::new(store);
self
}
#[must_use]
pub fn with_shared_store(mut self, store: Arc<dyn ParkStore>) -> Self {
self.store = store;
self
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn idle(&self) -> Duration {
self.idle
}
pub fn store(&self) -> &Arc<dyn ParkStore> {
&self.store
}
}
impl fmt::Debug for ParkOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParkOptions")
.field("enabled", &self.enabled)
.field("idle", &self.idle)
.finish_non_exhaustive()
}
}
impl PartialEq for ParkOptions {
fn eq(&self, other: &Self) -> bool {
self.enabled == other.enabled
&& self.idle == other.idle
&& Arc::ptr_eq(&self.store, &other.store)
}
}
impl Eq for ParkOptions {}
pub(crate) fn next_key() -> u64 {
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_memory_store_keeps_and_forgets_snapshots() {
let store = MemoryStore::new();
store.save(1, b"screen").unwrap();
assert_eq!(store.len_bytes(), 6);
assert_eq!(store.take(1).unwrap(), b"screen");
assert!(store.take(1).is_err(), "taken once");
store.save(2, b"x").unwrap();
store.remove(2);
assert_eq!(store.len_bytes(), 0);
}
#[test]
fn a_dir_store_writes_private_files_and_removes_them() {
let dir = std::env::temp_dir().join(format!("gpui-cn-park-{}", std::process::id()));
let store = DirStore::new(&dir).unwrap();
store.save(7, b"scrollback").unwrap();
let path = store.path(7);
assert!(path.exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600, "only the user can read it");
}
assert_eq!(store.take(7).unwrap(), b"scrollback");
assert!(!path.exists(), "a restore removes the file");
store.save(8, b"x").unwrap();
store.remove(8);
assert!(!store.path(8).exists(), "a close removes the file");
let _ = std::fs::remove_dir_all(dir);
}
}