877dd0a16304

fix: snapshot catch blocks — logging, CancellationException, withTimeoutOrNull (v2.9.15)

The inline snapshot catch blocks silently swallowed all exceptions with zero logging, making snapshot failures undiagnosable. - RelayAdoptionReceiver.kt receiver path: withTimeout → withTimeoutOrNull (timeout returns null instead of throwing CancellationException), explicit null-check with Log.i on timeout, Log.w with stack trace on other failures - RelayAdoptionReceiver.kt + RelayAdoptionHost.kt source paths: rethrow CancellationException (external coroutine cancellation must propagate), Log.w with stack trace on other failures - app/build.gradle: unitTests.returnDefaultValues = true (android.util.Log not mocked in JVM tests) Gate: full testDebugUnitTest + assembleRelease BUILD SUCCESSFUL.

time
2026-07-31 02:26 UTC · 2 months ago
machine
koh2
parent
9650c7059d5d
4 files VERSION+1 −1app/build.gradle+2 −1app/src/main/java/com/peachpasswords/sync/RelayAdoptionHost.kt+3 −1app/src/main/java/com/peachpasswords/sync/RelayAdoptionReceiver.kt+24 −17
VERSION+1 −1
@@ -1 +1 @@1-2.9.141+2.9.15

view file

app/build.gradle+2 −1
@@ -32,7 +32,7 @@3232         minSdk 263333         targetSdk 353434         // Keep the Android code in Gradle; the semantic name comes from root VERSION.35-        versionCode 24016035+        versionCode 2401613636         versionName semanticVersion3737 3838         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"@@ -116,6 +116,7 @@116116     }117117 118118     testOptions {119+        unitTests.returnDefaultValues = true119120         unitTests.all {120121             def coreDir = rootProject.file("../peach-browser/crypto-core")121122             def nativeLibrary = new File(

view file

app/src/main/java/com/peachpasswords/sync/RelayAdoptionHost.kt+3 −1
@@ -1,5 +1,6 @@11 package com.peachpasswords.sync22 3+import android.util.Log34 import java.security.MessageDigest45 import javax.inject.Inject56 import javax.inject.Singleton@@ -227,7 +228,8 @@227228                 }228229             } catch (error: Exception) {229230                 if (error is SnapshotDuressFrameTooLargeException) throw error230-                // Fallback: receiver uses the sync pipeline.231+                if (error is kotlinx.coroutines.CancellationException) throw error232+                Log.w("PeachAdopt", "source inline snapshot send failed, receiver falls back to sync", error)231233             }232234         }233235         return adopted

view file

app/src/main/java/com/peachpasswords/sync/RelayAdoptionReceiver.kt+24 −17
@@ -1,7 +1,9 @@11 package com.peachpasswords.sync22 3+import android.util.Log34 import javax.inject.Inject45 import javax.inject.Singleton6+import kotlinx.coroutines.CancellationException57 import kotlinx.coroutines.withTimeout68 import kotlinx.coroutines.withTimeoutOrNull79 import org.json.JSONObject@@ -238,24 +240,28 @@238240         if (sourceCanSendSnapshot) {239241             // Try to receive the inline vault snapshot.240242             try {241-                val snapshot = withTimeout(15_000) {243+                val snapshot = withTimeoutOrNull(15_000) {242244                     receiveVaultSnapshot(channel)243245                 }244-                boundDeps.applySnapshot(snapshot.entries, snapshot.tombstones, snapshot.duressSlot)245-                sendFrame(channel, AdoptionFrame.SnapshotApplied(246-                    entriesReceived = snapshot.entries.size,247-                    tombstonesReceived = snapshot.tombstones.size,248-                ))249-                // Optional device manifest from source250-                try {251-                    val manifestFrame = withTimeout(5_000) { receiveFrame(channel) }252-                    if (manifestFrame is AdoptionFrame.DeviceManifestFrame) {253-                        sendFrame(channel, AdoptionFrame.DeviceManifestApplied)254-                    }255-                } catch (_: Exception) {}256-                boundDeps.markSynced(adopted.deviceId)257-            } catch (_: Exception) {258-                // Fallback: sync pipeline fills the vault.246+                if (snapshot != null) {247+                    boundDeps.applySnapshot(snapshot.entries, snapshot.tombstones, snapshot.duressSlot)248+                    sendFrame(channel, AdoptionFrame.SnapshotApplied(249+                        entriesReceived = snapshot.entries.size,250+                        tombstonesReceived = snapshot.tombstones.size,251+                    ))252+                    // Optional device manifest from source253+                    try {254+                        val manifestFrame = withTimeoutOrNull(5_000) { receiveFrame(channel) }255+                        if (manifestFrame is AdoptionFrame.DeviceManifestFrame) {256+                            sendFrame(channel, AdoptionFrame.DeviceManifestApplied)257+                        }258+                    } catch (_: Exception) {}259+                    boundDeps.markSynced(adopted.deviceId)260+                } else {261+                    Log.i("PeachAdopt", "receiver inline snapshot timed out, falling back to sync")262+                }263+            } catch (e: Exception) {264+                Log.w("PeachAdopt", "receiver inline snapshot failed, falling back to sync", e)259265             }260266         }261267         return adopted@@ -355,7 +361,8 @@355361                 }356362             } catch (error: Exception) {357363                 if (error is SnapshotDuressFrameTooLargeException) throw error358-                // Fallback: receiver uses the sync pipeline.364+                if (error is CancellationException) throw error365+                Log.w("PeachAdopt", "source inline snapshot send failed, receiver falls back to sync", error)359366             }360367         }361368         return adopted

view file