PR-09 - VirtualFS

This commit is contained in:
bQUARKz 2026-01-16 22:24:24 +00:00 committed by Nilton Constantino
parent b9ef960488
commit 314246f981
No known key found for this signature in database
12 changed files with 497 additions and 3 deletions

3
.gitignore vendored
View File

@ -46,3 +46,6 @@ ehthumbs.db
.env
.env.*
mnt
mnt/**

View File

@ -0,0 +1,112 @@
use std::path::PathBuf;
use std::fs;
use prometeu_core::fs::{FsBackend, FsEntry, FsError};
pub struct HostDirBackend {
root: PathBuf,
}
impl HostDirBackend {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn resolve(&self, path: &str) -> PathBuf {
let path = path.trim_start_matches('/');
self.root.join(path)
}
}
impl FsBackend for HostDirBackend {
fn mount(&mut self) -> Result<(), FsError> {
if !self.root.exists() {
return Err(FsError::NotFound);
}
for dir in &["system", "apps", "media", "user"] {
let path = self.root.join(dir);
if !path.exists() {
fs::create_dir_all(&path).map_err(|e| FsError::IOError(e.to_string()))?;
}
}
Ok(())
}
fn unmount(&mut self) {}
fn list_dir(&self, path: &str) -> Result<Vec<FsEntry>, FsError> {
let full_path = self.resolve(path);
let entries = fs::read_dir(full_path).map_err(|e| FsError::IOError(e.to_string()))?;
let mut result = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| FsError::IOError(e.to_string()))?;
let metadata = entry.metadata().map_err(|e| FsError::IOError(e.to_string()))?;
result.push(FsEntry {
name: entry.file_name().to_string_lossy().into_owned(),
is_dir: metadata.is_dir(),
size: metadata.len(),
});
}
Ok(result)
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
let full_path = self.resolve(path);
fs::read(full_path).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => FsError::NotFound,
_ => FsError::IOError(e.to_string()),
})
}
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> {
let full_path = self.resolve(path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).map_err(|e| FsError::IOError(e.to_string()))?;
}
fs::write(full_path, data).map_err(|e| FsError::IOError(e.to_string()))
}
fn delete(&mut self, path: &str) -> Result<(), FsError> {
let full_path = self.resolve(path);
if full_path.is_dir() {
fs::remove_dir_all(full_path).map_err(|e| FsError::IOError(e.to_string()))
} else {
fs::remove_file(full_path).map_err(|e| FsError::IOError(e.to_string()))
}
}
fn exists(&self, path: &str) -> bool {
self.resolve(path).exists()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::env;
fn get_temp_dir(name: &str) -> PathBuf {
let mut path = env::temp_dir();
path.push(format!("prometeu_host_test_{}_{}", name, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn test_host_dir_backend_mount_and_dirs() {
let root = get_temp_dir("mount");
let mut backend = HostDirBackend::new(root.clone());
backend.mount().unwrap();
assert!(root.join("system").is_dir());
assert!(root.join("apps").is_dir());
assert!(root.join("media").is_dir());
assert!(root.join("user").is_dir());
let _ = fs::remove_dir_all(root);
}
}

View File

@ -1,13 +1,25 @@
mod audio_mixer;
mod prometeu_runner;
mod fs_desktop_backend;
use crate::prometeu_runner::PrometeuRunner;
use winit::event_loop::EventLoop;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let mut fs_root = None;
let mut i = 0;
while i < args.len() {
if args[i] == "--fs-root" && i + 1 < args.len() {
fs_root = Some(args[i + 1].clone());
i += 1;
}
i += 1;
}
let event_loop = EventLoop::new()?;
let mut runner = PrometeuRunner::new();
let mut runner = PrometeuRunner::new(fs_root);
event_loop.run_app(&mut runner)?;
Ok(())

View File

@ -1,4 +1,5 @@
use crate::audio_mixer::AudioMixer;
use crate::fs_desktop_backend::HostDirBackend;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use pixels::{Pixels, SurfaceTexture};
use prometeu_core::firmware::Firmware;
@ -23,6 +24,7 @@ pub struct PrometeuRunner {
firmware: Firmware,
input_signals: InputSignals,
fs_root: Option<String>,
frame_target_dt: Duration,
last_frame_time: Instant,
@ -39,15 +41,22 @@ pub struct PrometeuRunner {
}
impl PrometeuRunner {
pub(crate) fn new() -> Self {
pub(crate) fn new(fs_root: Option<String>) -> Self {
let target_fps = 60;
let mut firmware = Firmware::new();
if let Some(root) = &fs_root {
let backend = HostDirBackend::new(root);
firmware.os.mount_fs(Box::new(backend));
}
Self {
window: None,
pixels: None,
hardware: Hardware::new(),
firmware: Firmware::new(),
firmware,
input_signals: InputSignals::default(),
fs_root,
frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps),
last_frame_time: Instant::now(),
accumulator: Duration::ZERO,
@ -243,6 +252,17 @@ impl ApplicationHandler for PrometeuRunner {
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
// Atualiza estado do Filesystem no OS (específico do host-desktop)
if let Some(root) = &self.fs_root {
use prometeu_core::fs::FsState;
if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) {
if std::path::Path::new(root).exists() {
let backend = HostDirBackend::new(root);
self.firmware.os.mount_fs(Box::new(backend));
}
}
}
let now = Instant::now();
let mut frame_delta = now.duration_since(self.last_frame_time);

View File

@ -0,0 +1,12 @@
use crate::fs::{FsEntry, FsError};
pub trait FsBackend: Send + Sync {
fn mount(&mut self) -> Result<(), FsError>;
fn unmount(&mut self);
fn list_dir(&self, path: &str) -> Result<Vec<FsEntry>, FsError>;
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError>;
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError>;
fn delete(&mut self, path: &str) -> Result<(), FsError>;
fn exists(&self, path: &str) -> bool;
fn is_healthy(&self) -> bool { true }
}

View File

@ -0,0 +1,5 @@
pub struct FsEntry {
pub name: String,
pub is_dir: bool,
pub size: u64,
}

View File

@ -0,0 +1,24 @@
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum FsError {
NotFound,
AlreadyExists,
PermissionDenied,
NotMounted,
IOError(String),
Other(String),
}
impl fmt::Display for FsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FsError::NotFound => write!(f, "File or directory not found"),
FsError::AlreadyExists => write!(f, "Already exists"),
FsError::PermissionDenied => write!(f, "Permission denied"),
FsError::NotMounted => write!(f, "Filesystem not mounted"),
FsError::IOError(s) => write!(f, "IO Error: {}", s),
FsError::Other(s) => write!(f, "Error: {}", s),
}
}
}

View File

@ -0,0 +1,8 @@
use crate::fs::FsError;
#[derive(Debug, Clone, PartialEq)]
pub enum FsState {
Unmounted,
Mounted,
Error(FsError),
}

View File

@ -0,0 +1,11 @@
mod fs_error;
mod fs_state;
mod fs_entry;
mod fs_backend;
mod virtual_fs;
pub use fs_error::FsError;
pub use fs_state::FsState;
pub use fs_entry::FsEntry;
pub use fs_backend::FsBackend;
pub use virtual_fs::VirtualFS;

View File

@ -0,0 +1,147 @@
use crate::fs::{FsBackend, FsEntry, FsError};
pub struct VirtualFS {
backend: Option<Box<dyn FsBackend>>,
}
impl VirtualFS {
pub fn new() -> Self {
Self { backend: None }
}
pub fn mount(&mut self, mut backend: Box<dyn FsBackend>) -> Result<(), FsError> {
backend.mount()?;
self.backend = Some(backend);
Ok(())
}
pub fn unmount(&mut self) {
if let Some(mut backend) = self.backend.take() {
backend.unmount();
}
}
pub fn is_mounted(&self) -> bool {
self.backend.is_some()
}
fn normalize_path(&self, path: &str) -> String {
let mut normalized = path.replace('\\', "/");
if !normalized.starts_with('/') {
normalized = format!("/{}", normalized);
}
normalized
}
pub fn list_dir(&self, path: &str) -> Result<Vec<FsEntry>, FsError> {
let normalized = self.normalize_path(path);
let backend = self.backend.as_ref().ok_or(FsError::NotMounted)?;
backend.list_dir(&normalized)
}
pub fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
let normalized = self.normalize_path(path);
let backend = self.backend.as_ref().ok_or(FsError::NotMounted)?;
backend.read_file(&normalized)
}
pub fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> {
let normalized = self.normalize_path(path);
let backend = self.backend.as_mut().ok_or(FsError::NotMounted)?;
backend.write_file(&normalized, data)
}
pub fn delete(&mut self, path: &str) -> Result<(), FsError> {
let normalized = self.normalize_path(path);
let backend = self.backend.as_mut().ok_or(FsError::NotMounted)?;
backend.delete(&normalized)
}
pub fn exists(&self, path: &str) -> bool {
let normalized = self.normalize_path(path);
self.backend.as_ref().map(|b| b.exists(&normalized)).unwrap_or(false)
}
pub fn is_healthy(&self) -> bool {
self.backend.as_ref().map(|b| b.is_healthy()).unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct MockBackend {
files: HashMap<String, Vec<u8>>,
healthy: bool,
}
impl MockBackend {
fn new() -> Self {
Self {
files: HashMap::new(),
healthy: true,
}
}
}
impl FsBackend for MockBackend {
fn mount(&mut self) -> Result<(), FsError> { Ok(()) }
fn unmount(&mut self) {}
fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> {
Ok(self.files.keys().map(|name| FsEntry {
name: name.clone(),
is_dir: false,
size: 0,
}).collect())
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
self.files.get(path).cloned().ok_or(FsError::NotFound)
}
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> {
self.files.insert(path.to_string(), data.to_vec());
Ok(())
}
fn delete(&mut self, path: &str) -> Result<(), FsError> {
self.files.remove(path);
Ok(())
}
fn exists(&self, path: &str) -> bool {
self.files.contains_key(path)
}
fn is_healthy(&self) -> bool {
self.healthy
}
}
#[test]
fn test_virtual_fs_operations() {
let mut vfs = VirtualFS::new();
let backend = MockBackend::new();
vfs.mount(Box::new(backend)).unwrap();
let test_file = "/user/test.txt";
let content = b"hello world";
vfs.write_file(test_file, content).unwrap();
assert!(vfs.exists(test_file));
let read_content = vfs.read_file(test_file).unwrap();
assert_eq!(read_content, content);
vfs.delete(test_file).unwrap();
assert!(!vfs.exists(test_file));
}
#[test]
fn test_virtual_fs_health() {
let mut vfs = VirtualFS::new();
let mut backend = MockBackend::new();
backend.healthy = false;
vfs.mount(Box::new(backend)).unwrap();
assert!(!vfs.is_healthy());
}
}

View File

@ -2,6 +2,7 @@ pub mod hardware;
pub mod virtual_machine;
mod model;
pub mod firmware;
pub mod fs;
mod prometeu_os;
mod prometeu_hub;

View File

@ -2,7 +2,9 @@ use crate::hardware::{HardwareBridge, InputSignals};
use crate::model::{Cartridge, Color, Sample};
use crate::prometeu_os::NativeInterface;
use crate::virtual_machine::{Value, VirtualMachine};
use crate::fs::{VirtualFS, FsBackend, FsState};
use std::sync::Arc;
use std::collections::HashMap;
/// PrometeuOS (POS): O firmware/base do sistema.
/// Autoridade máxima do boot, periféricos, execução da PVM e tratamento de falhas.
@ -17,6 +19,12 @@ pub struct PrometeuOS {
pub sample_square: Option<Arc<Sample>>,
pub sample_kick: Option<Arc<Sample>>,
pub sample_snare: Option<Arc<Sample>>,
// Filesystem
pub fs: VirtualFS,
pub fs_state: FsState,
pub open_files: HashMap<u32, String>,
pub next_handle: u32,
}
impl PrometeuOS {
@ -33,6 +41,10 @@ impl PrometeuOS {
sample_square: None,
sample_kick: None,
sample_snare: None,
fs: VirtualFS::new(),
fs_state: FsState::Unmounted,
open_files: HashMap::new(),
next_handle: 1,
};
// Inicializa samples básicos (mesma lógica do LogicalHardware)
@ -41,6 +53,30 @@ impl PrometeuOS {
os
}
pub fn mount_fs(&mut self, backend: Box<dyn FsBackend>) {
match self.fs.mount(backend) {
Ok(_) => {
self.fs_state = FsState::Mounted;
}
Err(e) => {
self.fs_state = FsState::Error(e);
}
}
}
pub fn unmount_fs(&mut self) {
self.fs.unmount();
self.fs_state = FsState::Unmounted;
}
fn update_fs(&mut self) {
if self.fs_state == FsState::Mounted {
if !self.fs.is_healthy() {
self.unmount_fs();
}
}
}
pub fn reset(&mut self, vm: &mut VirtualMachine) {
*vm = VirtualMachine::default();
self.tick_index = 0;
@ -60,6 +96,8 @@ impl PrometeuOS {
let start = std::time::Instant::now();
self.tick_index += 1;
self.update_fs();
if !self.logical_frame_active {
self.logical_frame_active = true;
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
@ -311,6 +349,107 @@ impl NativeInterface for PrometeuOS {
}
Ok(300)
}
// --- Filesystem Syscalls (0x4000) ---
// FS_OPEN(path) -> handle
0x4001 => {
let path = match vm.pop()? {
Value::String(s) => s,
_ => return Err("Expected string path".into()),
};
if self.fs_state != FsState::Mounted {
vm.push(Value::Integer(-1));
return Ok(100);
}
let handle = self.next_handle;
self.open_files.insert(handle, path);
self.next_handle += 1;
vm.push(Value::Integer(handle as i64));
Ok(200)
}
// FS_READ(handle) -> content
0x4002 => {
let handle = vm.pop_integer()? as u32;
let path = self.open_files.get(&handle).ok_or("Invalid handle")?;
match self.fs.read_file(path) {
Ok(data) => {
let s = String::from_utf8_lossy(&data).into_owned();
vm.push(Value::String(s));
Ok(1000)
}
Err(_e) => {
vm.push(Value::Null);
Ok(100)
}
}
}
// FS_WRITE(handle, content)
0x4003 => {
let content = match vm.pop()? {
Value::String(s) => s,
_ => return Err("Expected string content".into()),
};
let handle = vm.pop_integer()? as u32;
let path = self.open_files.get(&handle).ok_or("Invalid handle")?;
match self.fs.write_file(path, content.as_bytes()) {
Ok(_) => {
vm.push(Value::Boolean(true));
Ok(1000)
}
Err(_) => {
vm.push(Value::Boolean(false));
Ok(100)
}
}
}
// FS_CLOSE(handle)
0x4004 => {
let handle = vm.pop_integer()? as u32;
self.open_files.remove(&handle);
Ok(100)
}
// FS_LISTDIR(path)
0x4005 => {
let path = match vm.pop()? {
Value::String(s) => s,
_ => return Err("Expected string path".into()),
};
match self.fs.list_dir(&path) {
Ok(entries) => {
// Por enquanto, retorna uma string separada por ';'
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
vm.push(Value::String(names.join(";")));
Ok(500)
}
Err(_) => {
vm.push(Value::Null);
Ok(100)
}
}
}
// FS_EXISTS(path) -> bool
0x4006 => {
let path = match vm.pop()? {
Value::String(s) => s,
_ => return Err("Expected string path".into()),
};
vm.push(Value::Boolean(self.fs.exists(&path)));
Ok(100)
}
// FS_DELETE(path)
0x4007 => {
let path = match vm.pop()? {
Value::String(s) => s,
_ => return Err("Expected string path".into()),
};
match self.fs.delete(&path) {
Ok(_) => vm.push(Value::Boolean(true)),
Err(_) => vm.push(Value::Boolean(false)),
}
Ok(500)
}
_ => Err(format!("Unknown syscall: 0x{:08X}", id)),
}
}