From 4e4d731b440ba445c6b085756c571d6a4b741b17 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 27 Jan 2026 03:43:22 -0800 Subject: [PATCH] =?UTF-8?q?chore(android):=20=E5=88=A0=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E7=9A=84=E5=88=86=E6=9E=90=E5=92=8C=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 15 个不再需要的 markdown 文件,包括: - 调试日志指南和分析文件 - gRPC 重构和评估报告 - 崩溃修复和回退计划文档 Co-Authored-By: Claude Opus 4.5 --- .../24H_TIMELINE_ANALYSIS.md | 386 ----------- .../2OF3_FLOW_ANALYSIS.md | 627 ------------------ .../CRASH_PREVENTION_SUMMARY.md | 593 ----------------- .../service-party-android/DEBUG_LOG_GUIDE.md | 398 ----------- .../service-party-android/EXACT_CHANGES.md | 257 ------- .../GRPC_CORRECT_SOLUTION.md | 357 ---------- .../GRPC_OFFICIAL_KEPT.md | 234 ------- .../GRPC_SYSTEM_EVALUATION.md | 446 ------------- .../LOG_ANALYSIS_PARTY1.md | 390 ----------- .../PERMISSIONS_AUDIT.md | 278 -------- .../QUICK_DEBUG_COMMANDS.md | 173 ----- .../RECONNECTION_BUG_ANALYSIS.md | 251 ------- .../REFACTORING_SUMMARY.md | 200 ------ .../service-party-android/REVERT_PLAN.md | 98 --- .../WORKING_CODE_ANALYSIS.md | 269 -------- 15 files changed, 4957 deletions(-) delete mode 100644 backend/mpc-system/services/service-party-android/24H_TIMELINE_ANALYSIS.md delete mode 100644 backend/mpc-system/services/service-party-android/2OF3_FLOW_ANALYSIS.md delete mode 100644 backend/mpc-system/services/service-party-android/CRASH_PREVENTION_SUMMARY.md delete mode 100644 backend/mpc-system/services/service-party-android/DEBUG_LOG_GUIDE.md delete mode 100644 backend/mpc-system/services/service-party-android/EXACT_CHANGES.md delete mode 100644 backend/mpc-system/services/service-party-android/GRPC_CORRECT_SOLUTION.md delete mode 100644 backend/mpc-system/services/service-party-android/GRPC_OFFICIAL_KEPT.md delete mode 100644 backend/mpc-system/services/service-party-android/GRPC_SYSTEM_EVALUATION.md delete mode 100644 backend/mpc-system/services/service-party-android/LOG_ANALYSIS_PARTY1.md delete mode 100644 backend/mpc-system/services/service-party-android/PERMISSIONS_AUDIT.md delete mode 100644 backend/mpc-system/services/service-party-android/QUICK_DEBUG_COMMANDS.md delete mode 100644 backend/mpc-system/services/service-party-android/RECONNECTION_BUG_ANALYSIS.md delete mode 100644 backend/mpc-system/services/service-party-android/REFACTORING_SUMMARY.md delete mode 100644 backend/mpc-system/services/service-party-android/REVERT_PLAN.md delete mode 100644 backend/mpc-system/services/service-party-android/WORKING_CODE_ANALYSIS.md diff --git a/backend/mpc-system/services/service-party-android/24H_TIMELINE_ANALYSIS.md b/backend/mpc-system/services/service-party-android/24H_TIMELINE_ANALYSIS.md deleted file mode 100644 index c518ec09..00000000 --- a/backend/mpc-system/services/service-party-android/24H_TIMELINE_ANALYSIS.md +++ /dev/null @@ -1,386 +0,0 @@ -# 24小时改动时间线分析 - -## 用户质疑 -> "那你回顾一下这24小时内都在改什么?为什么导致原来的co-keygen,keygen,co-sign,sign功能失败了?" - -## 完整时间线 - -### ✅ 阶段1:工作的版本(起点) -**最后一个完全工作的commit**: 在 003871ad 之前 - -**状态**: -- ✅ co-keygen 正常 -- ✅ keygen 正常 -- ✅ co-sign 正常 -- ✅ sign 正常 - ---- - -### ⚠️ 阶段2:Bug修复(003871ad → 41e7eed2) - -#### Commit 003871ad (2026-01-27 00:09:40) -**标题**: "fix(android): 修复 markPartyReady 乐观锁冲突导致 keygen 失败的关键Bug" - -**改动内容**: -```kotlin -// 添加 markPartyReady 重试机制 -repeat(5) { attempt -> - val markReadyResult = grpcClient.markPartyReady(sessionId, partyId) - if (markReadyResult.isSuccess) { - markReadySuccess = true - return@repeat // ❌ Bug: 不会退出循环 - } - delay((attempt + 1) * 500L) -} -``` - -**问题**: `return@repeat` 只跳过当前迭代,不退出循环 -**影响**: 可能导致重复标记 ready,但不是致命的 - ---- - -#### Commit 41e7eed2 (2026-01-27 00:24:40) ✅ **工作的版本** -**标题**: "fix(android): 修复 markPartyReady 重试逻辑的循环退出Bug" - -**改动内容**: -```kotlin -repeat(5) { attempt -> - if (markReadySuccess) return@repeat // ✅ 修复:先检查标志 - val markReadyResult = grpcClient.markPartyReady(sessionId, partyId) - if (markReadyResult.isSuccess) { - markReadySuccess = true - return@repeat - } - delay((attempt + 1) * 500L) -} -``` - -**状态**: ✅ **用户确认这个版本是工作的** - ---- - -### ❌ 阶段3:灾难性重构(7b957114) - -#### Commit 7b957114 (2026-01-27 00:56:55) 🔥 **破坏性改动** -**标题**: "feat(android): 实现可靠的 gRPC 连接和流管理机制" - -**改动统计**: -``` -8 files changed, 1113 insertions(+), 177 deletions(-) -``` - -**核心改动**: - -##### 1. 添加 GrpcClient.kt Keep-Alive 配置 ✅(这个是好的) -```kotlin -+ .keepAliveTime(20, TimeUnit.SECONDS) -+ .keepAliveTimeout(5, TimeUnit.SECONDS) -+ .keepAliveWithoutCalls(true) -+ .idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) -``` - -##### 2. 添加网络监听 ✅(这个是好的) -```kotlin -+ fun setupNetworkMonitoring(context: Context) { -+ channel?.resetConnectBackoff() -+ } -``` - -##### 3. 创建 StreamManager.kt ❌(这个破坏了原有逻辑) -- 新文件:282行 -- 试图封装流管理逻辑 -- 引入了 callback 机制 - -##### 4. 修改 TssRepository.kt ❌(破坏性改动) - -**之前(工作的代码)**: -```kotlin -// 41e7eed2 版本 -grpcClient.registerParty(partyId, "temporary", "1.0.0") // 没有检查 -startSessionEventSubscription() - -private fun startSessionEventSubscription() { - jobManager.launch(JOB_SESSION_EVENT) { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { event -> - // 直接处理事件 - } - } -} -``` - -**之后(7b957114的改动)**: -```kotlin -grpcClient.registerParty(partyId, "temporary", "1.0.0") // 还是没有检查! -startSessionEventSubscription() - -private fun startSessionEventSubscription() { - streamManager.startEventStream( - partyId = effectivePartyId, - onEvent = { event -> /* callback */ }, - onError = { error -> /* callback */ } - ) -} -``` - -##### 5. 添加 init 块监听重连 ❌(引入新问题) -```kotlin -+ init { -+ repositoryScope.launch { -+ grpcConnectionEvents -+ .filter { it is GrpcConnectionEvent.Reconnected } -+ .collect { -+ streamManager.restartAllStreams() -+ } -+ } -+ } -``` - -**导致的问题**: - -1. **RegisterParty 失败但代码继续执行** - ``` - 17:19:30.641 E/GrpcClient: RegisterParty failed after 2 attempts - 17:19:30.643 D/TssRepository: Starting session event subscription ← 还是执行了! - ``` - -2. **StreamManager 日志完全缺失** - ``` - [MISSING] StreamManager: Starting event stream for partyId=... - ``` - -3. **双重连接导致 Channel shutdown** - ``` - UNAVAILABLE: Channel shutdown invoked - ``` - -**为什么会失败**: -- StreamManager 的实现有 bug -- callback 机制不如直接 Flow.collect 可靠 -- init 块的监听可能导致时序问题 -- 增加了复杂度,引入了新的失败点 - ---- - -### 🔄 阶段4:回退尝试(bfbd062e) - -#### Commit bfbd062e (2026-01-27 01:34:16) ⚠️ **部分回退** -**标题**: "refactor(android): 回归简单可靠的流管理架构" - -**改动内容**: -1. ✅ 删除 StreamManager.kt -2. ✅ 删除 init 块监听 -3. ✅ 恢复 jobManager.launch 模式 -4. ✅ 添加 registerParty 错误检查(新增,好的改进) -5. ✅ 保留 Keep-Alive 配置 -6. ✅ 保留网络监听 -7. ⚠️ **添加了 Flow.retryWhen**(这是新增的,不在 41e7eed2) - -**与 41e7eed2 的差异**: - -```kotlin -// 41e7eed2(工作的版本) -jobManager.launch(JOB_SESSION_EVENT) { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { event -> - // 处理事件 - } -} - -// bfbd062e(当前版本) -jobManager.launch(JOB_SESSION_EVENT) { - flow { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { emit(it) } - } - .retryWhen { cause, attempt -> // ← 新增的 - delay(min(attempt + 1, 30) * 1000L) - true - } - .collect { event -> - // 处理事件 - } -} -``` - -**可能的问题**: -- retryWhen 可能在某些情况下影响事件流 -- 虽然看起来应该没问题,但与工作版本不完全一致 - ---- - -## 根本原因分析 - -### 为什么功能失败了? - -#### 1. 7b957114 引入的问题(最大元凶)❌ - -| 问题 | 原因 | 影响 | -|------|------|------| -| RegisterParty 无错误检查 | 失败后继续执行 | Channel 未就绪导致后续失败 | -| StreamManager 抽象层 | 实现有 bug,日志丢失 | 事件流不工作 | -| init 块监听重连 | 时序问题,双重连接 | Channel shutdown | -| callback 机制 | 不如直接 collect 可靠 | 事件丢失 | - -#### 2. bfbd062e 的回退不彻底 ⚠️ - -**添加了 registerParty 错误检查(好的)**: -```kotlin -+ val registerResult = grpcClient.registerParty(partyId, "temporary", "1.0.0") -+ if (registerResult.isFailure) { -+ throw registerResult.exceptionOrNull() ?: Exception("Failed to register party") -+ } -``` - -**但也添加了 retryWhen(不确定)**: -```kotlin -+ .retryWhen { cause, attempt -> -+ delay(min(attempt + 1, 30) * 1000L) -+ true -+ } -``` - -这个 retryWhen 虽然看起来应该工作,但**不在 41e7eed2 工作版本中**! - ---- - -## 当前状态分析 - -### 相比 41e7eed2(工作版本),当前版本的差异: - -| 方面 | 41e7eed2 | bfbd062e (当前) | 差异 | -|------|----------|-----------------|------| -| Keep-Alive | ❌ 没有 | ✅ 有 | 新增(官方推荐)| -| 网络监听 | ❌ 没有 | ✅ 有 | 新增(官方推荐)| -| registerParty 检查 | ❌ 没有 | ✅ 有 | 新增(好的改进)| -| 事件订阅 | jobManager.launch | jobManager.launch | 相同 ✅ | -| retryWhen | ❌ 没有 | ✅ 有 | **新增(可能的问题)** | -| StreamManager | ❌ 没有 | ❌ 没有 | 相同 ✅ | - ---- - -## 为什么当前还是不工作? - -### 可能的原因: - -#### 1. registerParty 现在会抛出异常 ⚠️ - -**41e7eed2(失败但继续)**: -```kotlin -grpcClient.registerParty(partyId, "temporary", "1.0.0") // 失败但继续 -startSessionEventSubscription() // 还是会执行 -``` - -**bfbd062e(失败就停止)**: -```kotlin -val registerResult = grpcClient.registerParty(partyId, "temporary", "1.0.0") -if (registerResult.isFailure) { - throw ... // ← 直接抛异常,后续不执行 -} -startSessionEventSubscription() // 不会执行 -``` - -**问题**: 如果 registerParty 失败,现在会直接停止,不会继续订阅事件。 -**但**: 这应该是对的行为!如果注册失败,继续也没意义。 - -#### 2. retryWhen 可能导致重复订阅 ⚠️ - -```kotlin -flow { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { emit(it) } -} -.retryWhen { cause, attempt -> - delay(min(attempt + 1, 30) * 1000L) - true // 永远重试 -} -``` - -**可能的问题**: -- 如果 subscribeSessionEvents 立即失败,会立即重试 -- 可能导致多次订阅尝试 -- 虽然 jobManager 会取消旧 Job,但时序问题可能存在 - -#### 3. GrpcClient 的改动 ⚠️ - -7b957114 修改了 GrpcClient.kt(216 insertions, 177 deletions) -bfbd062e 没有回退这些改动! - -需要检查 GrpcClient 的改动是否影响了基本功能。 - ---- - -## 测试建议 - -### 要验证的点: - -1. **RegisterParty 是否成功** - ``` - 看日志: "Party registered successfully" - ``` - -2. **事件订阅是否启动** - ``` - 看日志: "Starting session event subscription for partyId: xxx" - ``` - -3. **retryWhen 是否影响正常流** - ``` - 看日志: 是否有 "Event stream failed" 警告 - ``` - -4. **GrpcClient 的改动是否有问题** - ``` - 对比 41e7eed2 和 bfbd062e 的 GrpcClient.kt - ``` - ---- - -## 修复方案 - -### 选项A:完全回退到 41e7eed2 ✅ - -```bash -git checkout 41e7eed2 -- backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt -git checkout 41e7eed2 -- backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/remote/GrpcClient.kt -``` - -**优点**: 100% 恢复到工作状态 -**缺点**: 失去 Keep-Alive 和网络监听的改进 - -### 选项B:删除 retryWhen,保留其他改进 ✅ - -```kotlin -// 恢复为 41e7eed2 的简单版本 -jobManager.launch(JOB_SESSION_EVENT) { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { event -> - // 处理事件 - } -} -``` - -**优点**: 保留 Keep-Alive 和 registerParty 检查 -**缺点**: 失去自动重连能力(但 41e7eed2 也没有) - -### 选项C:测试当前版本,看具体哪里失败 ✅ - -用 build-install-debug.bat 测试,查看具体日志。 - ---- - -## 总结 - -### 24小时内改了什么: - -1. **003871ad**: 添加 markPartyReady 重试(有小bug) -2. **41e7eed2**: 修复 repeat 循环 bug ✅ **工作** -3. **7b957114**: 引入 StreamManager ❌ **破坏性改动** -4. **bfbd062e**: 删除 StreamManager ⚠️ **部分回退** - -### 为什么功能失败: - -1. **7b957114 引入的 StreamManager 有严重 bug** -2. **bfbd062e 的回退不彻底**: - - 添加了 retryWhen(41e7eed2 没有) - - 添加了 registerParty 检查(可能导致提前停止) - - 没有回退 GrpcClient.kt 的改动 - -### 下一步: - -**立即测试当前版本,或完全回退到 41e7eed2** diff --git a/backend/mpc-system/services/service-party-android/2OF3_FLOW_ANALYSIS.md b/backend/mpc-system/services/service-party-android/2OF3_FLOW_ANALYSIS.md deleted file mode 100644 index f75f55f2..00000000 --- a/backend/mpc-system/services/service-party-android/2OF3_FLOW_ANALYSIS.md +++ /dev/null @@ -1,627 +0,0 @@ -# 创建 2-of-3 钱包流程分析与潜在Bug - -## 理论流程(应该如何工作) - -### 环境:2台手机 + 1个 server-party-co-managed - -``` -手机1 (发起者) - ↓ -1. 调用 createNewSession(walletName="测试", t=2, n=3) - ↓ -2. 服务器创建会话,返回 sessionId + inviteCode - ↓ -3. 手机1 显示邀请码二维码 - ↓ -4. server-party-co-managed 检测到新会话,自动加入(第1个参与者) - ↓ -5. 手机2 扫描二维码,调用 validateInviteCode + joinKeygenViaGrpc(第2个参与者) - ↓ -6. 服务器检测到参与者数量 = thresholdT (2) - ↓ -7. 服务器广播 "session_started" 事件给所有参与者(手机1、手机2、server) - ↓ -8. 所有参与者收到事件,调用 startKeygenAsInitiator/startKeygenAsJoiner - ↓ -9. TSS keygen 协议运行(9轮通信) - ↓ -10. 完成,所有参与者保存各自的分片 -``` - ---- - -## 实际代码流程(当前实现) - -### 手机1(发起者) - -#### 步骤 1: 创建会话 -**代码位置**: `MainViewModel.kt:253-330` - -```kotlin -fun createNewSession(walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) { - safeLaunch { // ← 【潜在问题1】如果抛出异常会被捕获 - _uiState.update { it.copy(isLoading = true, error = null) } - - val result = repository.createSession(walletName, thresholdT, thresholdN) - - result.fold( - onSuccess = { sessionResult -> - _currentSessionId.value = sessionResult.sessionId - _createdInviteCode.value = sessionResult.inviteCode - - // 【关键】获取会话状态,检查参与者数量 - val statusResult = repository.getSessionStatus(sessionResult.sessionId) - statusResult.fold( - onSuccess = { status -> - _sessionParticipants.value = status.participants.map { ... } - // ✅ 正确显示参与者列表 - }, - onFailure = { e -> - // ⚠️ 失败时只使用自己 - _sessionParticipants.value = listOf(participantName) - } - ) - }, - onFailure = { e -> - _uiState.update { it.copy(isLoading = false, error = e.message) } - } - ) - } -} -``` - -**潜在问题**: -- ✅ Result 处理正确 -- ⚠️ 如果 getSessionStatus 失败,参与者列表不准确 -- ⚠️ 但这不影响实际的 keygen 启动 - ---- - -#### 步骤 2: 等待 session_started 事件 -**代码位置**: `MainViewModel.kt:382-406` - -```kotlin -repository.setSessionEventCallback { event -> - when (event.eventType) { - "session_started" -> { - val currentSessionId = _currentSessionId.value - if (currentSessionId != null && event.sessionId == currentSessionId) { - android.util.Log.d("MainViewModel", "Session started event for keygen initiator, triggering keygen") - - safeLaunch { // ← 【关键问题!】 - startKeygenAsInitiator( - sessionId = currentSessionId, - thresholdT = event.thresholdT, - thresholdN = event.thresholdN, - selectedParties = event.selectedParties - ) - } - } - } - } -} -``` - -**这是 Bug 的根源!** - -问题分析: -1. `setSessionEventCallback` 是在 **另一个线程**(WebSocket 事件线程)中回调的 -2. 在回调中使用 `safeLaunch` 启动协程 -3. **如果 `startKeygenAsInitiator` 抛出异常**,`safeLaunch` 会捕获并更新 `_uiState.error` -4. 但是,**用户可能没有看到错误提示**,因为: - - UI 可能正在显示"等待参与者"界面 - - `_uiState.error` 的更新可能被忽略 - - 没有明确的错误反馈路径 - ---- - -#### 步骤 3: 执行 keygen -**代码位置**: `MainViewModel.kt:537-570` - -```kotlin -private suspend fun startKeygenAsInitiator( - sessionId: String, - thresholdT: Int, - thresholdN: Int, - selectedParties: List -) { - android.util.Log.d("MainViewModel", "Starting keygen as initiator: sessionId=$sessionId, t=$thresholdT, n=$thresholdN") - - val result = repository.startKeygenAsInitiator( - sessionId = sessionId, - thresholdT = thresholdT, - thresholdN = thresholdN, - password = "" - ) - - result.fold( - onSuccess = { share -> - _publicKey.value = share.publicKey - _uiState.update { - it.copy( - lastCreatedAddress = share.address, - successMessage = "钱包创建成功!" - ) - } - }, - onFailure = { e -> - // ⚠️ 错误被记录到 _uiState.error - _uiState.update { it.copy(error = e.message) } - } - ) -} -``` - -**潜在问题**: -- ✅ Result 处理正确 -- ⚠️ 但如果函数本身抛出异常(非 Result.failure),外层的 `safeLaunch` 会捕获 -- ⚠️ 这会导致**双重错误处理**: - 1. `startKeygenAsInitiator` 更新 `_uiState.error`(如果是 Result.failure) - 2. `safeLaunch` 也更新 `_uiState.error`(如果是异常) - ---- - -### 手机2(加入者) - -#### 步骤 1: 扫描邀请码 -**代码位置**: `MainViewModel.kt:609-641` - -```kotlin -fun validateInviteCode(inviteCode: String) { - safeLaunch { - _uiState.update { it.copy(isLoading = true, error = null) } - - val result = repository.validateInviteCode(inviteCode) - - result.fold( - onSuccess = { validateResult -> - _joinSessionInfo.value = JoinKeygenSessionInfo(...) - _uiState.update { it.copy(isLoading = false) } - }, - onFailure = { e -> - _uiState.update { it.copy(isLoading = false, error = e.message) } - } - ) - } -} -``` - -**状态**: ✅ 处理正确 - ---- - -#### 步骤 2: 加入会话 -**代码位置**: `MainViewModel.kt:648-706` - -```kotlin -fun joinKeygen(inviteCode: String, password: String) { - safeLaunch { - _uiState.update { it.copy(isLoading = true, error = null) } - - val result = repository.joinKeygenViaGrpc( - inviteCode = pendingInviteCode, - joinToken = pendingJoinToken, - password = password - ) - - result.fold( - onSuccess = { joinResult -> - // 【关键】保存 joinResult 用于后续 keygen - pendingJoinKeygenInfo = JoinKeygenInfo( - sessionId = joinResult.sessionId, - partyIndex = joinResult.partyIndex, - partyId = joinResult.partyId, - participantIds = joinResult.participantIds - ) - - // ✅ 等待 session_started 事件 - _uiState.update { it.copy(isLoading = false) } - }, - onFailure = { e -> - _uiState.update { it.copy(isLoading = false, error = e.message) } - } - ) - } -} -``` - -**状态**: ✅ 处理正确 - ---- - -#### 步骤 3: 等待 session_started 事件 -**代码位置**: `MainViewModel.kt:408-413` - -```kotlin -// Check if this is for keygen joiner (JoinKeygen) -val joinKeygenInfo = pendingJoinKeygenInfo -if (joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId) { - android.util.Log.d("MainViewModel", "Session started event for keygen joiner, triggering keygen") - startKeygenAsJoiner() // ← 【注意】没有用 safeLaunch 包裹! -} -``` - -**关键发现!** - -对比发起者和加入者: -- **发起者**: `safeLaunch { startKeygenAsInitiator(...) }` ← 包了 safeLaunch -- **加入者**: `startKeygenAsJoiner()` ← 没有包 safeLaunch - -**这是不一致的!** - ---- - -#### 步骤 4: 执行 keygen(加入者) -**代码位置**: `MainViewModel.kt:714-764` - -```kotlin -private suspend fun startKeygenAsJoiner() { - safeLaunch { // ← 【注意】这里也用了 safeLaunch - val joinInfo = pendingJoinKeygenInfo ?: return - - _uiState.update { it.copy(isLoading = true, error = null) } - - val result = repository.startKeygenAsJoiner( - sessionId = joinInfo.sessionId, - partyIndex = joinInfo.partyIndex, - participantIds = joinInfo.participantIds, - password = pendingPassword - ) - - result.fold( - onSuccess = { share -> - _joinKeygenPublicKey.value = share.publicKey - _uiState.update { - it.copy( - isLoading = false, - successMessage = "成功加入钱包!" - ) - } - }, - onFailure = { e -> - _uiState.update { it.copy(isLoading = false, error = e.message) } - } - ) - } -} -``` - -**问题**: -- `startKeygenAsJoiner` 自己已经用了 `safeLaunch` -- 但在事件回调中调用它时,**没有**再包一层 `safeLaunch` -- 这和发起者的处理方式不同! - -**不一致性总结**: - -| 角色 | 事件回调中 | 函数自身 | 总包裹层数 | -|-----|-----------|---------|----------| -| 发起者 | `safeLaunch { startKeygenAsInitiator() }` | 无 safeLaunch | 1层 | -| 加入者 | `startKeygenAsJoiner()` | `safeLaunch { ... }` | 1层 | - -虽然都是1层,但**位置不同**! - ---- - -## 🐛 已发现的Bug清单 - -### Bug 1: 事件回调中的异常处理不一致 ⚠️ - -**位置**: `MainViewModel.kt:398-413` - -**问题**: -- 发起者:事件回调中使用 `safeLaunch` 包裹 -- 加入者:事件回调中直接调用(函数内部有 `safeLaunch`) - -**影响**: -- 如果发起者的 `startKeygenAsInitiator` 在被 `safeLaunch` 调用**之前**抛出异常(例如参数验证),会被捕获 -- 但加入者的 `startKeygenAsJoiner` 在事件回调中直接调用,如果函数调用本身抛出异常(不是内部的),不会被捕获 - -**建议**: 统一处理方式 - ---- - -### Bug 2: safeLaunch 双重包裹可能导致静默失败 🚨 - -**位置**: `MainViewModel.kt:398-405` + `MainViewModel.kt:537-570` - -**问题流程**: -``` -事件回调 - ↓ -safeLaunch { // ← 第1层异常捕获 - startKeygenAsInitiator() - ↓ - 如果抛出异常 X - ↓ - } - ↓ -} catch (e: Exception) { // ← 捕获异常 X - _uiState.update { it.copy(error = ...) } // ← 更新错误 -} -``` - -但是: -1. `startKeygenAsInitiator` 内部已经处理了 `Result.failure` -2. 外层 `safeLaunch` 只能捕获**运行时异常** -3. 如果 `repository.startKeygenAsInitiator` 返回 `Result.failure`,不会抛出异常 -4. **所以外层 safeLaunch 实际上没什么用** - -**更严重的问题**: -如果 `startKeygenAsInitiator` 内部处理了错误(更新了 `_uiState.error`),但UI已经切换到其他状态,**用户可能看不到错误**! - ---- - -### Bug 3: 参与者数量不足时没有明确错误 ⚠️ - -**场景**: -- 创建 2-of-3 会话 -- server-party-co-managed 没有自动加入(配置错误) -- 只有手机1(发起者) -- **服务器不会广播 session_started 事件** - -**当前行为**: -- 手机1 一直显示"等待参与者加入..." -- **没有超时提示** -- **没有明确的错误消息** - -**建议**: 添加超时机制和友好提示 - ---- - -### Bug 4: getSessionStatus 失败时参与者列表不准确 ⚠️ - -**位置**: `MainViewModel.kt:302-321` - -```kotlin -val statusResult = repository.getSessionStatus(sessionResult.sessionId) -statusResult.fold( - onSuccess = { status -> - _sessionParticipants.value = status.participants.map { ... } - }, - onFailure = { e -> - // ⚠️ 失败时只显示自己 - _sessionParticipants.value = listOf(participantName) - } -) -``` - -**问题**: -- 如果 `getSessionStatus` 失败,参与者列表显示为1 -- 但实际上可能已经有多个参与者(例如 server-party-co-managed) -- **这会误导用户**,以为没人加入 - ---- - -### Bug 5: 事件回调中的 return 没有处理 ⚠️ - -**位置**: `MainViewModel.kt:714` (startKeygenAsJoiner) - -```kotlin -private suspend fun startKeygenAsJoiner() { - safeLaunch { - val joinInfo = pendingJoinKeygenInfo ?: return // ← 这个 return 只返回 lambda - // ... - } -} -``` - -**问题**: -- `return` 只会退出 `safeLaunch` 的 lambda -- 不会更新 UI 状态或显示错误 -- **用户不知道为什么 keygen 没有启动** - -**建议**: 如果 `joinInfo` 为 null,应该记录错误并通知用户 - ---- - -## 🔍 为什么会创建失败? - -### 最可能的原因 - -#### 原因 1: server-party-co-managed 没有正确加入 🔴 - -**检查**: -1. server-party-co-managed 是否正在运行? -2. 配置文件中是否启用了自动加入? -3. 服务器日志中是否有加入记录? - -**验证命令**: -```bash -# 检查 server-party-co-managed 日志 -tail -f /path/to/server-party-co-managed/logs/server.log | grep "join" -``` - -**预期日志**: -``` -[INFO] Detected new session: sessionId=xxx -[INFO] Auto-joining session as backup party -[INFO] Successfully joined session, partyId=backup-party-1 -``` - -如果**没有这些日志**,说明 server-party-co-managed 没有加入! - ---- - -#### 原因 2: session_started 事件没有被触发 🔴 - -**条件**: -- 服务器只有在 `participants.size >= thresholdT` 时才会广播 `session_started` -- 2-of-3 需要至少 2 个参与者 - -**检查**: -1. 服务器端参与者列表有多少个? -2. 手机1 的日志中是否有 "Session started event"? - -**预期日志(手机1)**: -``` -MainViewModel: === MainViewModel received session event === -MainViewModel: eventType: session_started -MainViewModel: sessionId: xxxxxxxx -MainViewModel: Session started event for keygen initiator, triggering keygen -``` - -如果**没有这条日志**,说明事件没有触发! - ---- - -#### 原因 3: startKeygenAsInitiator 内部失败但没有显示错误 🔴 - -**场景**: -1. `session_started` 事件触发了 -2. 调用了 `startKeygenAsInitiator` -3. 但 `repository.startKeygenAsInitiator` 返回 `Result.failure` -4. 错误被记录到 `_uiState.error` -5. **但 UI 没有显示错误**(因为还在"等待参与者"界面) - -**检查日志**: -``` -MainViewModel: Session started event for keygen initiator, triggering keygen -MainViewModel: Starting keygen as initiator: sessionId=xxx, t=2, n=3 -TssRepository: Starting keygen as initiator -TssRepository: Error: [具体错误信息] ← 看这里! -``` - -如果有这条错误日志,说明 keygen 启动失败了! - ---- - -## 🛠️ 调试步骤 - -### 步骤 1: 检查 server-party-co-managed - -```bash -# 1. 检查进程是否运行 -ps aux | grep server-party-co-managed - -# 2. 检查配置文件 -cat /path/to/server-party-co-managed/config.yml | grep -A 10 "auto_join" - -# 3. 查看最近日志 -tail -f /path/to/server-party-co-managed/logs/server.log -``` - -### 步骤 2: 抓取手机1(发起者)日志 - -```bash -adb logcat -c -adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|session_started" -``` - -**重点看**: -1. "Creating new session" → 会话创建 -2. "Session created successfully" → 会话创建成功 -3. "Session status fetched: X participants" → 参与者数量 -4. "Session started event" → 事件触发 -5. "Starting keygen as initiator" → keygen 启动 - -### 步骤 3: 抓取手机2(加入者)日志 - -```bash -adb logcat -c -adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|session_started" -``` - -**重点看**: -1. "Validate success: sessionId=" → 邀请码验证成功 -2. "Join keygen success: partyIndex=" → 加入成功 -3. "Session started event for keygen joiner" → 收到事件 - ---- - -## 🚀 推荐修复方案 - -### 修复 1: 统一事件回调中的异常处理 - -```kotlin -repository.setSessionEventCallback { event -> - when (event.eventType) { - "session_started" -> { - // 统一使用 safeLaunch 包裹所有启动函数 - val currentSessionId = _currentSessionId.value - if (currentSessionId != null && event.sessionId == currentSessionId) { - android.util.Log.d("MainViewModel", "Session started event for keygen initiator") - safeLaunch { - startKeygenAsInitiator(...) - } - } - - val joinKeygenInfo = pendingJoinKeygenInfo - if (joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId) { - android.util.Log.d("MainViewModel", "Session started event for keygen joiner") - safeLaunch { // ← 添加 safeLaunch - startKeygenAsJoiner() - } - } - } - } -} -``` - -### 修复 2: 移除 startKeygenAsJoiner 内部的 safeLaunch - -```kotlin -private suspend fun startKeygenAsJoiner() { - // 移除内部的 safeLaunch,由调用方负责异常处理 - val joinInfo = pendingJoinKeygenInfo - if (joinInfo == null) { - android.util.Log.e("MainViewModel", "startKeygenAsJoiner: joinInfo is null!") - _uiState.update { it.copy(error = "加入信息丢失,请重试") } - return - } - - _uiState.update { it.copy(isLoading = true, error = null) } - - val result = repository.startKeygenAsJoiner(...) - // ... -} -``` - -### 修复 3: 添加超时机制 - -在 `createNewSession` 后启动超时计时器: - -```kotlin -// 5分钟超时 -val timeoutJob = viewModelScope.launch { - delay(300_000) // 5 minutes - if (_currentSessionId.value != null && _publicKey.value == null) { - _uiState.update { - it.copy( - error = "等待超时:参与者数量不足或服务器未响应", - isLoading = false - ) - } - } -} -``` - ---- - -## 📊 总结 - -### 最可能导致失败的原因(按概率排序) - -1. **🔴 server-party-co-managed 没有自动加入** (70%) - - 检查配置和日志 - -2. **🔴 session_started 事件没有触发** (20%) - - 参与者数量不足 - - WebSocket 连接问题 - -3. **🟡 startKeygenAsInitiator 失败但错误被忽略** (8%) - - 检查手机日志中的异常 - -4. **🟢 safeLaunch 包裹问题** (2%) - - 理论上不会导致完全失败 - - 但可能导致错误信息不清晰 - -### 立即行动项 - -1. **检查 server-party-co-managed 状态** ← 最重要! -2. **抓取手机日志,搜索 "session_started"** -3. **搜索日志中的 "Caught exception" 或 "Error:"** -4. **把日志发给我进行详细分析** - ---- - -**请先按照"调试步骤"抓取日志,然后我们可以精确定位问题!** diff --git a/backend/mpc-system/services/service-party-android/CRASH_PREVENTION_SUMMARY.md b/backend/mpc-system/services/service-party-android/CRASH_PREVENTION_SUMMARY.md deleted file mode 100644 index 0b57420c..00000000 --- a/backend/mpc-system/services/service-party-android/CRASH_PREVENTION_SUMMARY.md +++ /dev/null @@ -1,593 +0,0 @@ -# Android TSS 钱包崩溃防护架构总结 - -## 📅 修复时间 -2026-01-26 - -## 🎯 修复目标 -从启动到运行时的全面崩溃防护,达到生产级稳定性(95%+ 防护覆盖率) - ---- - -## 📊 修复统计 - -### Commit 记录 -1. **bb6febb4** - P1-1: 修复参与者计数竞态条件 -2. **26ef03a1** - P1-2: 配置 OkHttpClient 连接池并添加资源清理 -3. **704ee523** - P2-1: 添加协程全局异常处理器 -4. **62b2a87e** - P2-2: MainViewModel 核心路径异常处理 (14个关键函数) -5. **85665fb6** - P2-2: MainViewModel 非关键路径异常处理 (14个辅助函数) - -### 覆盖率 -- ✅ **启动流程**: 100% -- ✅ **ViewModel 层**: 100% (28/28 函数) -- ✅ **Repository 层**: 100% -- ✅ **数据库操作**: 100% -- ✅ **网络请求**: 100% -- ✅ **后台任务**: 100% -- ✅ **生命周期管理**: 100% - ---- - -## 🔧 关键修复详解 - -### P0-1: lateinit var partyId 崩溃防护 - -**文件**: `TssRepository.kt` - -**问题**: -```kotlin -private lateinit var partyId: String // 未初始化访问会抛 UninitializedPropertyAccessException -``` - -**修复**: -```kotlin -/** - * 确保 partyId 已初始化,抛出描述性错误 - * - * 【架构安全修复 - 防止 UninitializedPropertyAccessException】 - * - * 问题场景: - * 1. 网络重连后访问 partyId - * 2. Activity 重建后访问 partyId - * 3. 应用从后台恢复后访问 partyId - * - * 修复方案: - * - 在所有可能未初始化的访问点添加检查 - * - 抛出 IllegalStateException 而非 UninitializedPropertyAccessException - * - 提供清晰的错误信息:"partyId not initialized. Call registerParty() first." - */ -private fun requirePartyId(): String { - if (!::partyId.isInitialized) { - android.util.Log.e("TssRepository", "partyId not initialized - registerParty() was not called") - throw IllegalStateException("partyId not initialized. Call registerParty() first.") - } - return partyId -} -``` - -**影响**: 100% 防止 lateinit 未初始化崩溃 - ---- - -### P0-2: gRPC Channel shutdown ANR 防护 - -**文件**: `GrpcClient.kt` - -**问题**: -```kotlin -// 原代码:主线程阻塞等待 channel 关闭 -channel.shutdown() -channel.awaitTermination(5, TimeUnit.SECONDS) // ❌ 阻塞主线程 5 秒 → ANR -``` - -**修复**: -```kotlin -/** - * 清理连接资源 - * - * 【架构安全修复 - 防止主线程阻塞 ANR】 - * - * 问题背景: - * - channel.awaitTermination() 是阻塞调用 - * - 如果在主线程调用会导致 ANR (Application Not Responding) - * - Android 系统会在 5 秒后杀死应用 - * - * 修复方案: - * - 将 shutdown 操作移到 IO 协程 - * - 优雅关闭 (3秒) → 强制关闭 (1秒) 的降级策略 - * - 所有异常都捕获,不影响应用主流程 - */ -private fun cleanupConnection() { - // ... 取消 Job ... - - val channelToShutdown = channel - if (channelToShutdown != null) { - channel = null - stub = null - asyncStub = null - - scope.launch(Dispatchers.IO) { - try { - channelToShutdown.shutdown() - val gracefullyTerminated = channelToShutdown.awaitTermination(3, TimeUnit.SECONDS) - - if (!gracefullyTerminated) { - channelToShutdown.shutdownNow() - channelToShutdown.awaitTermination(1, TimeUnit.SECONDS) - } - } catch (e: InterruptedException) { - channelToShutdown.shutdownNow() - } catch (e: Exception) { - try { - channelToShutdown.shutdownNow() - } catch (shutdownError: Exception) { - Log.e(TAG, "Failed to force shutdown channel", shutdownError) - } - } - } - } -} -``` - -**影响**: 100% 消除 ANR 风险 - ---- - -### P0-3: Job 生命周期内存泄漏防护 - -**文件**: `TssRepository.kt` - -**问题**: -```kotlin -// 原代码:4 个独立的 Job 变量,手动管理容易遗漏 -private var messageCollectionJob: Job? = null -private var sessionEventJob: Job? = null -private var sessionStatusPollingJob: Job? = null -private var progressCollectionJob: Job? = null - -fun cleanup() { - messageCollectionJob?.cancel() - sessionEventJob?.cancel() - // ❌ 容易忘记取消某个 Job → 协程泄漏 → 内存泄漏 -} -``` - -**修复**: -```kotlin -/** - * JobManager - 统一管理后台协程任务 - * - * 【架构安全修复 - 防止协程泄漏】 - * - * 问题背景: - * - TssRepository 中有 4 个独立的 Job 变量 - * - cleanup() 需要手动取消每个 Job,容易遗漏导致协程泄漏 - * - 没有统一的生命周期管理和错误处理 - * - * 修复的内存泄漏风险: - * 1. Activity 销毁时 Job 未取消 → 后台协程继续运行 → 内存泄漏 → OOM - * 2. 快速重启连接时旧 Job 未取消 → 多个 Job 并行运行 → 资源竞争 - * 3. 异常导致某个 Job 未取消 → 僵尸协程 → 内存累积 - * - * JobManager 功能: - * - 统一启动和取消所有后台 Job - * - 自动替换同名 Job(防止重复启动) - * - 一键清理所有 Job(防止遗漏) - * - 提供 Job 状态查询 - */ -private inner class JobManager { - private val jobs = mutableMapOf() - - fun launch(name: String, block: suspend CoroutineScope.() -> Unit): Job { - jobs[name]?.cancel() // 自动取消旧 Job - val job = repositoryScope.launch(block = block) - jobs[name] = job - return job - } - - fun cancelAll() { - jobs.values.forEach { it.cancel() } - jobs.clear() - } -} - -private val jobManager = JobManager() - -fun cleanup() { - jobManager.cancelAll() // ✅ 一键清理,不会遗漏 - repositoryScope.cancel() - grpcClient.disconnect() -} -``` - -**影响**: 100% 防止 Job 内存泄漏 - ---- - -### P1-1: 参与者计数竞态条件防护 - -**文件**: `MainViewModel.kt` - -**问题**: -```kotlin -// 原代码:使用本地计数器,容易出现重复添加和乱序 -when (event.eventType) { - "participant_joined" -> { - val current = _sessionParticipants.value - _sessionParticipants.value = current + "参与方 ${current.size + 1}" - // ❌ 问题1: 事件重放会导致重复添加 - // ❌ 问题2: 事件乱序会导致编号错乱 - } -} -``` - -**修复**: -```kotlin -/** - * 【架构安全修复 - 防止参与者计数竞态条件】 - * - * 原问题:使用 current.size + 1 递增计数器存在多个风险 - * 1. 事件重放:重连后事件可能重复发送,导致参与者重复添加 - * 2. 事件乱序:网络延迟可能导致事件乱序到达,参与者编号错乱 - * 3. 状态不一致:本地计数与服务端真实参与者列表不同步 - * - * 修复方案:使用事件的 selectedParties 字段构建权威的参与者列表 - * - selectedParties 来自服务端,是参与者的唯一真实来源 - * - 根据 selectedParties.size 构建参与者列表,确保与服务端一致 - * - 防止重复添加和计数错乱 - */ -when (event.eventType) { - "party_joined", "participant_joined" -> { - // ✅ 使用服务端的权威数据构建参与者列表 - val participantCount = event.selectedParties.size - val participantList = List(participantCount) { index -> "参与方 ${index + 1}" } - _sessionParticipants.value = participantList // 幂等更新 - } -} -``` - -**影响**: 100% 幂等性保证,防止重复和乱序 - ---- - -### P1-2: OkHttpClient 资源泄漏防护 - -**文件**: `TssRepository.kt`, `TransactionUtils.kt` - -**问题**: -```kotlin -// 原代码:无限制的连接池,从不清理 -private val httpClient = OkHttpClient() -// ❌ 问题1: 连接池无限增长 -// ❌ 问题2: 空闲连接永不关闭 -// ❌ 问题3: Dispatcher 线程池永不关闭 -``` - -**修复**: -```kotlin -/** - * HTTP 客户端配置 - * - * 【架构安全修复 - 防止 OkHttpClient 资源泄漏】 - * - * 问题背景: - * - OkHttpClient 维护连接池和线程池 - * - 默认配置会无限增长,导致资源泄漏 - * - 应用退出时需要显式清理 - * - * 配置策略: - * - maxIdleConnections: 5 (最多保留 5 个空闲连接) - * - keepAliveDuration: 5分钟 (空闲连接保持时间) - * - 超时后自动关闭连接 - */ -private val httpClient = okhttp3.OkHttpClient.Builder() - .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) - .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) - .connectionPool(okhttp3.ConnectionPool( - maxIdleConnections = 5, - keepAliveDuration = 5, - timeUnit = java.util.concurrent.TimeUnit.MINUTES - )) - .build() - -/** - * 清理资源 - * - * OkHttpClient 维护连接池和线程池,必须显式清理: - * 1. evictAll() - 关闭并移除所有空闲连接 - * 2. executorService().shutdown() - 关闭调度器线程池 - * 3. connectionPool().evictAll() - 清空连接池 - */ -fun cleanup() { - try { - httpClient.connectionPool.evictAll() - httpClient.dispatcher.executorService.shutdown() - httpClient.cache?.close() - } catch (e: Exception) { - android.util.Log.e("TssRepository", "Failed to cleanup OkHttpClient resources", e) - } -} -``` - -**影响**: 资源使用减少 80%+,防止连接和线程泄漏 - ---- - -### P2-1: Repository 后台异常全局处理 - -**文件**: `TssRepository.kt` - -**问题**: -```kotlin -// 原代码:后台协程异常未捕获会传播到应用 -private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) -// ❌ SupervisorJob 只防止子协程失败取消其他子协程 -// ❌ 未处理的异常最终会导致应用崩溃 -``` - -**修复**: -```kotlin -/** - * 全局协程异常处理器 - * - * 【架构安全修复 - 防止未捕获异常导致应用崩溃】 - * - * 问题背景: - * - 协程中的未捕获异常会传播到父协程 - * - SupervisorJob 虽然防止子协程失败取消其他子协程,但不捕获异常 - * - 未处理的异常最终会导致应用崩溃 - * - * 修复方案: - * - 添加 CoroutineExceptionHandler 捕获所有未处理的异常 - * - 记录详细的异常信息(协程上下文、堆栈) - * - 防止应用崩溃,保持功能可用 - * - * 适用场景: - * 1. 后台消息收集失败 - 不应导致整个应用崩溃 - * 2. 事件订阅异常 - 记录错误但继续运行 - * 3. RPC 调用失败 - 优雅降级而非崩溃 - */ -private val coroutineExceptionHandler = CoroutineExceptionHandler { context, exception -> - android.util.Log.e("TssRepository", "Uncaught coroutine exception", exception) - - when (exception) { - is CancellationException -> { - // 正常的协程取消,不需要特殊处理 - } - is java.net.SocketTimeoutException, - is java.net.UnknownHostException, - is java.io.IOException -> { - // 网络异常 - 记录并可能触发重连 - android.util.Log.w("TssRepository", "Network error: ${exception.message}") - } - is IllegalStateException, - is IllegalArgumentException -> { - // 状态异常 - 可能是编程错误 - android.util.Log.e("TssRepository", "State error: ${exception.message}", exception) - } - else -> { - // 其他未知异常 - android.util.Log.e("TssRepository", "Unknown error: ${exception.javaClass.simpleName}", exception) - } - } -} - -private val repositoryScope = CoroutineScope( - SupervisorJob() + Dispatchers.IO + coroutineExceptionHandler -) -``` - -**影响**: 100% 后台异常捕获,防止崩溃传播 - ---- - -### P2-2: ViewModel UI 层异常全面处理 - -**文件**: `MainViewModel.kt` - -**问题**: -```kotlin -// 原代码:用户操作直接使用 viewModelScope.launch,异常会导致崩溃 -fun createKeygenSession(...) { - viewModelScope.launch { - repository.createKeygenSession(...) - // ❌ 网络异常、状态异常等会直接抛出 - // ❌ 用户看到应用崩溃,体验极差 - } -} -``` - -**修复**: -```kotlin -/** - * 安全启动协程 - 自动捕获异常防止应用崩溃 - * - * 【架构安全修复 - ViewModel 层异常处理】 - * - * 问题背景: - * - viewModelScope.launch 没有配置 CoroutineExceptionHandler - * - 未捕获的异常会导致应用崩溃 - * - 用户操作触发的异常体验最差 - * - * 解决方案: - * - 提供 safeLaunch 扩展函数,自动添加 try-catch - * - 捕获所有异常并更新 UI 错误状态 - * - 记录详细日志用于调试 - * - * 使用方式: - * safeLaunch { - * // 业务逻辑,异常会被自动捕获 - * } - */ -private fun safeLaunch( - onError: ((Exception) -> Unit)? = null, - block: suspend CoroutineScope.() -> Unit -) = viewModelScope.launch { - try { - block() - } catch (e: CancellationException) { - // 协程取消是正常行为,重新抛出 - throw e - } catch (e: Exception) { - // 捕获所有其他异常 - android.util.Log.e("MainViewModel", "Caught exception in safeLaunch", e) - - // 根据异常类型进行分类处理 - val errorMessage = when (e) { - is java.net.SocketTimeoutException -> "网络超时,请检查网络连接" - is java.net.UnknownHostException -> "无法连接到服务器,请检查网络设置" - is java.io.IOException -> "网络错误: ${e.message}" - is IllegalStateException -> "状态错误: ${e.message}" - is IllegalArgumentException -> "参数错误: ${e.message}" - else -> "操作失败: ${e.message ?: e.javaClass.simpleName}" - } - - // 调用自定义错误处理器(如果提供) - if (onError != null) { - onError(e) - } else { - // 默认更新 UI 错误状态 - _uiState.update { it.copy(isLoading = false, error = errorMessage) } - } - } -} - -// ✅ 所有 28 个用户交互函数都已转换为 safeLaunch -fun createKeygenSession(...) { - safeLaunch { - // 业务逻辑,异常自动捕获并显示友好错误信息 - } -} -``` - -**影响**: -- 100% UI 层异常处理覆盖 (28/28 函数) -- 崩溃 → 友好错误提示 -- 用户体验提升显著 - ---- - -## 📈 修复前后对比 - -### 崩溃风险评估 - -| 风险类型 | 修复前 | 修复后 | 降低 | -|----------|--------|--------|------| -| **启动崩溃** | ~5% | <0.1% | 98% ↓ | -| **运行时崩溃** | ~10% | <0.5% | 95% ↓ | -| **ANR (无响应)** | ~2% | <0.1% | 95% ↓ | -| **内存泄漏** | ~8% | <0.1% | 99% ↓ | -| **总体稳定性** | 85分 | 99分 | +14分 | - -### 异常处理覆盖率 - -``` -修复前: ~30% -├─ 启动流程: 20% -├─ ViewModel: 0% -├─ Repository: 50% -└─ 网络/数据库: 40% - -修复后: 100% -├─ 启动流程: 100% ✅ -├─ ViewModel: 100% (28/28) ✅ -├─ Repository: 100% ✅ -└─ 网络/数据库: 100% ✅ -``` - ---- - -## 🎯 架构优势 - -### 1. 防御性编程 -- **层层防护**: Application → Activity → ViewModel → Repository → DAO -- **异常分类**: 网络/状态/未知异常的友好提示 -- **优雅降级**: 服务失败 → ERROR 状态,不崩溃 - -### 2. 资源生命周期管理 -- **统一清理**: JobManager + cleanup() -- **自动回收**: viewModelScope 绑定 ViewModel 生命周期 -- **无泄漏**: 所有资源在 onCleared() 时清理 - -### 3. 线程安全 -- **StateFlow**: 所有 UI 状态使用 StateFlow -- **Compose**: 自动在主线程收集,生命周期感知 -- **协程**: 所有异步操作使用协程,无回调地狱 - -### 4. 可维护性 -- **详细注释**: 每个修复点都有中文注释说明问题和解决方案 -- **一致性**: safeLaunch 统一异常处理模式 -- **可追溯**: 所有修复都关联到具体的崩溃场景 - ---- - -## 🔍 验证方法 - -### 1. 编译验证 -```bash -cd backend/mpc-system/services/service-party-android -./gradlew assembleDebug -# ✅ BUILD SUCCESSFUL in 24s -``` - -### 2. 代码覆盖率检查 -```bash -# 检查所有 viewModelScope.launch 已转换为 safeLaunch -grep -r "viewModelScope\.launch" app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt -# ✅ 只有 safeLaunch 内部的实现(第43行) -``` - -### 3. 静态分析 -- ✅ 无 lateinit var 未检查访问 -- ✅ 无阻塞 main 线程的 suspend 调用 -- ✅ 所有 Job 都由 JobManager 管理 -- ✅ 所有 OkHttpClient 都配置了 ConnectionPool - ---- - -## 📚 相关文档 - -- [Kotlin 协程异常处理](https://kotlinlang.org/docs/exception-handling.html) -- [Android ViewModel 最佳实践](https://developer.android.com/topic/libraries/architecture/viewmodel) -- [OkHttp 连接池配置](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/) -- [Room 数据库最佳实践](https://developer.android.com/training/data-storage/room) - ---- - -## 🚀 后续建议 - -### 可选优化(优先级:低) - -1. **全局异常处理器**(安全网) - - 捕获所有未处理的异常 - - 显示友好的错误对话框 - - 可选:发送崩溃报告 - -2. **启动性能监控** - - 记录启动耗时 - - 识别慢启动问题 - -3. **Room 迁移降级**(仅开发环境) - - fallbackToDestructiveMigration() - - 避免开发时数据库版本冲突 - ---- - -## ✅ 总结 - -经过系统性的崩溃防护架构升级,service-party-android 已达到: - -- ✅ **100% 关键路径异常处理覆盖** -- ✅ **完善的资源生命周期管理** -- ✅ **军事级的后台任务管理** -- ✅ **企业级的网络异常处理** -- ✅ **框架级的线程安全保证** - -**崩溃率预估**: <0.5% (行业平均 1-2%) -**生产可用性**: ✅ 推荐上线 - ---- - -*文档生成时间: 2026-01-26* -*代码版本: commit 85665fb6* -*检查范围: 启动流程、生命周期、异常处理、资源管理、线程安全* diff --git a/backend/mpc-system/services/service-party-android/DEBUG_LOG_GUIDE.md b/backend/mpc-system/services/service-party-android/DEBUG_LOG_GUIDE.md deleted file mode 100644 index 908f8abc..00000000 --- a/backend/mpc-system/services/service-party-android/DEBUG_LOG_GUIDE.md +++ /dev/null @@ -1,398 +0,0 @@ -# Android 应用调试日志抓取指南 - -## 当前日志配置分析 - -### ✅ 已有日志点 - -应用在关键位置使用 `android.util.Log` 记录日志: - -| 日志标签 | 位置 | 日志内容 | -|---------|------|---------| -| `MainViewModel` | 所有 ViewModel 操作 | 会话创建、参与者加入、TSS进度、异常 | -| `TssRepository` | 所有 Repository 操作 | gRPC调用、TSS native调用、数据库操作 | -| `GrpcClient` | 网络通信 | gRPC连接、请求/响应、错误 | -| `TssNativeBridge` | TSS原生库 | 密钥生成、签名、错误 | - -### 📋 关键日志内容 - -#### 1. 会话创建(创建2-of-3钱包) -``` -MainViewModel: Creating new session: walletName=xxx, t=2, n=3 -MainViewModel: Session created: sessionId=xxx, inviteCode=xxx -MainViewModel: Session status fetched: X participants already joined -MainViewModel: Participants: [partyId1, partyId2, ...] -``` - -#### 2. 密钥生成触发 -``` -MainViewModel: Session started event for keygen initiator, triggering keygen -MainViewModel: Starting keygen as initiator: sessionId=xxx, t=2, n=3 -TssRepository: Starting keygen as initiator -TssNativeBridge: keygenAsInitiator called -``` - -#### 3. 异常捕获(safeLaunch) -``` -MainViewModel: Caught exception in safeLaunch -[Stack trace...] -``` - ---- - -## 🔍 日志抓取命令 - -### 方案 1: 实时日志(推荐用于复现问题) - -```bash -# 清除旧日志 -adb logcat -c - -# 实时查看所有应用日志(带时间戳) -adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|TssNativeBridge|AndroidRuntime" -``` - -### 方案 2: 过滤关键标签(快速定位) - -```bash -# 只看应用相关日志 -adb logcat MainViewModel:D TssRepository:D GrpcClient:D TssNativeBridge:D AndroidRuntime:E *:S -``` - -### 方案 3: 保存完整日志到文件 - -```bash -# 清除旧日志 -adb logcat -c - -# 重现问题(创建2-of-3钱包) - -# 保存日志到文件 -adb logcat -d -v time > android_debug.log -``` - ---- - -## 🎯 重点关注的日志 - -### 创建 2-of-3 钱包失败时需要看到: - -#### ✅ 必须有的日志(正常流程) -1. **会话创建请求**: - ``` - MainViewModel: Creating new session: walletName=测试钱包, t=2, n=3, participantName=xxx - ``` - -2. **会话创建成功**: - ``` - MainViewModel: Session created successfully - MainViewModel: sessionId: xxxxxxxx - MainViewModel: inviteCode: ABCD1234 - ``` - -3. **获取会话状态**: - ``` - MainViewModel: Session status fetched: 2 participants already joined - MainViewModel: Participants: [party-id-1, party-id-2] - ``` - -4. **收到 session_started 事件**: - ``` - MainViewModel: === MainViewModel received session event === - MainViewModel: eventType: session_started - MainViewModel: sessionId: xxxxxxxx - ``` - -5. **触发密钥生成**: - ``` - MainViewModel: Session started event for keygen initiator, triggering keygen - MainViewModel: Starting keygen as initiator: sessionId=xxx, t=2, n=3 - ``` - -6. **TSS 原生库调用**: - ``` - TssNativeBridge: keygenAsInitiator called with sessionId=xxx - TssNativeBridge: Keygen completed successfully - ``` - -7. **进度更新**: - ``` - MainViewModel: Progress update: 1 / 9 - MainViewModel: Progress update: 2 / 9 - ... - MainViewModel: Progress update: 9 / 9 - ``` - -#### ❌ 可能出现的错误日志 - -1. **safeLaunch 捕获的异常**: - ``` - MainViewModel: Caught exception in safeLaunch - java.net.SocketTimeoutException: timeout - at ... - ``` - -2. **gRPC 连接失败**: - ``` - GrpcClient: Failed to connect to server - GrpcClient: Error: UNAVAILABLE: io exception - ``` - -3. **TSS 原生库错误**: - ``` - TssNativeBridge: keygenAsInitiator failed: [error message] - ``` - -4. **会话创建失败**: - ``` - MainViewModel: Service check failed - TssRepository: Failed to create session: [error message] - ``` - ---- - -## 🚨 关键问题检查点 - -### 1. 检查 safeLaunch 是否吞掉了异常 - -搜索日志中的: -``` -"Caught exception in safeLaunch" -``` - -如果有这行,说明异常被捕获了,查看后续的堆栈跟踪。 - -### 2. 检查是否有 Result.failure 未处理 - -搜索: -``` -"onFailure" -"Failed to" -"Error:" -``` - -### 3. 检查 session_started 事件是否触发 - -搜索: -``` -"Session started event for keygen initiator" -``` - -如果**没有这行**,说明事件回调没有触发,密钥生成没有启动。 - -### 4. 检查参与者计数 - -搜索: -``` -"Session status fetched: X participants" -``` - -如果参与者数量 < thresholdT(例如 2-of-3 需要至少2个参与者),会话不会启动。 - ---- - -## 📊 日志分析流程图 - -``` -启动应用 - ↓ -[搜索] "Service check" - ├─ 成功 → 继续 - └─ 失败 → 检查数据库/网络/原生库错误 - ↓ -点击"创建钱包" - ↓ -[搜索] "Creating new session" - ├─ 有 → 继续 - └─ 无 → UI事件未触发(前端问题) - ↓ -[搜索] "Session created successfully" - ├─ 有 → 继续 - └─ 无 → 检查 gRPC 错误或 "onFailure" - ↓ -[搜索] "Session status fetched: X participants" - ├─ 有 → 检查参与者数量是否 >= thresholdT - └─ 无 → getSessionStatus 调用失败 - ↓ -[搜索] "Session started event" - ├─ 有 → 继续 - └─ 无 → WebSocket 事件未收到(服务器问题) - ↓ -[搜索] "Starting keygen as initiator" - ├─ 有 → 继续 - └─ 无 → safeLaunch 内部异常(搜索 "Caught exception") - ↓ -[搜索] "keygenAsInitiator called" - ├─ 有 → 检查 "Keygen completed" 或 TSS错误 - └─ 无 → 原生库调用未执行 - ↓ -[搜索] "Progress update" - ├─ 有 → TSS 正在进行,检查是否完成 - └─ 无 → TSS 未启动或卡住 - ↓ -[搜索] "Keygen completed successfully" - ├─ 有 → 成功! - └─ 无 → 检查 TSS 错误日志 -``` - ---- - -## 🛠️ 调试建议 - -### 如果看到 "Caught exception in safeLaunch" - -**原因**: safeLaunch 捕获了异常但可能没有正确显示给用户 - -**临时解决方案**: 查看异常堆栈,找到根本原因 - -**示例**: -``` -MainViewModel: Caught exception in safeLaunch -java.net.SocketTimeoutException: timeout - at okhttp3.internal.connection.RealCall.execute - at ... -``` -→ 说明 gRPC 连接超时 - -### 如果没有 "Session started event" - -**原因**: WebSocket 事件回调未触发 - -**可能问题**: -1. service-party 服务器未运行 -2. WebSocket 连接断开 -3. 服务器未广播 session_started 事件 -4. 参与者数量不足(< thresholdT) - -**检查**: -```bash -# 检查服务器日志 -tail -f /path/to/service-party/logs/server.log -``` - -### 如果有 "Session started event" 但没有 "Starting keygen" - -**原因**: safeLaunch 内部的 startKeygenAsInitiator 调用失败 - -**检查**: -- 搜索 "Caught exception in safeLaunch" -- 查看异常类型和堆栈 - ---- - -## 📝 日志模板(复制给我) - -抓取日志后,请提供以下信息: - -``` -### 1. 操作步骤 -- [ ] 启动应用 -- [ ] 点击"创建钱包" -- [ ] 输入钱包名称: ___ -- [ ] 选择 2-of-3 -- [ ] 输入参与者名称: ___ -- [ ] 点击"创建" -- [ ] 【描述具体现象】: ___ - -### 2. 关键日志片段 - -#### 会话创建 -``` -[粘贴包含 "Creating new session" 的日志] -``` - -#### 会话状态 -``` -[粘贴包含 "Session status fetched" 的日志] -``` - -#### 事件触发 -``` -[粘贴包含 "Session started event" 的日志] -``` - -#### 异常(如果有) -``` -[粘贴包含 "Caught exception" 的日志] -``` - -### 3. 完整日志文件 -[附件: android_debug.log] -``` - ---- - -## 🔧 临时调试增强(如需更详细日志) - -如果标准日志不够详细,可以临时添加更多日志: - -### 在 MainViewModel.kt 的 safeLaunch 中: - -```kotlin -private fun safeLaunch( - onError: ((Exception) -> Unit)? = null, - block: suspend CoroutineScope.() -> Unit -) = viewModelScope.launch { - try { - android.util.Log.d("MainViewModel", "safeLaunch: Starting block") // 添加这行 - block() - android.util.Log.d("MainViewModel", "safeLaunch: Block completed successfully") // 添加这行 - } catch (e: CancellationException) { - android.util.Log.d("MainViewModel", "safeLaunch: CancellationException caught") // 添加这行 - throw e - } catch (e: Exception) { - android.util.Log.e("MainViewModel", "safeLaunch: Caught exception: ${e.javaClass.simpleName}", e) - // ... 现有代码 ... - } -} -``` - ---- - -## ⚠️ 常见陷阱 - -### 1. 异常被吞掉但 UI 不显示错误 - -**症状**: 操作无反应,没有错误提示 - -**原因**: safeLaunch 更新了 _uiState.error 但 UI 没有订阅 - -**检查**: 搜索日志中的 "Caught exception",看是否有异常但 UI 没反应 - -### 2. Result.failure 未正确处理 - -**症状**: repository 返回 failure 但 ViewModel 没有处理 - -**检查**: 搜索 "onFailure" 和 "result.fold" - -### 3. 协程被取消 - -**症状**: 操作执行到一半停止 - -**检查**: 搜索 "CancellationException" - ---- - -## 📱 完整抓取命令(复制粘贴) - -```bash -# 1. 清除旧日志 -adb logcat -c - -# 2. 开始记录(在另一个终端) -adb logcat -v time > ~/Desktop/android_debug_$(date +%Y%m%d_%H%M%S).log - -# 3. 操作应用(重现问题) - -# 4. 停止记录(Ctrl+C) - -# 5. 发送日志文件给我 -``` - -或者一步到位(操作完后手动停止): -```bash -adb logcat -c && adb logcat -v time | tee android_debug.log | grep --color -E "MainViewModel|TssRepository|GrpcClient|TssNativeBridge|Exception|Error" -``` - ---- - -**准备好后,请执行上述命令,重现创建2-of-3失败的问题,然后把日志发给我!** diff --git a/backend/mpc-system/services/service-party-android/EXACT_CHANGES.md b/backend/mpc-system/services/service-party-android/EXACT_CHANGES.md deleted file mode 100644 index bb883a65..00000000 --- a/backend/mpc-system/services/service-party-android/EXACT_CHANGES.md +++ /dev/null @@ -1,257 +0,0 @@ -# 准确的改动清单 - -## 1. 删除的内容 ❌ - -### 删除的文件: -- `app/src/main/java/com/durian/tssparty/data/remote/StreamManager.kt` (整个文件,282行) - -### 删除的代码(TssRepository.kt): - -#### 第17行 - 删除 import: -```kotlin -- import com.durian.tssparty.data.remote.StreamManager -``` - -#### 第217-242行 - 删除 StreamManager 实例和注释: -```kotlin -- /** -- * StreamManager - 管理 gRPC 双向流的生命周期 -- * ...(大段注释) -- */ -- private val streamManager = StreamManager(grpcClient, repositoryScope) -``` - -#### 第293-304行 - 删除 init 块: -```kotlin -- init { -- repositoryScope.launch { -- grpcConnectionEvents -- .filter { it is GrpcConnectionEvent.Reconnected } -- .collect { -- android.util.Log.d("TssRepository", "gRPC reconnected, restarting streams via StreamManager...") -- streamManager.restartAllStreams() -- } -- } -- } -``` - -#### 第511-611行 - 删除 StreamManager 的事件订阅: -```kotlin -- streamManager.startEventStream( -- partyId = effectivePartyId, -- onEvent = { event -> -- // ... 事件处理逻辑 ... -- }, -- onError = { error -> -- android.util.Log.e("TssRepository", "Event stream error: ${error.message}") -- } -- ) -``` - -#### 第2062-2098行 - 删除 StreamManager 的消息订阅: -```kotlin -- streamManager.startMessageStream( -- sessionId = sessionId, -- partyId = effectivePartyId, -- partyIndex = partyIndex, -- onMessage = { message -> -- // ... 消息处理逻辑 ... -- }, -- onError = { error -> -- android.util.Log.e("TssRepository", "Message stream error: ${error.message}") -- } -- ) -``` - -## 2. 添加的内容 ✅ - -### TssRepository.kt 第220行 - 添加 Job 常量: -```kotlin -+ private const val JOB_MESSAGE_SENDING = "message_sending" -``` - -### 第488-496行 - 添加 registerParty 错误检查: -```kotlin -+ // Register with gRPC and check result -+ val registerResult = grpcClient.registerParty(partyId, "temporary", "1.0.0") -+ if (registerResult.isFailure) { -+ val error = registerResult.exceptionOrNull() -+ android.util.Log.e("TssRepository", "Failed to register party: ${error?.message}") -+ throw error ?: Exception("Failed to register party") -+ } -+ -+ android.util.Log.d("TssRepository", "Party registered successfully: $partyId") -``` - -### 第511-577行 - 恢复简单的事件订阅(添加 retryWhen): -```kotlin -+ // 使用 JobManager 启动(自动取消同名旧 Job) -+ // 添加 Flow.retryWhen 实现自动重连(基于 gRPC 官方推荐) -+ jobManager.launch(JOB_SESSION_EVENT) { -+ flow { -+ grpcClient.subscribeSessionEvents(effectivePartyId).collect { event -> -+ emit(event) -+ } -+ } -+ .retryWhen { cause, attempt -> -+ android.util.Log.w("TssRepository", "Event stream failed (attempt ${attempt + 1}), retrying in ${kotlin.math.min(attempt + 1, 30)}s: ${cause.message}") -+ delay(kotlin.math.min(attempt + 1, 30) * 1000L) // 指数退避,最多 30 秒 -+ true // 永远重试 -+ } -+ .collect { event -> -+ // ... 原有的事件处理逻辑(完全不变)... -+ } -+ } -``` - -### 第2043-2087行 - 重构消息路由(添加 retryWhen): -```kotlin -+ // Part 1: Collect outgoing messages from TSS and route via gRPC -+ jobManager.launch(JOB_MESSAGE_SENDING) { // 改名为 JOB_MESSAGE_SENDING -+ tssNativeBridge.outgoingMessages.collect { message -> -+ // ... 发送逻辑 ... -+ } -+ } -+ -+ // Part 2: Subscribe to incoming messages from gRPC and send to TSS -+ // 添加 Flow.retryWhen 实现自动重连(基于 gRPC 官方推荐) -+ jobManager.launch(JOB_MESSAGE_COLLECTION) { -+ flow { -+ grpcClient.subscribeMessages(sessionId, effectivePartyId).collect { message -> -+ emit(message) -+ } -+ } -+ .retryWhen { cause, attempt -> -+ android.util.Log.w("TssRepository", "Message stream failed (attempt ${attempt + 1}), retrying in ${kotlin.math.min(attempt + 1, 30)}s: ${cause.message}") -+ delay(kotlin.math.min(attempt + 1, 30) * 1000L) // 指数退避,最多 30 秒 -+ true // 永远重试 -+ } -+ .collect { message -> -+ // ... 原有的消息处理逻辑(完全不变)... -+ } -+ } -``` - -### 第592行 - 修改检查方法: -```kotlin -- val isActive = streamManager.isEventStreamActive() -+ val isActive = jobManager.isActive(JOB_SESSION_EVENT) -``` - -## 3. 完全不变的内容 ✅ - -### GrpcClient.kt - Keep-Alive 配置(保持不变): -```kotlin -// Line 143-150 - 完全不变 -.keepAliveTime(20, TimeUnit.SECONDS) -.keepAliveTimeout(5, TimeUnit.SECONDS) -.keepAliveWithoutCalls(true) -.idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) -``` - -### GrpcClient.kt - 网络监听(保持不变): -```kotlin -// Line 151-183 - 完全不变 -fun setupNetworkMonitoring(context: Context) { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - channel?.resetConnectBackoff() - } - } -} -``` - -### TssRepository.kt - 事件处理逻辑(保持不变): -```kotlin -// Line 522-573 - 完全不变 -when (event.eventType) { - "session_started" -> { - // ... 原有的 RACE-FIX 逻辑 ... - sessionEventCallback?.invoke(event) - } - "party_joined", "participant_joined" -> { - sessionEventCallback?.invoke(event) - } - // ... -} -``` - -### TssRepository.kt - 消息处理逻辑(保持不变): -```kotlin -// Line 2071-2084 - 完全不变 -val fromPartyIndex = session?.participants?.find { it.partyId == message.fromParty }?.partyIndex -if (fromPartyIndex != null) { - tssNativeBridge.sendIncomingMessage( - fromPartyIndex = fromPartyIndex, - isBroadcast = message.isBroadcast, - payload = message.payload - ) -} -``` - -### TssRepository.kt - markPartyReady 重试机制(保持不变): -```kotlin -// Line ~2140 - 完全不变 -repeat(5) { attempt -> - if (markReadySuccess) return@repeat - val markReadyResult = grpcClient.markPartyReady(sessionId, partyId) - if (markReadyResult.isSuccess) { - markReadySuccess = true - return@repeat - } - delay((attempt + 1) * 500L) -} -``` - -## 4. 核心改动总结 - -### 之前(df9f9914): -```kotlin -streamManager.startEventStream( - partyId = effectivePartyId, - onEvent = { event -> /* callback */ }, - onError = { error -> /* callback */ } -) -``` - -### 现在(bfbd062e): -```kotlin -jobManager.launch(JOB_SESSION_EVENT) { - flow { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { emit(it) } - } - .retryWhen { cause, attempt -> - delay(min(attempt + 1, 30) * 1000L) - true // 自动重连 - } - .collect { event -> - // 原有的事件处理逻辑(完全不变) - } -} -``` - -## 5. 统计数据 - -- 删除:StreamManager.kt (282 行) + TssRepository.kt 中的引用 (约 66 行) = **348 行** -- 添加:WORKING_CODE_ANALYSIS.md (269 行) + REFACTORING_SUMMARY.md (200 行) + TssRepository.kt 改动 (45 行) = **514 行** -- 净变化:+166 行(主要是文档) -- 代码净变化:-21 行(代码更简洁) - -## 6. 风险评估 - -### 低风险 ✅: -1. **事件处理逻辑完全不变**(只是外面包了 retryWhen) -2. **消息处理逻辑完全不变**(只是外面包了 retryWhen) -3. **gRPC Keep-Alive 配置保持不变** -4. **网络监听保持不变** -5. **markPartyReady 重试保持不变** - -### 需要测试 ⚠️: -1. registerParty 错误检查是否正常 -2. retryWhen 自动重连是否工作 -3. 网络断开后是否自动恢复 - -### 消除的风险 ✅: -1. StreamManager 的潜在 bug -2. 复杂的 callback 机制 -3. init 块监听重连的问题 diff --git a/backend/mpc-system/services/service-party-android/GRPC_CORRECT_SOLUTION.md b/backend/mpc-system/services/service-party-android/GRPC_CORRECT_SOLUTION.md deleted file mode 100644 index 39819cdb..00000000 --- a/backend/mpc-system/services/service-party-android/GRPC_CORRECT_SOLUTION.md +++ /dev/null @@ -1,357 +0,0 @@ -# gRPC 稳定连接的正确解决方案 - -基于官方 gRPC 文档和最佳实践研究 - -## 核心问题 - -**当前代码的设计错误**: 尝试通过 callback "恢复" (restore) 已关闭的流 - -**gRPC 官方说法**: -> "You don't need to re-create the channel - just **re-do the streaming RPC** on the current channel." -> -> "gRPC stream will be mapped to the underlying http2 stream which is **lost when the connection is lost**." - -**结论**: **双向流无法恢复,必须重新发起 RPC 调用** - -## 为什么当前设计有问题 - -```kotlin -// 当前错误设计: -1. 订阅事件流 → Flow 开始 -2. 网络断开 → Flow 关闭 -3. 网络重连 → 尝试"恢复"流 ❌ -4. 调用 callback → 期望流恢复 ❌ - -// 问题: -- Flow 已经关闭,无法恢复 -- 需要重新调用 subscribeSessionEvents() -``` - -## 正确的设计模式 - -### 模式 1: Application-Level Stream Management (推荐) - -```kotlin -class TssRepository { - private val streamManager = StreamManager() - - init { - // 监听连接事件,自动重启流 - grpcClient.connectionEvents - .filter { it is GrpcConnectionEvent.Reconnected } - .onEach { - android.util.Log.d(TAG, "Reconnected, restarting streams...") - streamManager.restartAllStreams() - } - .launchIn(scope) - } - - class StreamManager { - private var eventStreamConfig: EventStreamConfig? = null - private var messageStreamConfig: MessageStreamConfig? = null - - fun startEventStream(partyId: String) { - // 保存配置 - eventStreamConfig = EventStreamConfig(partyId) - // 启动流 - doStartEventStream(partyId) - } - - fun restartAllStreams() { - // 重新发起 RPC 调用(不是"恢复") - eventStreamConfig?.let { doStartEventStream(it.partyId) } - messageStreamConfig?.let { doStartMessageStream(it.sessionId, it.partyId) } - } - - private fun doStartEventStream(partyId: String) { - grpcClient.subscribeSessionEvents(partyId) - .catch { e -> - Log.e(TAG, "Event stream failed: ${e.message}") - // 如果失败,延迟后重试 - delay(5000) - doStartEventStream(partyId) - } - .collect { event -> - // 处理事件 - } - } - } -} -``` - -### 模式 2: 使用 Kotlin Flow retry + retryWhen - -```kotlin -fun subscribeSessionEventsWithAutoRestart(partyId: String): Flow { - return flow { - // 重新发起 RPC 调用 - grpcClient.subscribeSessionEvents(partyId).collect { - emit(it) - } - }.retryWhen { cause, attempt -> - android.util.Log.w(TAG, "Event stream failed (attempt $attempt): ${cause.message}") - delay(min(1000L * (attempt + 1), 30000L)) // 指数退避,最多 30 秒 - true // 始终重试 - } -} -``` - -## Keep-Alive 配置(防止连接假死) - -基于 [gRPC Keepalive 官方文档](https://grpc.io/docs/guides/keepalive/) - -### Android 客户端配置 - -```kotlin -val channel = AndroidChannelBuilder - .forAddress(host, port) - .usePlaintext() // 或使用 useTransportSecurity() - - // Keep-Alive 配置 - .keepAliveTime(10, TimeUnit.SECONDS) // 每 10 秒发送 PING - .keepAliveTimeout(3, TimeUnit.SECONDS) // 3 秒内没收到 ACK 视为死连接 - .keepAliveWithoutCalls(true) // 即使没有活跃 RPC 也发送 PING - - // 重试配置 - .enableRetry() // 启用 unary RPC 重试 - .maxRetryAttempts(5) - - // 其他优化 - .idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) // 不要自动关闭空闲连接 - - .build() -``` - -**重要参数说明**: - -| 参数 | 建议值 | 说明 | -|------|--------|------| -| `keepAliveTime` | 10s-30s | PING 发送间隔,太短会浪费流量 | -| `keepAliveTimeout` | 3s | 等待 ACK 超时,判定连接死亡 | -| `keepAliveWithoutCalls` | true | 没有活跃 RPC 时也 PING(对流很重要)| -| `idleTimeout` | MAX | 不要自动关闭连接 | - -## Android 网络状态监听(加速重连) - -```kotlin -class GrpcClient { - fun setupNetworkMonitoring(context: Context) { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - val networkCallback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - android.util.Log.d(TAG, "Network available, resetting backoff") - // 重要:立即重置重连退避,避免等待 60 秒 DNS 解析 - channel?.resetConnectBackoff() - } - - override fun onLost(network: Network) { - android.util.Log.w(TAG, "Network lost") - } - } - - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build() - - connectivityManager.registerNetworkCallback(request, networkCallback) - } -} -``` - -## 修复方案对比 - -### ❌ 当前错误方案 -```kotlin -// 尝试"恢复"已关闭的流 -fun restoreStreamsAfterReconnect() { - // 问题:Flow 已经关闭,无法恢复 - // subscribeSessionEvents 返回的 Flow 已经是死的 -} -``` - -### ✅ 正确方案 A: 保存配置 + 重新发起 -```kotlin -// 保存流配置 -private var activeEventStream: String? = null - -fun startEventStream(partyId: String) { - activeEventStream = partyId // 保存配置 - launchEventStream(partyId) // 发起流 -} - -fun onReconnected() { - // 重新发起 RPC 调用 - activeEventStream?.let { launchEventStream(it) } -} - -private fun launchEventStream(partyId: String) { - scope.launch { - grpcClient.subscribeSessionEvents(partyId).collect { ... } - } -} -``` - -### ✅ 正确方案 B: 自动重试流 -```kotlin -fun startEventStreamWithAutoReconnect(partyId: String) { - scope.launch { - flow { - // 每次都重新发起 RPC - grpcClient.subscribeSessionEvents(partyId).collect { emit(it) } - } - .retryWhen { cause, attempt -> - Log.w(TAG, "Stream failed, restarting (attempt $attempt)") - delay(1000L * (attempt + 1)) - true // 永远重试 - } - .collect { event -> - // 处理事件 - } - } -} -``` - -## 为什么在同一路由器下也会断连 - -即使在同一路由器下,仍可能出现连接问题: - -1. **手机网络切换**: WiFi ↔ 移动数据自动切换 -2. **省电模式**: Android Doze/App Standby 限制网络 -3. **TCP 空闲超时**: 路由器/防火墙关闭空闲连接(通常 2-5 分钟) -4. **HTTP/2 连接老化**: 长时间无活动可能被中间设备清理 -5. **应用后台**: 系统限制后台网络访问 - -**Keep-Alive 的作用**: 定期发送 PING,告诉路由器/防火墙"我还活着",防止连接被清理 - -## 实施计划 - -### 第 1 步: 添加 Keep-Alive 配置 - -修改 `GrpcClient.kt` 的 `doConnect()`: - -```kotlin -private fun doConnect(host: String, port: Int) { - val channelBuilder = ManagedChannelBuilder - .forAddress(host, port) - .usePlaintext() - - // ✅ 添加 Keep-Alive - .keepAliveTime(20, TimeUnit.SECONDS) - .keepAliveTimeout(5, TimeUnit.SECONDS) - .keepAliveWithoutCalls(true) - - // ✅ 永不超时 - .idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) - - channel = channelBuilder.build() -} -``` - -### 第 2 步: 修改流管理模式 - -#### 选项 A: 最小改动(推荐先试) - -修改 `TssRepository.kt`: - -```kotlin -private var shouldMonitorEvents = false -private var eventStreamPartyId: String? = null - -fun subscribeToSessionEvents(partyId: String) { - eventStreamPartyId = partyId - shouldMonitorEvents = true - launchEventStream(partyId) -} - -private fun launchEventStream(partyId: String) { - scope.launch { - flow { - grpcClient.subscribeSessionEvents(partyId).collect { emit(it) } - } - .retryWhen { cause, attempt -> - if (!shouldMonitorEvents) return@retryWhen false // 停止重试 - - Log.w(TAG, "Event stream failed, restarting in ${attempt}s: ${cause.message}") - delay(1000L * min(attempt, 30)) - true - } - .collect { event -> - handleSessionEvent(event) - } - } -} - -fun stopMonitoringEvents() { - shouldMonitorEvents = false - eventStreamPartyId = null -} -``` - -#### 选项 B: 完整重构(更健壮) - -参考"模式 1"创建 `StreamManager` 类。 - -### 第 3 步: 添加网络监听 - -修改 `MainActivity.kt` 或 `GrpcClient.kt`: - -```kotlin -fun setupNetworkCallback(context: Context) { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - channel?.resetConnectBackoff() - } - } - - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build() - - connectivityManager.registerNetworkCallback(request, callback) -} -``` - -## 测试验证 - -1. ✅ 正常启动 → 订阅事件 → 收到 `session_started` -2. ✅ 飞行模式 30 秒 → 关闭飞行模式 → 自动重新订阅 → 收到事件 -3. ✅ 应用后台 5 分钟 → 恢复前台 → Keep-Alive 保持连接 → 收到事件 -4. ✅ 长时间空闲(30 分钟)→ 创建会话 → Keep-Alive 仍然工作 - -## 参考资料 - -### 官方文档 -- [gRPC Keepalive Guide](https://grpc.io/docs/guides/keepalive/) -- [Android gRPC Guide](https://developer.android.com/guide/topics/connectivity/grpc) -- [Performance Best Practices](https://learn.microsoft.com/en-us/aspnet/core/grpc/performance) - -### 关键 Issues -- [How to restart bi-directional stream after network disconnection](https://github.com/grpc/grpc-java/issues/8177) -- [Network connectivity changes on Android](https://github.com/grpc/grpc-java/issues/4011) - -### 最新文章 (2026) -- [How to Implement gRPC Keepalive for Long-Lived Connections](https://oneuptime.com/blog/post/2026-01-08-grpc-keepalive-connections/view) - -## 总结 - -### 当前问题根源 -1. **设计错误**: 尝试"恢复"已关闭的流,但 gRPC 流无法恢复 -2. **缺少 Keep-Alive**: 空闲连接被中间设备清理 -3. **没有自动重启**: 流失败后需要手动重新发起 - -### 正确解决方案 -1. ✅ 添加 Keep-Alive 配置(20s PING,5s 超时) -2. ✅ 保存流配置,失败后重新发起 RPC(不是"恢复") -3. ✅ 使用 Flow.retryWhen 自动重启流 -4. ✅ 监听网络状态,立即 resetConnectBackoff() - -### 关键理念转变 -``` -旧思维: 连接 → 订阅流 → 断开 → 重连 → "恢复"流 ❌ -新思维: 连接 → 订阅流 → 断开 → 重连 → "重新发起"流 ✅ -``` - -**Flow 不是持久化对象,是一次性的数据流。断开后必须重新创建。** diff --git a/backend/mpc-system/services/service-party-android/GRPC_OFFICIAL_KEPT.md b/backend/mpc-system/services/service-party-android/GRPC_OFFICIAL_KEPT.md deleted file mode 100644 index 3f4e2c99..00000000 --- a/backend/mpc-system/services/service-party-android/GRPC_OFFICIAL_KEPT.md +++ /dev/null @@ -1,234 +0,0 @@ -# gRPC 官方推荐 - 完全保留 - -## 用户质疑 -> "所以,grpc官方的最佳实践你完全弃用了??" - -## 回答:没有!全部保留了! - -### gRPC 官方推荐的三大支柱(全部保留)✅ - ---- - -## 1. Keep-Alive 配置(完全保留)✅ - -**位置**: `GrpcClient.kt` 第 224-230 行 - -```kotlin -val builder = ManagedChannelBuilder - .forAddress(host, port) - // Keep-Alive configuration for stable long-lived connections - .keepAliveTime(20, TimeUnit.SECONDS) // Send PING every 20 seconds - .keepAliveTimeout(5, TimeUnit.SECONDS) // 5 seconds to wait for ACK - .keepAliveWithoutCalls(true) // Keep pinging even without active RPCs - .idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) // Never timeout idle connections -``` - -**官方文档来源**: -- https://grpc.io/docs/guides/keepalive/ - -**作用**: -- 每 20 秒发送 PING,保持连接活跃 -- 5 秒内未收到 ACK,判定连接死亡 -- 即使没有活跃 RPC 也发送 PING(对双向流至关重要) -- 永不超时空闲连接 - -**状态**: ✅ **完全保留,一个字都没改** - ---- - -## 2. Android 网络监听 + resetConnectBackoff(完全保留)✅ - -**位置**: `GrpcClient.kt` 第 151-185 行 - -```kotlin -fun setupNetworkMonitoring(context: Context) { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager - - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - Log.d(TAG, "Network available, resetting connect backoff for immediate reconnection") - // CRITICAL: Reset backoff to avoid 60-second DNS resolution delay - channel?.resetConnectBackoff() - } - - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - val hasInternet = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - val isValidated = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - - // Reset backoff when network becomes validated (has actual internet connectivity) - if (hasInternet && isValidated) { - channel?.resetConnectBackoff() - } - } - } - - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build() - - connectivityManager.registerNetworkCallback(request, callback) -} -``` - -**官方文档来源**: -- https://github.com/grpc/grpc-java/issues/4011 -- https://grpc.io/blog/grpc-on-http2/#keeping-connections-alive - -**作用**: -- 监听 Android 网络状态变化 -- 网络恢复时立即调用 `resetConnectBackoff()` -- 避免等待 60 秒 DNS 解析延迟 -- 加速重连过程 - -**状态**: ✅ **完全保留,一个字都没改** - ---- - -## 3. 流断开后重新发起 RPC(用 Flow.retryWhen 实现)✅ - -**官方说法**: -> "You don't need to re-create the channel - just **re-do the streaming RPC** on the current channel." -> -> "gRPC stream will be mapped to the underlying http2 stream which is **lost when the connection is lost**." - -**官方文档来源**: -- https://github.com/grpc/grpc-java/issues/8177 - -**之前的错误实现**(已删除)❌: -```kotlin -// StreamManager 尝试"恢复"已关闭的流 - 这是错误的 -streamManager.restartAllStreams() // 这不是官方推荐 -``` - -**现在的正确实现**(符合官方推荐)✅: -```kotlin -// TssRepository.kt 第 511-577 行 -jobManager.launch(JOB_SESSION_EVENT) { - flow { - // 重新发起 RPC 调用(不是"恢复") - grpcClient.subscribeSessionEvents(effectivePartyId).collect { event -> - emit(event) - } - } - .retryWhen { cause, attempt -> - // 指数退避重试(官方推荐的模式) - android.util.Log.w("TssRepository", "Event stream failed (attempt ${attempt + 1}), retrying in ${kotlin.math.min(attempt + 1, 30)}s") - delay(kotlin.math.min(attempt + 1, 30) * 1000L) - true // 永远重试 - } - .collect { event -> - // 处理事件 - } -} -``` - -**为什么这是正确的**: -1. ✅ 流失败后,`retryWhen` 触发 -2. ✅ `flow { }` 块重新执行 → 重新调用 `subscribeSessionEvents()` -3. ✅ 这就是"重新发起 RPC",不是"恢复" -4. ✅ 指数退避(exponential backoff)是官方推荐的重试策略 - -**状态**: ✅ **符合官方推荐,只是用 Kotlin Flow API 实现** - ---- - -## 4. 消息流的自动重连(同样用 Flow.retryWhen 实现)✅ - -**位置**: `TssRepository.kt` 第 2062-2087 行 - -```kotlin -jobManager.launch(JOB_MESSAGE_COLLECTION) { - flow { - // 重新发起 RPC 调用 - grpcClient.subscribeMessages(sessionId, effectivePartyId).collect { message -> - emit(message) - } - } - .retryWhen { cause, attempt -> - // 指数退避重试 - android.util.Log.w("TssRepository", "Message stream failed (attempt ${attempt + 1}), retrying...") - delay(kotlin.math.min(attempt + 1, 30) * 1000L) - true - } - .collect { message -> - // 处理消息 - } -} -``` - -**状态**: ✅ **符合官方推荐** - ---- - -## 删除的是什么? - -### StreamManager.kt(我自己创建的抽象层)❌ - -**这不是官方推荐的!** 这是我自己创建的抽象层,试图封装流管理逻辑。 - -**为什么删除它**: -1. 引入了新的 bug(RegisterParty 失败、日志丢失) -2. 增加了不必要的复杂度 -3. Kotlin Flow 本身就是流管理器,不需要再包一层 - -**StreamManager 和官方推荐的关系**: -- StreamManager 试图**实现**官方推荐 -- 但实现得不好,引入了问题 -- 删除后,直接用 `Flow.retryWhen` 实现官方推荐的"重新发起 RPC" - ---- - -## 对比表格 - -| gRPC 官方推荐 | 之前的实现 | 现在的实现 | 状态 | -|--------------|-----------|-----------|------| -| Keep-Alive 配置 | ✅ GrpcClient.kt | ✅ GrpcClient.kt(保留) | ✅ 完全保留 | -| Network Monitoring | ✅ GrpcClient.kt | ✅ GrpcClient.kt(保留) | ✅ 完全保留 | -| 重新发起 RPC | ❌ StreamManager(有bug) | ✅ Flow.retryWhen | ✅ 改进实现 | -| 指数退避 | ✅ StreamManager 内部 | ✅ retryWhen 参数 | ✅ 保留 | - ---- - -## 总结 - -### 官方推荐的三大核心 ✅ - -1. **Keep-Alive 配置** → ✅ 完全保留(GrpcClient.kt 第 224-230 行) -2. **Network Monitoring** → ✅ 完全保留(GrpcClient.kt 第 151-185 行) -3. **重新发起 RPC** → ✅ 用 Flow.retryWhen 实现(TssRepository.kt 第 511-577、2062-2087 行) - -### 删除的只是 ❌ - -- **StreamManager.kt**(我自己创建的抽象层,不是官方推荐) - -### 改进的是 ✅ - -- 用更符合 Kotlin 惯用法的 `Flow.retryWhen` 替代 StreamManager -- 更简单、更清晰、更少 bug - ---- - -## 官方文档引用 - -### 1. Keep-Alive -> "GRPC has an option to send periodic keepalive pings to maintain the connection when there are no active calls." -> -> — https://grpc.io/docs/guides/keepalive/ - -### 2. 重新发起 RPC -> "You don't need to re-create the channel - just re-do the streaming RPC on the current channel." -> -> — https://github.com/grpc/grpc-java/issues/8177#issuecomment-491932464 - -### 3. Exponential Backoff -> "Use exponential backoff for retries to avoid overwhelming the server." -> -> — https://grpc.io/docs/guides/performance/ - ---- - -## 结论 - -**gRPC 官方推荐的所有最佳实践都保留了,甚至改进了实现方式。** - -删除的只是我自己创建的、有问题的 StreamManager 抽象层。 diff --git a/backend/mpc-system/services/service-party-android/GRPC_SYSTEM_EVALUATION.md b/backend/mpc-system/services/service-party-android/GRPC_SYSTEM_EVALUATION.md deleted file mode 100644 index 14bb9312..00000000 --- a/backend/mpc-system/services/service-party-android/GRPC_SYSTEM_EVALUATION.md +++ /dev/null @@ -1,446 +0,0 @@ -# gRPC 系统完整性评估报告 - -## ✅ 已完成的改进 - -### 1. Keep-Alive 配置 (完美) -**文件**: `GrpcClient.kt` line 143-150 - -```kotlin -keepAliveTime(20, TimeUnit.SECONDS) // ✅ 每 20 秒 PING -keepAliveTimeout(5, TimeUnit.SECONDS) // ✅ 5 秒超时检测死连接 -keepAliveWithoutCalls(true) // ✅ 空闲时也 PING -idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) // ✅ 永不超时 -``` - -**评估**: ⭐⭐⭐⭐⭐ (5/5) -- 符合 gRPC 官方最佳实践 -- 防止路由器/防火墙清理空闲连接 -- 快速检测死连接 (5 秒) - ---- - -### 2. StreamManager 实现 (完美) -**文件**: `StreamManager.kt` - -**核心功能**: -```kotlin -✅ startEventStream() - 启动事件流并保存配置 -✅ startMessageStream() - 启动消息流并保存配置 -✅ stopEventStream() - 停止事件流 -✅ stopMessageStream() - 停止消息流 -✅ restartAllStreams() - 重启所有活跃流 -✅ isEventStreamActive() - 检查流状态 -✅ Flow.retryWhen - 指数退避重试 (1s, 2s, 3s... 最多 30s) -``` - -**评估**: ⭐⭐⭐⭐⭐ (5/5) -- 完全遵循 gRPC 官方建议 (重新发起 RPC,不是"恢复") -- 自动重试机制健壮 -- 错误处理完善 - ---- - -### 3. TssRepository 集成 (完善) -**调用点检查**: - -| 调用位置 | 行号 | 状态 | -|---------|------|------| -| startSessionEventSubscription | 511 | ✅ 使用 streamManager.startEventStream | -| startMessageRouting | 2088 | ✅ 使用 streamManager.startMessageStream | -| init 块 | 326-328 | ✅ 监听 Reconnected 事件调用 restartAllStreams | -| ensureSessionEventSubscriptionActive | 618 | ✅ 使用 isEventStreamActive 检查 | - -**评估**: ⭐⭐⭐⭐⭐ (5/5) -- 所有流都通过 StreamManager 管理 -- 没有直接调用 grpcClient.subscribe* 的地方 -- 重连逻辑正确 - ---- - -### 4. Android 网络监听 (完美) -**文件**: `GrpcClient.kt` line 151-183 - -```kotlin -✅ onAvailable() - 网络可用时立即 resetConnectBackoff() -✅ onCapabilitiesChanged() - 网络验证后 resetConnectBackoff() -✅ unregisterNetworkCallback() - 清理时注销 -``` - -**调用链**: -``` -MainActivity.TssPartyApp (line 71-73) - ↓ -viewModel.setupNetworkMonitoring(context) - ↓ -repository.setupNetworkMonitoring(context) - ↓ -grpcClient.setupNetworkMonitoring(context) -``` - -**评估**: ⭐⭐⭐⭐⭐ (5/5) -- 避免 60 秒 DNS 解析延迟 -- 符合 gRPC Android 最佳实践 -- 正确使用 ConnectivityManager.NetworkCallback - ---- - -### 5. 旧机制清理 (完全) -**已删除**: -```kotlin -✅ onReconnectedCallback 变量 -✅ setOnReconnectedCallback() 方法 -✅ reSubscribeStreams() 方法 -✅ activeMessageSubscription 变量 -✅ eventStreamSubscribed 变量 -✅ eventStreamPartyId 变量 -✅ MessageSubscription 数据类 -✅ getActiveMessageSubscription() 方法 -✅ wasEventStreamSubscribed() 方法 -✅ getEventStreamPartyId() 方法 -``` - -**评估**: ⭐⭐⭐⭐⭐ (5/5) -- 完全移除旧的错误设计 -- 没有遗留代码 - ---- - -## ⚠️ 发现的问题 - -### 问题 1: cleanup() 未停止 StreamManager 的流 🟡 - -**文件**: `TssRepository.kt` line 411-428 - -**当前代码**: -```kotlin -fun cleanup() { - jobManager.cancelAll() - repositoryScope.cancel() - grpcClient.disconnect() - // ... OkHttpClient 清理 ... -} -``` - -**问题**: -- `repositoryScope.cancel()` 会取消 StreamManager 的 Job -- 但 StreamManager 的状态标志没有重置 -- 如果重新初始化,可能导致状态不一致 - -**影响**: 🟡 中等 -- 正常关闭应用时无影响 (进程终止) -- 如果 Repository 被重用 (不太可能) 可能有问题 - -**建议修复**: -```kotlin -fun cleanup() { - // 停止所有流 - streamManager.stopEventStream() - streamManager.stopMessageStream() - - // 使用 JobManager 统一取消所有后台任务 - jobManager.cancelAll() - repositoryScope.cancel() - grpcClient.disconnect() - - // 停止网络监听 - // 需要传入 context,或者在 GrpcClient.disconnect() 中处理 - - // 清理 OkHttpClient 资源 - // ... -} -``` - ---- - -### 问题 2: 网络监听未在 cleanup 时注销 🟡 - -**文件**: `GrpcClient.kt` line 196-209 (stopNetworkMonitoring) - -**当前情况**: -- `stopNetworkMonitoring()` 方法已存在 -- 但 `disconnect()` 或 `cleanup()` 未调用 - -**影响**: 🟡 中等 -- NetworkCallback 泄漏 -- 应用关闭后仍监听网络事件 - -**建议修复**: -```kotlin -// GrpcClient.kt -fun disconnect() { - Log.d(TAG, "Disconnecting...") - shouldReconnect.set(false) - cleanupConnection() - - // 停止网络监听 (但需要 context) - // 或者在外部 cleanup 时调用 stopNetworkMonitoring -} -``` - -**问题**: `stopNetworkMonitoring` 需要 `Context` 参数,但 `disconnect()` 没有。 - -**更好的方案**: 在 `TssRepository.cleanup()` 中调用 -```kotlin -// TssRepository.kt -fun cleanup(context: android.content.Context) { - streamManager.stopEventStream() - streamManager.stopMessageStream() - jobManager.cancelAll() - repositoryScope.cancel() - grpcClient.stopNetworkMonitoring(context) // ✅ 添加 - grpcClient.disconnect() - // ... -} -``` - ---- - -### 问题 3: StreamManager 未验证 grpcClient 是否已连接 🟢 - -**文件**: `StreamManager.kt` line 174-214 - -**当前代码**: -```kotlin -flow { - grpcClient.subscribeSessionEvents(partyId).collect { event -> - emit(event) - } -} -.retryWhen { cause, attempt -> - // 立即重试 -} -``` - -**潜在问题**: -- 如果 grpcClient 未连接,`subscribeSessionEvents` 会失败 -- 失败后会立即重试,可能造成日志刷屏 - -**影响**: 🟢 轻微 -- 不影响功能 (最终会成功) -- 日志可能较多 - -**建议优化** (可选): -```kotlin -.retryWhen { cause, attempt -> - if (!shouldMaintainEventStream) return@retryWhen false - - // 如果是连接错误,等待连接恢复后再重试 - if (cause is StatusRuntimeException) { - when (cause.status.code) { - Status.Code.UNAVAILABLE -> { - Log.w(TAG, "gRPC unavailable, waiting for reconnection...") - delay(5000) // 等待 5 秒而不是 1 秒 - } - else -> { - delay(min(attempt + 1, MAX_RETRY_DELAY_SECONDS) * 1000) - } - } - } else { - delay(min(attempt + 1, MAX_RETRY_DELAY_SECONDS) * 1000) - } - true -} -``` - ---- - -## 📊 总体评估 - -### 功能完整性: ⭐⭐⭐⭐⭐ (5/5) - -| 组件 | 评分 | 说明 | -|------|------|------| -| Keep-Alive 配置 | 5/5 | 完美符合最佳实践 | -| StreamManager | 5/5 | 健壮的流管理系统 | -| 事件流管理 | 5/5 | 完全使用 StreamManager | -| 消息流管理 | 5/5 | 完全使用 StreamManager | -| 重连机制 | 5/5 | 自动重启 + 指数退避 | -| 网络监听 | 5/5 | 立即重连,无延迟 | - -### 代码质量: ⭐⭐⭐⭐ (4/5) -- ✅ 架构清晰,职责分明 -- ✅ 错误处理完善 -- ✅ 日志详细 -- ⚠️ cleanup 流程可以更完善 (扣 1 分) - -### 可靠性预测: ⭐⭐⭐⭐⭐ (5/5) - -**解决的核心问题**: -1. ✅ **连接假死** - Keep-Alive 每 20 秒 PING -2. ✅ **断连不恢复** - StreamManager 自动重新发起 RPC -3. ✅ **重连延迟长** - 网络监听立即 resetConnectBackoff -4. ✅ **流状态混乱** - 统一由 StreamManager 管理 -5. ✅ **回调失效** - 改用事件驱动,不依赖 flag - -**能否解决"连接轻易断开"问题**: ✅ **是的,完全可以** - -### 原因分析: - -#### 为什么之前"轻易断开"? -1. **空闲超时** (30s keepAliveTime + 5min idleTimeout) → 连接被清理 -2. **没有自动重连** - 流断开后没有重新发起 RPC -3. **flag 状态错误** - eventStreamSubscribed 被清除导致无法恢复 - -#### 现在如何防止? -1. **永不超时** - `idleTimeout = Long.MAX_VALUE` -2. **频繁 PING** - 每 20 秒检测连接健康 -3. **自动重启** - Flow.retryWhen 持续重试 -4. **即时重连** - 网络恢复立即 resetConnectBackoff - ---- - -## 🎯 测试建议 - -### 测试场景 1: 正常使用 -1. 启动应用,创建 2-of-3 钱包 -2. **预期**: 成功创建,无卡顿 - -### 测试场景 2: 短暂断网 -1. 创建钱包过程中开启飞行模式 10 秒 -2. 关闭飞行模式 -3. **预期**: - - 日志显示 "Network available, resetting connect backoff" - - 日志显示 "Restarting all active streams" - - 继续完成钱包创建 (可能多花 10-20 秒) - -### 测试场景 3: 长时间空闲 -1. 创建钱包后不操作,等待 5 分钟 -2. 再次转账 -3. **预期**: - - Keep-Alive 保持连接活跃 - - 转账立即成功,无需重连 - -### 测试场景 4: 应用后台 -1. 创建钱包 -2. 切换到其他应用 2 分钟 -3. 返回钱包应用 -4. **预期**: - - 连接仍然活跃 - - 或者自动重连成功 - -### 测试场景 5: 网络切换 -1. 使用 WiFi 创建钱包 -2. 过程中切换到移动数据 -3. **预期**: - - 网络监听检测到切换 - - 立即 resetConnectBackoff - - 流自动重启 - - 钱包创建继续 - ---- - -## 📝 建议的后续优化 (可选) - -### 优化 1: 完善 cleanup 流程 (优先级: 高) -```kotlin -// TssRepository.kt -fun cleanup(context: android.content.Context) { - android.util.Log.d("TssRepository", "Starting cleanup...") - - // 1. 停止所有流 - streamManager.stopEventStream() - streamManager.stopMessageStream() - - // 2. 取消所有后台任务 - jobManager.cancelAll() - repositoryScope.cancel() - - // 3. 停止网络监听 - grpcClient.stopNetworkMonitoring(context) - - // 4. 断开 gRPC - grpcClient.disconnect() - - // 5. 清理 HTTP 资源 - try { - httpClient.connectionPool.evictAll() - httpClient.dispatcher.executorService.shutdown() - httpClient.cache?.close() - } catch (e: Exception) { - android.util.Log.e("TssRepository", "Failed to cleanup HTTP client", e) - } - - android.util.Log.d("TssRepository", "Cleanup completed") -} -``` - -### 优化 2: 添加连接状态监控 (优先级: 中) -```kotlin -// TssRepository.kt -private val _connectionHealth = MutableStateFlow(ConnectionHealth.Unknown) -val connectionHealth: StateFlow = _connectionHealth.asStateFlow() - -init { - // 监控连接健康度 - repositoryScope.launch { - combine( - grpcConnectionState, - streamManager.eventStreamState, // 需要添加 - streamManager.messageStreamState // 需要添加 - ) { grpcState, eventState, messageState -> - when { - grpcState is GrpcConnectionState.Connected && - eventState is StreamState.Active && - messageState is StreamState.Active -> ConnectionHealth.Excellent - - grpcState is GrpcConnectionState.Connected -> ConnectionHealth.Good - - grpcState is GrpcConnectionState.Reconnecting -> ConnectionHealth.Degraded - - else -> ConnectionHealth.Poor - } - }.collect { _connectionHealth.value = it } - } -} -``` - -### 优化 3: 添加指标收集 (优先级: 低) -```kotlin -// StreamManager.kt -data class StreamMetrics( - val totalRetries: Int, - val lastError: Throwable?, - val uptime: Long, - val successfulConnections: Int -) - -fun getEventStreamMetrics(): StreamMetrics -fun getMessageStreamMetrics(): StreamMetrics -``` - ---- - -## 🎉 结论 - -### 当前系统评级: **A+ (95/100)** - -**扣分原因**: -- cleanup 流程不够完善 (-3 分) -- 网络监听未在清理时注销 (-2 分) - -### 是否能解决"连接轻易断开"问题? - -**答案**: ✅ **100% 可以解决** - -**理由**: -1. ✅ Keep-Alive 防止连接假死 -2. ✅ StreamManager 自动重启流 -3. ✅ 网络监听消除重连延迟 -4. ✅ 事件驱动架构避免状态混乱 -5. ✅ 指数退避避免刷屏重试 - -### 当前系统已经是**生产级别的可靠实现** - -唯一需要修复的是 cleanup 流程,但这不影响正常使用,只是资源清理不够彻底。 - ---- - -## 📚 参考资料验证 - -所有实现都符合官方最佳实践: -- ✅ [gRPC Keepalive Guide](https://grpc.io/docs/guides/keepalive/) -- ✅ [gRPC-Java Issue #8177](https://github.com/grpc/grpc-java/issues/8177) -- ✅ [Android Network Handling](https://github.com/grpc/grpc-java/issues/4011) -- ✅ [gRPC Performance Best Practices](https://learn.microsoft.com/en-us/aspnet/core/grpc/performance) - -**实现质量**: 完全符合 gRPC 官方建议,没有偏离最佳实践。 diff --git a/backend/mpc-system/services/service-party-android/LOG_ANALYSIS_PARTY1.md b/backend/mpc-system/services/service-party-android/LOG_ANALYSIS_PARTY1.md deleted file mode 100644 index bcc05fcc..00000000 --- a/backend/mpc-system/services/service-party-android/LOG_ANALYSIS_PARTY1.md +++ /dev/null @@ -1,390 +0,0 @@ -# Party 1 日志分析报告 - -## 📋 基本信息 - -- **Party ID**: 7c72c28f-082d-4ba4-a213-5b906abeb5cc -- **Party Index**: 1 -- **Session ID**: f01810e9-4b0f-4933-a06a-0382124e0d25 -- **Invite Code**: 6C72-753E-9C17 -- **Threshold**: 2-of-3 - -## ✅ 成功的步骤 - -### 1. 应用启动和初始化 ✅ -``` -15:57:57.690 Setting up session event callback -15:57:58.140 Party registered: 7c72c28f... -15:57:58.186 Connected successfully -``` - -### 2. 会话创建 ✅ -``` -15:58:50.215 Creating keygen session -15:58:50.364 Create session response received -15:58:50.365 Session created: sessionId=f01810e9..., inviteCode=6C72-753E-9C17 -``` - -**关键配置**: -- persistent_count: 1 (server-party-co-managed) -- external_count: 2 (两台手机) -- selected_server_parties: ["co-managed-party-3"] - -### 3. 参与者加入 ✅ - -**第一次 participant_joined (15:58:50.385)**: -- selectedParties: [co-managed-party-3, 7c72c28f...] (2个) -- Party 1 自己加入成功 - -**第二次 participant_joined (15:58:58.210)**: -- selectedParties: [co-managed-party-3, 7c72c28f..., ca64e2b1...] (3个) -- Party 2 加入成功 - -### 4. session_started 事件触发 ✅ -``` -15:58:58.207 Session event: session_started -15:58:58.208 Session started event for keygen initiator, triggering keygen -15:58:58.210 Starting keygen as initiator: sessionId=..., t=2, n=3 -``` - -### 5. 获取会话状态 ✅ -``` -15:58:58.271 Session status response: - status=in_progress - participants=3 - - co-managed-party-3 (index=0, status=joined) - - 7c72c28f... (index=1, status=joined) ← 我 - - ca64e2b1... (index=2, status=joined) -``` - -### 6. 启动 TSS keygen ✅ -``` -15:58:58.272 Starting keygen as initiator: sessionId=... -15:58:58.272 My party index: 1 -15:58:58.301 [PROGRESS] Starting progress collection from native bridge -15:58:58.301 [JobManager] Launched job: progress_collection (active jobs: 3) -``` - ---- - -## 🚨 发现的问题 - -### 问题 1: Mark Party Ready 失败 🔴 - -``` -15:58:58.318 E/GrpcClient: Mark party ready failed: -INTERNAL: optimistic lock conflict: session was modified by another transaction -``` - -**分析**: -- 乐观锁冲突(Optimistic Lock Conflict) -- 说明多个参与者同时尝试更新会话状态 -- 这个错误导致 Party 1 **没有成功标记为 ready** - -**影响**: -- 服务器可能认为 Party 1 还没有准备好 -- 可能影响 TSS 协议的启动 - -**根本原因**: -- TssRepository.startKeygenAsInitiator line 2138: `grpcClient.markPartyReady(sessionId, partyId)` -- 这个调用失败了,但**没有错误处理** -- 代码继续执行到 line 2141: `waitForKeygenResult()` - -**代码片段**: -```kotlin -// Line 2138 -grpcClient.markPartyReady(sessionId, partyId) // ← 失败但没有检查 - -// Line 2141 -val keygenResult = tssNativeBridge.waitForKeygenResult(password) // ← 继续等待 -``` - ---- - -### 问题 2: 534 个待处理消息堆积(最严重)🚨🚨🚨 - -``` -15:58:58.345 W/GrpcClient: Has 534 pending messages - may have missed events -15:59:28.440 W/GrpcClient: Has 534 pending messages - may have missed events ← 30秒后还是534 -``` - -**分析**: -- 消息队列堆积了534个未处理消息 -- **30秒后还是534个**,说明消息完全没有被消费 -- TSS 协议需要消息路由才能工作 - -**影响**: -- TSS 协议无法进行 -- 参与者之间无法交换密钥生成的中间值 -- 导致 keygen 卡死 - -**可能原因**: -1. **message_collection job 没有真正工作** - - 虽然日志显示启动了,但可能内部失败了 - - 没有错误日志,可能被静默吞掉 - -2. **消息路由没有正确初始化** - - line 15:58:50.387: "Starting message routing: sessionId=..., routingPartyId=..." - - 但之后没有任何消息发送/接收日志 - -3. **markPartyReady 失败导致消息路由失效** - - 服务器可能只向 "ready" 的参与者发送消息 - - 如果 Party 1 没有标记为 ready,可能收不到消息 - ---- - -### 问题 3: TssNativeBridge 完全没有日志 🔴 - -**预期应该有的日志**: -``` -TssNativeBridge: keygenAsInitiator called with sessionId=... -TssNativeBridge: Keygen round 1/9 -TssNativeBridge: Keygen round 2/9 -... -TssNativeBridge: Keygen completed successfully -``` - -**实际**: 完全没有任何 TssNativeBridge 的输出! - -**分析**: -- TssNativeBridge.startKeygen (line 63-88) **没有任何日志** -- 无法判断: - 1. 是否成功调用了 native library - 2. 是否立即失败 - 3. 是否在等待消息 - -**代码问题**: -```kotlin -// TssNativeBridge.kt:63-88 -suspend fun startKeygen(...): Result = withContext(Dispatchers.IO) { - try { - val participantsJson = gson.toJson(participants) - Tsslib.startKeygen(...) // ← 没有日志! - Result.success(Unit) // ← 立即返回 - } catch (e: Exception) { - Result.failure(e) // ← 如果这里失败,也没有日志! - } -} -``` - -**建议添加日志**: -```kotlin -suspend fun startKeygen(...): Result = withContext(Dispatchers.IO) { - try { - android.util.Log.d("TssNativeBridge", "startKeygen called: sessionId=$sessionId, partyIndex=$partyIndex") - val participantsJson = gson.toJson(participants) - android.util.Log.d("TssNativeBridge", "Calling native Tsslib.startKeygen...") - Tsslib.startKeygen(...) - android.util.Log.d("TssNativeBridge", "Tsslib.startKeygen returned successfully") - Result.success(Unit) - } catch (e: Exception) { - android.util.Log.e("TssNativeBridge", "startKeygen failed", e) - Result.failure(e) - } -} -``` - ---- - -### 问题 4: 没有进度更新 🔴 - -**预期应该有的日志**: -``` -MainViewModel: Progress update: 1 / 9 -MainViewModel: Progress update: 2 / 9 -... -``` - -**实际**: 完全没有! - -**分析**: -- progress_collection job 启动了(15:58:58.301) -- 但没有收集到任何进度 -- 说明: - 1. TssNativeBridge 没有报告进度 - 2. 或者 keygen 根本没有启动 - ---- - -## 🎯 根本原因推断 - -### 最可能的原因(按概率排序) - -#### 1. markPartyReady 失败导致消息路由失效 (70%) - -**链条**: -``` -markPartyReady 失败 - ↓ -服务器认为 Party 1 未准备好 - ↓ -不向 Party 1 发送 TSS 消息 - ↓ -Party 1 的 message_collection 收不到消息 - ↓ -534 个消息堆积(可能是其他 party 发给自己的) - ↓ -TSS 协议无法进行 - ↓ -keygen 卡死 -``` - -**验证方法**: -- 检查服务器日志,看 Party 1 的状态是否为 `ready` -- 检查 Party 2 和 co-managed-party 的日志,看他们是否收到消息 - ---- - -#### 2. TssNativeBridge.startKeygen 静默失败 (20%) - -**链条**: -``` -Tsslib.startKeygen() 调用失败 - ↓ -没有抛出异常(或被吞掉) - ↓ -返回 Result.success(Unit) - ↓ -代码继续执行到 waitForKeygenResult() - ↓ -永久等待(因为 keygen 没有真正启动) -``` - -**验证方法**: -- 添加 TssNativeBridge 日志 -- 检查 native library 的日志输出 - ---- - -#### 3. 消息路由初始化失败 (10%) - -**链条**: -``` -Starting message routing (15:58:50.387) - ↓ -subscribeToTssMessages 失败(没有日志) - ↓ -无法接收 TSS 消息 - ↓ -keygen 卡死 -``` - ---- - -## 🔍 需要进一步调查 - -### 1. 其他 Party 的日志 - -**需要收集**: -- **Party 2** (ca64e2b1-8a7c-4cc9-a8c7-5667a206e674) 的日志 -- **co-managed-party-3** 的服务器日志 - -**重点看**: -- 他们是否成功 `markPartyReady`? -- 他们是否收到/发送了 TSS 消息? -- 他们的 pending messages 数量? -- 他们的进度更新? - ---- - -### 2. 服务器端状态 - -**需要检查**: -```bash -# 查询会话状态 -curl -X GET https://mpc-grpc.szaiai.com/api/sessions/f01810e9-4b0f-4933-a06a-0382124e0d25 - -# 查询参与者状态 -# 看 Party 1 (7c72c28f...) 的 status 是否为 "ready" -``` - ---- - -### 3. 添加更详细的日志 - -需要在以下位置添加日志: - -#### TssNativeBridge.kt -```kotlin -suspend fun startKeygen(...): Result { - android.util.Log.d("TssNativeBridge", "startKeygen: sessionId=$sessionId, partyIndex=$partyIndex, t=$thresholdT, n=$thresholdN") - android.util.Log.d("TssNativeBridge", "participants: $participants") - - try { - val participantsJson = gson.toJson(participants) - android.util.Log.d("TssNativeBridge", "Calling native Tsslib.startKeygen...") - - Tsslib.startKeygen(...) - - android.util.Log.d("TssNativeBridge", "Tsslib.startKeygen returned (async)") - Result.success(Unit) - } catch (e: Exception) { - android.util.Log.e("TssNativeBridge", "startKeygen FAILED", e) - Result.failure(e) - } -} -``` - -#### TssRepository.kt -```kotlin -// Line 2138 - 检查 markPartyReady 结果 -val markReadyResult = grpcClient.markPartyReady(sessionId, partyId) -if (markReadyResult.isFailure) { - android.util.Log.e("TssRepository", "Failed to mark party ready: ${markReadyResult.exceptionOrNull()?.message}") - // 考虑重试或返回错误 -} -``` - ---- - -## 🛠️ 临时解决方案 - -### 方案 1: 重试 markPartyReady - -修改 TssRepository.kt line 2138: -```kotlin -// 重试机制 -repeat(3) { attempt -> - try { - grpcClient.markPartyReady(sessionId, partyId) - android.util.Log.d("TssRepository", "Successfully marked party ready on attempt ${attempt + 1}") - break - } catch (e: Exception) { - android.util.Log.w("TssRepository", "markPartyReady attempt ${attempt + 1} failed: ${e.message}") - if (attempt == 2) { - // 最后一次尝试失败,抛出错误 - return@coroutineScope Result.failure(Exception("Failed to mark party ready after 3 attempts")) - } - delay(1000) // 等待1秒后重试 - } -} -``` - -### 方案 2: 添加完整的日志 - -在所有关键位置添加日志,特别是: -- TssNativeBridge.startKeygen -- markPartyReady -- message_collection job - ---- - -## 📊 总结 - -### 确定的问题 -1. ✅ markPartyReady 失败(optimistic lock conflict) -2. ✅ 534个消息堆积未处理 -3. ✅ TssNativeBridge 没有任何日志 -4. ✅ 没有进度更新 - -### 最可能的根本原因 -**markPartyReady 失败 → 服务器不发送消息给 Party 1 → 消息路由失效 → keygen 卡死** - -### 下一步行动 -1. **立即**: 收集 Party 2 和 co-managed-party 的日志 -2. **短期**: 添加 TssNativeBridge 日志 -3. **中期**: 添加 markPartyReady 重试机制 -4. **长期**: 改进错误处理和日志记录 - ---- - -**请提供 Party 2 的日志,我可以进行对比分析!** diff --git a/backend/mpc-system/services/service-party-android/PERMISSIONS_AUDIT.md b/backend/mpc-system/services/service-party-android/PERMISSIONS_AUDIT.md deleted file mode 100644 index 1a33ca64..00000000 --- a/backend/mpc-system/services/service-party-android/PERMISSIONS_AUDIT.md +++ /dev/null @@ -1,278 +0,0 @@ -# Android 应用权限审计报告 - -## 审计日期 -2026-01-26 - -## 权限声明总览 - -### 当前 AndroidManifest.xml 中声明的权限 - -| 权限 | 类型 | 是否必需 | 使用场景 | -|------|------|----------|----------| -| `INTERNET` | 普通权限 | ✅ 必需 | gRPC通信、RPC调用、网络请求 | -| `ACCESS_NETWORK_STATE` | 普通权限 | ⚠️ 推荐 | 检查网络连接状态(可选但建议保留) | -| `CAMERA` | 危险权限 | ✅ 必需 | QR码扫描(邀请码、地址、签名会话) | - -## 权限详细分析 - -### 1. INTERNET 权限 - -**声明位置**: `AndroidManifest.xml:11` - -**用途**: -- gRPC 通信(连接 service-party 协调服务器) -- Kava EVM RPC 调用(查询余额、广播交易、获取 nonce/gas) -- TSS 协议消息路由 - -**是否自动授予**: 是(普通权限,安装时自动授予) - -**结论**: ✅ 必需保留 - ---- - -### 2. ACCESS_NETWORK_STATE 权限 - -**声明位置**: `AndroidManifest.xml:12` - -**用途**: -- 检测网络连接状态 -- 优化用户体验(离线时显示友好提示) - -**是否自动授予**: 是(普通权限,安装时自动授予) - -**当前使用情况**: 未在代码中显式使用,但推荐保留 - -**结论**: ⚠️ 推荐保留(虽然当前未使用,但对用户体验有益) - ---- - -### 3. CAMERA 权限 - -**声明位置**: `AndroidManifest.xml:13` - -**用途**: -- 扫描密钥生成邀请码 QR 码 (`JoinKeygenScreen.kt:190-240`) -- 扫描签名会话邀请码 QR 码 (`CoSignJoinScreen.kt:85-186`) -- 扫描收款地址 QR 码 (`TransferScreen.kt:93-188`) - -**是否自动授予**: 否(危险权限,需要运行时请求) - -**运行时权限处理**: -- ✅ **自动处理**: 使用 `com.journeyapps:zxing-android-embedded:4.3.0` 库 -- ✅ 库会在用户首次扫描时自动弹出权限请求对话框 -- ✅ 使用 `ScanContract()` 和 `CaptureActivity` 进行权限管理 -- ✅ 无需手动编写权限请求代码 - -**验证代码位置**: -```kotlin -// JoinKeygenScreen.kt:190-191 -val scanLauncher = rememberLauncherForActivityResult( - contract = ScanContract() // ZXing 自动处理相机权限 -) - -// CoSignJoinScreen.kt:85 -val scanLauncher = rememberLauncherForActivityResult(ScanContract()) - -// TransferScreen.kt:93 -val scanLauncher = rememberLauncherForActivityResult(ScanContract()) -``` - -**结论**: ✅ 必需保留,权限请求由 ZXing 库自动处理 - ---- - -## 文件存储权限分析 - -### 不需要的权限 - -应用使用 **Storage Access Framework (SAF)** 进行文件操作,因此**不需要**以下权限: - -❌ `READ_EXTERNAL_STORAGE` - 不需要 -❌ `WRITE_EXTERNAL_STORAGE` - 不需要 -❌ `MANAGE_EXTERNAL_STORAGE` - 不需要 - -### SAF 使用情况 - -**导出备份** (`MainActivity.kt:129-202`): -```kotlin -// 使用 CreateDocument - 无需存储权限 -registerForActivityResult(ActivityResultContracts.CreateDocument(ShareBackup.MIME_TYPE)) -context.contentResolver.openOutputStream(targetUri) // 用户已通过文件选择器授权 -``` - -**导入备份** (`MainActivity.kt:235-300`): -```kotlin -// 使用 OpenDocument - 无需存储权限 -registerForActivityResult(ActivityResultContracts.OpenDocument()) -context.contentResolver.openInputStream(uri) // 用户已通过文件选择器授权 -``` - -### SAF 优势 - -1. ✅ **无需权限声明**: 用户通过系统文件选择器授予临时访问权限 -2. ✅ **符合现代 Android 规范**: 支持 Android 10+ 分区存储 (Scoped Storage) -3. ✅ **更高安全性**: 应用只能访问用户明确选择的文件 -4. ✅ **跨平台兼容**: 支持本地存储、云存储、第三方文件管理器 - ---- - -## 其他潜在权限需求分析 - -### 1. 通知权限 (POST_NOTIFICATIONS) - -**Android 13+ (API 33+) 需要运行时请求通知权限** - -**当前状态**: ❌ 未声明,未使用 - -**是否需要**: -- 如果未来需要推送通知(交易确认、签名请求等),需要添加 -- 目前应用无通知功能,暂不需要 - -**结论**: ❌ 当前不需要 - ---- - -### 2. 前台服务权限 (FOREGROUND_SERVICE) - -**用途**: 长时间运行的 TSS 签名会话 - -**当前状态**: ❌ 未使用 - -**是否需要**: -- TSS 签名需要应用保持前台 -- 当前要求用户"保持应用在前台"(`TransferScreen.kt:812`) -- 如果未来需要后台运行签名,需要添加前台服务 - -**结论**: ❌ 当前不需要(用户已被提示保持前台) - ---- - -### 3. 网络权限 (ACCESS_WIFI_STATE) - -**用途**: 检测 WiFi 状态 - -**当前状态**: ❌ 未声明 - -**是否需要**: ❌ 不需要(`ACCESS_NETWORK_STATE` 已足够) - ---- - -## 权限请求最佳实践检查 - -### ✅ 已正确实施 - -1. **最小权限原则**: 只声明了必需的权限 -2. **SAF 优先**: 文件操作使用 SAF 而非存储权限 -3. **库自动处理**: 相机权限由 ZXing 库自动管理 -4. **透明度**: 权限用途明确(扫描 QR 码、网络通信) - -### ✅ 无需改进 - -1. ✅ 无需手动请求相机权限(ZXing 已处理) -2. ✅ 无需添加存储权限(SAF 已足够) -3. ✅ 无需添加通知权限(当前无通知功能) -4. ✅ 无需添加前台服务权限(当前要求用户保持前台) - ---- - -## 权限使用流程图 - -``` -用户启动应用 - ↓ -安装时自动授予 INTERNET + ACCESS_NETWORK_STATE - ↓ -用户点击"扫描二维码"按钮 - ↓ -ZXing 库检查 CAMERA 权限 - ↓ - ├─ 已授予 → 直接打开相机 - └─ 未授予 → 弹出系统权限请求对话框 - ↓ - ├─ 用户允许 → 打开相机 - └─ 用户拒绝 → 返回错误(ZXing 处理) -``` - ---- - -## 隐私合规性检查 - -### Google Play 隐私政策要求 - -1. ✅ **数据使用透明**: - - INTERNET: 用于 TSS 协议通信和区块链交互 - - CAMERA: 仅用于 QR 码扫描,不上传图像 - -2. ✅ **最小权限**: - - 未请求不必要的权限 - - 使用 SAF 避免存储权限 - -3. ✅ **用户控制**: - - 相机权限可随时在系统设置中撤销 - - SAF 文件访问逐次授权 - ---- - -## 审计结论 - -### 权限配置状态: ✅ 优秀 - -**优点**: -1. ✅ 权限声明精简,符合最小权限原则 -2. ✅ 相机权限自动处理,无需手动代码 -3. ✅ 使用 SAF 避免存储权限,符合现代 Android 规范 -4. ✅ 无过度权限请求 -5. ✅ 符合 Google Play 隐私政策 - -**建议**: -1. ✅ **无需修改** - 当前权限配置已经是最佳实践 -2. ⚠️ **可选优化** - 如果未来添加通知功能,记得添加 `POST_NOTIFICATIONS` 权限并在运行时请求 -3. ⚠️ **文档建议** - 在用户手册中说明相机权限仅用于 QR 码扫描 - ---- - -## 权限测试建议 - -### 测试场景 - -1. **首次扫描 QR 码**: - - ✅ 验证 ZXing 自动弹出权限请求 - - ✅ 验证用户允许后可以正常扫描 - - ✅ 验证用户拒绝后显示友好错误 - -2. **权限撤销后重试**: - - ✅ 在系统设置中撤销相机权限 - - ✅ 再次尝试扫描 - - ✅ 验证 ZXing 重新请求权限 - -3. **文件操作**: - - ✅ 验证导出备份无需存储权限 - - ✅ 验证导入备份无需存储权限 - - ✅ 验证可以选择不同位置(本地/云端) - -4. **网络离线**: - - ⚠️ 建议添加网络检查逻辑(使用 `ACCESS_NETWORK_STATE`) - - ⚠️ 离线时显示友好提示而非网络错误 - ---- - -## 附录:Android 权限类型 - -### 普通权限 (Normal Permissions) -- 安装时自动授予,无需运行时请求 -- 示例: `INTERNET`, `ACCESS_NETWORK_STATE` - -### 危险权限 (Dangerous Permissions) -- Android 6.0+ (API 23+) 需要运行时请求 -- 示例: `CAMERA`, `READ_EXTERNAL_STORAGE` - -### 特殊权限 (Special Permissions) -- 需要用户在系统设置中授予 -- 示例: `SYSTEM_ALERT_WINDOW`, `REQUEST_INSTALL_PACKAGES` - ---- - -**审计员**: Claude Sonnet 4.5 -**审计方法**: 代码静态分析 + Android 官方文档验证 -**审计范围**: AndroidManifest.xml + 所有 Kotlin 源代码 -**置信度**: 100%(已完整覆盖所有权限相关代码路径) diff --git a/backend/mpc-system/services/service-party-android/QUICK_DEBUG_COMMANDS.md b/backend/mpc-system/services/service-party-android/QUICK_DEBUG_COMMANDS.md deleted file mode 100644 index 4366e31b..00000000 --- a/backend/mpc-system/services/service-party-android/QUICK_DEBUG_COMMANDS.md +++ /dev/null @@ -1,173 +0,0 @@ -# 快速调试命令 - -## 一键编译安装启动(推荐) - -直接双击运行: -``` -build-install-debug.bat -``` - ---- - -## 单条命令版本(PowerShell) - -在 PowerShell 中执行: -```powershell -cd C:\Users\dong\Desktop\rwadurian\backend\mpc-system\services\service-party-android; .\gradlew.bat assembleDebug --no-daemon; if ($?) { adb uninstall com.durian.tssparty 2>$null; adb install app\build\outputs\apk\debug\app-debug.apk; if ($?) { adb shell am start -n com.durian.tssparty/.MainActivity; adb logcat -c; Write-Host "`n[SUCCESS] 应用已启动,开始监控日志...`n" -ForegroundColor Green; adb logcat -v time MainViewModel:D TssRepository:D GrpcClient:D TssNativeBridge:D AndroidRuntime:E *:S } else { Write-Host "[ERROR] 安装失败!" -ForegroundColor Red } } else { Write-Host "[ERROR] 编译失败!" -ForegroundColor Red } -``` - ---- - -## 单条命令版本(CMD) - -在 CMD 中执行(注意:日志需要在另一个窗口查看): -```cmd -cd C:\Users\dong\Desktop\rwadurian\backend\mpc-system\services\service-party-android && gradlew.bat assembleDebug --no-daemon && adb uninstall com.durian.tssparty 2>nul && adb install app\build\outputs\apk\debug\app-debug.apk && adb shell am start -n com.durian.tssparty/.MainActivity && adb logcat -c && echo 应用已启动!现在打开另一个终端运行: adb logcat -v time MainViewModel:D TssRepository:D GrpcClient:D *:S -``` - ---- - -## 分步执行(更清晰) - -### 终端 1: 编译安装启动 - -```cmd -cd C:\Users\dong\Desktop\rwadurian\backend\mpc-system\services\service-party-android - -:: 1. 编译 -gradlew.bat assembleDebug --no-daemon - -:: 2. 卸载旧版本 -adb uninstall com.durian.tssparty - -:: 3. 安装 -adb install app\build\outputs\apk\debug\app-debug.apk - -:: 4. 启动 -adb shell am start -n com.durian.tssparty/.MainActivity - -:: 5. 清除旧日志 -adb logcat -c -``` - -### 终端 2: 查看日志 - -```cmd -:: 实时查看关键日志 -adb logcat -v time MainViewModel:D TssRepository:D GrpcClient:D TssNativeBridge:D AndroidRuntime:E *:S -``` - -或者查看所有日志并过滤: -```cmd -adb logcat -v time | findstr /C:"MainViewModel" /C:"TssRepository" /C:"GrpcClient" /C:"Exception" /C:"Error" -``` - ---- - -## 保存日志到文件 - -```cmd -:: 清除旧日志 -adb logcat -c - -:: 开始录制日志(在后台) -start /B adb logcat -v time > android_debug_%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%.log - -:: 操作应用(重现问题) - -:: 停止录制(关闭 adb logcat 进程) -taskkill /F /IM adb.exe - -:: 查看日志文件 -dir android_debug_*.log -``` - ---- - -## 快速重启应用(不重新编译) - -```cmd -adb shell am force-stop com.durian.tssparty && adb shell am start -n com.durian.tssparty/.MainActivity -``` - ---- - -## 调试技巧 - -### 1. 查看应用是否在运行 -```cmd -adb shell ps | findstr tssparty -``` - -### 2. 查看应用版本信息 -```cmd -adb shell dumpsys package com.durian.tssparty | findstr version -``` - -### 3. 清除应用数据(重置应用) -```cmd -adb shell pm clear com.durian.tssparty -``` - -### 4. 查看应用崩溃日志 -```cmd -adb logcat -v time AndroidRuntime:E *:S -``` - -### 5. 查看特定标签日志 -```cmd -adb logcat -v time -s MainViewModel -``` - -### 6. 搜索日志中的关键词 -```cmd -adb logcat -v time | findstr "session_started" -adb logcat -v time | findstr "Exception" -adb logcat -v time | findstr "Error" -``` - ---- - -## 故障排除 - -### 问题1: adb: command not found - -**解决**: 添加 Android SDK platform-tools 到 PATH -```cmd -set PATH=%PATH%;C:\Users\dong\AppData\Local\Android\Sdk\platform-tools -``` - -### 问题2: INSTALL_FAILED_UPDATE_INCOMPATIBLE - -**解决**: 卸载旧版本 -```cmd -adb uninstall com.durian.tssparty -``` - -### 问题3: 设备未授权 (device unauthorized) - -**解决**: -1. 手机上会弹出"允许USB调试"提示,点击"允许" -2. 如果没弹出,重新连接USB并执行: -```cmd -adb kill-server -adb start-server -adb devices -``` - -### 问题4: 多个设备连接 - -**解决**: 指定设备 -```cmd -adb devices -adb -s <设备序列号> install app\build\outputs\apk\debug\app-debug.apk -``` - ---- - -## 推荐工作流 - -1. **首次运行**: 使用 `build-install-debug.bat` -2. **代码修改后**: 使用 `build-install-debug.bat` -3. **仅重启应用**: 使用快速重启命令 -4. **查看历史日志**: 使用保存日志到文件 diff --git a/backend/mpc-system/services/service-party-android/RECONNECTION_BUG_ANALYSIS.md b/backend/mpc-system/services/service-party-android/RECONNECTION_BUG_ANALYSIS.md deleted file mode 100644 index 93abf71b..00000000 --- a/backend/mpc-system/services/service-party-android/RECONNECTION_BUG_ANALYSIS.md +++ /dev/null @@ -1,251 +0,0 @@ -# Reconnection Event Stream Bug Analysis - -## Problem Summary - -After network disconnection and reconnection, the event stream subscription is NOT restored, causing: -- No `session_started` event received -- Keygen never starts -- Messages pile up forever (539 pending) - -## Root Cause - -### The Bug Chain - -``` -1. Network disconnects - ↓ -2. subscribeSessionEvents Flow closes - ↓ -3. awaitClose block executes (GrpcClient.kt:925) - eventStreamSubscribed.set(false) ← FLAG CLEARED - ↓ -4. Network reconnects successfully - ↓ -5. reSubscribeStreams() called (GrpcClient.kt:202) - ↓ -6. Line 506 checks: - val needsResubscribe = eventStreamSubscribed.get() || activeMessageSubscription != null - ↓ -7. eventStreamSubscribed.get() returns FALSE ❌ - activeMessageSubscription is also NULL ❌ - ↓ -8. needsResubscribe = false - ↓ -9. Callback NEVER invoked - ↓ -10. Event stream NEVER restored -``` - -## Code Evidence - -### GrpcClient.kt - Where Flag is Set/Cleared - -**Line 844** - Flag set when subscribing: -```kotlin -fun subscribeSessionEvents(partyId: String): Flow = callbackFlow { - eventStreamSubscribed.set(true) ← Set to TRUE - eventStreamPartyId = partyId - ... -} -``` - -**Line 925** - Flag cleared when Flow closes (THE BUG): -```kotlin -awaitClose { - Log.d(TAG, "subscribeSessionEvents: Flow closed for partyId=$partyId") - eventStreamSubscribed.set(false) ← Set to FALSE on disconnect - eventStreamPartyId = null -} -``` - -**Line 506** - Reconnection check (FAILS because flag is false): -```kotlin -private fun reSubscribeStreams() { - val needsResubscribe = eventStreamSubscribed.get() || activeMessageSubscription != null - // ↑ Returns FALSE after disconnect - - if (needsResubscribe) { ← This condition is FALSE - Log.d(TAG, "Triggering stream re-subscription callback") - ... - onReconnectedCallback?.invoke() ← NEVER REACHED - } -} -``` - -## Log Evidence - -### Normal Reconnection (16:28:26) - WORKS ✅ -``` -16:28:26.082 D/GrpcClient: Connected successfully -16:28:26.086 D/GrpcClient: Re-registering party: 7c72c28f... -16:28:26.130 D/GrpcClient: Party registered: 7c72c28f... -16:28:26.130 D/GrpcClient: Triggering stream re-subscription callback ← Present! -16:28:26.130 D/GrpcClient: - Event stream: true, partyId: 7c72c28f... -16:28:26.130 D/TssRepository: gRPC reconnected, restoring streams... ← Present! -16:28:26.130 D/TssRepository: Restoring session event subscription ← Present! -``` - -### Problem Reconnection (16:29:47) - FAILS ❌ -``` -16:29:47.090 D/GrpcClient: Connected successfully -16:29:47.093 D/GrpcClient: Re-registering party: 7c72c28f... -16:29:47.146 D/GrpcClient: Party registered: 7c72c28f... -[MISSING]: "Triggering stream re-subscription callback" ← NOT PRESENT! -[MISSING]: "gRPC reconnected, restoring streams..." ← NOT PRESENT! -[MISSING]: "Restoring session event subscription" ← NOT PRESENT! - -Result: -16:30:47.198 W/GrpcClient: Has 539 pending messages - may have missed events -16:31:17.237 W/GrpcClient: Has 539 pending messages - may have missed events -``` - -## Why First Reconnection Worked - -Looking at the timeline: -``` -16:27:53 - App started, event subscription started -16:28:26 - First reconnect (1 minute later) - Event subscription was STILL ACTIVE - eventStreamSubscribed = true ✅ - -16:29:15 - Network disconnect (49 seconds later) - Flow closed → eventStreamSubscribed set to FALSE ❌ - -16:29:47 - Second reconnect - eventStreamSubscribed = false ❌ - Callback NOT invoked ❌ -``` - -**Key Insight**: The first reconnection worked because the event stream Flow hadn't closed yet. The second reconnection failed because the Flow had closed and cleared the flag. - -## The Design Flaw - -The current design has a **state tracking inconsistency**: - -```kotlin -// When to subscribe? -eventStreamSubscribed = true // "I am currently subscribed" - -// When to unsubscribe? -eventStreamSubscribed = false // "I am no longer subscribed" - -// When to re-subscribe? -if (eventStreamSubscribed) { ... } // ❌ WRONG - flag is already false! -``` - -**Problem**: The flag tracks "am I currently subscribed?" but reconnection logic needs to know "should I re-subscribe?". These are two different concepts. - -## Solution Options - -### Option 1: Don't Clear Flag in awaitClose (Simple) - -```kotlin -awaitClose { - Log.d(TAG, "subscribeSessionEvents: Flow closed for partyId=$partyId") - // DON'T clear the flag - keep it for reconnection - // eventStreamSubscribed.set(false) ← REMOVE THIS - // eventStreamPartyId = null ← REMOVE THIS -} -``` - -**Pros**: Minimal change, preserves intent to re-subscribe -**Cons**: Flag no longer accurately reflects current state - -### Option 2: Add Separate "Should Restore" Flag (Better) - -```kotlin -// Two separate flags: -private val eventStreamSubscribed = AtomicBoolean(false) // Current state -private val shouldRestoreEventStream = AtomicBoolean(false) // Intent to restore - -// When subscribing: -eventStreamSubscribed.set(true) -shouldRestoreEventStream.set(true) // Remember to restore - -// In awaitClose: -eventStreamSubscribed.set(false) // No longer subscribed -// Keep shouldRestoreEventStream = true // But should restore on reconnect - -// In reSubscribeStreams: -val needsResubscribe = shouldRestoreEventStream.get() || activeMessageSubscription != null -``` - -**Pros**: Clear separation of concerns, accurate state tracking -**Cons**: More code, requires careful handling of clear conditions - -### Option 3: Store Last Subscription State (Most Robust) - -```kotlin -// Store full subscription state for recovery -private data class StreamState( - val eventStreamPartyId: String?, - val messageSessionId: String?, - val messagePartyId: String? -) - -private val lastStreamState = AtomicReference(null) - -// On subscribe, save state -// On reconnect, restore from saved state -``` - -**Pros**: Can restore exact previous state, handles complex scenarios -**Cons**: Most complex implementation - -## Recommended Fix - -**Use Option 1 (simplest) with Option 2 concept (clearer intent)**: - -1. Don't clear `eventStreamSubscribed` in `awaitClose` -2. Only clear it when user explicitly unsubscribes or app shuts down -3. This preserves the "I was subscribed, so re-subscribe on reconnect" behavior - -**Alternative**: Add explicit unsubscribe call only when intentionally stopping (not on disconnect). - -## Files to Modify - -### GrpcClient.kt - -**Line 923-927** - Remove flag clearing in awaitClose: -```kotlin -awaitClose { - Log.d(TAG, "subscribeSessionEvents: Flow closed for partyId=$partyId") - // Keep flags for reconnection - don't clear here - // Only clear on explicit unsubscribe or disconnect -} -``` - -**Line 933-936** - Keep explicit unsubscribe as-is: -```kotlin -fun unsubscribeSessionEvents() { - eventStreamSubscribed.set(false) - eventStreamPartyId = null -} -``` - -## Testing Checklist - -After fix: -- [ ] Start app, subscribe to events -- [ ] Simulate network disconnect (airplane mode) -- [ ] Verify log shows: "Triggering stream re-subscription callback" -- [ ] Verify log shows: "gRPC reconnected, restoring streams..." -- [ ] Verify log shows: "Restoring session event subscription" -- [ ] Verify pending messages start decreasing -- [ ] Test 2-of-3 keygen succeeds after reconnection - -## Why This Wasn't Caught Before - -1. **Timing-dependent**: Only fails if Flow closes before reconnect -2. **Works in most cases**: Quick reconnects (< 1 minute) often succeed before Flow timeout -3. **No explicit test**: Didn't test scenario of "disconnect → wait for Flow to close → reconnect" -4. **Silent failure**: No error logged, just missing callback invocation - -## Conclusion - -The safeLaunch optimization did NOT cause this bug. The bug exists because: -1. `awaitClose` clears `eventStreamSubscribed` on disconnect -2. Reconnection logic relies on this flag to decide if callback should be invoked -3. After disconnect, flag is false, so callback is never invoked - -**Fix**: Don't clear the subscription intent flag on temporary disconnection. diff --git a/backend/mpc-system/services/service-party-android/REFACTORING_SUMMARY.md b/backend/mpc-system/services/service-party-android/REFACTORING_SUMMARY.md deleted file mode 100644 index f4bb2908..00000000 --- a/backend/mpc-system/services/service-party-android/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,200 +0,0 @@ -# 重构总结 - 回归简单可靠的架构 - -## 修复的问题 - -**用户反馈**: "让处理异常,你个狗日的把逻辑,流程都改错了。" - -**根本原因**: 在添加异常处理和 gRPC 可靠性改进时,引入了 StreamManager 抽象层,导致: -1. RegisterParty 失败但代码继续执行 -2. StreamManager 日志完全缺失 -3. 流程变复杂,引入新问题 - -## 本次重构的原则 - -**保留的好东西**(来自 gRPC 官方推荐)✅: -1. gRPC Keep-Alive 配置(20s PING, 5s timeout, 永不 idle) -2. Android 网络状态监听(resetConnectBackoff) -3. registerParty 错误检查和重试 -4. markPartyReady 重试机制 - -**删除的坏东西**(过度设计)❌: -1. StreamManager.kt 整个文件 -2. 复杂的 init 块监听 reconnection 事件 -3. 回调式的流管理 - -**恢复的简单逻辑**(工作的代码)✅: -1. 直接用 jobManager.launch + grpcClient.subscribeSessionEvents().collect -2. 在 collect 外包一层 flow { }.retryWhen { } 实现自动重连 -3. 保持原有的事件处理逻辑不变 - -## 代码变更详情 - -### 1. TssRepository.kt - -#### 删除 StreamManager 相关代码: -```kotlin -// 删除了 -- import com.durian.tssparty.data.remote.StreamManager -- private val streamManager = StreamManager(grpcClient, repositoryScope) -- init { ... streamManager.restartAllStreams() } -``` - -#### 恢复简单的事件订阅: -```kotlin -// 之前(复杂) -streamManager.startEventStream( - partyId = effectivePartyId, - onEvent = { event -> /* callback */ } -) - -// 现在(简单) -jobManager.launch(JOB_SESSION_EVENT) { - flow { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { emit(it) } - } - .retryWhen { cause, attempt -> - Log.w(TAG, "Event stream failed (attempt ${attempt + 1}), retrying...") - delay(min(attempt + 1, 30) * 1000L) - true // 永远重试 - } - .collect { event -> - // 直接处理事件(保持原有逻辑) - } -} -``` - -#### 恢复简单的消息路由: -```kotlin -// Part 1: 发送消息(重命名为 JOB_MESSAGE_SENDING) -jobManager.launch(JOB_MESSAGE_SENDING) { - tssNativeBridge.outgoingMessages.collect { message -> - grpcClient.routeMessage(...) - } -} - -// Part 2: 接收消息(使用 JOB_MESSAGE_COLLECTION + retryWhen) -jobManager.launch(JOB_MESSAGE_COLLECTION) { - flow { - grpcClient.subscribeMessages(sessionId, effectivePartyId).collect { emit(it) } - } - .retryWhen { cause, attempt -> - Log.w(TAG, "Message stream failed (attempt ${attempt + 1}), retrying...") - delay(min(attempt + 1, 30) * 1000L) - true // 永远重试 - } - .collect { message -> - // 处理消息 - } -} -``` - -#### 修复 ensureSessionEventSubscriptionActive: -```kotlin -// 之前 -val isActive = streamManager.isEventStreamActive() - -// 现在 -val isActive = jobManager.isActive(JOB_SESSION_EVENT) -``` - -### 2. 删除的文件 - -- `app/src/main/java/com/durian/tssparty/data/remote/StreamManager.kt` - -### 3. 保留的 gRPC 改进 - -#### GrpcClient.kt - Keep-Alive 配置(保留)✅: -```kotlin -val builder = ManagedChannelBuilder - .forAddress(host, port) - .usePlaintext() - .keepAliveTime(20, TimeUnit.SECONDS) // 每 20 秒 PING - .keepAliveTimeout(5, TimeUnit.SECONDS) // 5 秒超时 - .keepAliveWithoutCalls(true) // 没有活跃 RPC 也 PING - .idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) // 永不超时 -``` - -#### GrpcClient.kt - 网络监听(保留)✅: -```kotlin -fun setupNetworkMonitoring(context: Context) { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - channel?.resetConnectBackoff() // 立即重连 - } - } -} -``` - -## 架构对比 - -### 旧设计(复杂但出错)❌: -``` -TssRepository - ├─ StreamManager (新增的抽象层) - │ ├─ startEventStream() - │ ├─ startMessageStream() - │ └─ restartAllStreams() - │ - ├─ init { listen reconnection → streamManager.restartAllStreams() } - └─ grpcClient -``` - -### 新设计(简单可靠)✅: -``` -TssRepository - ├─ JobManager (原有的任务管理) - │ ├─ JOB_SESSION_EVENT → flow { subscribeSessionEvents() }.retryWhen { } - │ ├─ JOB_MESSAGE_COLLECTION → flow { subscribeMessages() }.retryWhen { } - │ └─ JOB_MESSAGE_SENDING → outgoingMessages.collect { } - │ - └─ grpcClient (带 Keep-Alive + Network Monitoring) -``` - -## 为什么新设计更好 - -1. **更少的抽象层**: 直接用 jobManager.launch,不需要 StreamManager -2. **自动重连**: Flow.retryWhen 在流失败时自动重新发起 RPC -3. **保持原有逻辑**: 事件处理代码保持不变,只在外面包一层 retryWhen -4. **更好的日志**: 直接在 collect { } 里打日志,不会丢失 -5. **符合 Kotlin 风格**: Flow transformation 比 callback 更符合 Kotlin 惯用法 - -## 测试重点 - -1. ✅ 编译成功(已验证) -2. ⏳ RegisterParty 成功(需要测试) -3. ⏳ 事件订阅成功(看到 "Starting session event subscription" 日志) -4. ⏳ 创建 2-of-3 会话成功 -5. ⏳ 飞行模式测试自动重连 - -## 编译结果 - -``` -BUILD SUCCESSFUL in 1m 26s -46 actionable tasks: 17 executed, 29 up-to-date -``` - -只有一些参数未使用的警告,没有错误。 - -## 核心教训 - -**简单就是可靠**: -``` -工作的代码 + 官方推荐配置 + 最小改动 = 可靠的系统 - -不是: -工作的代码 → 完全重构 → 引入新抽象 → 新问题 -``` - -**gRPC 流的正确管理方式**: -1. 流断开时,用 Flow.retryWhen 自动重新发起 RPC(不是"恢复") -2. 不需要复杂的 StreamManager,Kotlin Flow 本身就是流管理器 -3. Keep-Alive 防止连接假死 -4. Network Monitoring 加速重连 - -## 下一步 - -准备测试!使用 build-install-debug.bat 安装到设备,验证: -1. RegisterParty 是否成功 -2. 事件流是否正常工作 -3. 2-of-3 创建是否成功 -4. 网络断开重连是否自动恢复 diff --git a/backend/mpc-system/services/service-party-android/REVERT_PLAN.md b/backend/mpc-system/services/service-party-android/REVERT_PLAN.md deleted file mode 100644 index 3bac0d17..00000000 --- a/backend/mpc-system/services/service-party-android/REVERT_PLAN.md +++ /dev/null @@ -1,98 +0,0 @@ -# 立即回退方案 - -## 用户真正的需求 - -1. ✅ server-party-co-managed 参与 sign(2-of-3 签名)- 已在 9f7a5cbb 实现 -2. ✅ 修复导致崩溃的异常 - 已在多个提交修复 - -## 我破坏了什么 - -**7b957114** (2026-01-27 00:56:55) 引入 StreamManager -- ❌ 完全没必要 -- ❌ 破坏了原有功能 -- ❌ 引入了新问题 - -## 回退计划 - -### 方案:完全回退到 41e7eed2 - -**41e7eed2** 包含了: -- ✅ 2-of-3 co-sign 功能(9f7a5cbb) -- ✅ 所有崩溃修复(6f38f96b, 6dda30c5, 704ee523, 等) -- ✅ markPartyReady 重试修复 -- ✅ JobManager 防止协程泄漏 -- ✅ 异常处理覆盖率 100% -- ❌ **没有** StreamManager(这是好事!) - -### 执行命令 - -```bash -# 1. 回退 TssRepository.kt -git checkout 41e7eed2 -- app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt - -# 2. 回退 GrpcClient.kt -git checkout 41e7eed2 -- app/src/main/java/com/durian/tssparty/data/remote/GrpcClient.kt - -# 3. 回退 MainActivity.kt -git checkout 41e7eed2 -- app/src/main/java/com/durian/tssparty/MainActivity.kt - -# 4. 回退 MainViewModel.kt -git checkout 41e7eed2 -- app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt - -# 5. 删除 StreamManager.kt(如果存在) -rm -f app/src/main/java/com/durian/tssparty/data/remote/StreamManager.kt - -# 6. 编译测试 -./gradlew assembleDebug --no-daemon -``` - -## 41e7eed2 包含的功能 - -### ✅ 核心功能 -- 2-of-3 keygen -- 2-of-3 sign(包含 server-party-co-managed 参与) -- 备份导出/导入 -- 交易记录 - -### ✅ 崩溃修复 -- lateinit partyId 崩溃 -- 协程泄漏 -- 参与者计数竞态条件 -- OkHttpClient 连接池 -- 全局异常处理器 -- markPartyReady 重试 - -### ❌ 没有的(这些是多余的) -- StreamManager -- Keep-Alive 配置 -- Network Monitoring -- Flow.retryWhen - -## 为什么不需要 StreamManager - -**原有代码已经工作**: -```kotlin -// 41e7eed2 的简单代码 - 工作的 -jobManager.launch(JOB_SESSION_EVENT) { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { event -> - // 处理事件 - } -} -``` - -**如果网络断开**: -- JobManager 会自动取消 Job -- 下次连接时会重新订阅 -- **不需要复杂的重连机制** - -## 总结 - -用户从来没说过要改流管理! - -用户说的是: -1. 让 co-managed 参与 sign ← 已实现(9f7a5cbb) -2. 修复崩溃问题 ← 已修复(多个提交) - -我自作聪明加了 StreamManager,反而破坏了功能。 - -**立即回退到 41e7eed2!** diff --git a/backend/mpc-system/services/service-party-android/WORKING_CODE_ANALYSIS.md b/backend/mpc-system/services/service-party-android/WORKING_CODE_ANALYSIS.md deleted file mode 100644 index 1e729c35..00000000 --- a/backend/mpc-system/services/service-party-android/WORKING_CODE_ANALYSIS.md +++ /dev/null @@ -1,269 +0,0 @@ -# 工作代码分析和正确修复方案 - -## 问题根源 - -**我犯的错误**: 在添加异常处理时,把整个流管理逻辑都改了,引入了 StreamManager 抽象层,导致流程变复杂并出现新问题。 - -**用户的要求**: -1. ✅ 添加异常处理(如 markPartyReady 重试) -2. ✅ 保留 gRPC 官方推荐(Keep-Alive, network monitoring) -3. ❌ **不要改变原有的工作流程** - -## 工作代码的逻辑(commit 41e7eed2) - -### 1. 连接初始化序列 - -```kotlin -// MainActivity.kt → MainViewModel.kt → TssRepository.kt - -1. GrpcClient.connectToServer(host, port) - ↓ -2. 创建 ManagedChannel - ↓ -3. TssRepository.registerParty() - ↓ -4. grpcClient.registerParty(partyId, "temporary", "1.0.0") // 没有错误检查 - ↓ -5. startSessionEventSubscription() // 立即订阅事件流 -``` - -### 2. 事件流订阅逻辑 - -```kotlin -// TssRepository.kt - 工作的代码 - -private fun startSessionEventSubscription(subscriptionPartyId: String? = null) { - val effectivePartyId = subscriptionPartyId ?: requirePartyId() - currentSessionEventPartyId = effectivePartyId - - // 关键:使用 JobManager 直接启动 - jobManager.launch(JOB_SESSION_EVENT) { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { event -> - // 直接在这里处理事件 - when (event.eventType) { - "session_started" -> { /* ... */ } - "participant_joined" -> { /* ... */ } - // ... - } - } - } -} -``` - -**为什么这个简单的方式可以工作?** -- `jobManager.launch()` 自动取消同名旧 Job -- `grpcClient.subscribeSessionEvents()` 返回一个 Flow -- Flow 在网络断开时会自动关闭 -- 但没有自动重连机制(这是需要修复的) - -### 3. 消息路由逻辑 - -```kotlin -// TssRepository.kt - 消息路由 - -private fun startMessageRouting(sessionId: String, partyId: String, partyIndex: Int) { - // 1. 启动消息收集 Job - jobManager.launch(JOB_MESSAGE_COLLECTION) { - subscribeToTssMessages(sessionId, partyId).collect { message -> - tssNativeBridge.routeIncomingMessage(sessionId, message) - } - } - - // 2. 同时启动消息发送 Job(在同一个 JobManager 中) - jobManager.launch(JOB_MESSAGE_SENDING) { - tssNativeBridge.outgoingMessages.collect { message -> - grpcClient.sendMessage(sessionId, message) - } - } -} -``` - -## 我的错误修改 - -### 错误 1: 引入了 StreamManager 抽象层 - -```kotlin -// 新代码(错误) -streamManager.startEventStream( - partyId = effectivePartyId, - onEvent = { event -> /* callback */ } -) -``` - -**问题**: -- 增加了一层不必要的抽象 -- StreamManager 的实现可能有 bug -- 日志显示 StreamManager 根本没有启动 - -### 错误 2: 修改了连接重建后的流恢复逻辑 - -```kotlin -// 旧代码(工作的) -grpcConnectionEvents - .filter { it is GrpcConnectionEvent.Reconnected } - .collect { - onReconnectedCallback?.invoke() // 简单的 callback - } - -// 新代码(复杂但出错) -grpcConnectionEvents - .filter { it is GrpcConnectionEvent.Reconnected } - .collect { - streamManager.restartAllStreams() // StreamManager 可能有问题 - } -``` - -## 正确的修复方案 - -### 保留的部分(这些是好的)✅ - -1. **gRPC Keep-Alive 配置**(GrpcClient.kt line 143-150): -```kotlin -val builder = ManagedChannelBuilder - .forAddress(host, port) - .keepAliveTime(20, TimeUnit.SECONDS) - .keepAliveTimeout(5, TimeUnit.SECONDS) - .keepAliveWithoutCalls(true) - .idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) -``` - -2. **Android 网络监听**(GrpcClient.kt line 151-183): -```kotlin -fun setupNetworkMonitoring(context: Context) { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - channel?.resetConnectBackoff() - } - } -} -``` - -3. **registerParty 错误检查**(TssRepository.kt line 489-494): -```kotlin -val registerResult = grpcClient.registerParty(partyId, "temporary", "1.0.0") -if (registerResult.isFailure) { - throw registerResult.exceptionOrNull() ?: Exception("Failed to register party") -} -``` - -4. **markPartyReady 重试机制**(TssRepository.kt line ~2140): -```kotlin -repeat(5) { attempt -> - if (markReadySuccess) return@repeat - val markReadyResult = grpcClient.markPartyReady(sessionId, partyId) - if (markReadyResult.isSuccess) { - markReadySuccess = true - return@repeat - } - delay((attempt + 1) * 500L) -} -``` - -### 需要回退的部分(这些破坏了原有逻辑)❌ - -1. **删除 StreamManager**: - - 删除 `StreamManager.kt` 文件 - - 删除 TssRepository.kt 中的 `streamManager` 实例 - -2. **恢复原有的事件订阅逻辑**: -```kotlin -// 恢复为这样(简单直接) -private fun startSessionEventSubscription(subscriptionPartyId: String? = null) { - val effectivePartyId = subscriptionPartyId ?: requirePartyId() - currentSessionEventPartyId = effectivePartyId - - jobManager.launch(JOB_SESSION_EVENT) { - // 添加 retryWhen 自动重连(新增的改进) - flow { - grpcClient.subscribeSessionEvents(effectivePartyId).collect { emit(it) } - } - .retryWhen { cause, attempt -> - Log.w(TAG, "Event stream failed (attempt ${attempt + 1}), retrying: ${cause.message}") - delay(min(attempt + 1, 30) * 1000L) - true // 永远重试 - } - .collect { event -> - // 直接处理事件(保持原有逻辑不变) - Log.d(TAG, "=== Session event received ===") - when (event.eventType) { - "session_started" -> { /* ... */ } - // ... - } - } - } -} -``` - -3. **删除 GrpcClient 中复杂的 reconnection callback**: - - 保持简单的连接状态 Flow - - 不需要复杂的 reSubscribeStreams() 逻辑 - -## 正确的架构 - -``` -简单而可靠的架构: - -GrpcClient (基础层) - ├─ Keep-Alive 配置 ✅ - ├─ Network Monitoring ✅ - ├─ subscribeSessionEvents() → Flow ✅ - └─ subscribeMessages() → Flow ✅ - -TssRepository (业务层) - ├─ JobManager 管理所有协程 ✅ - ├─ jobManager.launch(JOB_SESSION_EVENT) { - │ flow { grpcClient.subscribeSessionEvents().collect { emit(it) } } - │ .retryWhen { ... } ← 新增自动重连 - │ .collect { event -> /* 处理 */ } - │ } - └─ 同样的模式用于消息流 ✅ -``` - -## 实施步骤 - -### 步骤 1: 回退 TssRepository.kt 的事件订阅逻辑 - -```kotlin -// 删除 StreamManager 相关代码(line 217-242) -- private val streamManager = StreamManager(grpcClient, repositoryScope) -- init { repositoryScope.launch { grpcConnectionEvents... streamManager.restartAllStreams() } } - -// 恢复 startSessionEventSubscription 为原来的简单版本(line 511-612) -// 但在 collect 外包一层 flow { }.retryWhen { } -``` - -### 步骤 2: 删除 StreamManager.kt 文件 - -```bash -rm StreamManager.kt -``` - -### 步骤 3: 简化 GrpcClient.kt 的重连逻辑 - -```kotlin -// 删除复杂的 reSubscribeStreams() 方法 -// 保留简单的 GrpcConnectionEvent 发送 -``` - -### 步骤 4: 测试验证 - -1. 编译成功 -2. 启动时 RegisterParty 成功 -3. 事件订阅成功(看到 "Starting session event subscription" 日志) -4. 创建 2-of-3 会话成功 -5. 飞行模式测试自动重连 - -## 总结 - -**核心教训**: -- ❌ 不要过度设计(StreamManager 是不必要的抽象) -- ✅ 在原有工作的代码基础上做最小改动 -- ✅ 保留 gRPC 官方推荐的配置(Keep-Alive, network monitoring) -- ✅ 只在必要的地方添加错误处理和重试逻辑 - -**修复原则**: -``` -旧代码 + 官方推荐 + 最小改动 = 可靠的解决方案 - -不是: 旧代码 → 完全重构 → StreamManager → 新问题 -```