11//! Android JNI bindings for Forge App
22//!
33//! This module provides JNI functions to start and stop the Forge server from Android.
4+ //!
5+ //! ## Concurrency Safety
6+ //!
7+ //! This module uses proper synchronization to handle concurrent JNI calls:
8+ //! - `LazyLock` for safe static initialization of the mutex
9+ //! - Mutex lock error handling to avoid panics on poisoned mutexes
10+ //! - Double-start prevention to avoid spawning multiple servers
411
512#[ cfg( feature = "android" ) ]
613use jni:: JNIEnv ;
714#[ cfg( feature = "android" ) ]
815use jni:: objects:: JClass ;
916#[ cfg( feature = "android" ) ]
1017use jni:: sys:: jint;
11- use std:: sync:: { Mutex , OnceLock } ;
18+ use std:: sync:: { LazyLock , Mutex , OnceLock } ;
1219use tokio:: runtime:: Runtime ;
1320use tokio:: sync:: oneshot;
1421
1522static RUNTIME : OnceLock < Runtime > = OnceLock :: new ( ) ;
16- static SERVER_HANDLE : Mutex < Option < tokio:: task:: JoinHandle < ( ) > > > = Mutex :: new ( None ) ;
23+ static SERVER_HANDLE : LazyLock < Mutex < Option < tokio:: task:: JoinHandle < ( ) > > > > =
24+ LazyLock :: new ( || Mutex :: new ( None ) ) ;
1725
1826/// Initialize the Tokio runtime (called once)
1927fn get_runtime ( ) -> & ' static Runtime {
@@ -24,11 +32,28 @@ fn get_runtime() -> &'static Runtime {
2432 } )
2533}
2634
35+ /// Error code returned when the server is already running
36+ #[ cfg( feature = "android" ) ]
37+ const ERR_ALREADY_RUNNING : jint = -1 ;
38+ /// Error code returned when mutex lock fails (poisoned)
39+ #[ cfg( feature = "android" ) ]
40+ #[ allow( dead_code) ] // Reserved for future use
41+ const ERR_LOCK_FAILED : jint = -2 ;
42+ /// Error code returned when server fails to start
43+ #[ cfg( feature = "android" ) ]
44+ const ERR_SERVER_START_FAILED : jint = -3 ;
45+
2746/// Start the Forge server and return the port number
2847///
2948/// This function blocks until the server successfully binds to the port,
3049/// preventing race conditions where the WebView tries to connect before
3150/// the server is ready.
51+ ///
52+ /// # Returns
53+ /// - Positive value: The port number the server is listening on
54+ /// - `-1` (`ERR_ALREADY_RUNNING`): Server is already running
55+ /// - `-2` (`ERR_LOCK_FAILED`): Failed to acquire lock (mutex poisoned)
56+ /// - `-3` (`ERR_SERVER_START_FAILED`): Server failed to start
3257#[ cfg( feature = "android" ) ]
3358#[ unsafe( no_mangle) ]
3459pub extern "C" fn Java_ai_namastex_forge_MainActivity_startServer (
@@ -44,37 +69,91 @@ pub extern "C" fn Java_ai_namastex_forge_MainActivity_startServer(
4469 . and_then ( |p| p. parse ( ) . ok ( ) )
4570 . unwrap_or ( 8887 ) ;
4671
72+ // Acquire lock with proper error handling for poisoned mutex
73+ let mut guard = match SERVER_HANDLE . lock ( ) {
74+ Ok ( guard) => guard,
75+ Err ( poisoned) => {
76+ tracing:: warn!( "SERVER_HANDLE mutex was poisoned, recovering" ) ;
77+ // Recover from poisoned mutex - this is safe because we just store a JoinHandle
78+ poisoned. into_inner ( )
79+ }
80+ } ;
81+
82+ // Check if server is already running (double-start prevention)
83+ if let Some ( ref handle) = * guard {
84+ if !handle. is_finished ( ) {
85+ tracing:: warn!( "Server already running, ignoring duplicate start request" ) ;
86+ return ERR_ALREADY_RUNNING ;
87+ }
88+ // Previous server finished, clean up the old handle
89+ tracing:: info!( "Previous server finished, starting new instance" ) ;
90+ }
91+
4792 // Create oneshot channel to signal when server is ready
4893 let ( ready_tx, ready_rx) = oneshot:: channel ( ) ;
4994
5095 // Spawn server in background
5196 let handle = runtime. spawn ( async move {
5297 if let Err ( e) = crate :: run_server_with_readiness ( Some ( ready_tx) ) . await {
53- eprintln ! ( "Server error: {}" , e) ;
98+ tracing :: error !( "Server error: {}" , e) ;
5499 }
55100 } ) ;
56101
102+ // Store handle before blocking to allow stopServer to work during startup
103+ * guard = Some ( handle) ;
104+
105+ // Release the lock before blocking to avoid holding it during potentially long wait
106+ drop ( guard) ;
107+
57108 // Block until server is ready to accept connections
58- runtime. block_on ( async {
109+ let ready = runtime. block_on ( async {
59110 match ready_rx. await {
60- Ok ( _) => tracing:: info!( "Server ready on port {}" , port) ,
61- Err ( _) => eprintln ! ( "Server failed to signal readiness" ) ,
111+ Ok ( _) => {
112+ tracing:: info!( "Server ready on port {}" , port) ;
113+ true
114+ }
115+ Err ( _) => {
116+ tracing:: error!( "Server failed to signal readiness" ) ;
117+ false
118+ }
62119 }
63120 } ) ;
64121
65- * SERVER_HANDLE . lock ( ) . unwrap ( ) = Some ( handle) ;
66-
67- port as jint
122+ if ready {
123+ port as jint
124+ } else {
125+ ERR_SERVER_START_FAILED
126+ }
68127}
69128
70129/// Stop the Forge server
130+ ///
131+ /// This function safely stops the server if it is running.
132+ /// It handles mutex poisoning gracefully and logs all operations.
71133#[ cfg( feature = "android" ) ]
72134#[ unsafe( no_mangle) ]
73135pub extern "C" fn Java_ai_namastex_forge_MainActivity_stopServer (
74136 _env : JNIEnv ,
75137 _class : JClass ,
76138) {
77- if let Some ( handle) = SERVER_HANDLE . lock ( ) . unwrap ( ) . take ( ) {
78- handle. abort ( ) ;
139+ // Acquire lock with proper error handling for poisoned mutex
140+ let mut guard = match SERVER_HANDLE . lock ( ) {
141+ Ok ( guard) => guard,
142+ Err ( poisoned) => {
143+ tracing:: warn!( "SERVER_HANDLE mutex was poisoned during stop, recovering" ) ;
144+ poisoned. into_inner ( )
145+ }
146+ } ;
147+
148+ if let Some ( handle) = guard. take ( ) {
149+ if handle. is_finished ( ) {
150+ tracing:: info!( "Server was already stopped" ) ;
151+ } else {
152+ tracing:: info!( "Stopping server..." ) ;
153+ handle. abort ( ) ;
154+ tracing:: info!( "Server stop requested" ) ;
155+ }
156+ } else {
157+ tracing:: debug!( "stopServer called but no server was running" ) ;
79158 }
80159}
0 commit comments