feat(renaser): Fase 7c — persistencia inter-sesión por-app

Cada app tiene ahora su propia ranura de estado en el Manifiesto de
Génesis (EntradaApp.estado): guarda y recobra lo suyo, sobrevive al
reinicio, y no pisa a ninguna otra app.

- apps/memoriosa: app WASM interactiva nueva. Cuenta las teclas pulsadas
  y persiste el recuento; al reiniciar despierta con su cuenta intacta.
  Reemplaza la 2a instancia de hola en la genesis.
- kernel: capacidades sys_estado_cargar / sys_estado_guardar. El kernel
  custodia un manifiesto VIVO (Mutex<Manifiesto>); fijar_estado lo muta,
  lo re-graba en el grafo y lo re-ancla. ContextoCapacidades.indice_app
  da a cada app su identidad — su ranura, jamas la de otra.
- cargar_userspace instala el manifiesto vivo antes de instanciar las
  apps: el init de una app ya consulta su estado al despertar.

Verificado en QEMU (screendump + sendkey): disco virgen -> memoriosa con
0 celdas, testigo verde; 5 pulsaciones -> 5 celdas; reinicio -> 5 celdas
intactas, testigo ambar (init releyo el estado del grafo).

Cierra la Fase 7 — el userspace nace del grafo, completa.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 18:43:58 +00:00
parent 6a0781c0a8
commit 900cd19e49
15 changed files with 492 additions and 35 deletions
+2 -2
View File
@@ -258,8 +258,8 @@ pub fn manifiesto() -> Option<Hash> {
}
/// Ancla un objeto como el Manifiesto de Genesis y graba el cambio en el
/// superbloque. Gemelo de [`fijar_raiz`].
#[allow(dead_code)] // Lo usara la Fase 7c (persistencia inter-sesion).
/// superbloque. Gemelo de [`fijar_raiz`]. La Fase 7c lo invoca cada vez que una
/// app persiste su estado y el manifiesto debe re-anclarse.
pub fn fijar_manifiesto(hash: Hash) -> Result<(), &'static str> {
let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
let mut almacen = mutex.lock();
+16 -8
View File
@@ -133,7 +133,7 @@ async fn tarea_sonda_disco() {
/// despacha como tarea cooperativa del reactor. Si el bytecode falta, esta
/// corrupto, o la carga fracasa, se salda pintando la region de la app con
/// la baliza de desalojo — el kernel no se inmuta y sigue con las demas.
fn encender_app(ejecutor: &mut Executor, entrada: &manifiesto::EntradaApp) {
fn encender_app(ejecutor: &mut Executor, indice: usize, entrada: &manifiesto::EntradaApp) {
let region = manifiesto::region(entrada);
// Recuperar el bytecode del grafo. `recuperar` recomputa el hash del
// objeto y verifica su integridad: un bytecode corrupto se delata aqui
@@ -145,7 +145,9 @@ fn encender_app(ejecutor: &mut Executor, entrada: &manifiesto::EntradaApp) {
return;
}
};
match wasm::AplicacionWasm::cargar(&bytecode, region, entrada.techo_memoria as usize) {
// `indice` es la identidad de la app en el manifiesto: las capacidades de
// estado persistido (Fase 7c) la usan para hallar SU ranura `estado`.
match wasm::AplicacionWasm::cargar(&bytecode, region, entrada.techo_memoria as usize, indice) {
Ok(app) => ejecutor.spawn(tarea_aplicacion(app)),
Err(_) => consola::pintar_desalojo(region, Color::DESALOJO),
}
@@ -163,10 +165,11 @@ fn reportar(linea: &str) {
}
/// FASE 7 :: puebla el userspace DESDE EL GRAFO. Carga el Manifiesto de
/// Genesis que `boot` sembro en la imagen de disco y, por cada `EntradaApp`,
/// enciende su aplicacion. Toda falla se reporta a la consola y NO detiene el
/// arranque: el kernel se levanta con las apps que pueda — o con ninguna, si
/// el grafo no tiene userspace.
/// Genesis que `boot` sembro en la imagen de disco, lo instala como el
/// manifiesto VIVO del kernel y, por cada `EntradaApp`, enciende su
/// aplicacion. Toda falla se reporta a la consola y NO detiene el arranque: el
/// kernel se levanta con las apps que pueda — o con ninguna, si el grafo no
/// tiene userspace.
fn cargar_userspace(ejecutor: &mut Executor) {
let manifiesto = match manifiesto::cargar() {
Ok(Some(m)) => Some(m),
@@ -192,8 +195,13 @@ fn cargar_userspace(ejecutor: &mut Executor) {
}
if let Some(m) = manifiesto {
for entrada in &m.apps {
encender_app(ejecutor, entrada);
// Instalar el manifiesto VIVO ANTES de instanciar las apps: el `init`
// de cada app puede consultar su estado persistido (Fase 7c), y esa
// consulta lee del manifiesto vivo. Se instala una copia; la otra se
// itera para encender cada app con su indice — su identidad.
manifiesto::instalar(m.clone());
for (indice, entrada) in m.apps.iter().enumerate() {
encender_app(ejecutor, indice, entrada);
}
}
}
+61 -6
View File
@@ -7,17 +7,24 @@
// cuota, en que region— lo dicta este Manifiesto de Genesis, que tambien
// habita el grafo. El superbloque guarda su hash en un ancla propia.
//
// ESTADO: Fase 7b. El kernel ya NO empotra una sola app. La siembra de la
// imagen —grabar los objetos de bytecode y el manifiesto en un disco virgen—
// la hace por completo el constructor de imagen `boot`, en el anfitrion. Este
// modulo se reduce a su esencia: LEER el manifiesto del grafo al arrancar.
// ESTADO: Fase 7c. El manifiesto deja de ser de solo lectura. El kernel
// conserva una copia VIVA —`VIVO`, un `Mutex<Manifiesto>`—; cuando una app
// persiste su estado (`sys_estado_guardar`), el kernel actualiza la ranura
// `estado` de su `EntradaApp`, re-graba el manifiesto en el grafo y lo
// re-ancla. Asi el estado de cada app sobrevive, por separado, a un reinicio.
//
// Los tipos `Manifiesto` / `EntradaApp` y su (de)serializacion viven en la
// crate `formato`, el nucleo `no_std` compartido con `boot`. Aqui solo queda
// lo que es del kernel: recuperar el manifiesto del grafo y traducir las
// regiones de su formato en disco (`u32`) a la `RegionPantalla` del kernel.
// lo que es del kernel: cargar el manifiesto, traducir regiones, custodiar la
// copia viva y mutarla cuando una app graba su estado.
// =============================================================================
use alloc::vec::Vec;
use spin::{Mutex, Once};
use formato::Hash;
use crate::almacen;
use crate::grafico::RegionPantalla;
@@ -25,6 +32,12 @@ use crate::grafico::RegionPantalla;
// resto del kernel los nombre `manifiesto::EntradaApp` / `manifiesto::Manifiesto`.
pub use formato::{EntradaApp, Manifiesto};
/// El manifiesto VIVO del kernel: la copia en memoria, mutable, del Manifiesto
/// de Genesis. Las apps la actualizan al persistir su estado (Fase 7c); el
/// kernel la re-graba en el grafo y la re-ancla en el superbloque. Se instala
/// una sola vez, en el arranque, con [`instalar`].
static VIVO: Once<Mutex<Manifiesto>> = Once::new();
/// Traduce la sub-region de una `EntradaApp` —campos `u32` de ancho fijo, el
/// formato en disco— a la `RegionPantalla` que el kernel entiende (`usize`,
/// ancho de plataforma). El puente entre lo que el disco guarda y lo que el
@@ -54,3 +67,45 @@ pub fn cargar() -> Result<Option<Manifiesto>, &'static str> {
let manifiesto = Manifiesto::deserializar(&objeto.datos)?;
Ok(Some(manifiesto))
}
/// Instala el manifiesto recien cargado como el manifiesto VIVO del kernel. Se
/// invoca una sola vez, en el arranque, ANTES de instanciar las apps — el
/// `init` de cada app ya consulta su estado persistido, y eso lee de aqui.
pub fn instalar(manifiesto: Manifiesto) {
VIVO.call_once(|| Mutex::new(manifiesto));
}
/// El hash del estado persistido de la app `indice`, si tiene uno anclado. Lo
/// consulta la capacidad `sys_estado_cargar` cuando una app despierta.
pub fn estado_de(indice: usize) -> Option<Hash> {
VIVO.get()
.and_then(|vivo| vivo.lock().apps.get(indice).and_then(|app| app.estado))
}
/// Registra `hash` como el nuevo estado persistido de la app `indice`: muta el
/// manifiesto vivo, lo re-serializa, lo graba como un objeto NUEVO del grafo y
/// lo re-ancla en el superbloque. Desde esta llamada, el estado de esa app
/// sobrevive a un reinicio. La invoca la capacidad `sys_estado_guardar`.
pub fn fijar_estado(indice: usize, hash: Hash) -> Result<(), &'static str> {
let vivo = VIVO.get().ok_or("manifiesto :: no hay manifiesto vivo")?;
let mut manifiesto = vivo.lock();
let entrada = manifiesto
.apps
.get_mut(indice)
.ok_or("manifiesto :: indice de app fuera de rango")?;
entrada.estado = Some(hash);
// Re-grabar el manifiesto mutado. Sus `hijos` son, como en la siembra de
// `boot`, los objetos de bytecode deduplicados: el grafo lo sigue leyendo
// como el nodo padre del userspace. El objeto nuevo se ancla en el
// superbloque; el viejo queda en el log, inerte e inofensivo.
let bytes = manifiesto.serializar()?;
let mut hijos: Vec<Hash> = Vec::new();
for app in &manifiesto.apps {
if !hijos.contains(&app.bytecode) {
hijos.push(app.bytecode);
}
}
let nuevo = almacen::almacenar(bytes, hijos)?;
almacen::fijar_manifiesto(nuevo)
}
+91 -1
View File
@@ -11,7 +11,9 @@
// * sys_object_datos — leer la carga util de un objeto;
// * sys_object_hijo — recorrer las aristas del DAG;
// * sys_object_raiz — leer la raiz del grafo;
// * sys_object_fijar_raiz — coronar un objeto como raiz.
// * sys_object_fijar_raiz — coronar un objeto como raiz;
// * sys_estado_cargar — leer el estado persistido de la app (Fase 7c);
// * sys_estado_guardar — anclar el estado persistido de la app (Fase 7c).
//
// GUARDARRAIL: el kernel valida MATEMATICAMENTE todo puntero que el modulo le
// entrega contra los limites reales de su memoria lineal. No se confia en que
@@ -40,6 +42,10 @@ pub(crate) struct ContextoCapacidades {
/// El techo de recursos de la aplicacion — hoy, su memoria lineal maxima.
/// `wasmi` lo consulta en cada `memory.grow` via `Store::limiter`.
pub(crate) limites: StoreLimits,
/// El indice de esta app en el Manifiesto de Genesis — su identidad. Las
/// capacidades de estado (Fase 7c) lo usan para hallar la `EntradaApp`
/// correcta: cada app persiste en SU ranura, jamas en la de otra.
pub(crate) indice_app: usize,
}
/// Recupera la memoria lineal exportada por el modulo. Que no la exporte es un
@@ -345,5 +351,89 @@ pub(crate) fn enlazar_capacidades(
},
)?;
// --- CAPACIDAD 8 :: sys_estado_cargar(salida, capacidad) -> i32 ---
// Copia el estado persistido de ESTA app —el objeto que su `EntradaApp` del
// manifiesto tiene anclado— en `salida`. Devuelve el numero de bytes
// copiados, 0 si la app no tiene estado previo, -1 si el objeto anclado no
// existe, -2 si `capacidad` no basta, -3 si el almacenamiento fallo.
enlazador.func_wrap(
"renaser",
"sys_estado_cargar",
|mut caller: Caller<'_, ContextoCapacidades>,
salida: u32,
capacidad: u32|
-> Result<i32, Error> {
let indice = caller.data().indice_app;
// El hash del estado de esta app, segun el manifiesto vivo.
let hash = match crate::manifiesto::estado_de(indice) {
Some(hash) => hash,
None => return Ok(0), // Sin estado previo: nada que cargar.
};
let objeto = match crate::almacen::recuperar(&hash) {
Ok(Some(objeto)) => objeto,
Ok(None) => return Ok(-1),
Err(_) => return Ok(-3),
};
if objeto.datos.len() > capacidad as usize {
return Ok(-2);
}
let memoria = obtener_memoria(&caller)?;
// Verificar que el destino cabe, y solo entonces copiar.
{
let m = memoria.data(&caller);
rango(
m,
salida,
objeto.datos.len(),
"WASM :: sys_estado_cargar desbordo la memoria lineal (salida)",
)?;
}
let n = objeto.datos.len();
let m = memoria.data_mut(&mut caller);
m[salida as usize..salida as usize + n].copy_from_slice(&objeto.datos);
Ok(n as i32)
},
)?;
// --- CAPACIDAD 9 :: sys_estado_guardar(datos, datos_len) -> i32 ---
// Graba `datos` como el estado persistido de ESTA app: el kernel lo
// almacena como un objeto del grafo y ancla su hash en la `EntradaApp` de
// la app, re-grabando y re-anclando el manifiesto. El estado sobrevivira al
// reinicio. Devuelve 0 si se logro, -3 si el almacenamiento fallo.
enlazador.func_wrap(
"renaser",
"sys_estado_guardar",
|caller: Caller<'_, ContextoCapacidades>,
datos_ptr: u32,
datos_len: u32|
-> Result<i32, Error> {
let indice = caller.data().indice_app;
let memoria = obtener_memoria(&caller)?;
// Leer el estado de la memoria lineal, con limites firmes.
let datos = {
let m = memoria.data(&caller);
rango(
m,
datos_ptr,
datos_len as usize,
"WASM :: sys_estado_guardar desbordo la memoria lineal (datos)",
)?
.to_vec()
};
// Grabar el objeto de estado. Un fallo del almacen NO es culpa de
// la app: se le devuelve -3, y ella decide que hacer.
let hash = match crate::almacen::almacenar(datos, alloc::vec::Vec::new()) {
Ok(hash) => hash,
Err(_) => return Ok(-3),
};
// Anclarlo: muta el manifiesto vivo, lo re-graba y lo re-ancla.
match crate::manifiesto::fijar_estado(indice, hash) {
Ok(()) => Ok(0),
Err(_) => Ok(-3),
}
},
)?;
Ok(())
}
+5 -1
View File
@@ -85,11 +85,14 @@ impl AplicacionWasm {
/// el reactor en cada pulso del reloj.
///
/// `techo_memoria` es la cuota de memoria lineal de ESTA app, en bytes —
/// desde la Fase 7 la dicta su `EntradaApp` del manifiesto.
/// desde la Fase 7 la dicta su `EntradaApp` del manifiesto. `indice_app` es
/// su posicion en el manifiesto: su identidad para las capacidades de
/// estado persistido (Fase 7c).
pub fn cargar(
bytecode: &[u8],
region: RegionPantalla,
techo_memoria: usize,
indice_app: usize,
) -> Result<AplicacionWasm, FallaApp> {
// 1. El motor, con metricas de combustible y compilacion ANTICIPADA: la
// traduccion del modulo ocurre ahora, de modo que el `fuel` mida
@@ -119,6 +122,7 @@ impl AplicacionWasm {
region,
canal,
limites,
indice_app,
},
);
// Ligar el limitador de recursos: `wasmi` lo consultara en cada