fix: rethrow CancellationException in suspend catch blocks

Three suspend functions in BleRadioTransport caught Exception without
rethrowing CancellationException, which could silently swallow
coroutine cancellation:
- findDevice(): scan retry loop would continue even after cancellation
- attemptConnection(): bond() failure handler swallowed cancellation
- onConnected(): RSSI read failure handler swallowed cancellation
Also replace runCatching with safeCatching in two suspend contexts:
- RadioConfigViewModel.probeMqttConnection()
- LegacyDfuTransport DFU version read
Per project rules: use safeCatching in coroutine/suspend contexts,
keep runCatching only in cleanup/teardown or non-suspend code.
This commit is contained in:
James Rich
2026-04-30 19:52:48 -05:00
parent 321f8f256a
commit f1d42b956e
3 changed files with 9 additions and 2 deletions
@@ -182,6 +182,8 @@ class BleRadioTransport(
}
}
if (d != null) return d
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Logger.v(e) { "[$address] Scan attempt failed or timed out" }
}
@@ -243,6 +245,8 @@ class BleRadioTransport(
try {
bluetoothRepository.bond(device)
Logger.i { "[$address] Bonding successful" }
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Logger.w(e) { "[$address] Bonding failed, attempting connection anyway" }
}
@@ -301,6 +305,8 @@ class BleRadioTransport(
val rssi = retryBleOperation(tag = address) { device.readRssi() }
Logger.d { "[$address] Connection confirmed. Initial RSSI: $rssi dBm" }
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Logger.w(e) { "[$address] Failed to read initial connection RSSI" }
}
@@ -142,7 +142,7 @@ class LegacyDfuTransport(
// Best-effort DFU Version read — gate out unsupported old bootloaders (SDK ≤ 6).
val versionChar = service.characteristic(LEGACY_DFU_VERSION_UUID)
val version =
runCatching { service.read(versionChar) }
safeCatching { service.read(versionChar) }
.map { bytes ->
if (bytes.size >= 2) (bytes[0].toInt() and 0xFF) or ((bytes[1].toInt() and 0xFF) shl 8) else -1
}
@@ -35,6 +35,7 @@ import org.jetbrains.compose.resources.StringResource
import org.koin.core.annotation.InjectedParam
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.safeCatching
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
@@ -165,7 +166,7 @@ open class RadioConfigViewModel(
probeJob =
viewModelScope.launch {
val result =
runCatching { mqttManager.probe(address, tlsEnabled, username, password) }
safeCatching { mqttManager.probe(address, tlsEnabled, username, password) }
.getOrElse { e ->
Logger.w(e) { "MQTT probe threw" }
MqttProbeStatus.Other(message = e.message)