Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ Messages are framed with a **4-byte little-endian length prefix** followed by a
│ │ ├── ComapeoCoreReactActivityLifecycleListener.kt
│ │ ├── ComapeoCorePackage.kt
│ │ ├── Actions.kt
│ │ ├── watchForFile.kt
│ │ └── log.kt
│ ├── src/main/cpp/
│ │ ├── jni-bridge.cpp # JNI bridge to libnode.so + stdout/stderr → logcat
Expand Down
180 changes: 0 additions & 180 deletions android/src/androidTest/java/com/comapeo/core/WatchForFileTest.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ComapeoCoreModule : Module() {
*
* The control socket file lives in the app's filesDir, accessible from
* any process that shares the same UID (i.e. the FGS and the main
* process). The IPC's `waitForFile` polls until the FGS / Node binds.
* process). The IPC's connect loop retries until the FGS / Node binds.
*/
private lateinit var controlIpc: NodeJSIPC

Expand Down
80 changes: 52 additions & 28 deletions android/src/main/java/com/comapeo/core/NodeJSIPC.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.EOFException
Expand Down Expand Up @@ -110,7 +112,6 @@ class NodeJSIPC(
}
}
}
waitForFile(socketFile)
if (::socket.isInitialized) {
try {
socket.close()
Expand Down Expand Up @@ -233,37 +234,60 @@ class NodeJSIPC(
}
}

/**
* Connect with a fixed-cadence retry loop bounded by an overall deadline.
*
* Retries fire on every `IOException` from `LocalSocket.connect`, which covers
* both "socket file does not exist yet" (`ENOENT`) and "file exists but the
* server is not yet `accept`ing" (`ECONNREFUSED`) — the same primitive handles
* both phases of backend startup. The 50 ms cadence is fast enough to be
* invisible to TTI; the 30 s deadline matches the prior `waitForFile` timeout
* so the cumulative startup wait budget is unchanged.
*
* No exponential backoff: this is a one-shot startup wait, not a network call,
* and the failure mode we're tolerating is "backend not finished booting yet"
* — it doesn't get worse from retrying tightly.
*/
private suspend fun connectWithRetry(
socketAddress: LocalSocketAddress,
maxRetries: Int = 5,
initialDelayMs: Long = 100,
maxDelayMs: Long = 5000,
backoffMultiplier: Double = 2.0
deadlineMs: Long = 30_000,
intervalMs: Long = 50,
): LocalSocket {
var currentDelay = initialDelayMs
var lastException: IOException? = null

repeat(maxRetries) { attempt ->
try {
val socket = LocalSocket()
socket.connect(socketAddress)
log("Connected on attempt ${attempt + 1}")
return socket
} catch (e: IOException) {
lastException = e

if (attempt < maxRetries - 1) {
delay(currentDelay)
currentDelay = minOf(
(currentDelay * backoffMultiplier).toLong(),
maxDelayMs
)
var lastFailure: IOException? = null
var attempts = 0
val connected = try {
withTimeout(deadlineMs) {
// `LocalSocket.connect` opens a real fd before it can throw
// (`LocalSocketImpl.create` runs before `connectLocal`), so
// each failed attempt's socket has to be closed before the
// next iteration — otherwise we'd accumulate hundreds of
// file descriptors over the deadline window.
lateinit var s: LocalSocket
while (true) {
attempts++
val candidate = LocalSocket()
try {
candidate.connect(socketAddress)
s = candidate
break
} catch (e: IOException) {
try { candidate.close() } catch (_: Exception) {}
lastFailure = e
delay(intervalMs)
}
}
s
}
} catch (e: TimeoutCancellationException) {
// Translate the timeout into an IOException carrying the last
// connect failure as the cause; otherwise `State.Error` would
// surface only "Timed out for 30000 ms" with no hint of which
// syscall was failing or how many attempts ran.
throw IOException(
"Timed out connecting to socket after ${deadlineMs}ms across $attempts attempts",
lastFailure,
)
}

throw IOException(
"Failed to connect after $maxRetries attempts",
lastException
)
log("Connected on attempt $attempts")
return connected
}
57 changes: 0 additions & 57 deletions android/src/main/java/com/comapeo/core/watchForFile.kt

This file was deleted.

Loading
Loading