use crate::VirtualMachineRuntime; use crate::fs::{FsState, VirtualFS}; use crate::os::{ FsFacade, GameLifecycleEvent, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade, }; use crate::process::ProcessManager; use crate::services::foreground::ForegroundStack; use crate::services::memcard::MemcardService; use crate::services::vm_session::VmSessionRegistry; use crate::services::windows::WindowManager; use crate::task::TaskManager; use prometeu_hal::log::{LogEvent, LogLevel, LogService, LogSource}; use prometeu_hal::telemetry::CertificationConfig; use std::collections::HashMap; use std::sync::Arc; pub struct SystemOS { pub(super) vm_runtime: VirtualMachineRuntime, pub(super) process_manager: ProcessManager, pub(super) task_manager: TaskManager, pub(super) foreground_stack: ForegroundStack, pub(super) vm_sessions: VmSessionRegistry, pub(super) game_lifecycle_events: Vec, pub(super) window_manager: WindowManager, pub(super) log_service: LogService, pub(super) fs: VirtualFS, pub(super) fs_state: FsState, pub(super) memcard: MemcardService, pub(super) open_files: HashMap, pub(super) next_handle: u32, } impl SystemOS { pub fn new(cap_config: Option) -> Self { let log_service = LogService::new(4096); let mut os = Self { vm_runtime: VirtualMachineRuntime::new_with_log_counter( cap_config, Arc::clone(&log_service.logs_count), ), process_manager: ProcessManager::new(), task_manager: TaskManager::new(), foreground_stack: ForegroundStack::new(), vm_sessions: VmSessionRegistry::new(), game_lifecycle_events: Vec::new(), window_manager: WindowManager::new(), log_service, fs: VirtualFS::new(), fs_state: FsState::Unmounted, memcard: MemcardService::new(), open_files: HashMap::new(), next_handle: 1, }; os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string()); os } pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) { self.vm_runtime.log(&mut self.log_service, level, source, tag, msg); } pub fn trace(&mut self, source: LogSource, msg: String) { self.log(LogLevel::Trace, source, 0, msg); } pub fn trace_tag(&mut self, source: LogSource, tag: u16, msg: String) { self.log(LogLevel::Trace, source, tag, msg); } pub fn debug(&mut self, source: LogSource, msg: String) { self.log(LogLevel::Debug, source, 0, msg); } pub fn debug_tag(&mut self, source: LogSource, tag: u16, msg: String) { self.log(LogLevel::Debug, source, tag, msg); } pub fn info(&mut self, source: LogSource, msg: String) { self.log(LogLevel::Info, source, 0, msg); } pub fn info_tag(&mut self, source: LogSource, tag: u16, msg: String) { self.log(LogLevel::Info, source, tag, msg); } pub fn warn(&mut self, source: LogSource, msg: String) { self.log(LogLevel::Warn, source, 0, msg); } pub fn warn_tag(&mut self, source: LogSource, tag: u16, msg: String) { self.log(LogLevel::Warn, source, tag, msg); } pub fn error(&mut self, source: LogSource, msg: String) { self.log(LogLevel::Error, source, 0, msg); } pub fn error_tag(&mut self, source: LogSource, tag: u16, msg: String) { self.log(LogLevel::Error, source, tag, msg); } pub fn recent_logs(&self, n: usize) -> Vec { self.log_service.get_recent(n) } pub fn logs_after(&self, seq: u64) -> Vec { self.log_service.get_after(seq) } pub fn frame_index(&mut self) -> u64 { self.vm().logical_frame_index() } pub fn lifecycle(&mut self) -> LifecycleFacade<'_> { LifecycleFacade { os: self } } pub fn sessions(&mut self) -> SessionsFacade<'_> { SessionsFacade { os: self } } pub fn vm(&mut self) -> VmFacade<'_> { VmFacade { os: self } } pub fn fs(&mut self) -> FsFacade<'_> { FsFacade { os: self } } pub fn windows(&mut self) -> WindowFacade<'_> { WindowFacade { os: self } } pub(super) fn clear_cartridge_service_state(&mut self) { self.open_files.clear(); self.next_handle = 1; self.memcard.clear_all_staging(); } } #[cfg(test)] mod tests { use super::*; use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation, SessionError}; use crate::process::{ProcessId, ProcessKind, ProcessState}; use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState}; use crate::task::{TaskId, TaskState}; use prometeu_hal::app_mode::AppMode; use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge}; use prometeu_hal::syscalls::caps; fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState { let task = os.task_manager.get(task_id).expect("task should exist"); os.process_manager.get(task.process_id).expect("process should exist").state } fn session_test_cartridge(app_mode: AppMode, app_id: u32, title: &str) -> Cartridge { Cartridge { app_id, title: title.to_string(), app_version: "1.0.0".to_string(), app_mode, capabilities: caps::NONE, program: vec![], assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], } } #[test] fn set_foreground_task_marks_task_and_process_running() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().suspend_task(task_id).expect("suspend should succeed"); os.lifecycle().set_foreground_task(task_id).expect("foreground should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, TaskState::Foreground ); assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running); } #[test] fn foreground_stack_starts_at_hub() { let os = SystemOS::new(None); assert_eq!(os.foreground_stack.owner(), ForegroundOwner::Hub); assert_eq!(os.foreground_stack.resident_game(), None); } #[test] fn game_session_registers_single_resident_game() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id)); assert_eq!( os.lifecycle().resident_game(), Some(ResidentGame { task_id, state: ResidentGameState::Foreground }) ); } #[test] fn second_resident_game_is_rejected() { let mut os = SystemOS::new(None); let first = os.sessions().create_vm_game_task(42, "Sector Crawl"); let process_id = os.process_manager.spawn_vm_game(43, "Second"); let second = os.task_manager.create_game_task(process_id, 43, "Second"); assert_eq!( os.lifecycle().set_game_foreground_task(second), Err(LifecycleError::ResidentGameAlreadyExists { existing: first, requested: second }) ); } #[test] fn vm_game_task_creates_vm_session() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); let sessions = os.sessions(); let session = sessions.vm_session_for_task(task_id).expect("VM session should exist"); assert_eq!(session.task_id, task_id); assert_eq!(session.app_id, 42); assert_eq!(session.title, "Sector Crawl"); assert_eq!(session.app_mode, AppMode::Game); assert_eq!(sessions.vm_session_count(), 1); } #[test] fn vm_shell_task_creates_vm_session() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_shell_task(7, "Settings"); let sessions = os.sessions(); let session = sessions.vm_session_for_task(task_id).expect("VM session should exist"); assert_eq!(session.task_id, task_id); assert_eq!(session.app_id, 7); assert_eq!(session.title, "Settings"); assert_eq!(session.app_mode, AppMode::Shell); assert_eq!(sessions.vm_session_count(), 1); } #[test] fn native_shell_task_does_not_create_vm_session() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_native_shell_task(7, "Settings"); let sessions = os.sessions(); assert!(sessions.vm_session_for_task(task_id).is_none()); assert_eq!(sessions.vm_session_count(), 0); } #[test] fn same_resident_game_app_reuses_existing_vm_session() { let mut os = SystemOS::new(None); let first = os.sessions().create_vm_game_task(42, "Sector Crawl"); let second = os.sessions().create_vm_game_task(42, "Sector Crawl"); let sessions = os.sessions(); assert_eq!(second, first); assert_eq!( sessions.resident_game_session_for_app(42).map(|session| session.task_id), Some(first) ); assert_eq!(sessions.vm_session_count(), 1); } #[test] fn vm_sessions_hold_independent_runtime_state() { let mut os = SystemOS::new(None); let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().request_home_from_game(game).expect("home request should succeed"); os.lifecycle().advance_game_pause_budget(game).expect("budget should expire"); let shell = os.sessions().create_vm_shell_task(7, "Settings"); { let mut sessions = os.sessions(); let game_session = sessions.vm_session_for_task_mut(game).expect("game VM session should exist"); game_session.runtime.tick_index = 100; game_session.open_files.insert(1, "/game.dat".to_string()); } { let mut sessions = os.sessions(); let shell_session = sessions.vm_session_for_task_mut(shell).expect("shell VM session should exist"); shell_session.runtime.tick_index = 5; shell_session.open_files.insert(1, "/shell.dat".to_string()); } let sessions = os.sessions(); let game_session = sessions.vm_session_for_task(game).expect("game VM session should exist"); let shell_session = sessions.vm_session_for_task(shell).expect("shell VM session should exist"); assert_eq!(game_session.runtime.tick_index, 100); assert_eq!(shell_session.runtime.tick_index, 5); assert_eq!(game_session.open_files.get(&1).map(String::as_str), Some("/game.dat")); assert_eq!(shell_session.open_files.get(&1).map(String::as_str), Some("/shell.dat")); } #[test] fn vm_debug_operations_target_foreground_shell_before_resident_game() { let mut os = SystemOS::new(None); let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().request_home_from_game(game).expect("home request should succeed"); os.lifecycle().advance_game_pause_budget(game).expect("budget should expire"); let shell = os.sessions().create_vm_shell_task(7, "Settings"); { let mut sessions = os.sessions(); let game_session = sessions.vm_session_for_task_mut(game).expect("game VM session should exist"); game_session.vm.insert_breakpoint(10); } assert!(os.vm().insert_active_breakpoint(20)); assert_eq!(os.vm().active_breakpoints_list(), vec![20]); let sessions = os.sessions(); assert_eq!( sessions .vm_session_for_task(game) .expect("game VM session should exist") .vm .breakpoints_list(), vec![10] ); assert_eq!( sessions .vm_session_for_task(shell) .expect("shell VM session should exist") .vm .breakpoints_list(), vec![20] ); } #[test] fn home_request_notifies_game_before_budget_suspends_it() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().request_home_from_game(task_id).expect("home request should succeed"); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id)); assert_eq!( os.lifecycle().resident_game(), Some(ResidentGame { task_id, state: ResidentGameState::PauseRequested { remaining_ticks: 1 }, }) ); assert_eq!( os.lifecycle().pending_game_lifecycle_events()[0].kind, GameLifecycleEventKind::Pause ); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, TaskState::Foreground ); assert!(os.lifecycle().advance_game_pause_budget(task_id).expect("budget should advance")); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub); assert_eq!( os.lifecycle().resident_game(), Some(ResidentGame { task_id, state: ResidentGameState::PausedSuspended }) ); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, TaskState::Suspended ); assert_eq!(process_state_for_task(&os, task_id), ProcessState::Suspended); } #[test] fn shell_foreground_keeps_game_resident_and_marks_it_suspended() { let mut os = SystemOS::new(None); let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().request_home_from_game(game).expect("home request should succeed"); os.lifecycle().advance_game_pause_budget(game).expect("budget should expire"); let shell = os.sessions().create_vm_shell_task(7, "Settings"); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell)); assert_eq!( os.lifecycle().resident_game(), Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) ); } #[test] fn vm_shell_foreground_suspends_foreground_resident_game() { let mut os = SystemOS::new(None); let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); let shell = os.sessions().create_vm_shell_task(7, "Settings"); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell)); assert_eq!( os.lifecycle().resident_game(), Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) ); assert_eq!( os.task_manager.get(game).expect("game should exist").state, TaskState::Suspended ); assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended); } #[test] fn native_shell_foreground_suspends_foreground_resident_game() { let mut os = SystemOS::new(None); let game = os.sessions().create_vm_game_task(42, "Sector Crawl"); let shell = os.sessions().create_native_shell_task(7, "Settings"); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell)); assert_eq!( os.lifecycle().resident_game(), Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended }) ); assert_eq!( os.task_manager.get(game).expect("game should exist").state, TaskState::Suspended ); assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended); } #[test] fn lifecycle_exposes_process_kind_for_shell_tasks() { let mut os = SystemOS::new(None); let vm_shell = os.sessions().create_vm_shell_task(7, "Settings"); os.lifecycle().close_task(vm_shell).expect("vm shell should close"); let native_shell = os.sessions().create_native_shell_task(8, "Files"); assert_eq!(os.lifecycle().process_kind_for_task(vm_shell), Ok(ProcessKind::VmShell)); assert_eq!( os.lifecycle().process_kind_for_task(native_shell), Ok(ProcessKind::NativeShell) ); } #[test] fn second_foreground_shell_is_rejected() { let mut os = SystemOS::new(None); let first = os.sessions().create_vm_shell_task(7, "Settings"); let process_id = os.process_manager.spawn_vm_shell(8, "Files"); let second = os.task_manager.create_shell_task(process_id, 8, "Files"); assert_eq!( os.lifecycle().set_shell_foreground_task(second), Err(LifecycleError::ForegroundShellAlreadyExists { existing: first, requested: second, }) ); } #[test] fn vm_shell_load_returns_typed_error_when_foreground_shell_exists() { let mut os = SystemOS::new(None); let first = os.sessions().create_vm_shell_task(7, "Settings"); let cartridge = session_test_cartridge(AppMode::Shell, 8, "Files"); let error = os.sessions().load_vm_cartridge(&cartridge).expect_err("load should fail"); match error { SessionError::Lifecycle(LifecycleError::ForegroundShellAlreadyExists { existing, requested, }) => { assert_eq!(existing, first); assert_ne!(requested, first); } other => panic!("expected foreground shell lifecycle error, got {:?}", other), } } #[test] fn vm_session_creation_returns_typed_error_for_missing_task() { let mut os = SystemOS::new(None); assert_eq!( os.sessions().try_create_vm_session_for_task(TaskId(999)), Err(SessionError::TaskNotFound(TaskId(999))) ); } #[test] fn vm_session_creation_returns_typed_error_for_missing_process() { let mut os = SystemOS::new(None); let task_id = os.task_manager.create_game_task(ProcessId(999), 42, "Broken"); assert_eq!( os.sessions().try_create_vm_session_for_task(task_id), Err(SessionError::ProcessNotFound(ProcessId(999))) ); } #[test] fn session_cartridge_initialization_returns_typed_error_for_missing_session() { let mut os = SystemOS::new(None); let process_id = os.process_manager.spawn_vm_game(42, "Sector Crawl"); let task_id = os.task_manager.create_game_task(process_id, 42, "Sector Crawl"); let cartridge = session_test_cartridge(AppMode::Game, 42, "Sector Crawl"); assert_eq!( os.sessions().try_initialize_session_cartridge(task_id, &cartridge), Err(SessionError::MissingVmSession(task_id)) ); } #[test] fn closing_foreground_shell_returns_to_hub() { let mut os = SystemOS::new(None); let shell = os.sessions().create_vm_shell_task(7, "Settings"); os.lifecycle().close_task(shell).expect("close should succeed"); assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub); assert_eq!( os.task_manager.get(shell).expect("shell should remain").state, TaskState::Closed ); } #[test] fn suspend_task_marks_task_and_process_suspended() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().suspend_task(task_id).expect("suspend should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, TaskState::Suspended ); assert_eq!(process_state_for_task(&os, task_id), ProcessState::Suspended); } #[test] fn resume_task_marks_suspended_task_foreground_and_process_running() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().suspend_task(task_id).expect("suspend should succeed"); os.lifecycle().resume_task(task_id).expect("resume should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, TaskState::Foreground ); assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running); assert_eq!( os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind), Some(GameLifecycleEventKind::ResumeForeground) ); } #[test] fn close_task_marks_task_closed_and_process_stopped_without_removing_entities() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); let process_id = os.task_manager.get(task_id).expect("task should exist").process_id; os.lifecycle().close_task(task_id).expect("close should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should remain").state, TaskState::Closed ); assert_eq!( os.process_manager.get(process_id).expect("process should remain").state, ProcessState::Stopped ); } #[test] fn crash_task_marks_task_and_process_crashed() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); os.lifecycle().crash_task(task_id, None).expect("crash should succeed"); assert_eq!( os.task_manager.get(task_id).expect("task should exist").state, TaskState::Crashed ); assert_eq!(process_state_for_task(&os, task_id), ProcessState::Crashed); } #[test] fn lifecycle_operation_missing_task_returns_typed_error() { let mut os = SystemOS::new(None); assert_eq!( os.lifecycle().suspend_task(TaskId(999)), Err(LifecycleError::TaskNotFound(TaskId(999))) ); } #[test] fn lifecycle_operation_missing_process_returns_typed_error() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); let process_id = os.task_manager.get(task_id).expect("task should exist").process_id; os.process_manager.mark_stopped(process_id); os.process_manager.remove_stopped(); assert_eq!( os.lifecycle().suspend_task(task_id), Err(LifecycleError::ProcessNotFound(process_id)) ); } #[test] fn resume_task_from_non_suspended_state_returns_invalid_transition() { let mut os = SystemOS::new(None); let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl"); assert_eq!( os.lifecycle().resume_task(task_id), Err(LifecycleError::InvalidTransition { task_id, from: TaskState::Foreground, operation: LifecycleOperation::Resume, }) ); } }