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.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<NetworkQualityState, Nothing>() {
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<NetworkQualityState, Nothing>() {
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<NetworkQualityState, Nothing>() {
}
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<NetworkQualityState, Nothing>() {
)
}
standaloneTest = null
grpcJob = null
nqSession = null
}
}
@@ -205,7 +214,7 @@ class NetworkQualityViewModel : BaseViewModel<NetworkQualityState, Nothing>() {
if (!currentState.isRunning) return@launch
updateState { copy(isRunning = false) }
standaloneTest = null
grpcJob = null
nqSession = null
if (message != null) {
sendErrorMessage(message)
}
@@ -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<STUNTestState, Nothing>() {
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<STUNTestState, Nothing>() {
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<STUNTestState, Nothing>() {
}
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<STUNTestState, Nothing>() {
)
}
standaloneTest = null
grpcJob = null
stunSession = null
}
}
@@ -135,7 +144,7 @@ class STUNTestViewModel : BaseViewModel<STUNTestState, Nothing>() {
if (!currentState.isRunning) return@launch
updateState { copy(isRunning = false) }
standaloneTest = null
grpcJob = null
stunSession = null
if (message != null) {
sendErrorMessage(message)
}
@@ -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<TailscalePingState, Nothing>() {
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<TailscalePingState, Nothing>() {
)
}
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) }
}
@@ -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<TailscaleStatusState, Nothing>() {
private var grpcJob: Job? = null
private var statusSubscription: TailscaleStatusSubscription? = null
override fun createInitialState() = TailscaleStatusState()
@@ -71,39 +71,43 @@ class TailscaleStatusViewModel : BaseViewModel<TailscaleStatusState, Nothing>()
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) }
}