feat(session-server): add session server for app launching, remove apps from init manifest

Init now only starts servers. The session server holds ProcessCreate cap
and launches apps on demand. Snake is no longer in the boot manifest.
This commit is contained in:
2026-10-18 04:09:28 +02:00
parent 29434805a6
commit 41f7e7db10
10 changed files with 347 additions and 24 deletions

View File

@@ -0,0 +1,9 @@
[package]
name = "session-server"
version = "0.1.0"
edition = "2024"
[dependencies]
types = { workspace = true }
userspace = { workspace = true }
server-framework = { workspace = true }

View File

@@ -0,0 +1,8 @@
#![no_std]
pub mod protocol;
pub mod server;
#[cfg(test)]
#[path = "tests/mod.rs"]
mod tests;

View File

@@ -0,0 +1,77 @@
use server_framework::{Decode, Encode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AppId(u16);
impl AppId {
pub const fn new(id: u16) -> Self { Self(id) }
pub const fn raw(&self) -> u16 { self.0 }
}
#[derive(Debug, PartialEq)]
pub enum SessionRequest {
Launch { app: AppId },
Kill { app: AppId },
ListApps,
ActiveApp,
}
#[derive(Debug, PartialEq)]
pub enum SessionResponse {
Launched { app: AppId },
Killed,
AppList { count: usize },
Active { app: AppId },
NotFound,
Error,
}
impl Encode for SessionRequest {
fn encode(&self) -> [usize; 4] {
match self {
SessionRequest::Launch { app } => [0, app.raw() as usize, 0, 0],
SessionRequest::Kill { app } => [1, app.raw() as usize, 0, 0],
SessionRequest::ListApps => [2, 0, 0, 0],
SessionRequest::ActiveApp => [3, 0, 0, 0],
}
}
}
impl Decode for SessionRequest {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(SessionRequest::Launch { app: AppId::new(words[1] as u16) }),
1 => Some(SessionRequest::Kill { app: AppId::new(words[1] as u16) }),
2 => Some(SessionRequest::ListApps),
3 => Some(SessionRequest::ActiveApp),
_ => None,
}
}
}
impl Encode for SessionResponse {
fn encode(&self) -> [usize; 4] {
match self {
SessionResponse::Launched { app } => [0, app.raw() as usize, 0, 0],
SessionResponse::Killed => [1, 0, 0, 0],
SessionResponse::AppList { count } => [2, *count, 0, 0],
SessionResponse::Active { app } => [3, app.raw() as usize, 0, 0],
SessionResponse::NotFound => [4, 0, 0, 0],
SessionResponse::Error => [5, 0, 0, 0],
}
}
}
impl Decode for SessionResponse {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(SessionResponse::Launched { app: AppId::new(words[1] as u16) }),
1 => Some(SessionResponse::Killed),
2 => Some(SessionResponse::AppList { count: words[1] }),
3 => Some(SessionResponse::Active { app: AppId::new(words[1] as u16) }),
4 => Some(SessionResponse::NotFound),
5 => Some(SessionResponse::Error),
_ => None,
}
}
}

View File

@@ -0,0 +1,115 @@
use server_framework::Server;
use crate::protocol::{AppId, SessionRequest, SessionResponse};
const MAX_APPS: usize = 16;
#[derive(Clone, Copy)]
pub struct AppEntry {
pub name: &'static str,
pub path: &'static str,
pub registered: bool,
}
#[derive(Clone, Copy)]
struct RunningApp {
app: AppId,
pid: u16,
}
pub struct SessionServer {
apps: [Option<AppEntry>; MAX_APPS],
app_count: usize,
running: [Option<RunningApp>; MAX_APPS],
active: Option<AppId>,
create_cap: usize,
display_cap: usize,
input_cap: usize,
}
impl SessionServer {
pub fn new(create_cap: usize, display_cap: usize, input_cap: usize) -> Self {
Self {
apps: [const { None }; MAX_APPS],
app_count: 0,
running: [const { None }; MAX_APPS],
active: None,
create_cap,
display_cap,
input_cap,
}
}
pub fn register_app(&mut self, name: &'static str, path: &'static str) -> AppId {
let id = AppId::new(self.app_count as u16);
self.apps[self.app_count] = Some(AppEntry { name, path, registered: true });
self.app_count += 1;
id
}
pub fn app_count(&self) -> usize {
self.app_count
}
pub fn active_app(&self) -> Option<AppId> {
self.active
}
fn is_running(&self, app: AppId) -> bool {
self.running[app.raw() as usize].is_some()
}
fn launch_app(&mut self, app: AppId) -> SessionResponse {
let idx = app.raw() as usize;
if idx >= self.app_count {
return SessionResponse::NotFound;
}
if self.is_running(app) {
return SessionResponse::Error;
}
let _app_entry = match &self.apps[idx] {
Some(entry) => entry,
None => return SessionResponse::NotFound,
};
let pid = userspace::syscall::create_process(self.create_cap);
self.running[idx] = Some(RunningApp { app, pid: pid as u16 });
self.active = Some(app);
SessionResponse::Launched { app }
}
fn kill_app(&mut self, app: AppId) -> SessionResponse {
let idx = app.raw() as usize;
match self.running[idx].take() {
Some(running) => {
let _ = running.pid;
if self.active == Some(app) {
self.active = None;
}
SessionResponse::Killed
}
None => SessionResponse::NotFound,
}
}
}
impl Server for SessionServer {
type Request = SessionRequest;
type Response = SessionResponse;
fn handle(&mut self, request: SessionRequest) -> SessionResponse {
match request {
SessionRequest::Launch { app } => self.launch_app(app),
SessionRequest::Kill { app } => self.kill_app(app),
SessionRequest::ListApps => SessionResponse::AppList { count: self.app_count },
SessionRequest::ActiveApp => match self.active {
Some(app) => SessionResponse::Active { app },
None => SessionResponse::NotFound,
},
}
}
}

View File

@@ -0,0 +1,2 @@
mod protocol;
mod server;

View File

@@ -0,0 +1,46 @@
use crate::protocol::*;
use server_framework::{Decode, Encode};
#[test]
fn launch_roundtrip() {
let req = SessionRequest::Launch { app: AppId::new(3) };
assert_eq!(SessionRequest::decode(req.encode()), Some(req));
}
#[test]
fn kill_roundtrip() {
let req = SessionRequest::Kill { app: AppId::new(1) };
assert_eq!(SessionRequest::decode(req.encode()), Some(req));
}
#[test]
fn list_apps_roundtrip() {
let req = SessionRequest::ListApps;
assert_eq!(SessionRequest::decode(req.encode()), Some(req));
}
#[test]
fn active_app_roundtrip() {
let req = SessionRequest::ActiveApp;
assert_eq!(SessionRequest::decode(req.encode()), Some(req));
}
#[test]
fn all_response_variants() {
let variants = [
SessionResponse::Launched { app: AppId::new(0) },
SessionResponse::Killed,
SessionResponse::AppList { count: 5 },
SessionResponse::Active { app: AppId::new(2) },
SessionResponse::NotFound,
SessionResponse::Error,
];
for resp in variants {
assert_eq!(SessionResponse::decode(resp.encode()), Some(resp));
}
}
#[test]
fn invalid_request_returns_none() {
assert!(SessionRequest::decode([99, 0, 0, 0]).is_none());
}

View File

@@ -0,0 +1,78 @@
use crate::protocol::*;
use crate::server::SessionServer;
use server_framework::Server;
fn test_server() -> SessionServer {
let mut s = SessionServer::new(0, 0, 0);
s.register_app("snake", "/apps/snake.elf");
s.register_app("clock", "/apps/clock.elf");
s
}
#[test]
fn list_apps_returns_count() {
let mut s = test_server();
let resp = s.handle(SessionRequest::ListApps);
assert_eq!(resp, SessionResponse::AppList { count: 2 });
}
#[test]
fn launch_valid_app() {
let mut s = test_server();
let resp = s.handle(SessionRequest::Launch { app: AppId::new(0) });
assert!(matches!(resp, SessionResponse::Launched { .. }));
}
#[test]
fn launch_sets_active() {
let mut s = test_server();
s.handle(SessionRequest::Launch { app: AppId::new(0) });
let resp = s.handle(SessionRequest::ActiveApp);
assert_eq!(resp, SessionResponse::Active { app: AppId::new(0) });
}
#[test]
fn launch_invalid_app_returns_not_found() {
let mut s = test_server();
let resp = s.handle(SessionRequest::Launch { app: AppId::new(99) });
assert_eq!(resp, SessionResponse::NotFound);
}
#[test]
fn launch_already_running_returns_error() {
let mut s = test_server();
s.handle(SessionRequest::Launch { app: AppId::new(0) });
let resp = s.handle(SessionRequest::Launch { app: AppId::new(0) });
assert_eq!(resp, SessionResponse::Error);
}
#[test]
fn kill_running_app() {
let mut s = test_server();
s.handle(SessionRequest::Launch { app: AppId::new(0) });
let resp = s.handle(SessionRequest::Kill { app: AppId::new(0) });
assert_eq!(resp, SessionResponse::Killed);
}
#[test]
fn kill_clears_active() {
let mut s = test_server();
s.handle(SessionRequest::Launch { app: AppId::new(0) });
s.handle(SessionRequest::Kill { app: AppId::new(0) });
let resp = s.handle(SessionRequest::ActiveApp);
assert_eq!(resp, SessionResponse::NotFound);
}
#[test]
fn kill_not_running_returns_not_found() {
let mut s = test_server();
let resp = s.handle(SessionRequest::Kill { app: AppId::new(0) });
assert_eq!(resp, SessionResponse::NotFound);
}
#[test]
fn no_active_app_initially() {
let mut s = test_server();
let resp = s.handle(SessionRequest::ActiveApp);
assert_eq!(resp, SessionResponse::NotFound);
}