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
5- #[ cfg( target_os = "android" ) ]
12+ #[ cfg( feature = "android" ) ]
613use jni:: JNIEnv ;
7- #[ cfg( target_os = "android" ) ]
8- use jni:: objects:: { JClass , JString } ;
9- #[ cfg( target_os = "android" ) ]
14+ #[ cfg( feature = "android" ) ]
15+ use jni:: objects:: JClass ;
16+ #[ 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 ) ;
17- static LAST_ERROR : Mutex < Option < String > > = Mutex :: new ( None ) ;
23+ static SERVER_HANDLE : LazyLock < Mutex < Option < tokio:: task:: JoinHandle < ( ) > > > > =
24+ LazyLock :: new ( || Mutex :: new ( None ) ) ;
1825
1926/// Initialize the Tokio runtime (called once)
2027fn get_runtime ( ) -> & ' static Runtime {
2128 RUNTIME . get_or_init ( || {
22- // Initialize android_logger for Android (once)
23- #[ cfg( target_os = "android" ) ]
24- android_logger:: init_once (
25- android_logger:: Config :: default ( )
26- . with_max_level ( log:: LevelFilter :: Debug )
27- . with_tag ( "ForgeApp" ) ,
28- ) ;
29-
30- #[ cfg( target_os = "android" ) ]
31- unsafe {
32- std:: env:: set_var ( "RUST_LOG" , "debug" ) ;
33- }
34-
35- // Initialize tracing subscriber
36- #[ cfg( not( target_os = "android" ) ) ]
29+ // Initialize tracing for Android (once)
3730 tracing_subscriber:: fmt:: init ( ) ;
38-
3931 Runtime :: new ( ) . expect ( "Failed to create Tokio runtime" )
4032 } )
4133}
4234
43- fn set_last_error ( error : String ) {
44- * LAST_ERROR . lock ( ) . unwrap ( ) = Some ( error. clone ( ) ) ;
45-
46- if let Ok ( data_dir) = std:: env:: var ( "FORGE_DATA_DIR" ) {
47- let error_file = format ! ( "{}/forge-last-error.txt" , data_dir) ;
48- if let Err ( e) = std:: fs:: write ( & error_file, & error) {
49- tracing:: error!( "Failed to write error to file {}: {}" , error_file, e) ;
50- }
51- }
52- }
53-
54- #[ cfg( target_os = "android" ) ]
55- #[ unsafe( no_mangle) ]
56- pub extern "C" fn Java_ai_namastex_forge_MainActivity_getLastError < ' local > (
57- env : JNIEnv < ' local > ,
58- _class : JClass < ' local > ,
59- ) -> JString < ' local > {
60- let error = LAST_ERROR
61- . lock ( )
62- . unwrap ( )
63- . clone ( )
64- . unwrap_or_else ( || "Unknown error" . to_string ( ) ) ;
65-
66- env. new_string ( error)
67- . unwrap_or_else ( |_| env. new_string ( "Failed to create error string" ) . unwrap ( ) )
68- }
69-
70- #[ cfg( target_os = "android" ) ]
71- #[ unsafe( no_mangle) ]
72- pub extern "C" fn Java_ai_namastex_forge_MainActivity_setDataDir (
73- mut env : JNIEnv ,
74- _class : JClass ,
75- data_dir : JString ,
76- ) {
77- let data_dir_str: String = env
78- . get_string ( & data_dir)
79- . expect ( "Failed to get data_dir string" )
80- . into ( ) ;
81-
82- unsafe {
83- std:: env:: set_var ( "FORGE_DATA_DIR" , & data_dir_str) ;
84- std:: env:: set_var (
85- "DATABASE_URL" ,
86- format ! ( "sqlite:///{}/forge.db" , data_dir_str) ,
87- ) ;
88- std:: env:: set_var (
89- "SQLX_DATABASE_URL" ,
90- format ! ( "sqlite:///{}/forge.db" , data_dir_str) ,
91- ) ;
92- }
93-
94- tracing:: info!( "Android data directory set to: {}" , data_dir_str) ;
95- }
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 ;
9645
9746/// Start the Forge server and return the port number
9847///
9948/// This function blocks until the server successfully binds to the port,
10049/// preventing race conditions where the WebView tries to connect before
10150/// the server is ready.
102- #[ cfg( target_os = "android" ) ]
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
57+ #[ cfg( feature = "android" ) ]
10358#[ unsafe( no_mangle) ]
10459pub extern "C" fn Java_ai_namastex_forge_MainActivity_startServer (
10560 _env : JNIEnv ,
@@ -114,95 +69,91 @@ pub extern "C" fn Java_ai_namastex_forge_MainActivity_startServer(
11469 . and_then ( |p| p. parse ( ) . ok ( ) )
11570 . unwrap_or ( 8887 ) ;
11671
117- tracing:: info!( "Starting Forge server on port {}" , port) ;
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+ }
11891
11992 // Create oneshot channel to signal when server is ready
12093 let ( ready_tx, ready_rx) = oneshot:: channel ( ) ;
12194
122- // Spawn server in background with error capture
95+ // Spawn server in background
12396 let handle = runtime. spawn ( async move {
12497 if let Err ( e) = crate :: run_server_with_readiness ( Some ( ready_tx) ) . await {
125- let error_msg = format ! ( "Server initialization failed: {}" , e) ;
126- tracing:: error!( "{}" , error_msg) ;
127- set_last_error ( error_msg) ;
98+ tracing:: error!( "Server error: {}" , e) ;
12899 }
129100 } ) ;
130101
131- // Block until server is ready to accept connections (with timeout)
132- let ready_result = runtime. block_on ( async {
133- tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 10 ) , ready_rx) . await
134- } ) ;
102+ // Store handle before blocking to allow stopServer to work during startup
103+ * guard = Some ( handle) ;
135104
136- match ready_result {
137- Ok ( Ok ( _) ) => {
138- tracing:: info!( "Server ready on port {}" , port) ;
139- * SERVER_HANDLE . lock ( ) . unwrap ( ) = Some ( handle) ;
140- port as jint
141- }
142- Ok ( Err ( _) ) => {
143- runtime. block_on ( async {
144- for _ in 0 ..10 {
145- if LAST_ERROR . lock ( ) . unwrap ( ) . is_some ( ) {
146- break ;
147- }
148- tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) . await ;
149- }
150- } ) ;
151-
152- let has_specific_error = LAST_ERROR . lock ( ) . unwrap ( ) . is_some ( ) ;
153- if !has_specific_error {
154- set_last_error (
155- "Server failed to signal readiness - check initialization" . to_string ( ) ,
156- ) ;
105+ // Release the lock before blocking to avoid holding it during potentially long wait
106+ drop ( guard) ;
107+
108+ // Block until server is ready to accept connections
109+ let ready = runtime. block_on ( async {
110+ match ready_rx. await {
111+ Ok ( _) => {
112+ tracing:: info!( "Server ready on port {}" , port) ;
113+ true
157114 }
158- tracing:: error!( "Server failed to signal readiness" ) ;
159- handle. abort ( ) ;
160- -1
161- }
162- Err ( _) => {
163- runtime. block_on ( async {
164- for _ in 0 ..10 {
165- if LAST_ERROR . lock ( ) . unwrap ( ) . is_some ( ) {
166- break ;
167- }
168- tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) . await ;
169- }
170- } ) ;
171-
172- let has_specific_error = LAST_ERROR . lock ( ) . unwrap ( ) . is_some ( ) ;
173- if !has_specific_error {
174- set_last_error (
175- "Server startup timeout (10s) - initialization took too long" . to_string ( ) ,
176- ) ;
115+ Err ( _) => {
116+ tracing:: error!( "Server failed to signal readiness" ) ;
117+ false
177118 }
178- tracing:: error!( "Server startup timeout" ) ;
179- handle. abort ( ) ;
180- -1
181119 }
120+ } ) ;
121+
122+ if ready {
123+ port as jint
124+ } else {
125+ ERR_SERVER_START_FAILED
182126 }
183127}
184128
185129/// Stop the Forge server
186- #[ cfg( target_os = "android" ) ]
130+ ///
131+ /// This function safely stops the server if it is running.
132+ /// It handles mutex poisoning gracefully and logs all operations.
133+ #[ cfg( feature = "android" ) ]
187134#[ unsafe( no_mangle) ]
188- pub extern "C" fn Java_ai_namastex_forge_MainActivity_getLogsPath < ' local > (
189- env : JNIEnv < ' local > ,
190- _class : JClass < ' local > ,
191- ) -> JString < ' local > {
192- let logs_path = if let Ok ( data_dir) = std:: env:: var ( "FORGE_DATA_DIR" ) {
193- format ! ( "{}/forge-debug.log" , data_dir)
194- } else {
195- "/tmp/forge-debug.log" . to_string ( )
135+ pub extern "C" fn Java_ai_namastex_forge_MainActivity_stopServer (
136+ _env : JNIEnv ,
137+ _class : JClass ,
138+ ) {
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+ }
196146 } ;
197147
198- env. new_string ( logs_path)
199- . unwrap_or_else ( |_| env. new_string ( "" ) . unwrap ( ) )
200- }
201-
202- #[ cfg( target_os = "android" ) ]
203- #[ unsafe( no_mangle) ]
204- pub extern "C" fn Java_ai_namastex_forge_MainActivity_stopServer ( _env : JNIEnv , _class : JClass ) {
205- if let Some ( handle) = SERVER_HANDLE . lock ( ) . unwrap ( ) . take ( ) {
206- handle. abort ( ) ;
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" ) ;
207158 }
208159}
0 commit comments