2026-07-05 00:30:56 +01:00

645 lines
23 KiB
Rust

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<GameLifecycleEvent>,
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<u32, String>,
pub(super) next_handle: u32,
}
impl SystemOS {
pub fn new(cap_config: Option<CertificationConfig>) -> 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<LogEvent> {
self.log_service.get_recent(n)
}
pub fn logs_after(&self, seq: u64) -> Vec<LogEvent> {
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,
})
);
}
}