@@ -8,6 +8,7 @@ import android.net.VpnService
88import android.os.Build
99import android.os.ParcelFileDescriptor
1010import java.util.concurrent.atomic.AtomicBoolean
11+ import kotlinx.coroutines.CoroutineExceptionHandler
1112import kotlinx.coroutines.CoroutineScope
1213import kotlinx.coroutines.Dispatchers
1314import kotlinx.coroutines.SupervisorJob
@@ -135,8 +136,24 @@ class LanternVpnService :
135136 }
136137
137138 // Create a CoroutineScope tied to the service's lifecycle.
138- // SupervisorJob ensures that failure in one child doesn't cancel the whole scope.
139- private val serviceScope = CoroutineScope (Dispatchers .IO + SupervisorJob ())
139+ //
140+ // SupervisorJob keeps one child's failure from cancelling its siblings, but it does
141+ // not handle the exception: without a CoroutineExceptionHandler an uncaught throw in
142+ // any launch{} below reaches the thread's default handler and takes the process down
143+ // with no Kotlin log and no Go crash file, which reads as a silent death. The handler
144+ // makes that case diagnosable and reports it to the UI like any other VPN error.
145+ private val serviceScope =
146+ CoroutineScope (
147+ Dispatchers .IO + SupervisorJob () +
148+ CoroutineExceptionHandler { _, e ->
149+ AppLogger .e(TAG , " Uncaught exception in service coroutine" , e)
150+ VpnStatusManager .postVPNError(
151+ errorCode = " service_coroutine_uncaught" ,
152+ errorMessage = " Unexpected VPN service error" ,
153+ error = e,
154+ )
155+ },
156+ )
140157
141158 override fun onStartCommand (
142159 intent : Intent ? ,
@@ -386,10 +403,16 @@ class LanternVpnService :
386403 VpnStatusManager .postVPNStatus(VPNStatus .MissingPermission )
387404 return @withContext
388405 }
389- // Show foreground notification immediately — required by the OS as soon as
390- // VPN service starts, replaced by connected notification on success.
391- notificationHelper.showStartingVPNConnectedNotification(this @LanternVpnService)
392406 runCatching {
407+ // Show foreground notification immediately — required by the OS as soon as
408+ // VPN service starts, replaced by the connected notification on success.
409+ // This is startForeground() underneath, which the OS can refuse over
410+ // foreground-service type, permission, or vendor policy. Inside the block
411+ // so a refusal takes this operation's own failure path — errorCode-tagged
412+ // reporting, network-monitor teardown, and serviceCleanUp when the caller
413+ // asked for it — rather than the service-wide handler, which only logs and
414+ // posts a generic error.
415+ notificationHelper.showStartingVPNConnectedNotification(this @LanternVpnService)
393416 // Radiance is pre-warmed via ACTION_START_RADIANCE, but as a background
394417 // service it may have been killed by the OS before setup completed.
395418 // Re-run setup here under the foreground notification so it is guaranteed
0 commit comments