From 2b2cc15208c9cd1939114689278fc13499c61e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 26 May 2026 09:59:35 +0800 Subject: [PATCH] tools: Fix missing cleanup --- .../screen/tools/NetworkQualityViewModel.kt | 43 +++++---- .../compose/screen/tools/STUNTestViewModel.kt | 29 ++++-- .../screen/tools/TailscalePingViewModel.kt | 91 +++++++++---------- .../screen/tools/TailscaleStatusViewModel.kt | 48 +++++----- 4 files changed, 113 insertions(+), 98 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/NetworkQualityViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/NetworkQualityViewModel.kt index 30b0fbc..b57e2a0 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/NetworkQualityViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/NetworkQualityViewModel.kt @@ -7,10 +7,10 @@ import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.NetworkQualityProgress import io.nekohasekai.libbox.NetworkQualityResult import io.nekohasekai.libbox.NetworkQualityTestHandler +import io.nekohasekai.libbox.NetworkQualityTestSession import io.nekohasekai.sfa.R import io.nekohasekai.sfa.compose.base.BaseViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -41,7 +41,7 @@ data class NetworkQualityState( class NetworkQualityViewModel : BaseViewModel() { private var standaloneTest: io.nekohasekai.libbox.NetworkQualityTest? = null - private var grpcJob: Job? = null + private var nqSession: NetworkQualityTestSession? = null override fun createInitialState() = NetworkQualityState() @@ -114,22 +114,23 @@ class NetworkQualityViewModel : BaseViewModel() { val handler = createHandler() if (vpnRunning) { - grpcJob = viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { try { - Libbox.newStandaloneCommandClient() - .startNetworkQualityTest( - configURL, - outboundTag, - serial, - maxRuntimeSeconds, - http3, - handler, - ) + nqSession = + Libbox.newStandaloneCommandClient() + .startNetworkQualityTest( + configURL, + outboundTag, + serial, + maxRuntimeSeconds, + http3, + handler, + ) } catch (e: Exception) { withContext(Dispatchers.Main) { if (!currentState.isRunning) return@withContext updateState { copy(isRunning = false) } - grpcJob = null + nqSession = null sendError(e) } } @@ -146,13 +147,21 @@ class NetworkQualityViewModel : BaseViewModel() { } fun cancelTest() { - grpcJob?.cancel() - grpcJob = null + try { + nqSession?.close() + } catch (_: Exception) { + } + nqSession = null standaloneTest?.cancel() standaloneTest = null updateState { copy(isRunning = false) } } + override fun onCleared() { + cancelTest() + super.onCleared() + } + private fun createHandler(): NetworkQualityTestHandler { return object : NetworkQualityTestHandler { override fun onProgress(progress: NetworkQualityProgress?) { @@ -196,7 +205,7 @@ class NetworkQualityViewModel : BaseViewModel() { ) } standaloneTest = null - grpcJob = null + nqSession = null } } @@ -205,7 +214,7 @@ class NetworkQualityViewModel : BaseViewModel() { if (!currentState.isRunning) return@launch updateState { copy(isRunning = false) } standaloneTest = null - grpcJob = null + nqSession = null if (message != null) { sendErrorMessage(message) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/STUNTestViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/STUNTestViewModel.kt index 0b7405c..19073b8 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/STUNTestViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/STUNTestViewModel.kt @@ -5,9 +5,9 @@ import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.STUNTestHandler import io.nekohasekai.libbox.STUNTestProgress import io.nekohasekai.libbox.STUNTestResult +import io.nekohasekai.libbox.STUNTestSession import io.nekohasekai.sfa.compose.base.BaseViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -25,7 +25,7 @@ data class STUNTestState( class STUNTestViewModel : BaseViewModel() { private var standaloneTest: io.nekohasekai.libbox.STUNTest? = null - private var grpcJob: Job? = null + private var stunSession: STUNTestSession? = null override fun createInitialState() = STUNTestState() @@ -60,15 +60,16 @@ class STUNTestViewModel : BaseViewModel() { val handler = createHandler() if (vpnRunning) { - grpcJob = viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { try { - Libbox.newStandaloneCommandClient() - .startSTUNTest(server, outboundTag, handler) + stunSession = + Libbox.newStandaloneCommandClient() + .startSTUNTest(server, outboundTag, handler) } catch (e: Exception) { withContext(Dispatchers.Main) { if (!currentState.isRunning) return@withContext updateState { copy(isRunning = false) } - grpcJob = null + stunSession = null sendError(e) } } @@ -85,13 +86,21 @@ class STUNTestViewModel : BaseViewModel() { } fun cancelTest() { - grpcJob?.cancel() - grpcJob = null + try { + stunSession?.close() + } catch (_: Exception) { + } + stunSession = null standaloneTest?.cancel() standaloneTest = null updateState { copy(isRunning = false) } } + override fun onCleared() { + cancelTest() + super.onCleared() + } + private fun createHandler(): STUNTestHandler { return object : STUNTestHandler { override fun onProgress(progress: STUNTestProgress?) { @@ -126,7 +135,7 @@ class STUNTestViewModel : BaseViewModel() { ) } standaloneTest = null - grpcJob = null + stunSession = null } } @@ -135,7 +144,7 @@ class STUNTestViewModel : BaseViewModel() { if (!currentState.isRunning) return@launch updateState { copy(isRunning = false) } standaloneTest = null - grpcJob = null + stunSession = null if (message != null) { sendErrorMessage(message) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscalePingViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscalePingViewModel.kt index d197edd..3967fe8 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscalePingViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscalePingViewModel.kt @@ -1,13 +1,12 @@ package io.nekohasekai.sfa.compose.screen.tools import androidx.lifecycle.viewModelScope -import io.nekohasekai.libbox.CommandClient import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.TailscalePingHandler import io.nekohasekai.libbox.TailscalePingResult +import io.nekohasekai.libbox.TailscalePingSession import io.nekohasekai.sfa.compose.base.BaseViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -23,8 +22,7 @@ data class TailscalePingState( class TailscalePingViewModel : BaseViewModel() { private val maxHistorySize = 30 - private var commandClient: CommandClient? = null - private var grpcJob: Job? = null + private var pingSession: TailscalePingSession? = null override fun createInitialState() = TailscalePingState() @@ -37,67 +35,62 @@ class TailscalePingViewModel : BaseViewModel() { ) } - val client = Libbox.newStandaloneCommandClient() - commandClient = client - - grpcJob = viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { try { - client.startTailscalePing( - endpointTag, - peerIP, - object : TailscalePingHandler { - override fun onPingResult(result: TailscalePingResult?) { - result ?: return - viewModelScope.launch { - if (!currentState.isRunning) return@launch - if (result.error.isNotEmpty()) return@launch - val newHistory = currentState.latencyHistory.toMutableList() - newHistory.add(result.latencyMs.toFloat()) - if (newHistory.size > maxHistorySize) { - newHistory.removeFirstOrNull() + pingSession = + Libbox.newStandaloneCommandClient() + .startTailscalePing( + endpointTag, + peerIP, + object : TailscalePingHandler { + override fun onPingResult(result: TailscalePingResult?) { + result ?: return + viewModelScope.launch { + if (!currentState.isRunning) return@launch + if (result.error.isNotEmpty()) return@launch + val newHistory = currentState.latencyHistory.toMutableList() + newHistory.add(result.latencyMs.toFloat()) + if (newHistory.size > maxHistorySize) { + newHistory.removeFirstOrNull() + } + updateState { + copy( + hasResult = true, + latencyMs = result.latencyMs, + isDirect = result.isDirect, + derpRegionCode = result.derpRegionCode, + endpoint = result.endpoint, + latencyHistory = newHistory, + ) + } + } } - updateState { - copy( - hasResult = true, - latencyMs = result.latencyMs, - isDirect = result.isDirect, - derpRegionCode = result.derpRegionCode, - endpoint = result.endpoint, - latencyHistory = newHistory, - ) - } - } - } - override fun onError(message: String?) { - viewModelScope.launch { - if (!currentState.isRunning) return@launch - updateState { copy(isRunning = false) } - commandClient = null - grpcJob = null - } - } - }, - ) + override fun onError(message: String?) { + viewModelScope.launch { + if (!currentState.isRunning) return@launch + updateState { copy(isRunning = false) } + pingSession = null + } + } + }, + ) } catch (e: Exception) { withContext(Dispatchers.Main) { if (!currentState.isRunning) return@withContext updateState { copy(isRunning = false) } - commandClient = null - grpcJob = null + pingSession = null } } } } fun stopPing() { - grpcJob?.cancel() - grpcJob = null try { - commandClient?.disconnect() + pingSession?.close() } catch (_: Exception) { } - commandClient = null + pingSession = null updateState { copy(isRunning = false) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscaleStatusViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscaleStatusViewModel.kt index 4ad59e5..5ceb068 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscaleStatusViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/TailscaleStatusViewModel.kt @@ -3,10 +3,10 @@ package io.nekohasekai.sfa.compose.screen.tools import androidx.lifecycle.viewModelScope import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.TailscaleStatusHandler +import io.nekohasekai.libbox.TailscaleStatusSubscription import io.nekohasekai.libbox.TailscaleStatusUpdate import io.nekohasekai.sfa.compose.base.BaseViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.launch data class TailscalePeerData( @@ -63,7 +63,7 @@ data class TailscaleStatusState( ) class TailscaleStatusViewModel : BaseViewModel() { - private var grpcJob: Job? = null + private var statusSubscription: TailscaleStatusSubscription? = null override fun createInitialState() = TailscaleStatusState() @@ -71,39 +71,43 @@ class TailscaleStatusViewModel : BaseViewModel() if (currentState.isSubscribed) return updateState { copy(isSubscribed = true) } - grpcJob = viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { try { - Libbox.newStandaloneCommandClient() - .subscribeTailscaleStatus(object : TailscaleStatusHandler { - override fun onStatusUpdate(status: TailscaleStatusUpdate) { - val endpoints = convertUpdate(status) - viewModelScope.launch { - if (!currentState.isSubscribed) return@launch - updateState { copy(endpoints = endpoints) } + statusSubscription = + Libbox.newStandaloneCommandClient() + .subscribeTailscaleStatus(object : TailscaleStatusHandler { + override fun onStatusUpdate(status: TailscaleStatusUpdate) { + val endpoints = convertUpdate(status) + viewModelScope.launch { + if (!currentState.isSubscribed) return@launch + updateState { copy(endpoints = endpoints) } + } } - } - override fun onError(message: String) { - viewModelScope.launch { - if (!currentState.isSubscribed) return@launch - updateState { copy(endpoints = emptyList(), isSubscribed = false) } - grpcJob = null - sendErrorMessage(message) + override fun onError(message: String) { + viewModelScope.launch { + if (!currentState.isSubscribed) return@launch + updateState { copy(endpoints = emptyList(), isSubscribed = false) } + statusSubscription = null + sendErrorMessage(message) + } } - } - }) + }) } catch (_: Exception) { viewModelScope.launch { updateState { copy(endpoints = emptyList(), isSubscribed = false) } - grpcJob = null + statusSubscription = null } } } } fun cancel() { - grpcJob?.cancel() - grpcJob = null + try { + statusSubscription?.close() + } catch (_: Exception) { + } + statusSubscription = null updateState { copy(endpoints = emptyList(), isSubscribed = false) } }