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
8 changes: 7 additions & 1 deletion app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,13 @@ class BaseService {
}

data.changeState(State.Connecting)
runOnMainDispatcher {
// Track the connect coroutine so stopRunner()/reload() can cancel an in-flight
// start. Without this, data.connectingJob stays null and stopRunner's
// cancelAndJoin() is a no-op: a superseded start's awaitExternalProcessesReady()
// keeps polling a now-killed sidecar port for its full (60s for MasterDnsVPN)
// window and then throws "sidecar listener not ready", surfacing a false
// "connection failed" even though the live instance is already carrying traffic.
data.connectingJob = runOnMainDispatcher {
try {
data.notification = createNotification(ServiceNotification.genTitle(profile))

Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ abstract class BoxInstance(
val deadline = SystemClock.elapsedRealtime() + readinessTimeoutMs
val pending = ports.toMutableSet()
while (pending.isNotEmpty() && SystemClock.elapsedRealtime() < deadline) {
// Honor cancellation promptly: if this start was superseded (reload/profile
// switch), the connect job is cancelled and the sidecars are torn down. Exiting
// here stops us from polling a now-dead port for the full (60s for MasterDnsVPN)
// window and then throwing a false "sidecar listener not ready".
ensureActive()
val iterator = pending.iterator()
while (iterator.hasNext()) {
val port = iterator.next()
Expand All @@ -306,6 +311,16 @@ abstract class BoxInstance(
if (pending.isNotEmpty()) delay(50)
}
if (pending.isNotEmpty()) {
// If the process pool is no longer active, its sidecars were torn down (e.g. a
// superseded start during reload). A port that never bound on a dead pool is an
// orphan, not a real failure - drop it instead of throwing.
if (!processes.isActive) {
Logs.w(
"sidecar listener not ready on port(s): ${pending.joinToString()}; " +
"process pool already stopped (superseded start), ignoring"
)
return@withContext
}
// MasterDnsVPN must have its listener up before the first dial (it crashed
// otherwise), so a timeout there is fatal. Other sidecars (Mieru/Naïve/
// TrojanGo/Hysteria) were historically fire-and-forget: the first sing-box
Expand Down
Loading