Skip to content

Commit 854f35f

Browse files
committed
fix: resolve unsafe concurrency in Android JNI layer
- Use LazyLock for safe static Mutex initialization instead of direct Mutex::new() which could have initialization races - Handle mutex poisoning gracefully by recovering instead of panicking on .unwrap() calls - Add double-start prevention to avoid spawning multiple server instances - Store handle before blocking to allow stopServer to work during startup - Release lock before blocking on readiness to avoid holding mutex during long waits - Replace eprintln with tracing macros for consistent logging - Add error codes for JNI callers to handle failures appropriately - Document concurrency safety guarantees in module docs
1 parent aaade9a commit 854f35f

File tree

1 file changed

+90
-11
lines changed

1 file changed

+90
-11
lines changed

forge-app/src/android.rs

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
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")]
613
use jni::JNIEnv;
714
#[cfg(feature = "android")]
815
use jni::objects::JClass;
916
#[cfg(feature = "android")]
1017
use jni::sys::jint;
11-
use std::sync::{Mutex, OnceLock};
18+
use std::sync::{LazyLock, Mutex, OnceLock};
1219
use tokio::runtime::Runtime;
1320
use tokio::sync::oneshot;
1421

1522
static 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)
1927
fn 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)]
3459
pub 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)]
73135
pub 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

Comments
 (0)