diff --git a/docs/tasks.md b/docs/tasks.md index e3a0f78..22120a9 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -40,7 +40,7 @@ 3. [x] allow multi vibebox to connect to the same vm. 4. [x] use vm.lock to ensure process concurrency safety. 5. [ ] wire up SessionManager. -6. [ ] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself). +6. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself). 7. [ ] setup vibebox commands 8. [ ] setup cli commands. 9. [ ] fix ui overlap. @@ -53,8 +53,8 @@ ## Stage 2 -1. [ ] Redirect vm output to log. -2. [ ] Redirect vm output to vibebox starting it. -3. [ ] use anyhow to sync api. - -[ ] +1. [ ] retouch the cli ux. +2. [ ] refactor the code. +3. [ ] Redirect vm output to log. +4. [ ] Redirect vm output to vibebox starting it. +5. [ ] use anyhow to sync api. diff --git a/src/bin/vibebox-cli.rs b/src/bin/vibebox-cli.rs index e68dd38..312c667 100644 --- a/src/bin/vibebox-cli.rs +++ b/src/bin/vibebox-cli.rs @@ -1,26 +1,18 @@ use std::{ env, ffi::OsString, - fs, io::{self, IsTerminal, Write}, - path::Path, sync::{Arc, Mutex}, }; use color_eyre::Result; -use serde::Deserialize; use tracing_subscriber::EnvFilter; use vibebox::tui::{AppState, VmInfo}; -use vibebox::{instance, tui, vm, vm_manager}; +use vibebox::{SessionManager, config, instance, tui, vm, vm_manager}; const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000; -#[derive(Debug, Default, Deserialize)] -struct ProjectConfig { - auto_shutdown_ms: Option, -} - fn main() -> Result<()> { init_tracing(); color_eyre::install()?; @@ -62,6 +54,13 @@ fn main() -> Result<()> { }; let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; tracing::info!(cwd = %cwd.display(), "starting vibebox cli"); + if let Ok(manager) = SessionManager::new() { + if let Err(err) = manager.update_global_sessions(&cwd) { + tracing::warn!(error = %err, "failed to update global session list"); + } + } else { + tracing::warn!("failed to initialize session manager"); + } let app = Arc::new(Mutex::new(AppState::new(cwd.clone(), vm_info))); { @@ -74,7 +73,10 @@ fn main() -> Result<()> { stdout.flush()?; } - let auto_shutdown_ms = load_auto_shutdown_ms(&cwd)?; + let auto_shutdown_ms = config::load_config(&cwd) + .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))? + .auto_shutdown_ms + .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS); tracing::info!(auto_shutdown_ms, "auto shutdown config"); let manager_conn = vm_manager::ensure_manager(&raw_args, auto_shutdown_ms) .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; @@ -84,16 +86,6 @@ fn main() -> Result<()> { Ok(()) } -fn load_auto_shutdown_ms(project_root: &Path) -> Result { - let path = project_root.join("vibebox.toml"); - let config = match fs::read_to_string(&path) { - Ok(raw) => toml::from_str::(&raw)?, - Err(err) if err.kind() == io::ErrorKind::NotFound => ProjectConfig::default(), - Err(err) => return Err(err.into()), - }; - Ok(config.auto_shutdown_ms.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS)) -} - fn init_tracing() { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err(); diff --git a/src/bin/vibebox-supervisor.rs b/src/bin/vibebox-supervisor.rs index acc6401..1665315 100644 --- a/src/bin/vibebox-supervisor.rs +++ b/src/bin/vibebox-supervisor.rs @@ -6,7 +6,7 @@ use std::{ use color_eyre::Result; use tracing_subscriber::EnvFilter; -use vibebox::{vm, vm_manager}; +use vibebox::{instance, vm, vm_manager}; const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000; @@ -15,6 +15,10 @@ fn main() -> Result<()> { color_eyre::install()?; tracing::info!("starting vm supervisor"); + let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; + let instance_dir = instance::ensure_instance_dir(&cwd) + .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; + let _ = instance::touch_last_active(&instance_dir); let args = vm::parse_cli().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS") .ok() @@ -22,7 +26,9 @@ fn main() -> Result<()> { .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS); tracing::info!(auto_shutdown_ms, "vm supervisor config"); - if let Err(err) = vm_manager::run_manager(args, auto_shutdown_ms) { + let result = vm_manager::run_manager(args, auto_shutdown_ms); + let _ = instance::touch_last_active(&instance_dir); + if let Err(err) = result { tracing::error!(error = %err, "vm supervisor exited"); return Err(color_eyre::eyre::eyre!(err.to_string())); } diff --git a/src/instance.rs b/src/instance.rs index 7f3e062..b8956d5 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,3 +1,4 @@ +use crate::session_manager::INSTANCE_TOML_FILENAME; use std::{ env, fs, io::{self, Write}, @@ -15,6 +16,7 @@ use std::{ use serde::{Deserialize, Serialize}; use std::sync::mpsc::Sender; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use uuid::Uuid; use crate::{ @@ -23,7 +25,6 @@ use crate::{ vm::{self, LoginAction, VmInput}, }; -const INSTANCE_TOML: &str = "instance.toml"; const SSH_KEY_NAME: &str = "ssh_key"; #[allow(dead_code)] const SERIAL_LOG_NAME: &str = "serial.log"; @@ -34,11 +35,15 @@ const SSH_SETUP_SCRIPT: &str = include_str!("ssh.sh"); #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct InstanceConfig { + #[serde(default)] + id: String, #[serde(default = "default_ssh_user")] ssh_user: String, #[serde(default)] sudo_password: String, #[serde(default)] + last_active: Option, + #[serde(default)] pub(crate) vm_ipv4: Option, } @@ -69,7 +74,7 @@ pub fn run_with_ssh(manager_conn: UnixStream) -> Result<(), Box Result { +pub fn ensure_instance_dir(project_root: &Path) -> Result { let instance_dir = project_root.join(INSTANCE_DIR_NAME); fs::create_dir_all(&instance_dir)?; Ok(instance_dir) @@ -121,14 +126,16 @@ pub(crate) fn ensure_ssh_keypair( pub(crate) fn load_or_create_instance_config( instance_dir: &Path, ) -> Result> { - let config_path = instance_dir.join(INSTANCE_TOML); + let config_path = instance_dir.join(INSTANCE_TOML_FILENAME); let mut config = if config_path.exists() { let raw = fs::read_to_string(&config_path)?; toml::from_str::(&raw)? } else { InstanceConfig { + id: String::new(), ssh_user: default_ssh_user(), sudo_password: String::new(), + last_active: None, vm_ipv4: None, } }; @@ -139,6 +146,11 @@ pub(crate) fn load_or_create_instance_config( changed = true; } + if config.id.trim().is_empty() { + config.id = Uuid::now_v7().to_string(); + changed = true; + } + if config.sudo_password.trim().is_empty() { config.sudo_password = generate_password(); changed = true; @@ -151,6 +163,14 @@ pub(crate) fn load_or_create_instance_config( Ok(config) } +pub fn touch_last_active(instance_dir: &Path) -> Result<(), Box> { + let mut config = load_or_create_instance_config(instance_dir)?; + let now = OffsetDateTime::now_utc().format(&Rfc3339)?; + config.last_active = Some(now); + write_instance_config(&instance_dir.join(INSTANCE_TOML_FILENAME), &config)?; + Ok(()) +} + pub(crate) fn write_instance_config( path: &Path, config: &InstanceConfig, @@ -357,7 +377,7 @@ fn spawn_ssh_io( let ssh_ready = Arc::new(AtomicBool::new(false)); let input_tx_holder: Arc>>> = Arc::new(Mutex::new(None)); - let instance_path = instance_dir.join(INSTANCE_TOML); + let instance_path = instance_dir.join(INSTANCE_TOML_FILENAME); let config_for_output = config.clone(); let log_for_output = log_file.clone(); let ssh_connected_for_output = ssh_connected.clone(); diff --git a/src/lib.rs b/src/lib.rs index e1eaad0..5bd766f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub mod vm; pub mod vm_manager; pub use session_manager::{SessionError, SessionManager, SessionRecord}; +pub mod config; diff --git a/src/session_manager.rs b/src/session_manager.rs index 45444a2..c32a1d8 100644 --- a/src/session_manager.rs +++ b/src/session_manager.rs @@ -5,35 +5,43 @@ use std::{ }; use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use uuid::Uuid; pub const INSTANCE_DIR_NAME: &str = ".vibebox"; +pub const GLOBAL_CACHE_DIR_NAME: &str = "vibebox"; pub const GLOBAL_DIR_NAME: &str = ".vibebox"; -pub const SESSION_INDEX_FILENAME: &str = "sessions.toml"; +pub const GLOBAL_SESSION_FILENAME: &str = "session.toml"; pub const SESSION_TEMP_PREFIX: &str = "sessions"; pub const SESSION_TOML_SUFFIX: &str = ".toml"; +use crate::config::CONFIG_FILENAME; +pub const INSTANCE_TOML_FILENAME: &str = "instance.toml"; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SessionRecord { - pub id: Uuid, pub directory: PathBuf, - #[serde(with = "time::serde::rfc3339")] - pub last_active: OffsetDateTime, #[serde(default)] - pub ref_count: u64, + pub id: Option, + #[serde(default)] + pub last_active: Option, } #[derive(Debug, Default, Serialize, Deserialize)] -struct SessionIndex { +struct GlobalSessionIndex { #[serde(default)] - sessions: Vec, + directories: Vec, +} + +#[derive(Debug, Default, Deserialize)] +struct InstanceMetadata { + #[serde(default)] + id: Option, + #[serde(default)] + last_active: Option, } #[derive(Debug)] pub struct SessionManager { global_dir: PathBuf, - index_path: PathBuf, + session_path: PathBuf, } #[derive(Debug, thiserror::Error)] @@ -44,12 +52,6 @@ pub enum SessionError { NonAbsoluteDirectory(PathBuf), #[error("Session directory does not exist: {0}")] MissingDirectory(PathBuf), - #[error("Session already exists for directory: {0}")] - DirectoryAlreadyHasSession(PathBuf), - #[error("Session not found: {0}")] - SessionNotFound(Uuid), - #[error("Ref count underflow for session: {0}")] - RefCountUnderflow(Uuid), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] @@ -67,163 +69,55 @@ impl SessionManager { } pub fn with_global_dir(global_dir: PathBuf) -> Self { - let index_path = global_dir.join(SESSION_INDEX_FILENAME); + let session_path = global_dir.join(GLOBAL_SESSION_FILENAME); Self { global_dir, - index_path, + session_path, } } pub fn index_path(&self) -> &Path { - &self.index_path + &self.session_path } - pub fn create_session(&self, directory: &Path) -> Result { + pub fn update_global_sessions(&self, directory: &Path) -> Result, SessionError> { let directory = self.normalize_directory(directory)?; - let mut index = self.read_index()?; + let mut index = self.read_global_index()?; + let mut changed = false; - if index.sessions.iter().any(|s| s.directory == directory) { - return Err(SessionError::DirectoryAlreadyHasSession(directory)); + let removed = prune_invalid_dirs(&mut index); + if removed > 0 { + changed = true; } - let now = OffsetDateTime::now_utc(); - let session = SessionRecord { - id: Uuid::now_v7(), - directory: directory.clone(), - last_active: now, - ref_count: 1, - }; - - fs::create_dir_all(self.instance_dir_for(&directory))?; - index.sessions.push(session.clone()); - self.write_index(&index)?; - - Ok(session) - } - - pub fn get_or_create_session(&self, directory: &Path) -> Result { - let directory = self.normalize_directory(directory)?; - let mut index = self.read_index()?; - - if let Some(pos) = index.sessions.iter().position(|s| s.directory == directory) { - if !self - .instance_dir_for(&index.sessions[pos].directory) - .is_dir() - { - index.sessions.remove(pos); - } else { - let now = OffsetDateTime::now_utc(); - let session = &mut index.sessions[pos]; - session.ref_count = session.ref_count.saturating_add(1); - session.last_active = now; - let updated = session.clone(); - self.write_index(&index)?; - return Ok(updated); - } + if is_vibebox_dir(&directory) && !index.directories.iter().any(|dir| dir == &directory) { + index.directories.push(directory); + changed = true; } - let now = OffsetDateTime::now_utc(); - let session = SessionRecord { - id: Uuid::now_v7(), - directory: directory.clone(), - last_active: now, - ref_count: 1, - }; + if changed { + self.write_global_index(&index)?; + } - fs::create_dir_all(self.instance_dir_for(&directory))?; - index.sessions.push(session.clone()); - self.write_index(&index)?; - - Ok(session) + Ok(index.directories) } pub fn list_sessions(&self) -> Result, SessionError> { - let mut index = self.read_index()?; - let removed = self.remove_orphans(&mut index); + let mut index = self.read_global_index()?; + let removed = prune_invalid_dirs(&mut index); if removed > 0 { - self.write_index(&index)?; + self.write_global_index(&index)?; } - Ok(index.sessions) - } - - pub fn delete_session(&self, id: Uuid) -> Result { - let mut index = self.read_index()?; - let Some(pos) = index.sessions.iter().position(|s| s.id == id) else { - return Ok(false); - }; - - let session = index.sessions.remove(pos); - let instance_dir = self.instance_dir_for(&session.directory); - match fs::remove_dir_all(&instance_dir) { - Ok(_) => {} - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), + let mut sessions = Vec::with_capacity(index.directories.len()); + for directory in index.directories { + let meta = read_instance_metadata(&directory)?; + sessions.push(SessionRecord { + directory, + id: meta.id, + last_active: meta.last_active, + }); } - - self.write_index(&index)?; - Ok(true) - } - - pub fn bump_last_active(&self, id: Uuid) -> Result { - let now = OffsetDateTime::now_utc(); - self.update_session(id, |session| { - session.last_active = now; - Ok(()) - }) - } - - pub fn increment_ref_count(&self, id: Uuid) -> Result { - let now = OffsetDateTime::now_utc(); - self.update_session(id, |session| { - session.ref_count = session.ref_count.saturating_add(1); - session.last_active = now; - Ok(()) - }) - } - - pub fn decrement_ref_count(&self, id: Uuid) -> Result { - let now = OffsetDateTime::now_utc(); - self.update_session(id, |session| { - if session.ref_count == 0 { - return Err(SessionError::RefCountUnderflow(id)); - } - session.ref_count -= 1; - session.last_active = now; - Ok(()) - }) - } - - pub fn cleanup_orphans(&self) -> Result { - let mut index = self.read_index()?; - let removed = self.remove_orphans(&mut index); - if removed > 0 { - self.write_index(&index)?; - } - Ok(removed) - } - - fn update_session(&self, id: Uuid, mut update: F) -> Result - where - F: FnMut(&mut SessionRecord) -> Result<(), SessionError>, - { - let mut index = self.read_index()?; - let Some(pos) = index.sessions.iter().position(|s| s.id == id) else { - return Err(SessionError::SessionNotFound(id)); - }; - - if !self - .instance_dir_for(&index.sessions[pos].directory) - .is_dir() - { - index.sessions.remove(pos); - self.write_index(&index)?; - return Err(SessionError::SessionNotFound(id)); - } - - update(&mut index.sessions[pos])?; - let updated = index.sessions[pos].clone(); - self.write_index(&index)?; - Ok(updated) + Ok(sessions) } fn normalize_directory(&self, directory: &Path) -> Result { @@ -236,34 +130,57 @@ impl SessionManager { Ok(directory.canonicalize()?) } - fn instance_dir_for(&self, directory: &Path) -> PathBuf { - directory.join(INSTANCE_DIR_NAME) - } - - fn remove_orphans(&self, index: &mut SessionIndex) -> usize { - let before = index.sessions.len(); - index - .sessions - .retain(|s| self.instance_dir_for(&s.directory).is_dir()); - before - index.sessions.len() - } - - fn read_index(&self) -> Result { - if !self.index_path.exists() { - return Ok(SessionIndex::default()); + fn read_global_index(&self) -> Result { + if !self.session_path.exists() { + return Ok(GlobalSessionIndex::default()); } - let content = fs::read_to_string(&self.index_path)?; + let content = fs::read_to_string(&self.session_path)?; Ok(toml::from_str(&content)?) } - fn write_index(&self, index: &SessionIndex) -> Result<(), SessionError> { + fn write_global_index(&self, index: &GlobalSessionIndex) -> Result<(), SessionError> { fs::create_dir_all(&self.global_dir)?; let content = toml::to_string_pretty(index)?; - atomic_write(&self.index_path, content.as_bytes())?; + atomic_write(&self.session_path, content.as_bytes())?; Ok(()) } } +fn is_vibebox_dir(directory: &Path) -> bool { + if !directory.is_absolute() { + return false; + } + directory.join(CONFIG_FILENAME).is_file() +} + +fn prune_invalid_dirs(index: &mut GlobalSessionIndex) -> usize { + let before = index.directories.len(); + index.directories.retain(|dir| is_vibebox_dir(dir)); + before - index.directories.len() +} + +fn read_instance_metadata(directory: &Path) -> Result { + let instance_path = directory + .join(INSTANCE_DIR_NAME) + .join(INSTANCE_TOML_FILENAME); + if !instance_path.exists() { + return Ok(InstanceMetadata::default()); + } + let raw = fs::read_to_string(&instance_path)?; + let mut meta: InstanceMetadata = toml::from_str(&raw)?; + if let Some(id) = &meta.id { + if id.trim().is_empty() { + meta.id = None; + } + } + if let Some(last_active) = &meta.last_active { + if last_active.trim().is_empty() { + meta.last_active = None; + } + } + Ok(meta) +} + fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> { let Some(parent) = path.parent() else { return Err(io::Error::new( @@ -288,7 +205,6 @@ mod tests { use super::*; use std::fs; use tempfile::TempDir; - use time::OffsetDateTime; fn manager(temp: &TempDir) -> SessionManager { SessionManager::with_global_dir(temp.path().join("global")) @@ -301,104 +217,17 @@ mod tests { } #[test] - fn create_session_writes_index_and_instance_dir() { + fn update_global_sessions_adds_directory() { let temp = TempDir::new().unwrap(); let mgr = manager(&temp); let project_dir = create_project_dir(&temp); + fs::write(project_dir.join(VIBEBOX_CONFIG_FILENAME), "").unwrap(); - let session = mgr.create_session(&project_dir).unwrap(); + let dirs = mgr.update_global_sessions(&project_dir).unwrap(); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0], project_dir.canonicalize().unwrap()); assert!(mgr.index_path().exists()); - assert_eq!(session.directory, project_dir.canonicalize().unwrap()); - assert_eq!(session.ref_count, 1); - assert!(project_dir.join(INSTANCE_DIR_NAME).is_dir()); - } - - #[test] - fn create_session_rejects_non_absolute_directory() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - - let err = mgr.create_session(Path::new("relative/path")).unwrap_err(); - - assert!(matches!(err, SessionError::NonAbsoluteDirectory(_))); - } - - #[test] - fn create_session_rejects_missing_directory() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let missing = temp.path().join("missing"); - - let err = mgr.create_session(&missing).unwrap_err(); - - assert!(matches!(err, SessionError::MissingDirectory(_))); - } - - #[test] - fn create_session_rejects_duplicate_directory() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let _session = mgr.create_session(&project_dir).unwrap(); - let err = mgr.create_session(&project_dir).unwrap_err(); - - assert!(matches!(err, SessionError::DirectoryAlreadyHasSession(_))); - } - - #[test] - fn get_or_create_increments_ref_count_for_existing_session() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let first = mgr.create_session(&project_dir).unwrap(); - let second = mgr.get_or_create_session(&project_dir).unwrap(); - - assert_eq!(first.id, second.id); - assert_eq!(second.ref_count, 2); - } - - #[test] - fn decrement_ref_count_errors_on_underflow() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let session = mgr.create_session(&project_dir).unwrap(); - let session = mgr.decrement_ref_count(session.id).unwrap(); - assert_eq!(session.ref_count, 0); - - let err = mgr.decrement_ref_count(session.id).unwrap_err(); - assert!(matches!(err, SessionError::RefCountUnderflow(_))); - } - - #[test] - fn list_sessions_cleans_orphans() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let _session = mgr.create_session(&project_dir).unwrap(); - fs::remove_dir_all(project_dir.join(INSTANCE_DIR_NAME)).unwrap(); - - let sessions = mgr.list_sessions().unwrap(); - assert!(sessions.is_empty()); - } - - #[test] - fn delete_session_removes_instance_dir_and_index() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let session = mgr.create_session(&project_dir).unwrap(); - let removed = mgr.delete_session(session.id).unwrap(); - - assert!(removed); - assert!(!project_dir.join(INSTANCE_DIR_NAME).exists()); - assert!(mgr.list_sessions().unwrap().is_empty()); } #[test] @@ -415,73 +244,45 @@ mod tests { } #[test] - fn bump_last_active_updates_timestamp() { + fn update_global_sessions_removes_missing_vibebox_toml() { let temp = TempDir::new().unwrap(); let mgr = manager(&temp); let project_dir = create_project_dir(&temp); + fs::write(project_dir.join(VIBEBOX_CONFIG_FILENAME), "").unwrap(); - let session = mgr.create_session(&project_dir).unwrap(); - let before = session.last_active; - std::thread::sleep(std::time::Duration::from_millis(5)); - let updated = mgr.bump_last_active(session.id).unwrap(); - let now = OffsetDateTime::now_utc(); + let _ = mgr.update_global_sessions(&project_dir).unwrap(); + fs::remove_file(project_dir.join(VIBEBOX_CONFIG_FILENAME)).unwrap(); - assert!(updated.last_active >= before); - assert!(updated.last_active <= now); + let dirs = mgr.update_global_sessions(&project_dir).unwrap(); + assert!(dirs.is_empty()); } #[test] - fn cleanup_orphans_returns_removed_count() { + fn list_sessions_reads_instance_metadata() { let temp = TempDir::new().unwrap(); let mgr = manager(&temp); let project_dir = create_project_dir(&temp); + fs::write(project_dir.join(VIBEBOX_CONFIG_FILENAME), "").unwrap(); - let _session = mgr.create_session(&project_dir).unwrap(); - fs::remove_dir_all(project_dir.join(INSTANCE_DIR_NAME)).unwrap(); + let instance_dir = project_dir.join(INSTANCE_DIR_NAME); + fs::create_dir_all(&instance_dir).unwrap(); + fs::write( + instance_dir.join(INSTANCE_TOML_FILENAME), + "id = \"019bf290-cccc-7c23-ba1d-dce7e6d40693\"\nlast_active = \"2026-02-07T05:00:00Z\"\n", + ) + .unwrap(); - let removed = mgr.cleanup_orphans().unwrap(); - assert_eq!(removed, 1); - assert!(mgr.list_sessions().unwrap().is_empty()); - } - - #[test] - fn increment_ref_count_updates_last_active() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let session = mgr.create_session(&project_dir).unwrap(); - std::thread::sleep(std::time::Duration::from_millis(5)); - let updated = mgr.increment_ref_count(session.id).unwrap(); - - assert_eq!(updated.ref_count, session.ref_count + 1); - assert!(updated.last_active >= session.last_active); - } - - #[test] - fn decrement_ref_count_updates_last_active() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let session = mgr.create_session(&project_dir).unwrap(); - std::thread::sleep(std::time::Duration::from_millis(5)); - let updated = mgr.decrement_ref_count(session.id).unwrap(); - - assert_eq!(updated.ref_count, session.ref_count - 1); - assert!(updated.last_active >= session.last_active); - } - - #[test] - fn list_sessions_returns_active_sessions() { - let temp = TempDir::new().unwrap(); - let mgr = manager(&temp); - let project_dir = create_project_dir(&temp); - - let session = mgr.create_session(&project_dir).unwrap(); + let _ = mgr.update_global_sessions(&project_dir).unwrap(); let sessions = mgr.list_sessions().unwrap(); assert_eq!(sessions.len(), 1); - assert_eq!(sessions[0].id, session.id); + assert_eq!( + sessions[0].id.as_deref(), + Some("019bf290-cccc-7c23-ba1d-dce7e6d40693") + ); + assert_eq!( + sessions[0].last_active.as_deref(), + Some("2026-02-07T05:00:00Z") + ); } } diff --git a/src/vm.rs b/src/vm.rs index 336e3dd..347d588 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1,4 +1,4 @@ -use crate::session_manager::INSTANCE_DIR_NAME; +use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME}; use std::{ env, ffi::OsString, @@ -146,7 +146,7 @@ where let cache_home = env::var("XDG_CACHE_HOME") .map(PathBuf::from) .unwrap_or_else(|_| home.join(".cache")); - let cache_dir = cache_home.join("vibe"); + let cache_dir = cache_home.join(GLOBAL_CACHE_DIR_NAME); let guest_mise_cache = cache_dir.join(".guest-mise-cache"); let instance_dir = project_root.join(INSTANCE_DIR_NAME); diff --git a/vibebox.toml b/vibebox.toml new file mode 100644 index 0000000..e69de29