fix: ping as engine instance method — same lifecycle as call
Some checks failed
Mirror to GitHub / mirror (push) Failing after 7s
Build Release Binaries / build-amd64 (push) Failing after 19s

Ping was a static JNI method that loaded the .so before nativeInit,
crashing jemalloc. Now ping is an instance method on WzpEngine:

- Engine is created once (nativeInit), reused for both ping and call
- pingRelay() uses same tokio runtime pattern as startCall()
- Auto-pings all servers on app launch (after engine init)
- No process restart needed
- TOFU fingerprints saved on first successful ping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-07 09:49:33 +04:00
parent eeb85aeac2
commit 18f7faa279
7 changed files with 303 additions and 150 deletions

View File

@@ -153,20 +153,21 @@ class WzpEngine(private val callback: WzpCallback) {
private external fun nativeWriteAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, sampleCount: Int): Int
private external fun nativeReadAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, maxSamples: Int): Int
private external fun nativeDestroy(handle: Long)
private external fun nativePingRelay(handle: Long, relay: String): String?
/**
* Ping a relay server. Requires engine to be initialized.
* Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` or null.
*/
fun pingRelay(address: String): String? {
if (nativeHandle == 0L) return null
return nativePingRelay(nativeHandle, address)
}
companion object {
init {
System.loadLibrary("wzp_android")
}
/**
* Ping a relay server. Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}`
* or null if unreachable. Does not require an engine instance.
*/
fun pingRelay(address: String): String? = nativePingRelay(address)
@JvmStatic
private external fun nativePingRelay(relay: String): String?
}
}

View File

@@ -1,36 +1,12 @@
package com.wzp.net
/**
* Relay ping via native QUIC — requires loading the native .so.
* After ping completes, the process must be restarted (System.exit)
* because jemalloc initialization during .so load corrupts state
* on Android 16 MTE devices.
*
* Flow: ping all servers → save results → exit → app restarts → load results
*/
object RelayPinger {
// Relay pinging is now done via WzpEngine.pingRelay() (instance method).
// This file kept for the data class only.
object RelayPinger {
data class PingResult(
val rttMs: Int,
val reachable: Boolean,
val serverFingerprint: String = "",
)
/**
* Ping a relay via the native QUIC stack.
* WARNING: After calling this, the process must be restarted.
*/
fun ping(address: String): PingResult {
return try {
val json = com.wzp.engine.WzpEngine.pingRelay(address) ?: return PingResult(0, false)
val obj = org.json.JSONObject(json)
PingResult(
rttMs = obj.getInt("rtt_ms"),
reachable = true,
serverFingerprint = obj.optString("server_fingerprint", ""),
)
} catch (e: Exception) {
PingResult(0, false)
}
}
}

View File

@@ -208,55 +208,61 @@ class CallViewModel : ViewModel(), WzpCallback {
settings?.saveSelectedServer(_selectedServer.value)
}
/** Load saved ping results from last ping-and-exit cycle. */
fun loadSavedPingResults() {
val s = settings ?: return
val results = mutableMapOf<String, PingResult>()
val known = mutableMapOf<String, String>()
_servers.value.forEach { server ->
val rtt = s.loadPingRtt(server.address)
val fp = s.loadServerFingerprint(server.address)
if (rtt >= 0) {
results[server.address] = PingResult(rttMs = rtt, serverFingerprint = fp ?: "")
}
fp?.let { known[server.address] = it }
}
_pingResults.value = results
_knownFingerprints.value = known
}
/**
* Ping all servers via native QUIC, save results, then exit process.
* On restart, saved results are loaded. This avoids the jemalloc crash
* by ensuring the native .so is only loaded once per process lifetime.
* Ping all servers via native QUIC. Requires engine to be initialized.
* Creates engine if needed, pings, keeps engine alive for subsequent Connect.
*/
fun pingAndExit() {
fun pingAllServers() {
viewModelScope.launch {
val results = mutableMapOf<String, PingResult>()
_servers.value.forEach { server ->
val pr = withContext(Dispatchers.IO) {
com.wzp.net.RelayPinger.ping(server.address)
// Ensure engine exists
if (engine == null || engine?.isInitialized != true) {
try {
engine = WzpEngine(this@CallViewModel).also { it.init() }
engineInitialized = true
} catch (e: Exception) {
Log.w(TAG, "engine init for ping failed: $e")
return@launch
}
results[server.address] = PingResult(
rttMs = pr.rttMs,
serverFingerprint = pr.serverFingerprint,
)
// Save results
settings?.savePingRtt(server.address, pr.rttMs)
if (pr.serverFingerprint.isNotEmpty()) {
val saved = settings?.loadServerFingerprint(server.address)
if (saved == null) {
settings?.saveServerFingerprint(server.address, pr.serverFingerprint)
}
}
val eng = engine ?: return@launch
val results = mutableMapOf<String, PingResult>()
val known = mutableMapOf<String, String>()
_servers.value.forEach { server ->
val json = withContext(Dispatchers.IO) {
eng.pingRelay(server.address)
}
if (json != null) {
try {
val obj = JSONObject(json)
val rtt = obj.getInt("rtt_ms")
val fp = obj.optString("server_fingerprint", "")
results[server.address] = PingResult(rttMs = rtt, serverFingerprint = fp)
// TOFU
if (fp.isNotEmpty()) {
val saved = settings?.loadServerFingerprint(server.address)
if (saved == null) settings?.saveServerFingerprint(server.address, fp)
known[server.address] = saved ?: fp
}
} catch (_: Exception) {}
}
}
_pingResults.value = results
// Exit process — next launch loads saved results, native .so reinits cleanly
delay(300) // let UI update
System.exit(0)
_knownFingerprints.value = known
}
}
/** Load saved TOFU fingerprints. */
fun loadSavedFingerprints() {
val known = mutableMapOf<String, String>()
_servers.value.forEach { server ->
settings?.loadServerFingerprint(server.address)?.let {
known[server.address] = it
}
}
_knownFingerprints.value = known
}
/** Get lock status for a server. */
fun lockStatus(address: String): LockStatus {
val pr = _pingResults.value[address] ?: return LockStatus.UNKNOWN

View File

@@ -90,8 +90,11 @@ fun InCallScreen(
var showManageRelays by remember { mutableStateOf(false) }
// Load saved ping results from last ping-and-exit cycle
LaunchedEffect(Unit) { viewModel.loadSavedPingResults() }
// Ping servers on launch — engine init + QUIC ping, no restart needed
LaunchedEffect(Unit) {
viewModel.loadSavedFingerprints()
viewModel.pingAllServers()
}
Surface(
modifier = Modifier.fillMaxSize(),
@@ -227,21 +230,6 @@ fun InCallScreen(
)
}
Spacer(modifier = Modifier.height(8.dp))
// Ping button — pings all servers via native QUIC, saves results, exits app
OutlinedButton(
onClick = { viewModel.pingAndExit() },
modifier = Modifier.fillMaxWidth().height(40.dp),
shape = RoundedCornerShape(8.dp),
) {
Text(
"Ping Servers (restarts app)",
style = MaterialTheme.typography.labelSmall,
color = TextDim
)
}
errorMessage?.let { err ->
Spacer(modifier = Modifier.height(8.dp))
Text(text = err, color = Red, style = MaterialTheme.typography.bodySmall)