Compare commits
2 Commits
f36fd1ed7c
...
4d2ea6400d
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d2ea6400d | |||
| bfe900797f |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
# ===== Rust / Cargo =====
|
# ===== Rust / Cargo =====
|
||||||
|
.cargo/config.toml
|
||||||
/target/
|
/target/
|
||||||
**/target/
|
**/target/
|
||||||
|
|
||||||
@ -10,6 +11,7 @@
|
|||||||
# RustRover / IntelliJ
|
# RustRover / IntelliJ
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
.junie
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@ -308,12 +308,11 @@ mod tests {
|
|||||||
other => panic!("expected AppCrashes state, got {:?}", other),
|
other => panic!("expected AppCrashes state, got {:?}", other),
|
||||||
}
|
}
|
||||||
|
|
||||||
let task = firmware.os.task_manager.get(task_id).expect("task should exist");
|
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Crashed));
|
||||||
let process =
|
assert_eq!(
|
||||||
firmware.os.process_manager.get(task.process_id).expect("process should exist");
|
firmware.os.lifecycle().process_state_for_task(task_id),
|
||||||
|
Ok(ProcessState::Crashed)
|
||||||
assert_eq!(task.state, TaskState::Crashed);
|
);
|
||||||
assert_eq!(process.state, ProcessState::Crashed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -351,8 +350,8 @@ mod tests {
|
|||||||
firmware.tick(&signals, &mut hardware);
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
assert!(matches!(firmware.state, FirmwareState::SystemRunning(_)));
|
assert!(matches!(firmware.state, FirmwareState::SystemRunning(_)));
|
||||||
assert!(firmware.os.window_manager.focused.is_some());
|
assert!(firmware.os.window().focused_window().is_some());
|
||||||
assert_eq!(firmware.os.window_manager.windows.len(), 1);
|
assert_eq!(firmware.os.window().window_count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -364,11 +363,11 @@ mod tests {
|
|||||||
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
|
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
|
||||||
firmware.tick(&signals, &mut hardware);
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
let tick_index_before_system_update = firmware.os.vm_runtime.tick_index;
|
let tick_index_before_system_update = firmware.os.vm().tick_index();
|
||||||
firmware.tick(&signals, &mut hardware);
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
assert!(matches!(firmware.state, FirmwareState::SystemRunning(_)));
|
assert!(matches!(firmware.state, FirmwareState::SystemRunning(_)));
|
||||||
assert_eq!(firmware.os.vm_runtime.tick_index, tick_index_before_system_update + 1);
|
assert_eq!(firmware.os.vm().tick_index(), tick_index_before_system_update + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -19,7 +19,7 @@ impl GameRunningStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||||
let Some(task_state) = ctx.os.task_manager.get(self.task_id).map(|task| task.state) else {
|
let Some(task_state) = ctx.os.lifecycle().task_state(self.task_id) else {
|
||||||
// TODO: is it panic?
|
// TODO: is it panic?
|
||||||
let report =
|
let report =
|
||||||
CrashReport::VmPanic { message: "task is gone, panic!".to_string(), pc: None };
|
CrashReport::VmPanic { message: "task is gone, panic!".to_string(), pc: None };
|
||||||
@ -32,18 +32,18 @@ impl GameRunningStep {
|
|||||||
message: "should be running as foreground, panic!".to_string(),
|
message: "should be running as foreground, panic!".to_string(),
|
||||||
pc: None,
|
pc: None,
|
||||||
};
|
};
|
||||||
let _ = ctx.os.crash_task(self.task_id, Some(&report));
|
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = ctx.os.tick_vm(ctx.vm, ctx.signals, ctx.hw);
|
let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.hw);
|
||||||
|
|
||||||
if !ctx.os.vm_runtime.logical_frame_active {
|
if !ctx.os.vm().logical_frame_active() {
|
||||||
ctx.hw.gfx_mut().present();
|
ctx.hw.gfx_mut().present();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(report) = result {
|
if let Some(report) = result {
|
||||||
let _ = ctx.os.crash_task(self.task_id, Some(&report));
|
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ impl LoadCartridgeStep {
|
|||||||
self.cartridge.assets.clone(),
|
self.cartridge.assets.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.init_error = ctx.os.initialize_vm(ctx.vm, &self.cartridge).err();
|
self.init_error = ctx.os.vm().initialize(ctx.vm, &self.cartridge).err();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||||
@ -43,20 +43,24 @@ impl LoadCartridgeStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.cartridge.app_mode == AppMode::Shell {
|
if self.cartridge.app_mode == AppMode::Shell {
|
||||||
let id = ctx.os.window_manager.add_window(
|
let id = ctx.os.window().add_window(
|
||||||
self.cartridge.title.clone(),
|
self.cartridge.title.clone(),
|
||||||
Rect { x: 40, y: 20, w: 240, h: 140 },
|
Rect { x: 40, y: 20, w: 240, h: 140 },
|
||||||
Color::WHITE,
|
Color::WHITE,
|
||||||
);
|
);
|
||||||
ctx.os.window_manager.set_focus(id);
|
ctx.os.window().set_focus(id);
|
||||||
|
|
||||||
let task_id =
|
let task_id = ctx
|
||||||
ctx.os.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone());
|
.os
|
||||||
|
.sessions()
|
||||||
|
.create_vm_shell_task(self.cartridge.app_id, self.cartridge.title.clone());
|
||||||
return Some(FirmwareState::SystemRunning(SystemRunningStep::new(task_id)));
|
return Some(FirmwareState::SystemRunning(SystemRunningStep::new(task_id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let task_id =
|
let task_id = ctx
|
||||||
ctx.os.create_vm_game_task(self.cartridge.app_id, self.cartridge.title.clone());
|
.os
|
||||||
|
.sessions()
|
||||||
|
.create_vm_game_task(self.cartridge.app_id, self.cartridge.title.clone());
|
||||||
Some(FirmwareState::GameRunning(GameRunningStep::new(task_id)))
|
Some(FirmwareState::GameRunning(GameRunningStep::new(task_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ pub struct ResetStep;
|
|||||||
impl ResetStep {
|
impl ResetStep {
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||||
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string());
|
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Firmware Reset".to_string());
|
||||||
ctx.os.vm_runtime.reset(ctx.vm);
|
ctx.os.vm().reset(ctx.vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ impl SystemRunningStep {
|
|||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||||
let outcome = ctx.hub.update_system_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw);
|
let outcome = ctx.hub.update_system_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw);
|
||||||
if let Some(report) = outcome.crash {
|
if let Some(report) = outcome.crash {
|
||||||
let _ = ctx.os.crash_task(self.task_id, Some(&report));
|
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
crates/console/prometeu-system/src/os/facades/fs.rs
Normal file
26
crates/console/prometeu-system/src/os/facades/fs.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use crate::fs::{FsBackend, FsState};
|
||||||
|
use crate::os::SystemOS;
|
||||||
|
|
||||||
|
pub struct FsFacade<'a> {
|
||||||
|
pub(in crate::os) os: &'a mut SystemOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FsFacade<'a> {
|
||||||
|
pub fn needs_mount(&self) -> bool {
|
||||||
|
matches!(self.os.fs_state, FsState::Unmounted | FsState::Error(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mount(&mut self, backend: Box<dyn FsBackend>) {
|
||||||
|
self.os.vm_runtime.mount_fs(
|
||||||
|
&mut self.os.log_service,
|
||||||
|
&mut self.os.fs,
|
||||||
|
&mut self.os.fs_state,
|
||||||
|
backend,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmount(&mut self) {
|
||||||
|
self.os.fs.unmount();
|
||||||
|
self.os.fs_state = FsState::Unmounted;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
crates/console/prometeu-system/src/os/facades/lifecycle.rs
Normal file
94
crates/console/prometeu-system/src/os/facades/lifecycle.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use crate::CrashReport;
|
||||||
|
use crate::os::SystemOS;
|
||||||
|
use crate::os::{LifecycleError, LifecycleOperation};
|
||||||
|
use crate::process::{ProcessId, ProcessState};
|
||||||
|
use crate::task::{TaskId, TaskState};
|
||||||
|
|
||||||
|
pub struct LifecycleFacade<'a> {
|
||||||
|
pub(in crate::os) os: &'a mut SystemOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LifecycleFacade<'a> {
|
||||||
|
pub fn task_state(&self, task_id: TaskId) -> Option<TaskState> {
|
||||||
|
self.os.task_manager.get(task_id).map(|task| task.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_state_for_task(&self, task_id: TaskId) -> Result<ProcessState, LifecycleError> {
|
||||||
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
self.os
|
||||||
|
.process_manager
|
||||||
|
.get(process_id)
|
||||||
|
.map(|process| process.state)
|
||||||
|
.ok_or(LifecycleError::ProcessNotFound(process_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
||||||
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
|
||||||
|
self.os.task_manager.set_foreground(task_id);
|
||||||
|
self.os.process_manager.mark_running(process_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
||||||
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
|
||||||
|
self.os.task_manager.mark_suspended(task_id);
|
||||||
|
self.os.process_manager.mark_suspended(process_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
||||||
|
let task =
|
||||||
|
self.os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
||||||
|
|
||||||
|
if task.state != TaskState::Suspended {
|
||||||
|
return Err(LifecycleError::InvalidTransition {
|
||||||
|
task_id,
|
||||||
|
from: task.state,
|
||||||
|
operation: LifecycleOperation::Resume,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
|
||||||
|
self.os.task_manager.set_foreground(task_id);
|
||||||
|
self.os.process_manager.mark_running(process_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
||||||
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
|
||||||
|
self.os.task_manager.close_task(task_id);
|
||||||
|
self.os.process_manager.mark_stopped(process_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn crash_task(
|
||||||
|
&mut self,
|
||||||
|
task_id: TaskId,
|
||||||
|
_report: Option<&CrashReport>,
|
||||||
|
) -> Result<(), LifecycleError> {
|
||||||
|
let process_id = process_id_for_task(self.os, task_id)?;
|
||||||
|
|
||||||
|
self.os.task_manager.mark_crashed(task_id);
|
||||||
|
self.os.process_manager.mark_crashed(process_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result<ProcessId, LifecycleError> {
|
||||||
|
let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
||||||
|
|
||||||
|
if !os.process_manager.contains(task.process_id) {
|
||||||
|
return Err(LifecycleError::ProcessNotFound(task.process_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(task.process_id)
|
||||||
|
}
|
||||||
11
crates/console/prometeu-system/src/os/facades/mod.rs
Normal file
11
crates/console/prometeu-system/src/os/facades/mod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mod fs;
|
||||||
|
mod lifecycle;
|
||||||
|
mod sessions;
|
||||||
|
mod vm;
|
||||||
|
mod window;
|
||||||
|
|
||||||
|
pub use fs::FsFacade;
|
||||||
|
pub use lifecycle::LifecycleFacade;
|
||||||
|
pub use sessions::SessionsFacade;
|
||||||
|
pub use vm::VmFacade;
|
||||||
|
pub use window::WindowFacade;
|
||||||
44
crates/console/prometeu-system/src/os/facades/sessions.rs
Normal file
44
crates/console/prometeu-system/src/os/facades/sessions.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use crate::os::SystemOS;
|
||||||
|
use crate::task::TaskId;
|
||||||
|
|
||||||
|
pub struct SessionsFacade<'a> {
|
||||||
|
pub(in crate::os) os: &'a mut SystemOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SessionsFacade<'a> {
|
||||||
|
pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
||||||
|
let title = title.into();
|
||||||
|
let process_id = self.os.process_manager.spawn_vm_game(app_id, title.clone());
|
||||||
|
let task_id = self.os.task_manager.create_game_task(process_id, app_id, title);
|
||||||
|
self.os
|
||||||
|
.lifecycle()
|
||||||
|
.set_foreground_task(task_id)
|
||||||
|
.expect("newly created game task should have an associated process");
|
||||||
|
|
||||||
|
task_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
||||||
|
let title = title.into();
|
||||||
|
let process_id = self.os.process_manager.spawn_vm_shell(app_id, title.clone());
|
||||||
|
let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title);
|
||||||
|
self.os
|
||||||
|
.lifecycle()
|
||||||
|
.set_foreground_task(task_id)
|
||||||
|
.expect("newly created shell task should have an associated process");
|
||||||
|
|
||||||
|
task_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_native_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
||||||
|
let title = title.into();
|
||||||
|
let process_id = self.os.process_manager.spawn_native_shell(app_id, title.clone());
|
||||||
|
let task_id = self.os.task_manager.create_shell_task(process_id, app_id, title);
|
||||||
|
self.os
|
||||||
|
.lifecycle()
|
||||||
|
.set_foreground_task(task_id)
|
||||||
|
.expect("newly created native shell task should have an associated process");
|
||||||
|
|
||||||
|
task_id
|
||||||
|
}
|
||||||
|
}
|
||||||
121
crates/console/prometeu-system/src/os/facades/vm.rs
Normal file
121
crates/console/prometeu-system/src/os/facades/vm.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
use crate::CrashReport;
|
||||||
|
use crate::os::SystemOS;
|
||||||
|
use prometeu_hal::cartridge::{AppMode, Cartridge};
|
||||||
|
use prometeu_hal::telemetry::{CertificationConfig, TelemetryFrame};
|
||||||
|
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||||
|
use prometeu_vm::VirtualMachine;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
pub struct VmFacade<'a> {
|
||||||
|
pub(in crate::os) os: &'a mut SystemOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> VmFacade<'a> {
|
||||||
|
pub fn initialize(
|
||||||
|
&mut self,
|
||||||
|
vm: &mut VirtualMachine,
|
||||||
|
cartridge: &Cartridge,
|
||||||
|
) -> Result<(), CrashReport> {
|
||||||
|
self.os.clear_cartridge_service_state();
|
||||||
|
self.os.vm_runtime.initialize_vm(&mut self.os.log_service, vm, cartridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(
|
||||||
|
&mut self,
|
||||||
|
vm: &mut VirtualMachine,
|
||||||
|
signals: &InputSignals,
|
||||||
|
hw: &mut dyn HardwareBridge,
|
||||||
|
) -> Option<CrashReport> {
|
||||||
|
self.os.vm_runtime.tick(
|
||||||
|
&mut self.os.log_service,
|
||||||
|
&mut self.os.fs,
|
||||||
|
&mut self.os.fs_state,
|
||||||
|
&mut self.os.memcard,
|
||||||
|
&mut self.os.open_files,
|
||||||
|
&mut self.os.next_handle,
|
||||||
|
vm,
|
||||||
|
signals,
|
||||||
|
hw,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self, vm: &mut VirtualMachine) {
|
||||||
|
self.os.vm_runtime.reset(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_step_instruction(
|
||||||
|
&mut self,
|
||||||
|
vm: &mut VirtualMachine,
|
||||||
|
hw: &mut dyn HardwareBridge,
|
||||||
|
) -> Option<CrashReport> {
|
||||||
|
self.os.vm_runtime.debug_step_instruction(&mut self.os.log_service, vm, hw)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paused(&self) -> bool {
|
||||||
|
self.os.vm_runtime.paused
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_paused(&mut self, paused: bool) {
|
||||||
|
self.os.vm_runtime.paused = paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_inspection_active(&mut self, active: bool) {
|
||||||
|
self.os.vm_runtime.inspection_active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_debug_step(&mut self) {
|
||||||
|
self.os.vm_runtime.debug_step_request = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_step_requested(&self) -> bool {
|
||||||
|
self.os.vm_runtime.debug_step_request
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logical_frame_active(&self) -> bool {
|
||||||
|
self.os.vm_runtime.logical_frame_active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick_index(&self) -> u64 {
|
||||||
|
self.os.vm_runtime.tick_index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logical_frame_index(&self) -> u64 {
|
||||||
|
self.os.vm_runtime.logical_frame_index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_frame_cpu_time_us(&self) -> u64 {
|
||||||
|
self.os.vm_runtime.last_frame_cpu_time_us
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn completed_logical_frames(&self) -> u64 {
|
||||||
|
self.os.vm_runtime.atomic_telemetry.completed_logical_frames.load(Ordering::Relaxed).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn telemetry_snapshot(&self) -> TelemetryFrame {
|
||||||
|
self.os.vm_runtime.atomic_telemetry.snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cert_config(&self) -> &CertificationConfig {
|
||||||
|
&self.os.vm_runtime.certifier.config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_crash_report(&self) -> Option<&CrashReport> {
|
||||||
|
self.os.vm_runtime.last_crash_report.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_app_id(&self) -> u32 {
|
||||||
|
self.os.vm_runtime.current_app_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_cartridge_title(&self) -> String {
|
||||||
|
self.os.vm_runtime.current_cartridge_title.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_cartridge_app_version(&self) -> String {
|
||||||
|
self.os.vm_runtime.current_cartridge_app_version.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_cartridge_app_mode(&self) -> AppMode {
|
||||||
|
self.os.vm_runtime.current_cartridge_app_mode
|
||||||
|
}
|
||||||
|
}
|
||||||
37
crates/console/prometeu-system/src/os/facades/window.rs
Normal file
37
crates/console/prometeu-system/src/os/facades/window.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
use crate::os::SystemOS;
|
||||||
|
use prometeu_hal::color::Color;
|
||||||
|
use prometeu_hal::window::{Rect, Window, WindowId};
|
||||||
|
|
||||||
|
pub struct WindowFacade<'a> {
|
||||||
|
pub(in crate::os) os: &'a mut SystemOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WindowFacade<'a> {
|
||||||
|
pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId {
|
||||||
|
self.os.window_manager.add_window(title, viewport, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_focus(&mut self, id: WindowId) {
|
||||||
|
self.os.window_manager.set_focus(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused_window(&self) -> Option<WindowId> {
|
||||||
|
self.os.window_manager.focused
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_window(&mut self, id: WindowId) {
|
||||||
|
self.os.window_manager.remove_window(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_all_windows(&mut self) {
|
||||||
|
self.os.window_manager.remove_all_windows();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn windows(&self) -> &[Window] {
|
||||||
|
&self.os.window_manager.windows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_count(&self) -> usize {
|
||||||
|
self.os.window_manager.windows.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
mod facades;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
mod system_os;
|
mod system_os;
|
||||||
|
|
||||||
|
pub use facades::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade};
|
||||||
pub use lifecycle::{LifecycleError, LifecycleOperation};
|
pub use lifecycle::{LifecycleError, LifecycleOperation};
|
||||||
pub use system_os::SystemOS;
|
pub use system_os::SystemOS;
|
||||||
|
|||||||
@ -1,31 +1,26 @@
|
|||||||
use crate::CrashReport;
|
|
||||||
use crate::VirtualMachineRuntime;
|
use crate::VirtualMachineRuntime;
|
||||||
use crate::fs::{FsBackend, FsState, VirtualFS};
|
use crate::fs::{FsState, VirtualFS};
|
||||||
use crate::os::{LifecycleError, LifecycleOperation};
|
use crate::os::{FsFacade, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade};
|
||||||
use crate::process::ProcessManager;
|
use crate::process::ProcessManager;
|
||||||
use crate::services::memcard::MemcardService;
|
use crate::services::memcard::MemcardService;
|
||||||
use crate::services::window_manager::WindowManager;
|
use crate::services::window_manager::WindowManager;
|
||||||
use crate::task::TaskManager;
|
use crate::task::TaskManager;
|
||||||
use crate::task::{TaskId, TaskState};
|
use prometeu_hal::log::{LogEvent, LogLevel, LogService, LogSource};
|
||||||
use prometeu_hal::cartridge::Cartridge;
|
|
||||||
use prometeu_hal::log::{LogLevel, LogService, LogSource};
|
|
||||||
use prometeu_hal::telemetry::CertificationConfig;
|
use prometeu_hal::telemetry::CertificationConfig;
|
||||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
|
||||||
use prometeu_vm::VirtualMachine;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct SystemOS {
|
pub struct SystemOS {
|
||||||
pub vm_runtime: VirtualMachineRuntime,
|
pub(super) vm_runtime: VirtualMachineRuntime,
|
||||||
pub process_manager: ProcessManager,
|
pub(super) process_manager: ProcessManager,
|
||||||
pub task_manager: TaskManager,
|
pub(super) task_manager: TaskManager,
|
||||||
pub window_manager: WindowManager,
|
pub(super) window_manager: WindowManager,
|
||||||
pub log_service: LogService,
|
pub(super) log_service: LogService,
|
||||||
pub fs: VirtualFS,
|
pub(super) fs: VirtualFS,
|
||||||
pub fs_state: FsState,
|
pub(super) fs_state: FsState,
|
||||||
pub memcard: MemcardService,
|
pub(super) memcard: MemcardService,
|
||||||
pub open_files: HashMap<u32, String>,
|
pub(super) open_files: HashMap<u32, String>,
|
||||||
pub next_handle: u32,
|
pub(super) next_handle: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemOS {
|
impl SystemOS {
|
||||||
@ -56,156 +51,47 @@ impl SystemOS {
|
|||||||
self.vm_runtime.log(&mut self.log_service, level, source, tag, msg);
|
self.vm_runtime.log(&mut self.log_service, level, source, tag, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize_vm(
|
pub fn recent_logs(&self, n: usize) -> Vec<LogEvent> {
|
||||||
&mut self,
|
self.log_service.get_recent(n)
|
||||||
vm: &mut VirtualMachine,
|
|
||||||
cartridge: &Cartridge,
|
|
||||||
) -> Result<(), CrashReport> {
|
|
||||||
self.clear_cartridge_service_state();
|
|
||||||
self.vm_runtime.initialize_vm(&mut self.log_service, vm, cartridge)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick_vm(
|
pub fn logs_after(&self, seq: u64) -> Vec<LogEvent> {
|
||||||
&mut self,
|
self.log_service.get_after(seq)
|
||||||
vm: &mut VirtualMachine,
|
|
||||||
signals: &InputSignals,
|
|
||||||
hw: &mut dyn HardwareBridge,
|
|
||||||
) -> Option<CrashReport> {
|
|
||||||
self.vm_runtime.tick(
|
|
||||||
&mut self.log_service,
|
|
||||||
&mut self.fs,
|
|
||||||
&mut self.fs_state,
|
|
||||||
&mut self.memcard,
|
|
||||||
&mut self.open_files,
|
|
||||||
&mut self.next_handle,
|
|
||||||
vm,
|
|
||||||
signals,
|
|
||||||
hw,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mount_fs(&mut self, backend: Box<dyn FsBackend>) {
|
pub fn lifecycle(&mut self) -> LifecycleFacade<'_> {
|
||||||
self.vm_runtime.mount_fs(&mut self.log_service, &mut self.fs, &mut self.fs_state, backend);
|
LifecycleFacade { os: self }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unmount_fs(&mut self) {
|
pub fn sessions(&mut self) -> SessionsFacade<'_> {
|
||||||
self.fs.unmount();
|
SessionsFacade { os: self }
|
||||||
self.fs_state = FsState::Unmounted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_cartridge_service_state(&mut self) {
|
pub fn vm(&mut self) -> VmFacade<'_> {
|
||||||
|
VmFacade { os: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fs(&mut self) -> FsFacade<'_> {
|
||||||
|
FsFacade { os: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window(&mut self) -> WindowFacade<'_> {
|
||||||
|
WindowFacade { os: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear_cartridge_service_state(&mut self) {
|
||||||
self.open_files.clear();
|
self.open_files.clear();
|
||||||
self.next_handle = 1;
|
self.next_handle = 1;
|
||||||
self.memcard.clear_all_staging();
|
self.memcard.clear_all_staging();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_vm_game_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
|
||||||
let title = title.into();
|
|
||||||
let process_id = self.process_manager.spawn_vm_game(app_id, title.clone());
|
|
||||||
let task_id = self.task_manager.create_game_task(process_id, app_id, title);
|
|
||||||
self.set_foreground_task(task_id)
|
|
||||||
.expect("newly created game task should have an associated process");
|
|
||||||
|
|
||||||
task_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_vm_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
|
||||||
let title = title.into();
|
|
||||||
let process_id = self.process_manager.spawn_vm_shell(app_id, title.clone());
|
|
||||||
let task_id = self.task_manager.create_shell_task(process_id, app_id, title);
|
|
||||||
self.set_foreground_task(task_id)
|
|
||||||
.expect("newly created shell task should have an associated process");
|
|
||||||
|
|
||||||
task_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_native_shell_task(&mut self, app_id: u32, title: impl Into<String>) -> TaskId {
|
|
||||||
let title = title.into();
|
|
||||||
let process_id = self.process_manager.spawn_native_shell(app_id, title.clone());
|
|
||||||
let task_id = self.task_manager.create_shell_task(process_id, app_id, title);
|
|
||||||
self.set_foreground_task(task_id)
|
|
||||||
.expect("newly created native shell task should have an associated process");
|
|
||||||
|
|
||||||
task_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = self.process_id_for_task(task_id)?;
|
|
||||||
|
|
||||||
self.task_manager.set_foreground(task_id);
|
|
||||||
self.process_manager.mark_running(process_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = self.process_id_for_task(task_id)?;
|
|
||||||
|
|
||||||
self.task_manager.mark_suspended(task_id);
|
|
||||||
self.process_manager.mark_suspended(process_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let task = self.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
|
||||||
|
|
||||||
if task.state != TaskState::Suspended {
|
|
||||||
return Err(LifecycleError::InvalidTransition {
|
|
||||||
task_id,
|
|
||||||
from: task.state,
|
|
||||||
operation: LifecycleOperation::Resume,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let process_id = self.process_id_for_task(task_id)?;
|
|
||||||
|
|
||||||
self.task_manager.set_foreground(task_id);
|
|
||||||
self.process_manager.mark_running(process_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = self.process_id_for_task(task_id)?;
|
|
||||||
|
|
||||||
self.task_manager.close_task(task_id);
|
|
||||||
self.process_manager.mark_stopped(process_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn crash_task(
|
|
||||||
&mut self,
|
|
||||||
task_id: TaskId,
|
|
||||||
_report: Option<&CrashReport>,
|
|
||||||
) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = self.process_id_for_task(task_id)?;
|
|
||||||
|
|
||||||
self.task_manager.mark_crashed(task_id);
|
|
||||||
self.process_manager.mark_crashed(process_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_id_for_task(
|
|
||||||
&self,
|
|
||||||
task_id: TaskId,
|
|
||||||
) -> Result<crate::process::ProcessId, LifecycleError> {
|
|
||||||
let task = self.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
|
||||||
|
|
||||||
if !self.process_manager.contains(task.process_id) {
|
|
||||||
return Err(LifecycleError::ProcessNotFound(task.process_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(task.process_id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::os::{LifecycleError, LifecycleOperation};
|
||||||
use crate::process::ProcessState;
|
use crate::process::ProcessState;
|
||||||
|
use crate::task::{TaskId, TaskState};
|
||||||
|
|
||||||
fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState {
|
fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState {
|
||||||
let task = os.task_manager.get(task_id).expect("task should exist");
|
let task = os.task_manager.get(task_id).expect("task should exist");
|
||||||
@ -215,10 +101,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn set_foreground_task_marks_task_and_process_running() {
|
fn set_foreground_task_marks_task_and_process_running() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
let task_id = os.create_vm_game_task(42, "Sector Crawl");
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
||||||
|
|
||||||
os.suspend_task(task_id).expect("suspend should succeed");
|
os.lifecycle().suspend_task(task_id).expect("suspend should succeed");
|
||||||
os.set_foreground_task(task_id).expect("foreground should succeed");
|
os.lifecycle().set_foreground_task(task_id).expect("foreground should succeed");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
os.task_manager.get(task_id).expect("task should exist").state,
|
os.task_manager.get(task_id).expect("task should exist").state,
|
||||||
@ -230,9 +116,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn suspend_task_marks_task_and_process_suspended() {
|
fn suspend_task_marks_task_and_process_suspended() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
let task_id = os.create_vm_game_task(42, "Sector Crawl");
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
||||||
|
|
||||||
os.suspend_task(task_id).expect("suspend should succeed");
|
os.lifecycle().suspend_task(task_id).expect("suspend should succeed");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
os.task_manager.get(task_id).expect("task should exist").state,
|
os.task_manager.get(task_id).expect("task should exist").state,
|
||||||
@ -244,10 +130,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resume_task_marks_suspended_task_foreground_and_process_running() {
|
fn resume_task_marks_suspended_task_foreground_and_process_running() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
let task_id = os.create_vm_game_task(42, "Sector Crawl");
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
||||||
|
|
||||||
os.suspend_task(task_id).expect("suspend should succeed");
|
os.lifecycle().suspend_task(task_id).expect("suspend should succeed");
|
||||||
os.resume_task(task_id).expect("resume should succeed");
|
os.lifecycle().resume_task(task_id).expect("resume should succeed");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
os.task_manager.get(task_id).expect("task should exist").state,
|
os.task_manager.get(task_id).expect("task should exist").state,
|
||||||
@ -259,10 +145,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn close_task_marks_task_closed_and_process_stopped_without_removing_entities() {
|
fn close_task_marks_task_closed_and_process_stopped_without_removing_entities() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
let task_id = os.create_vm_game_task(42, "Sector Crawl");
|
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;
|
let process_id = os.task_manager.get(task_id).expect("task should exist").process_id;
|
||||||
|
|
||||||
os.close_task(task_id).expect("close should succeed");
|
os.lifecycle().close_task(task_id).expect("close should succeed");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
os.task_manager.get(task_id).expect("task should remain").state,
|
os.task_manager.get(task_id).expect("task should remain").state,
|
||||||
@ -277,9 +163,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn crash_task_marks_task_and_process_crashed() {
|
fn crash_task_marks_task_and_process_crashed() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
let task_id = os.create_vm_game_task(42, "Sector Crawl");
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
||||||
|
|
||||||
os.crash_task(task_id, None).expect("crash should succeed");
|
os.lifecycle().crash_task(task_id, None).expect("crash should succeed");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
os.task_manager.get(task_id).expect("task should exist").state,
|
os.task_manager.get(task_id).expect("task should exist").state,
|
||||||
@ -292,28 +178,34 @@ mod tests {
|
|||||||
fn lifecycle_operation_missing_task_returns_typed_error() {
|
fn lifecycle_operation_missing_task_returns_typed_error() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
|
|
||||||
assert_eq!(os.suspend_task(TaskId(999)), Err(LifecycleError::TaskNotFound(TaskId(999))));
|
assert_eq!(
|
||||||
|
os.lifecycle().suspend_task(TaskId(999)),
|
||||||
|
Err(LifecycleError::TaskNotFound(TaskId(999)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lifecycle_operation_missing_process_returns_typed_error() {
|
fn lifecycle_operation_missing_process_returns_typed_error() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
let task_id = os.create_vm_game_task(42, "Sector Crawl");
|
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;
|
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.mark_stopped(process_id);
|
||||||
os.process_manager.remove_stopped();
|
os.process_manager.remove_stopped();
|
||||||
|
|
||||||
assert_eq!(os.suspend_task(task_id), Err(LifecycleError::ProcessNotFound(process_id)));
|
assert_eq!(
|
||||||
|
os.lifecycle().suspend_task(task_id),
|
||||||
|
Err(LifecycleError::ProcessNotFound(process_id))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resume_task_from_non_suspended_state_returns_invalid_transition() {
|
fn resume_task_from_non_suspended_state_returns_invalid_transition() {
|
||||||
let mut os = SystemOS::new(None);
|
let mut os = SystemOS::new(None);
|
||||||
let task_id = os.create_vm_game_task(42, "Sector Crawl");
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
os.resume_task(task_id),
|
os.lifecycle().resume_task(task_id),
|
||||||
Err(LifecycleError::InvalidTransition {
|
Err(LifecycleError::InvalidTransition {
|
||||||
task_id,
|
task_id,
|
||||||
from: TaskState::Foreground,
|
from: TaskState::Foreground,
|
||||||
|
|||||||
@ -61,14 +61,15 @@ impl PrometeuHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some((title, rect, color)) = next_window {
|
if let Some((title, rect, color)) = next_window {
|
||||||
os.window_manager.remove_all_windows();
|
let mut window = os.window();
|
||||||
let id = os.window_manager.add_window(title, rect, color);
|
window.remove_all_windows();
|
||||||
os.window_manager.set_focus(id);
|
let id = window.add_window(title, rect, color);
|
||||||
|
window.set_focus(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) {
|
pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) {
|
||||||
for window in &os.window_manager.windows {
|
for window in os.window().windows() {
|
||||||
hw.gfx_mut().fill_rect(
|
hw.gfx_mut().fill_rect(
|
||||||
window.viewport.x,
|
window.viewport.x,
|
||||||
window.viewport.y,
|
window.viewport.y,
|
||||||
@ -90,17 +91,17 @@ impl PrometeuHub {
|
|||||||
|
|
||||||
self.gui_update(os, hw);
|
self.gui_update(os, hw);
|
||||||
|
|
||||||
if let Some(focused_id) = os.window_manager.focused {
|
if let Some(focused_id) = os.window().focused_window() {
|
||||||
if hw.pad().start().down {
|
if hw.pad().start().down {
|
||||||
os.window_manager.remove_window(focused_id);
|
os.window().close_window(focused_id);
|
||||||
} else {
|
} else {
|
||||||
crash = os.tick_vm(vm, signals, hw);
|
crash = os.vm().tick(vm, signals, hw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.render(os, hw);
|
self.render(os, hw);
|
||||||
hw.gfx_mut().present();
|
hw.gfx_mut().present();
|
||||||
|
|
||||||
SystemProfileUpdate { crash, focused_window_active: os.window_manager.focused.is_some() }
|
SystemProfileUpdate { crash, focused_window_active: os.window().focused_window().is_some() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,11 +58,7 @@ impl HostDebugger {
|
|||||||
// Pre-load cartridge metadata so the Handshake message can contain
|
// Pre-load cartridge metadata so the Handshake message can contain
|
||||||
// valid information about the App being debugged.
|
// valid information about the App being debugged.
|
||||||
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
||||||
let _ = firmware.os.vm_runtime.initialize_vm(
|
let _ = firmware.os.vm().initialize(&mut firmware.vm, &cartridge);
|
||||||
&mut firmware.os.log_service,
|
|
||||||
&mut firmware.vm,
|
|
||||||
&cartridge,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
|
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
|
||||||
@ -121,10 +117,10 @@ impl HostDebugger {
|
|||||||
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
||||||
runtime_version: "0.1".to_string(),
|
runtime_version: "0.1".to_string(),
|
||||||
cartridge: HandshakeCartridge {
|
cartridge: HandshakeCartridge {
|
||||||
app_id: firmware.os.vm_runtime.current_app_id,
|
app_id: firmware.os.vm().current_app_id(),
|
||||||
title: firmware.os.vm_runtime.current_cartridge_title.clone(),
|
title: firmware.os.vm().current_cartridge_title(),
|
||||||
app_version: firmware.os.vm_runtime.current_cartridge_app_version.clone(),
|
app_version: firmware.os.vm().current_cartridge_app_version(),
|
||||||
app_mode: firmware.os.vm_runtime.current_cartridge_app_mode,
|
app_mode: firmware.os.vm().current_cartridge_app_mode(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
self.send_response(handshake);
|
self.send_response(handshake);
|
||||||
@ -142,7 +138,7 @@ impl HostDebugger {
|
|||||||
println!("[Debugger] Connection closed by remote.");
|
println!("[Debugger] Connection closed by remote.");
|
||||||
self.stream = None;
|
self.stream = None;
|
||||||
// Resume VM execution if it was paused waiting for the debugger.
|
// Resume VM execution if it was paused waiting for the debugger.
|
||||||
firmware.os.vm_runtime.paused = false;
|
firmware.os.vm().set_paused(false);
|
||||||
self.waiting_for_start = false;
|
self.waiting_for_start = false;
|
||||||
}
|
}
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
@ -169,7 +165,7 @@ impl HostDebugger {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[Debugger] Connection error: {}", e);
|
eprintln!("[Debugger] Connection error: {}", e);
|
||||||
self.stream = None;
|
self.stream = None;
|
||||||
firmware.os.vm_runtime.paused = false;
|
firmware.os.vm().set_paused(false);
|
||||||
self.waiting_for_start = false;
|
self.waiting_for_start = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,27 +190,23 @@ impl HostDebugger {
|
|||||||
println!("[Debugger] Starting execution...");
|
println!("[Debugger] Starting execution...");
|
||||||
self.waiting_for_start = false;
|
self.waiting_for_start = false;
|
||||||
}
|
}
|
||||||
firmware.os.vm_runtime.paused = false;
|
firmware.os.vm().set_paused(false);
|
||||||
}
|
}
|
||||||
DebugCommand::Pause => {
|
DebugCommand::Pause => {
|
||||||
firmware.os.vm_runtime.paused = true;
|
firmware.os.vm().set_paused(true);
|
||||||
}
|
}
|
||||||
DebugCommand::Resume => {
|
DebugCommand::Resume => {
|
||||||
firmware.os.vm_runtime.paused = false;
|
firmware.os.vm().set_paused(false);
|
||||||
}
|
}
|
||||||
DebugCommand::Step => {
|
DebugCommand::Step => {
|
||||||
// Execute exactly one instruction and keep paused.
|
// Execute exactly one instruction and keep paused.
|
||||||
firmware.os.vm_runtime.paused = true;
|
firmware.os.vm().set_paused(true);
|
||||||
let _ = firmware.os.vm_runtime.debug_step_instruction(
|
let _ = firmware.os.vm().debug_step_instruction(&mut firmware.vm, hardware);
|
||||||
&mut firmware.os.log_service,
|
|
||||||
&mut firmware.vm,
|
|
||||||
hardware,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
DebugCommand::StepFrame => {
|
DebugCommand::StepFrame => {
|
||||||
// Execute until the end of the current logical frame.
|
// Execute until the end of the current logical frame.
|
||||||
firmware.os.vm_runtime.paused = false;
|
firmware.os.vm().set_paused(false);
|
||||||
firmware.os.vm_runtime.debug_step_request = true;
|
firmware.os.vm().request_debug_step();
|
||||||
}
|
}
|
||||||
DebugCommand::GetState => {
|
DebugCommand::GetState => {
|
||||||
// Return detailed VM register and stack state.
|
// Return detailed VM register and stack state.
|
||||||
@ -223,8 +215,8 @@ impl HostDebugger {
|
|||||||
let resp = DebugResponse::GetState {
|
let resp = DebugResponse::GetState {
|
||||||
pc: firmware.vm.pc(),
|
pc: firmware.vm.pc(),
|
||||||
stack_top,
|
stack_top,
|
||||||
frame_index: firmware.os.vm_runtime.logical_frame_index,
|
frame_index: firmware.os.vm().logical_frame_index(),
|
||||||
app_id: firmware.os.vm_runtime.current_app_id,
|
app_id: firmware.os.vm().current_app_id(),
|
||||||
};
|
};
|
||||||
self.send_response(resp);
|
self.send_response(resp);
|
||||||
}
|
}
|
||||||
@ -309,14 +301,14 @@ impl HostDebugger {
|
|||||||
|
|
||||||
/// Scans the system for new information to push to the debugger client.
|
/// Scans the system for new information to push to the debugger client.
|
||||||
fn stream_events(&mut self, firmware: &mut Firmware) {
|
fn stream_events(&mut self, firmware: &mut Firmware) {
|
||||||
if let Some(report) = firmware.os.vm_runtime.last_crash_report.as_ref() {
|
if let Some(report) = firmware.os.vm().last_crash_report() {
|
||||||
self.stream_fault(report);
|
self.stream_fault(report);
|
||||||
} else {
|
} else {
|
||||||
self.last_fault_summary = None;
|
self.last_fault_summary = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Process and send new log entries.
|
// 1. Process and send new log entries.
|
||||||
let new_events = firmware.os.log_service.get_after(self.last_log_seq);
|
let new_events = firmware.os.logs_after(self.last_log_seq);
|
||||||
for event in new_events {
|
for event in new_events {
|
||||||
self.last_log_seq = event.seq;
|
self.last_log_seq = event.seq;
|
||||||
|
|
||||||
@ -324,17 +316,17 @@ impl HostDebugger {
|
|||||||
if event.tag == 0xDEB1 {
|
if event.tag == 0xDEB1 {
|
||||||
self.send_event(DebugEvent::BreakpointHit {
|
self.send_event(DebugEvent::BreakpointHit {
|
||||||
pc: firmware.vm.pc(),
|
pc: firmware.vm.pc(),
|
||||||
frame_index: firmware.os.vm_runtime.logical_frame_index,
|
frame_index: firmware.os.vm().logical_frame_index(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (0xCA01..=0xCA07).contains(&event.tag)
|
if (0xCA01..=0xCA07).contains(&event.tag)
|
||||||
&& let Some(cert_event) = Self::cert_event_from_snapshot(
|
&& let Some(cert_event) = {
|
||||||
event.tag,
|
let telemetry = firmware.os.vm().telemetry_snapshot();
|
||||||
firmware.os.vm_runtime.atomic_telemetry.snapshot(),
|
let cert_config = *firmware.os.vm().cert_config();
|
||||||
&firmware.os.vm_runtime.certifier.config,
|
let frame_index = firmware.os.vm().logical_frame_index();
|
||||||
firmware.os.vm_runtime.logical_frame_index,
|
Self::cert_event_from_snapshot(event.tag, telemetry, &cert_config, frame_index)
|
||||||
)
|
}
|
||||||
{
|
{
|
||||||
self.send_event(cert_event);
|
self.send_event(cert_event);
|
||||||
}
|
}
|
||||||
@ -347,9 +339,9 @@ impl HostDebugger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Send telemetry snapshots at the completion of every frame.
|
// 2. Send telemetry snapshots at the completion of every frame.
|
||||||
let current_frame = firmware.os.vm_runtime.logical_frame_index;
|
let current_frame = firmware.os.vm().logical_frame_index();
|
||||||
if current_frame > self.last_telemetry_frame {
|
if current_frame > self.last_telemetry_frame {
|
||||||
let tel = firmware.os.vm_runtime.atomic_telemetry.snapshot();
|
let tel = firmware.os.vm().telemetry_snapshot();
|
||||||
self.send_event(Self::telemetry_event_from_snapshot(tel));
|
self.send_event(Self::telemetry_event_from_snapshot(tel));
|
||||||
self.last_telemetry_frame = current_frame;
|
self.last_telemetry_frame = current_frame;
|
||||||
}
|
}
|
||||||
@ -407,14 +399,14 @@ mod tests {
|
|||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
|
|
||||||
debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut hardware);
|
debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut hardware);
|
||||||
assert!(firmware.os.vm_runtime.paused);
|
assert!(firmware.os.vm().paused());
|
||||||
|
|
||||||
debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut hardware);
|
debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut hardware);
|
||||||
assert!(!firmware.os.vm_runtime.paused);
|
assert!(!firmware.os.vm().paused());
|
||||||
|
|
||||||
debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut hardware);
|
debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut hardware);
|
||||||
assert!(!firmware.os.vm_runtime.paused);
|
assert!(!firmware.os.vm().paused());
|
||||||
assert!(firmware.os.vm_runtime.debug_step_request);
|
assert!(firmware.os.vm().debug_step_requested());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -424,11 +416,11 @@ mod tests {
|
|||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
|
|
||||||
debugger.waiting_for_start = true;
|
debugger.waiting_for_start = true;
|
||||||
firmware.os.vm_runtime.paused = true;
|
firmware.os.vm().set_paused(true);
|
||||||
|
|
||||||
debugger.handle_command(DebugCommand::Start, &mut firmware, &mut hardware);
|
debugger.handle_command(DebugCommand::Start, &mut firmware, &mut hardware);
|
||||||
|
|
||||||
assert!(!debugger.waiting_for_start);
|
assert!(!debugger.waiting_for_start);
|
||||||
assert!(!firmware.os.vm_runtime.paused);
|
assert!(!firmware.os.vm().paused());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,9 +57,9 @@ struct FrameCanvas<'a> {
|
|||||||
height: usize,
|
height: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> OverlaySnapshot {
|
pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &mut Firmware) -> OverlaySnapshot {
|
||||||
let tel = firmware.os.vm_runtime.atomic_telemetry.snapshot();
|
let tel = firmware.os.vm().telemetry_snapshot();
|
||||||
let recent_logs = firmware.os.log_service.get_recent(10);
|
let recent_logs = firmware.os.recent_logs(10);
|
||||||
let violations_count =
|
let violations_count =
|
||||||
recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count();
|
recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count();
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(report) = firmware.os.vm_runtime.last_crash_report.as_ref() {
|
if let Some(report) = firmware.os.vm().last_crash_report() {
|
||||||
footer.push(OverlayMetric {
|
footer.push(OverlayMetric {
|
||||||
label: "CRASH",
|
label: "CRASH",
|
||||||
value: truncate_value(&report.summary(), 28),
|
value: truncate_value(&report.summary(), 28),
|
||||||
@ -86,9 +86,8 @@ pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &Firmware) -> Overla
|
|||||||
let cycles_ratio = ratio(tel.cycles_used, tel.cycles_budget);
|
let cycles_ratio = ratio(tel.cycles_used, tel.cycles_budget);
|
||||||
let heap_total_bytes = firmware
|
let heap_total_bytes = firmware
|
||||||
.os
|
.os
|
||||||
.vm_runtime
|
.vm()
|
||||||
.certifier
|
.cert_config()
|
||||||
.config
|
|
||||||
.max_heap_bytes
|
.max_heap_bytes
|
||||||
.or(if tel.heap_max_bytes > 0 { Some(tel.heap_max_bytes) } else { None })
|
.or(if tel.heap_max_bytes > 0 { Some(tel.heap_max_bytes) } else { None })
|
||||||
.unwrap_or(OVERLAY_HEAP_FALLBACK_BYTES);
|
.unwrap_or(OVERLAY_HEAP_FALLBACK_BYTES);
|
||||||
|
|||||||
@ -153,7 +153,7 @@ impl HostRunner {
|
|||||||
let mut firmware = Firmware::new(cap_config);
|
let mut firmware = Firmware::new(cap_config);
|
||||||
if let Some(root) = &fs_root {
|
if let Some(root) = &fs_root {
|
||||||
let backend = HostDirBackend::new(root);
|
let backend = HostDirBackend::new(root);
|
||||||
firmware.os.mount_fs(Box::new(backend));
|
firmware.os.fs().mount(Box::new(backend));
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -203,8 +203,8 @@ impl HostRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn machine_running(&self) -> bool {
|
fn machine_running(&mut self) -> bool {
|
||||||
!self.firmware.os.vm_runtime.paused && !self.debugger.waiting_for_start
|
!self.firmware.os.vm().paused() && !self.debugger.waiting_for_start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +266,7 @@ impl ApplicationHandler for HostRunner {
|
|||||||
|
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
let overlay_snapshot =
|
let overlay_snapshot =
|
||||||
self.overlay_enabled.then(|| capture_snapshot(&self.stats, &self.firmware));
|
self.overlay_enabled.then(|| capture_snapshot(&self.stats, &mut self.firmware));
|
||||||
|
|
||||||
// Get Pixels directly from the field (not via helper that gets the entire &mut self)
|
// Get Pixels directly from the field (not via helper that gets the entire &mut self)
|
||||||
let pixels = self.pixels.as_mut().expect("pixels not initialized");
|
let pixels = self.pixels.as_mut().expect("pixels not initialized");
|
||||||
@ -320,25 +320,25 @@ impl ApplicationHandler for HostRunner {
|
|||||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
let was_debugger_connected = self.debugger.stream.is_some();
|
let was_debugger_connected = self.debugger.stream.is_some();
|
||||||
let was_waiting_for_start = self.debugger.waiting_for_start;
|
let was_waiting_for_start = self.debugger.waiting_for_start;
|
||||||
let was_paused = self.firmware.os.vm_runtime.paused;
|
let was_paused = self.firmware.os.vm().paused();
|
||||||
|
|
||||||
// 1. Process pending debug commands from the network.
|
// 1. Process pending debug commands from the network.
|
||||||
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
|
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
|
||||||
|
|
||||||
// Sync inspection mode state. This is host-owned overlay/debugger control,
|
// Sync inspection mode state. This is host-owned overlay/debugger control,
|
||||||
// not a guest-visible debug ABI switch.
|
// not a guest-visible debug ABI switch.
|
||||||
self.firmware.os.vm_runtime.inspection_active =
|
self.firmware
|
||||||
self.overlay_enabled || self.debugger.stream.is_some();
|
.os
|
||||||
|
.vm()
|
||||||
|
.set_inspection_active(self.overlay_enabled || self.debugger.stream.is_some());
|
||||||
|
|
||||||
// 2. Maintain filesystem connection if it was lost (e.g., directory removed).
|
// 2. Maintain a filesystem connection if it was lost (e.g., directory removed).
|
||||||
if let Some(root) = &self.fs_root {
|
if let Some(root) = &self.fs_root
|
||||||
use prometeu_system::fs::FsState;
|
&& self.firmware.os.fs().needs_mount()
|
||||||
if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_))
|
&& std::path::Path::new(root).exists()
|
||||||
&& std::path::Path::new(root).exists()
|
{
|
||||||
{
|
let backend = HostDirBackend::new(root);
|
||||||
let backend = HostDirBackend::new(root);
|
self.firmware.os.fs().mount(Box::new(backend));
|
||||||
self.firmware.os.mount_fs(Box::new(backend));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Timing Management (The heart of determinism).
|
// 3. Timing Management (The heart of determinism).
|
||||||
@ -362,13 +362,13 @@ impl ApplicationHandler for HostRunner {
|
|||||||
// Unless the debugger is waiting for a 'start' command, advance the system.
|
// Unless the debugger is waiting for a 'start' command, advance the system.
|
||||||
if !self.debugger.waiting_for_start {
|
if !self.debugger.waiting_for_start {
|
||||||
self.firmware.tick(&self.input.signals, &mut self.hardware);
|
self.firmware.tick(&self.input.signals, &mut self.hardware);
|
||||||
self.stats.record_host_cpu_time(self.firmware.os.vm_runtime.last_frame_cpu_time_us);
|
self.stats.record_host_cpu_time(self.firmware.os.vm().last_frame_cpu_time_us());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync pause state with audio.
|
// Sync pause state with audio.
|
||||||
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
|
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
|
||||||
// being cleared by the OS if a new logical frame starts in this tick.
|
// being cleared by the OS if a new logical frame starts in this tick.
|
||||||
let is_paused = self.firmware.os.vm_runtime.paused || self.debugger.waiting_for_start;
|
let is_paused = self.firmware.os.vm().paused() || self.debugger.waiting_for_start;
|
||||||
if is_paused != self.last_paused_state {
|
if is_paused != self.last_paused_state {
|
||||||
self.last_paused_state = is_paused;
|
self.last_paused_state = is_paused;
|
||||||
let cmd =
|
let cmd =
|
||||||
@ -383,11 +383,11 @@ impl ApplicationHandler for HostRunner {
|
|||||||
self.stats.record_frame();
|
self.stats.record_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.presentation.note_published_frame(self.firmware.os.vm_runtime.logical_frame_index);
|
self.presentation.note_published_frame(self.firmware.os.vm().logical_frame_index());
|
||||||
|
|
||||||
if was_debugger_connected != self.debugger.stream.is_some()
|
if was_debugger_connected != self.debugger.stream.is_some()
|
||||||
|| was_waiting_for_start != self.debugger.waiting_for_start
|
|| was_waiting_for_start != self.debugger.waiting_for_start
|
||||||
|| was_paused != self.firmware.os.vm_runtime.paused
|
|| was_paused != self.firmware.os.vm().paused()
|
||||||
{
|
{
|
||||||
self.invalidate_host_surface();
|
self.invalidate_host_surface();
|
||||||
}
|
}
|
||||||
@ -396,14 +396,14 @@ impl ApplicationHandler for HostRunner {
|
|||||||
self.audio.update_stats(&mut self.stats);
|
self.audio.update_stats(&mut self.stats);
|
||||||
|
|
||||||
// Update technical statistics displayed in the window title.
|
// Update technical statistics displayed in the window title.
|
||||||
self.stats.update(now, self.window, &self.hardware, &self.firmware);
|
self.stats.update(now, self.window, &self.hardware, &mut self.firmware);
|
||||||
|
|
||||||
// Synchronize system logs to the host console.
|
// Synchronize system logs to the host console.
|
||||||
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);
|
let last_seq = self.log_sink.last_seq().unwrap_or(u64::MAX);
|
||||||
let new_events = if last_seq == u64::MAX {
|
let new_events = if last_seq == u64::MAX {
|
||||||
self.firmware.os.log_service.get_recent(4096)
|
self.firmware.os.recent_logs(4096)
|
||||||
} else {
|
} else {
|
||||||
self.firmware.os.log_service.get_after(last_seq)
|
self.firmware.os.logs_after(last_seq)
|
||||||
};
|
};
|
||||||
self.log_sink.process_events(new_events);
|
self.log_sink.process_events(new_events);
|
||||||
|
|
||||||
@ -813,7 +813,7 @@ mod tests {
|
|||||||
stream.write_all(b"{\"type\":\"pause\"}\n").expect("Should write pause");
|
stream.write_all(b"{\"type\":\"pause\"}\n").expect("Should write pause");
|
||||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||||
assert!(runner.firmware.os.vm_runtime.paused, "VM should be paused");
|
assert!(runner.firmware.os.vm().paused(), "VM should be paused");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Disconnect (stream goes out of scope)
|
// 2. Disconnect (stream goes out of scope)
|
||||||
@ -821,7 +821,7 @@ mod tests {
|
|||||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||||
|
|
||||||
// 3. Check if unpaused
|
// 3. Check if unpaused
|
||||||
assert!(!runner.firmware.os.vm_runtime.paused, "VM should have unpaused after disconnect");
|
assert!(!runner.firmware.os.vm().paused(), "VM should have unpaused after disconnect");
|
||||||
assert!(!runner.debugger.waiting_for_start, "VM should have left waiting_for_start state");
|
assert!(!runner.debugger.waiting_for_start, "VM should have left waiting_for_start state");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ impl HostStats {
|
|||||||
now: Instant,
|
now: Instant,
|
||||||
window: Option<&Window>,
|
window: Option<&Window>,
|
||||||
_hardware: &Hardware,
|
_hardware: &Hardware,
|
||||||
firmware: &Firmware,
|
firmware: &mut Firmware,
|
||||||
) {
|
) {
|
||||||
let stats_elapsed = now.duration_since(self.last_stats_update);
|
let stats_elapsed = now.duration_since(self.last_stats_update);
|
||||||
if stats_elapsed >= Duration::from_secs(1) {
|
if stats_elapsed >= Duration::from_secs(1) {
|
||||||
@ -88,14 +88,9 @@ impl HostStats {
|
|||||||
self.current_fps,
|
self.current_fps,
|
||||||
cpu_load_core,
|
cpu_load_core,
|
||||||
cpu_load_audio,
|
cpu_load_audio,
|
||||||
firmware.os.vm_runtime.tick_index,
|
firmware.os.vm().tick_index(),
|
||||||
firmware.os.vm_runtime.logical_frame_index,
|
firmware.os.vm().logical_frame_index(),
|
||||||
firmware
|
firmware.os.vm().completed_logical_frames(),
|
||||||
.os
|
|
||||||
.vm_runtime
|
|
||||||
.atomic_telemetry
|
|
||||||
.completed_logical_frames
|
|
||||||
.load(std::sync::atomic::Ordering::Relaxed),
|
|
||||||
);
|
);
|
||||||
window.set_title(&title);
|
window.set_title(&title);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{"type":"meta","next_id":{"DSC":34,"AGD":34,"DEC":26,"PLN":58,"LSN":41,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":35,"AGD":35,"DEC":27,"PLN":59,"LSN":41,"CLSN":1}}
|
||||||
|
{"type":"discussion","id":"DSC-0034","status":"in_progress","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-agenda-systemos-domain-facades.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0026","file":"DEC-0026-systemos-domain-facades.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0058","file":"PLN-0058-systemos-domain-facades-first-wave.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0026"]}],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
|||||||
@ -0,0 +1,353 @@
|
|||||||
|
---
|
||||||
|
id: AGD-0034
|
||||||
|
ticket: system-os-domain-facades
|
||||||
|
title: Agenda - SystemOS Domain Facades
|
||||||
|
status: accepted
|
||||||
|
created: 2026-05-15
|
||||||
|
resolved: 2026-05-15
|
||||||
|
decision: DEC-0026
|
||||||
|
tags: [runtime, os, services, api-surface, lifecycle, fs]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agenda - SystemOS Domain Facades
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
Depois de `DEC-0024` e `DEC-0025`, `SystemOS` passou a concentrar ownership e
|
||||||
|
coordenação de vários domínios do Prometeu OS:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SystemOS
|
||||||
|
vm_runtime
|
||||||
|
process_manager
|
||||||
|
task_manager
|
||||||
|
window_manager
|
||||||
|
log_service
|
||||||
|
fs / fs_state / open_files / next_handle
|
||||||
|
memcard
|
||||||
|
lifecycle methods
|
||||||
|
VM initialize/tick helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
Essa concentração é correta do ponto de vista de ownership: o OS deve ser dono
|
||||||
|
ou mediador dos serviços do console. O problema agora é a superfície pública do
|
||||||
|
root. Se tudo fica como `os.bla()`, `os.campo.bla()` ou campos públicos soltos,
|
||||||
|
o `SystemOS` vira um objeto largo demais e deixa de comunicar domínio.
|
||||||
|
|
||||||
|
A intenção proposta é separar a superfície em compartimentos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
os.lifecycle().suspend_task(...)
|
||||||
|
os.lifecycle().resume_task(...)
|
||||||
|
os.lifecycle().crash_task(...)
|
||||||
|
|
||||||
|
os.sessions().create_vm_game_task(...)
|
||||||
|
os.sessions().create_vm_shell_task(...)
|
||||||
|
os.sessions().create_native_shell_task(...)
|
||||||
|
|
||||||
|
os.vm().initialize(...)
|
||||||
|
os.vm().tick(...)
|
||||||
|
|
||||||
|
os.fs().mount(...)
|
||||||
|
os.fs().open(...)
|
||||||
|
os.fs().read(...)
|
||||||
|
os.fs().write(...)
|
||||||
|
os.fs().close(...)
|
||||||
|
|
||||||
|
os.window().add_window(...)
|
||||||
|
os.window().set_focus(...)
|
||||||
|
os.window().focused_window(...)
|
||||||
|
os.window().close_window(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Logging pode continuar na raiz como operação transversal:
|
||||||
|
|
||||||
|
```text
|
||||||
|
os.log(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
O `SystemOS` está crescendo wild na raiz. Isso cria alguns riscos:
|
||||||
|
|
||||||
|
- novos domínios tendem a adicionar mais métodos diretamente em `SystemOS`;
|
||||||
|
- lifecycle, filesystem, window, VM runtime e memcard competem pela mesma
|
||||||
|
superfície;
|
||||||
|
- testes e firmware continuam alcançando managers internos diretamente;
|
||||||
|
- fica difícil distinguir API semântica de OS de detalhe interno de serviço;
|
||||||
|
- a raiz pública pode virar uma coleção de campos públicos em vez de uma
|
||||||
|
interface de sistema.
|
||||||
|
|
||||||
|
O ponto importante: não queremos desfazer o ownership do `SystemOS`. Queremos
|
||||||
|
compartimentalizar o acesso.
|
||||||
|
|
||||||
|
## Pontos Criticos
|
||||||
|
|
||||||
|
- `SystemOS` deve continuar sendo o boundary de ownership e coordenação.
|
||||||
|
- Facades não devem virar owners independentes que duplicam estado.
|
||||||
|
- A raiz do OS deve ficar pequena e expressiva.
|
||||||
|
- Lifecycle deve continuar coordenando `TaskState` e `ProcessState` junto.
|
||||||
|
- Filesystem tem estado composto (`VirtualFS`, `FsState`, `open_files`,
|
||||||
|
`next_handle`) e por isso se beneficia de uma facade própria.
|
||||||
|
- Window management já é serviço do OS, mas `PrometeuHub` e firmware ainda
|
||||||
|
acessam `os.window_manager` diretamente.
|
||||||
|
- VM runtime talvez também precise de facade ou área própria, mas isso pode ser
|
||||||
|
uma etapa posterior para não misturar com lifecycle/fs.
|
||||||
|
- O formato da facade precisa respeitar borrow rules em Rust; campos públicos
|
||||||
|
simples podem não ser viáveis quando uma operação precisa de múltiplos campos
|
||||||
|
internos do `SystemOS`.
|
||||||
|
|
||||||
|
## Opcoes
|
||||||
|
|
||||||
|
### Opcao A - Manter tudo na raiz do SystemOS
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
Continuar adicionando métodos e campos diretamente em `SystemOS`.
|
||||||
|
|
||||||
|
**Vantagens:**
|
||||||
|
- menor mudança imediata;
|
||||||
|
- callsites simples;
|
||||||
|
- menos tipos auxiliares;
|
||||||
|
- evita atrito com borrow checker.
|
||||||
|
|
||||||
|
**Custos / Riscos:**
|
||||||
|
- a raiz continua crescendo;
|
||||||
|
- domínios ficam misturados;
|
||||||
|
- incentiva acesso direto a managers internos;
|
||||||
|
- torna mais difícil explicar a API do OS;
|
||||||
|
- cada nova capability aumenta a pressão no mesmo arquivo e no mesmo objeto.
|
||||||
|
|
||||||
|
**Manutenibilidade:**
|
||||||
|
Fraca no médio prazo.
|
||||||
|
|
||||||
|
### Opcao B - Criar facades de domínio em torno do SystemOS
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
Criar facades estreitas por domínio, expostas como acesso semântico:
|
||||||
|
|
||||||
|
```text
|
||||||
|
os.lifecycle().suspend_task(...)
|
||||||
|
os.fs().mount(...)
|
||||||
|
os.window().set_focus(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou, se o design permitir sem duplicar ownership:
|
||||||
|
|
||||||
|
```text
|
||||||
|
os.lifecycle.suspend_task(...)
|
||||||
|
os.fs.mount(...)
|
||||||
|
os.window.set_focus(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
As facades seriam views/handles sobre os campos internos do `SystemOS`, não
|
||||||
|
owners independentes.
|
||||||
|
|
||||||
|
**Vantagens:**
|
||||||
|
- deixa a raiz do OS menor;
|
||||||
|
- agrupa API por domínio;
|
||||||
|
- torna mais claro o que é lifecycle, fs, window, memcard e VM;
|
||||||
|
- ajuda a esconder managers internos;
|
||||||
|
- reduz a chance de firmware coordenar detalhes por fora.
|
||||||
|
|
||||||
|
**Custos / Riscos:**
|
||||||
|
- exige decidir padrão de borrowing;
|
||||||
|
- pode adicionar tipos pequenos e boilerplate;
|
||||||
|
- `os.lifecycle.bla()` como campo direto pode ser difícil se a facade precisar
|
||||||
|
acessar `task_manager` e `process_manager` que vivem no mesmo `SystemOS`;
|
||||||
|
- talvez precise começar com métodos `os.lifecycle()` em vez de campos públicos.
|
||||||
|
|
||||||
|
**Manutenibilidade:**
|
||||||
|
Forte se as facades forem views estreitas e não novos owners.
|
||||||
|
|
||||||
|
### Opcao C - Criar services owners independentes dentro do SystemOS
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
Mover estado e lógica para structs owned diretamente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SystemOS
|
||||||
|
lifecycle: LifecycleService
|
||||||
|
fs: FileSystemService
|
||||||
|
window: WindowService
|
||||||
|
```
|
||||||
|
|
||||||
|
Cada service seria owner do seu próprio estado ou de parte dele.
|
||||||
|
|
||||||
|
**Vantagens:**
|
||||||
|
- aproxima o shape desejado `os.lifecycle.bla()`;
|
||||||
|
- reduz métodos no root;
|
||||||
|
- pode simplificar alguns domínios autocontidos.
|
||||||
|
|
||||||
|
**Custos / Riscos:**
|
||||||
|
- lifecycle precisa coordenar task/process, então ownership separado pode criar
|
||||||
|
borrow/coordenação mais difícil;
|
||||||
|
- filesystem hoje cruza VM syscall state, `VirtualFS`, `FsState`, handles e
|
||||||
|
logging;
|
||||||
|
- pode reabrir decisões de ownership que acabaram de estabilizar;
|
||||||
|
- risco de mover estado cedo demais para caber numa estética de API.
|
||||||
|
|
||||||
|
**Manutenibilidade:**
|
||||||
|
Boa para domínios autocontidos, arriscada para lifecycle neste momento.
|
||||||
|
|
||||||
|
## Sugestao / Recomendacao
|
||||||
|
|
||||||
|
A direção recomendada é **Opção B: facades de domínio como views estreitas sobre
|
||||||
|
o `SystemOS`**.
|
||||||
|
|
||||||
|
O corte recomendado é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SystemOS
|
||||||
|
lifecycle()
|
||||||
|
set_foreground_task
|
||||||
|
suspend_task
|
||||||
|
resume_task
|
||||||
|
close_task
|
||||||
|
crash_task
|
||||||
|
|
||||||
|
sessions()
|
||||||
|
create_vm_game_task
|
||||||
|
create_vm_shell_task
|
||||||
|
create_native_shell_task
|
||||||
|
|
||||||
|
vm()
|
||||||
|
initialize
|
||||||
|
tick
|
||||||
|
|
||||||
|
fs()
|
||||||
|
mount
|
||||||
|
open
|
||||||
|
read
|
||||||
|
write
|
||||||
|
close
|
||||||
|
|
||||||
|
window()
|
||||||
|
add_window
|
||||||
|
set_focus
|
||||||
|
focused_window
|
||||||
|
close_window
|
||||||
|
|
||||||
|
log(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
E evitar que a raiz acumule:
|
||||||
|
|
||||||
|
```text
|
||||||
|
suspend_task
|
||||||
|
resume_task
|
||||||
|
close_task
|
||||||
|
crash_task
|
||||||
|
mount_fs
|
||||||
|
initialize_vm
|
||||||
|
tick_vm
|
||||||
|
create_vm_game_task
|
||||||
|
create_vm_shell_task
|
||||||
|
create_native_shell_task
|
||||||
|
window_manager direto
|
||||||
|
task_manager direto
|
||||||
|
process_manager direto
|
||||||
|
vm_runtime direto
|
||||||
|
fs direto
|
||||||
|
memcard direto
|
||||||
|
```
|
||||||
|
|
||||||
|
Para Rust, o primeiro corte provavelmente deve usar métodos de acesso que
|
||||||
|
retornam views:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
os.lifecycle().suspend_task(task_id)
|
||||||
|
os.fs().mount(backend)
|
||||||
|
os.window().set_focus(window_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso é menos bonito que campo direto, mas evita congelar ownership errado. Se
|
||||||
|
mais tarde algum domínio puder virar service owner real, podemos migrar para
|
||||||
|
campo direto sem mudar a semântica.
|
||||||
|
|
||||||
|
Logging pode permanecer na raiz:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
os.log(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Motivo: logging é transversal, curto, usado por várias etapas do firmware, e não
|
||||||
|
carrega sozinho uma política complexa de estado.
|
||||||
|
|
||||||
|
## Perguntas em Aberto
|
||||||
|
|
||||||
|
- [x] Queremos a sintaxe ideal `os.lifecycle.bla()` mesmo que isso force
|
||||||
|
services owners reais, ou aceitamos `os.lifecycle().bla()` como primeira forma
|
||||||
|
borrow-friendly? quando escrevi os.lifecycle.bla() eu queria mesmo era os.lifecycle().bla()
|
||||||
|
- [x] Quais domínios entram no primeiro corte: só `lifecycle` e `fs`, ou também
|
||||||
|
`window`? nao precisamos criar nada, mas lifecycle, fs, window e vm sao easy wins no meu ponto de vista
|
||||||
|
- [x] `create_vm_game_task` e `create_vm_shell_task` pertencem a
|
||||||
|
`os.lifecycle()`, a `os.processes()/os.tasks()`, ou a um futuro domínio
|
||||||
|
`os.apps()/os.sessions()`? vamos de os.session() para esses.
|
||||||
|
- [x] `tick_vm` e `initialize_vm` devem ficar temporariamente na raiz, ou já
|
||||||
|
entrar em `os.vm()`? isso, os.vm().
|
||||||
|
- [x] Campos como `task_manager`, `process_manager`, `window_manager`,
|
||||||
|
`vm_runtime`, `fs`, `memcard` devem deixar de ser públicos no mesmo corte ou
|
||||||
|
em planos graduais? eu nao os deixaria publicos desde jah, isso forca a politica de facades dentro de system os.
|
||||||
|
- [x] Como preservar testes sem criar APIs públicas só para inspeção? Testes devem ser feitos nos subdomínios; SystemOS só testa composição e coordenação que não pertence isoladamente a um domínio.
|
||||||
|
|
||||||
|
## Resolucao Proposta
|
||||||
|
|
||||||
|
A agenda deve fechar pela **Opção B**.
|
||||||
|
|
||||||
|
O `SystemOS` continua sendo o owner e boundary de coordenação, mas sua superfície
|
||||||
|
pública deve ser compartimentalizada em facades de domínio. A forma normativa
|
||||||
|
inicial deve ser por métodos que retornam views borrow-friendly:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
os.lifecycle().suspend_task(task_id)
|
||||||
|
os.sessions().create_vm_game_task(app_id, title)
|
||||||
|
os.vm().initialize(vm, cartridge)
|
||||||
|
os.fs().mount(backend)
|
||||||
|
os.window().set_focus(window_id)
|
||||||
|
os.log(level, source, tag, msg)
|
||||||
|
```
|
||||||
|
|
||||||
|
A sintaxe com campo direto (`os.lifecycle.suspend_task(...)`) não é requisito
|
||||||
|
deste corte. A intenção semântica é domínio explícito; a forma prática inicial
|
||||||
|
deve respeitar ownership e borrowing em Rust.
|
||||||
|
|
||||||
|
Domínios do primeiro corte:
|
||||||
|
|
||||||
|
- `lifecycle()`: lifecycle semântico de task/process.
|
||||||
|
- `sessions()`: criação de tasks/processos de jogo e shell.
|
||||||
|
- `vm()`: inicialização e tick da VM.
|
||||||
|
- `fs()`: filesystem operacional.
|
||||||
|
- `window()`: operações mínimas de janela/foco.
|
||||||
|
- `log(...)`: permanece na raiz como operação transversal.
|
||||||
|
|
||||||
|
Campos internos como `task_manager`, `process_manager`, `window_manager`,
|
||||||
|
`vm_runtime`, `fs`, `fs_state`, `memcard`, `open_files` e `next_handle` devem
|
||||||
|
deixar de ser públicos já neste corte, salvo se um plan específico demonstrar
|
||||||
|
necessidade temporária de migração. A política desejada é forçar acesso por
|
||||||
|
facades dentro do `SystemOS`.
|
||||||
|
|
||||||
|
Testes devem migrar para os subdomínios quando estiverem validando comportamento
|
||||||
|
de domínio. `SystemOS` deve testar apenas composição e coordenação que realmente
|
||||||
|
não pertence isoladamente a um domínio.
|
||||||
|
|
||||||
|
## Criterio para Encerrar
|
||||||
|
|
||||||
|
Esta agenda pode virar decisão quando fecharmos:
|
||||||
|
|
||||||
|
- qual forma de facade será normativa: campo direto, método view, ou service
|
||||||
|
owner; **resolvido: método view borrow-friendly.**
|
||||||
|
- quais domínios entram no primeiro corte;
|
||||||
|
**resolvido: lifecycle, sessions, vm, fs, window e log na raiz.**
|
||||||
|
- quais APIs permanecem no root;
|
||||||
|
**resolvido: `new`/construção e `log(...)`; demais APIs devem migrar para
|
||||||
|
facades.**
|
||||||
|
- se campos internos deixam de ser públicos agora ou em ondas;
|
||||||
|
**resolvido: deixar de ser públicos já no corte, com exceção transitória
|
||||||
|
somente se plan demonstrar necessidade.**
|
||||||
|
- como lifecycle, fs e window serão acessados por firmware, Hub e testes.
|
||||||
|
**resolvido: por facades; testes de domínio nos subdomínios.**
|
||||||
|
|
||||||
|
## Proximo Passo
|
||||||
|
|
||||||
|
Esta agenda está pronta para decisão normativa se a resolução proposta for
|
||||||
|
aceita.
|
||||||
@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
id: DEC-0026
|
||||||
|
ticket: system-os-domain-facades
|
||||||
|
title: SystemOS Domain Facades
|
||||||
|
status: accepted
|
||||||
|
created: 2026-05-15
|
||||||
|
ref_agenda: AGD-0034
|
||||||
|
tags: [runtime, os, services, api-surface, lifecycle, fs]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted. Esta decisão foi aceita em 2026-05-15 e passa a ser referência
|
||||||
|
normativa para compartimentalizar a superfície pública do `SystemOS`.
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
`DEC-0024` consolidou `SystemOS` como owner de serviços do Prometeu OS.
|
||||||
|
`DEC-0025` colocou o lifecycle de `Task` e `Process` sob autoridade do
|
||||||
|
`SystemOS`.
|
||||||
|
|
||||||
|
Essas decisões estão corretas em termos de ownership, mas fizeram a raiz do
|
||||||
|
`SystemOS` crescer como superfície pública. Hoje a raiz mistura runtime VM,
|
||||||
|
filesystem, memcard, window management, lifecycle, managers internos, handles
|
||||||
|
operacionais e helpers de criação de tasks/processos.
|
||||||
|
|
||||||
|
Esta decisão não muda o ownership: `SystemOS` continua sendo o boundary do OS.
|
||||||
|
O que muda é a forma normativa de acesso aos domínios.
|
||||||
|
|
||||||
|
## Decisao
|
||||||
|
|
||||||
|
`SystemOS` SHALL expose OS behavior through domain facades.
|
||||||
|
|
||||||
|
The canonical first-wave access shape SHALL be method-based, borrow-friendly
|
||||||
|
views:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
os.lifecycle().suspend_task(task_id)
|
||||||
|
os.sessions().create_vm_game_task(app_id, title)
|
||||||
|
os.vm().initialize(vm, cartridge)
|
||||||
|
os.fs().mount(backend)
|
||||||
|
os.window().set_focus(window_id)
|
||||||
|
os.log(level, source, tag, msg)
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct field syntax such as `os.lifecycle.suspend_task(...)` is NOT required in
|
||||||
|
this wave. Domain clarity is normative; direct field ownership is not.
|
||||||
|
|
||||||
|
The first-wave `SystemOS` root SHALL keep only minimal root operations:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SystemOS
|
||||||
|
new(...)
|
||||||
|
log(...)
|
||||||
|
lifecycle()
|
||||||
|
sessions()
|
||||||
|
vm()
|
||||||
|
fs()
|
||||||
|
window()
|
||||||
|
```
|
||||||
|
|
||||||
|
`log(...)` MAY remain on the root because logging is a cross-cutting operation
|
||||||
|
used by multiple firmware and OS domains.
|
||||||
|
|
||||||
|
The first-wave domain surface SHALL be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
lifecycle()
|
||||||
|
set_foreground_task
|
||||||
|
suspend_task
|
||||||
|
resume_task
|
||||||
|
close_task
|
||||||
|
crash_task
|
||||||
|
|
||||||
|
sessions()
|
||||||
|
create_vm_game_task
|
||||||
|
create_vm_shell_task
|
||||||
|
create_native_shell_task
|
||||||
|
|
||||||
|
vm()
|
||||||
|
initialize
|
||||||
|
tick
|
||||||
|
|
||||||
|
fs()
|
||||||
|
mount
|
||||||
|
open
|
||||||
|
read
|
||||||
|
write
|
||||||
|
close
|
||||||
|
|
||||||
|
window()
|
||||||
|
add_window
|
||||||
|
set_focus
|
||||||
|
focused_window
|
||||||
|
close_window
|
||||||
|
```
|
||||||
|
|
||||||
|
Internal service state SHALL NOT remain public on `SystemOS` once the
|
||||||
|
corresponding facade exists. This includes:
|
||||||
|
|
||||||
|
- `task_manager`
|
||||||
|
- `process_manager`
|
||||||
|
- `window_manager`
|
||||||
|
- `vm_runtime`
|
||||||
|
- `fs`
|
||||||
|
- `fs_state`
|
||||||
|
- `memcard`
|
||||||
|
- `open_files`
|
||||||
|
- `next_handle`
|
||||||
|
|
||||||
|
Plans MAY use temporary transitional access only when needed to keep migrations
|
||||||
|
small, but the completed first-wave facade migration MUST remove direct public
|
||||||
|
access for migrated domains.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
`SystemOS` should remain the owner of OS services, but callers should not see it
|
||||||
|
as a bag of unrelated fields and root methods. Domain facades make the API read
|
||||||
|
like an OS surface:
|
||||||
|
|
||||||
|
- lifecycle operations live under lifecycle;
|
||||||
|
- session construction lives under sessions;
|
||||||
|
- VM execution lives under vm;
|
||||||
|
- filesystem operations live under fs;
|
||||||
|
- window operations live under window;
|
||||||
|
- logging remains root-level because it cuts across domains.
|
||||||
|
|
||||||
|
Method-based facades are preferred over direct fields in this wave because they
|
||||||
|
preserve Rust borrowing flexibility. A facade can be a temporary view over
|
||||||
|
multiple internal fields without forcing premature service ownership splits.
|
||||||
|
|
||||||
|
This also protects `DEC-0025`: lifecycle remains the semantic coordinator for
|
||||||
|
`TaskState` and `ProcessState`, but callers reach it through
|
||||||
|
`os.lifecycle()`, not through root methods or direct manager access.
|
||||||
|
|
||||||
|
## Invariantes / Contrato
|
||||||
|
|
||||||
|
- `SystemOS` MUST remain the OS ownership and coordination boundary.
|
||||||
|
- Facades MUST be views or handles over `SystemOS` internals unless a later
|
||||||
|
decision explicitly promotes a domain to an independently owned service.
|
||||||
|
- Facades MUST NOT duplicate state.
|
||||||
|
- Root-level `SystemOS` API MUST stay minimal.
|
||||||
|
- Lifecycle operations MUST move from root-level `SystemOS` methods to
|
||||||
|
`os.lifecycle()`.
|
||||||
|
- Session creation helpers MUST move to `os.sessions()`.
|
||||||
|
- VM initialization and ticking MUST move to `os.vm()`.
|
||||||
|
- Filesystem operations MUST move to `os.fs()`.
|
||||||
|
- Window operations MUST move to `os.window()`.
|
||||||
|
- Logging MAY remain as `os.log(...)`.
|
||||||
|
- Firmware, Hub and tests SHOULD use facades instead of direct internal fields.
|
||||||
|
- Tests for domain behavior SHOULD live at the facade/domain level.
|
||||||
|
- `SystemOS` tests SHOULD focus on composition and cross-domain coordination.
|
||||||
|
|
||||||
|
## Impactos
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/src/os/` should gain facade/view types for
|
||||||
|
lifecycle, sessions, VM, filesystem and window domains.
|
||||||
|
- Existing root methods such as `suspend_task`, `resume_task`, `close_task`,
|
||||||
|
`crash_task`, `initialize_vm`, `tick_vm`, `mount_fs`, `create_vm_game_task`,
|
||||||
|
`create_vm_shell_task` and `create_native_shell_task` should migrate behind
|
||||||
|
domain facades.
|
||||||
|
- Direct access to managers and service fields from firmware, Hub and tests
|
||||||
|
should be removed or narrowed.
|
||||||
|
- Filesystem state may require a dedicated facade because it spans `VirtualFS`,
|
||||||
|
`FsState`, file handles, memcard-adjacent state and logging.
|
||||||
|
- Window access should move away from `os.window_manager` to `os.window()`.
|
||||||
|
- Specs do not need immediate updates unless these facades become part of a
|
||||||
|
published external API.
|
||||||
|
|
||||||
|
## Referencias
|
||||||
|
|
||||||
|
- Agenda: `AGD-0034`
|
||||||
|
- Related decision: `DEC-0024`
|
||||||
|
- Related decision: `DEC-0025`
|
||||||
|
|
||||||
|
## Propagacao Necessaria
|
||||||
|
|
||||||
|
- Plans: create small plans for lifecycle facade, sessions/vm facade, fs facade,
|
||||||
|
window facade and internal field encapsulation.
|
||||||
|
- Code: update `prometeu-system`, firmware, Hub and tests to use domain
|
||||||
|
facades.
|
||||||
|
- Tests: move lifecycle/session/fs/window assertions to domain-level tests where
|
||||||
|
possible.
|
||||||
|
- Discussion: keep this decision separate from future decisions about true
|
||||||
|
service ownership splits.
|
||||||
|
|
||||||
|
## Revisao
|
||||||
|
|
||||||
|
- 2026-05-15: Initial accepted decision from `AGD-0034`.
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0058
|
||||||
|
ticket: system-os-domain-facades
|
||||||
|
title: SystemOS Domain Facades First Wave
|
||||||
|
status: open
|
||||||
|
created: 2026-05-15
|
||||||
|
ref_decisions: [DEC-0026]
|
||||||
|
tags: [runtime, os, services, api-surface, lifecycle, fs]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Implement the first wave of `SystemOS` domain facades required by `DEC-0026`.
|
||||||
|
`SystemOS` remains the OS ownership boundary, but callers must access behavior
|
||||||
|
through domain views instead of root-level methods or public internal fields.
|
||||||
|
|
||||||
|
## Source Decisions
|
||||||
|
|
||||||
|
- `DEC-0026`: `SystemOS` must expose method-based domain facades for lifecycle,
|
||||||
|
sessions, VM, filesystem and window operations, while keeping `log(...)` on
|
||||||
|
the root.
|
||||||
|
- Related: `DEC-0025` keeps lifecycle coordination under `SystemOS`.
|
||||||
|
- Related: `DEC-0024` keeps services owned or mediated by `SystemOS`.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Create these first-wave access shapes:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
os.lifecycle().suspend_task(task_id)
|
||||||
|
os.sessions().create_vm_game_task(app_id, title)
|
||||||
|
os.vm().initialize(vm, cartridge)
|
||||||
|
os.vm().tick(vm, signals, hw)
|
||||||
|
os.fs().mount(backend)
|
||||||
|
os.window().set_focus(window_id)
|
||||||
|
os.log(level, source, tag, msg)
|
||||||
|
```
|
||||||
|
|
||||||
|
After this plan, migrated domains must no longer require public direct access
|
||||||
|
to `task_manager`, `process_manager`, `window_manager`, `vm_runtime`, `fs`,
|
||||||
|
`fs_state`, `memcard`, `open_files` or `next_handle`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add facade/view types under `crates/console/prometeu-system/src/os/`.
|
||||||
|
- Add `SystemOS` methods:
|
||||||
|
- `lifecycle()`
|
||||||
|
- `sessions()`
|
||||||
|
- `vm()`
|
||||||
|
- `fs()`
|
||||||
|
- `window()`
|
||||||
|
- Move root lifecycle methods behind `lifecycle()`.
|
||||||
|
- Move task/process session creation behind `sessions()`.
|
||||||
|
- Move VM initialization/ticking/reset or execution helpers behind `vm()`.
|
||||||
|
- Move filesystem mount/open/read/write/close entry points behind `fs()` when
|
||||||
|
the operation exists at OS level.
|
||||||
|
- Move window operations behind `window()`.
|
||||||
|
- Keep `SystemOS::log(...)` at root.
|
||||||
|
- Migrate firmware, Hub and tests to facade access.
|
||||||
|
- Make migrated internal `SystemOS` fields private.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Changing service ownership established by `DEC-0024`.
|
||||||
|
- Changing lifecycle semantics established by `DEC-0025`.
|
||||||
|
- Introducing `os.lifecycle` as a direct public field.
|
||||||
|
- Background lifecycle semantics.
|
||||||
|
- New filesystem syscall behavior beyond exposing existing OS-owned operations.
|
||||||
|
- Diagnostics/crash report persistence.
|
||||||
|
- Redesigning Hub UI or app switching.
|
||||||
|
- Replacing VM runtime tests that intentionally test `VirtualMachineRuntime`
|
||||||
|
directly.
|
||||||
|
|
||||||
|
## Execution Plan
|
||||||
|
|
||||||
|
1. Create facade modules and view types.
|
||||||
|
- Target files:
|
||||||
|
- `crates/console/prometeu-system/src/os/mod.rs`
|
||||||
|
- `crates/console/prometeu-system/src/os/system_os.rs`
|
||||||
|
- new facade modules under `crates/console/prometeu-system/src/os/`
|
||||||
|
- Define mutable view structs such as `LifecycleFacade<'a>`,
|
||||||
|
`SessionsFacade<'a>`, `VmFacade<'a>`, `FsFacade<'a>` and
|
||||||
|
`WindowFacade<'a>`.
|
||||||
|
- Each facade should borrow the needed `SystemOS` internals and must not own
|
||||||
|
duplicated state.
|
||||||
|
|
||||||
|
2. Add `SystemOS` facade accessors.
|
||||||
|
- Add `SystemOS::lifecycle(&mut self) -> LifecycleFacade<'_>`.
|
||||||
|
- Add `SystemOS::sessions(&mut self) -> SessionsFacade<'_>`.
|
||||||
|
- Add `SystemOS::vm(&mut self) -> VmFacade<'_>`.
|
||||||
|
- Add `SystemOS::fs(&mut self) -> FsFacade<'_>`.
|
||||||
|
- Add `SystemOS::window(&mut self) -> WindowFacade<'_>`.
|
||||||
|
- Keep `SystemOS::new(...)` and `SystemOS::log(...)` on the root.
|
||||||
|
|
||||||
|
3. Move lifecycle operations.
|
||||||
|
- Target existing logic in `crates/console/prometeu-system/src/os/system_os.rs`.
|
||||||
|
- Move or delegate:
|
||||||
|
- `set_foreground_task`
|
||||||
|
- `suspend_task`
|
||||||
|
- `resume_task`
|
||||||
|
- `close_task`
|
||||||
|
- `crash_task`
|
||||||
|
- The facade must preserve typed `LifecycleError` behavior.
|
||||||
|
- Callers should use `os.lifecycle().crash_task(...)` rather than
|
||||||
|
`os.crash_task(...)`.
|
||||||
|
|
||||||
|
4. Move session creation operations.
|
||||||
|
- Move or delegate:
|
||||||
|
- `create_vm_game_task`
|
||||||
|
- `create_vm_shell_task`
|
||||||
|
- `create_native_shell_task`
|
||||||
|
- These operations may call the lifecycle facade internally to foreground
|
||||||
|
newly created tasks.
|
||||||
|
- Firmware should use `ctx.os.sessions().create_vm_game_task(...)` and
|
||||||
|
`ctx.os.sessions().create_vm_shell_task(...)`.
|
||||||
|
|
||||||
|
5. Move VM operations.
|
||||||
|
- Move or delegate:
|
||||||
|
- `initialize_vm` as `os.vm().initialize(...)`
|
||||||
|
- `tick_vm` as `os.vm().tick(...)`
|
||||||
|
- Include reset access if needed to remove direct `os.vm_runtime.reset(...)`
|
||||||
|
from firmware.
|
||||||
|
- Preserve VM runtime behavior and logging.
|
||||||
|
|
||||||
|
6. Move filesystem operations.
|
||||||
|
- Move or delegate existing root filesystem operations:
|
||||||
|
- `mount_fs` as `os.fs().mount(...)`
|
||||||
|
- `unmount_fs` if still present, even though `DEC-0026` only names
|
||||||
|
mount/open/read/write/close for the target surface.
|
||||||
|
- Expose OS-owned open/read/write/close only if those operations already have
|
||||||
|
a suitable service-level implementation; otherwise leave them as explicit
|
||||||
|
follow-up within the facade module without inventing new behavior.
|
||||||
|
- Keep filesystem state private to `SystemOS`/facade internals.
|
||||||
|
|
||||||
|
7. Move window operations.
|
||||||
|
- Target:
|
||||||
|
- `crates/console/prometeu-system/src/services/window_manager.rs`
|
||||||
|
- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`
|
||||||
|
- firmware launch tests and cartridge loading.
|
||||||
|
- Provide facade operations:
|
||||||
|
- `add_window`
|
||||||
|
- `set_focus`
|
||||||
|
- `focused_window`
|
||||||
|
- `close_window`
|
||||||
|
- Add any narrow helper needed to preserve existing behavior such as removing
|
||||||
|
all windows, but keep it under `window()` rather than exposing
|
||||||
|
`window_manager`.
|
||||||
|
|
||||||
|
8. Encapsulate migrated fields.
|
||||||
|
- In `SystemOS`, remove `pub` from migrated internals once callsites have
|
||||||
|
moved:
|
||||||
|
- `vm_runtime`
|
||||||
|
- `process_manager`
|
||||||
|
- `task_manager`
|
||||||
|
- `window_manager`
|
||||||
|
- `fs`
|
||||||
|
- `fs_state`
|
||||||
|
- `memcard`
|
||||||
|
- `open_files`
|
||||||
|
- `next_handle`
|
||||||
|
- Keep `log_service` private unless a plan proves a public log handle is
|
||||||
|
required.
|
||||||
|
- Avoid adding compatibility root methods for the old surface.
|
||||||
|
|
||||||
|
9. Update tests.
|
||||||
|
- Move `SystemOS` lifecycle tests to facade calls.
|
||||||
|
- Update firmware tests to observe state through facades or domain-specific
|
||||||
|
inspection helpers rather than direct internal fields.
|
||||||
|
- Preserve direct `VirtualMachineRuntime` tests where they test the VM runtime
|
||||||
|
service itself, not `SystemOS`.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `SystemOS` exposes `lifecycle()`, `sessions()`, `vm()`, `fs()` and `window()`.
|
||||||
|
- Root `SystemOS` keeps `new(...)` and `log(...)`.
|
||||||
|
- Lifecycle callsites use `os.lifecycle()`.
|
||||||
|
- Session creation callsites use `os.sessions()`.
|
||||||
|
- VM initialization/tick/reset callsites use `os.vm()`.
|
||||||
|
- Window callsites use `os.window()`.
|
||||||
|
- Existing root methods migrated by this plan are removed or made private.
|
||||||
|
- Migrated internal fields no longer remain public on `SystemOS`.
|
||||||
|
- No facade duplicates service state.
|
||||||
|
- Existing behavior remains unchanged.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Run `cargo test -p prometeu-system`.
|
||||||
|
- Run `cargo test -p prometeu-firmware`.
|
||||||
|
- Run `discussion validate`.
|
||||||
|
- Confirm with search that migrated callsites no longer use:
|
||||||
|
- `os.task_manager`
|
||||||
|
- `os.process_manager`
|
||||||
|
- `os.window_manager`
|
||||||
|
- `os.vm_runtime`
|
||||||
|
- `os.suspend_task`
|
||||||
|
- `os.resume_task`
|
||||||
|
- `os.close_task`
|
||||||
|
- `os.crash_task`
|
||||||
|
- `os.initialize_vm`
|
||||||
|
- `os.tick_vm`
|
||||||
|
- `os.create_vm_game_task`
|
||||||
|
- `os.create_vm_shell_task`
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Borrowing may require facades to expose narrow methods rather than returning
|
||||||
|
long-lived mutable references. Keep facades short-lived and method-based.
|
||||||
|
- Some tests currently inspect internal fields directly. Prefer domain-level
|
||||||
|
inspection helpers over keeping fields public.
|
||||||
|
- Filesystem `open/read/write/close` may not all exist as OS-level operations
|
||||||
|
yet. Do not invent new filesystem semantics while implementing the facade.
|
||||||
|
- Removing all public internals in one change can be broad. If a field cannot be
|
||||||
|
hidden without unrelated redesign, document the temporary exception in the
|
||||||
|
implementation and leave a follow-up plan.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/src/os/**`
|
||||||
|
- `crates/console/prometeu-system/src/programs/prometeu_hub/**`
|
||||||
|
- `crates/console/prometeu-system/src/services/window_manager.rs`
|
||||||
|
- `crates/console/prometeu-firmware/src/firmware/**`
|
||||||
|
- `discussion/index.ndjson`
|
||||||
|
- `discussion/workflow/plans/PLN-0058-systemos-domain-facades-first-wave.md`
|
||||||
Loading…
x
Reference in New Issue
Block a user