tools: Fix missing cleanup

This commit is contained in:
世界
2026-07-06 21:15:24 +08:00
parent 90c0e2a0fb
commit 2b2cc15208
4 changed files with 113 additions and 98 deletions
@@ -7,10 +7,10 @@ import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.NetworkQualityProgress import io.nekohasekai.libbox.NetworkQualityProgress
import io.nekohasekai.libbox.NetworkQualityResult import io.nekohasekai.libbox.NetworkQualityResult
import io.nekohasekai.libbox.NetworkQualityTestHandler import io.nekohasekai.libbox.NetworkQualityTestHandler
import io.nekohasekai.libbox.NetworkQualityTestSession
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.base.BaseViewModel import io.nekohasekai.sfa.compose.base.BaseViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -41,7 +41,7 @@ data class NetworkQualityState(
class NetworkQualityViewModel : BaseViewModel<NetworkQualityState, Nothing>() { class NetworkQualityViewModel : BaseViewModel<NetworkQualityState, Nothing>() {
private var standaloneTest: io.nekohasekai.libbox.NetworkQualityTest? = null private var standaloneTest: io.nekohasekai.libbox.NetworkQualityTest? = null
private var grpcJob: Job? = null private var nqSession: NetworkQualityTestSession? = null
override fun createInitialState() = NetworkQualityState() override fun createInitialState() = NetworkQualityState()
@@ -114,22 +114,23 @@ class NetworkQualityViewModel : BaseViewModel<NetworkQualityState, Nothing>() {
val handler = createHandler() val handler = createHandler()
if (vpnRunning) { if (vpnRunning) {
grpcJob = viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
Libbox.newStandaloneCommandClient() nqSession =
.startNetworkQualityTest( Libbox.newStandaloneCommandClient()
configURL, .startNetworkQualityTest(
outboundTag, configURL,
serial, outboundTag,
maxRuntimeSeconds, serial,
http3, maxRuntimeSeconds,
handler, http3,
) handler,
)
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (!currentState.isRunning) return@withContext if (!currentState.isRunning) return@withContext
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
grpcJob = null nqSession = null
sendError(e) sendError(e)
} }
} }
@@ -146,13 +147,21 @@ class NetworkQualityViewModel : BaseViewModel<NetworkQualityState, Nothing>() {
} }
fun cancelTest() { fun cancelTest() {
grpcJob?.cancel() try {
grpcJob = null nqSession?.close()
} catch (_: Exception) {
}
nqSession = null
standaloneTest?.cancel() standaloneTest?.cancel()
standaloneTest = null standaloneTest = null
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
} }
override fun onCleared() {
cancelTest()
super.onCleared()
}
private fun createHandler(): NetworkQualityTestHandler { private fun createHandler(): NetworkQualityTestHandler {
return object : NetworkQualityTestHandler { return object : NetworkQualityTestHandler {
override fun onProgress(progress: NetworkQualityProgress?) { override fun onProgress(progress: NetworkQualityProgress?) {
@@ -196,7 +205,7 @@ class NetworkQualityViewModel : BaseViewModel<NetworkQualityState, Nothing>() {
) )
} }
standaloneTest = null standaloneTest = null
grpcJob = null nqSession = null
} }
} }
@@ -205,7 +214,7 @@ class NetworkQualityViewModel : BaseViewModel<NetworkQualityState, Nothing>() {
if (!currentState.isRunning) return@launch if (!currentState.isRunning) return@launch
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
standaloneTest = null standaloneTest = null
grpcJob = null nqSession = null
if (message != null) { if (message != null) {
sendErrorMessage(message) sendErrorMessage(message)
} }
@@ -5,9 +5,9 @@ import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.STUNTestHandler import io.nekohasekai.libbox.STUNTestHandler
import io.nekohasekai.libbox.STUNTestProgress import io.nekohasekai.libbox.STUNTestProgress
import io.nekohasekai.libbox.STUNTestResult import io.nekohasekai.libbox.STUNTestResult
import io.nekohasekai.libbox.STUNTestSession
import io.nekohasekai.sfa.compose.base.BaseViewModel import io.nekohasekai.sfa.compose.base.BaseViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -25,7 +25,7 @@ data class STUNTestState(
class STUNTestViewModel : BaseViewModel<STUNTestState, Nothing>() { class STUNTestViewModel : BaseViewModel<STUNTestState, Nothing>() {
private var standaloneTest: io.nekohasekai.libbox.STUNTest? = null private var standaloneTest: io.nekohasekai.libbox.STUNTest? = null
private var grpcJob: Job? = null private var stunSession: STUNTestSession? = null
override fun createInitialState() = STUNTestState() override fun createInitialState() = STUNTestState()
@@ -60,15 +60,16 @@ class STUNTestViewModel : BaseViewModel<STUNTestState, Nothing>() {
val handler = createHandler() val handler = createHandler()
if (vpnRunning) { if (vpnRunning) {
grpcJob = viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
Libbox.newStandaloneCommandClient() stunSession =
.startSTUNTest(server, outboundTag, handler) Libbox.newStandaloneCommandClient()
.startSTUNTest(server, outboundTag, handler)
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (!currentState.isRunning) return@withContext if (!currentState.isRunning) return@withContext
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
grpcJob = null stunSession = null
sendError(e) sendError(e)
} }
} }
@@ -85,13 +86,21 @@ class STUNTestViewModel : BaseViewModel<STUNTestState, Nothing>() {
} }
fun cancelTest() { fun cancelTest() {
grpcJob?.cancel() try {
grpcJob = null stunSession?.close()
} catch (_: Exception) {
}
stunSession = null
standaloneTest?.cancel() standaloneTest?.cancel()
standaloneTest = null standaloneTest = null
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
} }
override fun onCleared() {
cancelTest()
super.onCleared()
}
private fun createHandler(): STUNTestHandler { private fun createHandler(): STUNTestHandler {
return object : STUNTestHandler { return object : STUNTestHandler {
override fun onProgress(progress: STUNTestProgress?) { override fun onProgress(progress: STUNTestProgress?) {
@@ -126,7 +135,7 @@ class STUNTestViewModel : BaseViewModel<STUNTestState, Nothing>() {
) )
} }
standaloneTest = null standaloneTest = null
grpcJob = null stunSession = null
} }
} }
@@ -135,7 +144,7 @@ class STUNTestViewModel : BaseViewModel<STUNTestState, Nothing>() {
if (!currentState.isRunning) return@launch if (!currentState.isRunning) return@launch
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
standaloneTest = null standaloneTest = null
grpcJob = null stunSession = null
if (message != null) { if (message != null) {
sendErrorMessage(message) sendErrorMessage(message)
} }
@@ -1,13 +1,12 @@
package io.nekohasekai.sfa.compose.screen.tools package io.nekohasekai.sfa.compose.screen.tools
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.nekohasekai.libbox.CommandClient
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.TailscalePingHandler import io.nekohasekai.libbox.TailscalePingHandler
import io.nekohasekai.libbox.TailscalePingResult import io.nekohasekai.libbox.TailscalePingResult
import io.nekohasekai.libbox.TailscalePingSession
import io.nekohasekai.sfa.compose.base.BaseViewModel import io.nekohasekai.sfa.compose.base.BaseViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -23,8 +22,7 @@ data class TailscalePingState(
class TailscalePingViewModel : BaseViewModel<TailscalePingState, Nothing>() { class TailscalePingViewModel : BaseViewModel<TailscalePingState, Nothing>() {
private val maxHistorySize = 30 private val maxHistorySize = 30
private var commandClient: CommandClient? = null private var pingSession: TailscalePingSession? = null
private var grpcJob: Job? = null
override fun createInitialState() = TailscalePingState() override fun createInitialState() = TailscalePingState()
@@ -37,67 +35,62 @@ class TailscalePingViewModel : BaseViewModel<TailscalePingState, Nothing>() {
) )
} }
val client = Libbox.newStandaloneCommandClient() viewModelScope.launch(Dispatchers.IO) {
commandClient = client
grpcJob = viewModelScope.launch(Dispatchers.IO) {
try { try {
client.startTailscalePing( pingSession =
endpointTag, Libbox.newStandaloneCommandClient()
peerIP, .startTailscalePing(
object : TailscalePingHandler { endpointTag,
override fun onPingResult(result: TailscalePingResult?) { peerIP,
result ?: return object : TailscalePingHandler {
viewModelScope.launch { override fun onPingResult(result: TailscalePingResult?) {
if (!currentState.isRunning) return@launch result ?: return
if (result.error.isNotEmpty()) return@launch viewModelScope.launch {
val newHistory = currentState.latencyHistory.toMutableList() if (!currentState.isRunning) return@launch
newHistory.add(result.latencyMs.toFloat()) if (result.error.isNotEmpty()) return@launch
if (newHistory.size > maxHistorySize) { val newHistory = currentState.latencyHistory.toMutableList()
newHistory.removeFirstOrNull() 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?) { override fun onError(message: String?) {
viewModelScope.launch { viewModelScope.launch {
if (!currentState.isRunning) return@launch if (!currentState.isRunning) return@launch
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
commandClient = null pingSession = null
grpcJob = null }
} }
} },
}, )
)
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (!currentState.isRunning) return@withContext if (!currentState.isRunning) return@withContext
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
commandClient = null pingSession = null
grpcJob = null
} }
} }
} }
} }
fun stopPing() { fun stopPing() {
grpcJob?.cancel()
grpcJob = null
try { try {
commandClient?.disconnect() pingSession?.close()
} catch (_: Exception) { } catch (_: Exception) {
} }
commandClient = null pingSession = null
updateState { copy(isRunning = false) } updateState { copy(isRunning = false) }
} }
@@ -3,10 +3,10 @@ package io.nekohasekai.sfa.compose.screen.tools
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.TailscaleStatusHandler import io.nekohasekai.libbox.TailscaleStatusHandler
import io.nekohasekai.libbox.TailscaleStatusSubscription
import io.nekohasekai.libbox.TailscaleStatusUpdate import io.nekohasekai.libbox.TailscaleStatusUpdate
import io.nekohasekai.sfa.compose.base.BaseViewModel import io.nekohasekai.sfa.compose.base.BaseViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class TailscalePeerData( data class TailscalePeerData(
@@ -63,7 +63,7 @@ data class TailscaleStatusState(
) )
class TailscaleStatusViewModel : BaseViewModel<TailscaleStatusState, Nothing>() { class TailscaleStatusViewModel : BaseViewModel<TailscaleStatusState, Nothing>() {
private var grpcJob: Job? = null private var statusSubscription: TailscaleStatusSubscription? = null
override fun createInitialState() = TailscaleStatusState() override fun createInitialState() = TailscaleStatusState()
@@ -71,39 +71,43 @@ class TailscaleStatusViewModel : BaseViewModel<TailscaleStatusState, Nothing>()
if (currentState.isSubscribed) return if (currentState.isSubscribed) return
updateState { copy(isSubscribed = true) } updateState { copy(isSubscribed = true) }
grpcJob = viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
Libbox.newStandaloneCommandClient() statusSubscription =
.subscribeTailscaleStatus(object : TailscaleStatusHandler { Libbox.newStandaloneCommandClient()
override fun onStatusUpdate(status: TailscaleStatusUpdate) { .subscribeTailscaleStatus(object : TailscaleStatusHandler {
val endpoints = convertUpdate(status) override fun onStatusUpdate(status: TailscaleStatusUpdate) {
viewModelScope.launch { val endpoints = convertUpdate(status)
if (!currentState.isSubscribed) return@launch viewModelScope.launch {
updateState { copy(endpoints = endpoints) } if (!currentState.isSubscribed) return@launch
updateState { copy(endpoints = endpoints) }
}
} }
}
override fun onError(message: String) { override fun onError(message: String) {
viewModelScope.launch { viewModelScope.launch {
if (!currentState.isSubscribed) return@launch if (!currentState.isSubscribed) return@launch
updateState { copy(endpoints = emptyList(), isSubscribed = false) } updateState { copy(endpoints = emptyList(), isSubscribed = false) }
grpcJob = null statusSubscription = null
sendErrorMessage(message) sendErrorMessage(message)
}
} }
} })
})
} catch (_: Exception) { } catch (_: Exception) {
viewModelScope.launch { viewModelScope.launch {
updateState { copy(endpoints = emptyList(), isSubscribed = false) } updateState { copy(endpoints = emptyList(), isSubscribed = false) }
grpcJob = null statusSubscription = null
} }
} }
} }
} }
fun cancel() { fun cancel() {
grpcJob?.cancel() try {
grpcJob = null statusSubscription?.close()
} catch (_: Exception) {
}
statusSubscription = null
updateState { copy(endpoints = emptyList(), isSubscribed = false) } updateState { copy(endpoints = emptyList(), isSubscribed = false) }
} }