@ -1,7 +1,6 @@
//! Axum HTTP/WS server — admin UI is built-in, modules are Lua-side.
//! Axum HTTP/WS server — admin UI is built-in, modules are Lua-side.
use axum ::{
use axum ::{
body ::Body ,
extract ::{ Path , ws ::{ Message , WebSocket , WebSocketUpgrade } } ,
extract ::{ Path , ws ::{ Message , WebSocket , WebSocketUpgrade } } ,
http ::{ header , StatusCode } ,
http ::{ header , StatusCode } ,
response ::{ Html , IntoResponse , Response } ,
response ::{ Html , IntoResponse , Response } ,
@ -9,7 +8,7 @@ use axum::{
Router ,
Router ,
} ;
} ;
use std ::sync ::atomic ::{ AtomicBool , Ordering as AtomicOrdering } ;
use std ::sync ::atomic ::{ AtomicBool , Ordering as AtomicOrdering } ;
use std ::sync ::{ Mutex , OnceLock} ;
use std ::sync ::{ Mutex , MutexGuard, OnceLock} ;
use std ::collections ::HashMap ;
use std ::collections ::HashMap ;
use crate ::bridge ;
use crate ::bridge ;
@ -39,30 +38,35 @@ fn module_dirs() -> &'static Mutex<HashMap<String, String>> {
MODULE_DIRS . get_or_init ( | | Mutex ::new ( HashMap ::new ( ) ) )
MODULE_DIRS . get_or_init ( | | Mutex ::new ( HashMap ::new ( ) ) )
}
}
/// Lock a mutex, recovering from poison if needed.
fn lock_or_recover < T > ( mutex : & Mutex < T > ) -> MutexGuard < ' _ , T > {
mutex . lock ( ) . unwrap_or_else ( | e | e . into_inner ( ) )
}
pub fn register_module ( name : & str , static_dir : & str ) {
pub fn register_module ( name : & str , static_dir : & str ) {
if static_dir . is_empty ( ) {
if static_dir . is_empty ( ) {
module_dirs ( ) . lock ( ) . unwrap ( ) . remove ( name ) ;
lock_or_recover( module_dirs( ) ) . remove ( name ) ;
} else {
} else {
module_dirs ( ) . lock ( ) . unwrap ( ) . insert ( name . to_string ( ) , static_dir . to_string ( ) ) ;
lock_or_recover( module_dirs( ) ) . insert ( name . to_string ( ) , static_dir . to_string ( ) ) ;
}
}
}
}
pub fn unregister_module ( name : & str ) {
pub fn unregister_module ( name : & str ) {
module_dirs( ) . lock ( ) . unwrap ( ) . remove ( name ) ;
lock_or_recover( module_dirs( ) ) . remove ( name ) ;
}
}
pub fn list_modules ( ) -> Vec < String > {
pub fn list_modules ( ) -> Vec < String > {
module_dirs( ) . lock ( ) . unwrap ( ) . keys ( ) . cloned ( ) . collect ( )
lock_or_recover( module_dirs( ) ) . keys ( ) . cloned ( ) . collect ( )
}
}
/// Check if a module has a static/index.html registered
/// Check if a module has a static/index.html registered
pub fn module_has_static ( name : & str ) -> bool {
pub fn module_has_static ( name : & str ) -> bool {
let dirs = module_dirs( ) . lock ( ) . unwrap ( ) ;
let dirs = lock_or_recover( module_dirs( ) ) ;
dirs . get ( name ) . map ( | d | ! d . is_empty ( ) ) . unwrap_or ( false )
dirs . get ( name ) . map ( | d | ! d . is_empty ( ) ) . unwrap_or ( false )
}
}
pub fn register_command ( name : & str , owner : & str ) {
pub fn register_command ( name : & str , owner : & str ) {
let mut cmds = commands( ) . lock ( ) . unwrap ( ) ;
let mut cmds = lock_or_recover( commands( ) ) ;
// Avoid duplicates
// Avoid duplicates
if ! cmds . iter ( ) . any ( | ( n , _ ) | n = = name ) {
if ! cmds . iter ( ) . any ( | ( n , _ ) | n = = name ) {
cmds . push ( ( name . to_string ( ) , owner . to_string ( ) ) ) ;
cmds . push ( ( name . to_string ( ) , owner . to_string ( ) ) ) ;
@ -70,7 +74,7 @@ pub fn register_command(name: &str, owner: &str) {
}
}
pub fn get_commands_json ( ) -> String {
pub fn get_commands_json ( ) -> String {
let cmds = commands( ) . lock ( ) . unwrap ( ) ;
let cmds = lock_or_recover( commands( ) ) ;
let items : Vec < String > = cmds . iter ( )
let items : Vec < String > = cmds . iter ( )
. map ( | ( n , o ) | format! ( r#"{{"name":"{}","owner":"{}"}}"# , n , o ) )
. map ( | ( n , o ) | format! ( r#"{{"name":"{}","owner":"{}"}}"# , n , o ) )
. collect ( ) ;
. collect ( ) ;
@ -113,7 +117,13 @@ pub fn start(port: u16) -> Result<(), String> {
Err ( _ ) = > return ,
Err ( _ ) = > return ,
} ;
} ;
socket . set_reuseaddr ( true ) . ok ( ) ;
socket . set_reuseaddr ( true ) . ok ( ) ;
let bind_addr : std ::net ::SocketAddr = addr . parse ( ) . unwrap ( ) ;
let bind_addr : std ::net ::SocketAddr = match addr . parse ( ) {
Ok ( a ) = > a ,
Err ( e ) = > {
logging ::log ( "ERROR" , "SERVER" , & format! ( "invalid bind address: {e}" ) ) ;
return ;
}
} ;
if socket . bind ( bind_addr ) . is_err ( ) { return ; }
if socket . bind ( bind_addr ) . is_err ( ) { return ; }
if let Ok ( listener ) = socket . listen ( 128 ) {
if let Ok ( listener ) = socket . listen ( 128 ) {
SHUTDOWN . store ( false , AtomicOrdering ::Relaxed ) ;
SHUTDOWN . store ( false , AtomicOrdering ::Relaxed ) ;
@ -157,10 +167,7 @@ async fn admin_handler() -> Html<String> {
async fn commands_list_handler ( ) -> impl IntoResponse {
async fn commands_list_handler ( ) -> impl IntoResponse {
let json = get_commands_json ( ) ;
let json = get_commands_json ( ) ;
Response ::builder ( )
( [ ( header ::CONTENT_TYPE , "application/json" ) ] , json ) . into_response ( )
. header ( header ::CONTENT_TYPE , "application/json" )
. body ( Body ::from ( json ) )
. unwrap ( )
}
}
// --- API ---
// --- API ---
@ -195,16 +202,10 @@ async fn api_handler(
) . await ;
) . await ;
match result {
match result {
Ok ( r ) = > Response ::builder ( )
Ok ( r ) = > ( [ ( header ::CONTENT_TYPE , "application/json" ) ] , r ) . into_response ( ) ,
. header ( header ::CONTENT_TYPE , "application/json" )
. body ( Body ::from ( r ) )
. unwrap ( ) ,
Err ( _ ) = > {
Err ( _ ) = > {
logging ::log ( "WARN" , "API" , & format! ( "timeout: {module}/{action}" ) ) ;
logging ::log ( "WARN" , "API" , & format! ( "timeout: {module}/{action}" ) ) ;
Response ::builder ( )
( StatusCode ::GATEWAY_TIMEOUT , r#"{"error":"lua timeout"}"# ) . into_response ( )
. status ( StatusCode ::GATEWAY_TIMEOUT )
. body ( Body ::from ( r#"{"error":"lua timeout"}"# ) )
. unwrap ( )
}
}
}
}
}
}
@ -223,7 +224,7 @@ async fn static_file_handler(uri: axum::http::Uri) -> Response {
} ;
} ;
let base_dir = {
let base_dir = {
let dirs = module_dirs( ) . lock ( ) . unwrap ( ) ;
let dirs = lock_or_recover( module_dirs( ) ) ;
match dirs . get ( module ) {
match dirs . get ( module ) {
Some ( d ) if d = = "__render__" = > {
Some ( d ) if d = = "__render__" = > {
// Module uses render() API — serve auto-UI page
// Module uses render() API — serve auto-UI page