fix: daemon gui tunnel state sync issues

This commit is contained in:
zaneschepke
2026-03-27 04:41:29 -04:00
parent 66a6cd6958
commit d19db57265
15 changed files with 191 additions and 182 deletions
@@ -18,6 +18,7 @@ import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.utils.io.*
import io.ktor.websocket.*
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -131,7 +132,7 @@ class UdsBackendService(
}
} catch (e: Exception) {
if (e is CancellationException) throw e
delay(DAEMON_WS_RECONNECT_DELAY_MILLIS)
delay(DAEMON_WS_RECONNECT_DELAY_MILLIS.milliseconds)
}
}
@@ -142,13 +143,10 @@ class UdsBackendService(
@OptIn(ExperimentalCoroutinesApi::class)
override fun statusFlow(): Flow<BackendStatus> =
basicStatusFlow()
.flatMapLatest { basic ->
flow {
.transformLatest { basic ->
while (true) {
emit(enrichWithActiveConfigs(basic))
while (true) {
delay(ACTIVE_CONFIG_INTERVAL)
emit(enrichWithActiveConfigs(basic))
}
delay(ACTIVE_CONFIG_INTERVAL.milliseconds)
}
}
.flowOn(Dispatchers.IO)
@@ -109,7 +109,7 @@ val serviceModule = module {
}
single<DaemonService> { UdsDaemonService(get(), get(), get()) }
single<TunnelService> { UdsTunnelService(get(), tunnelRepository = get()) }
single<BackendService> { UdsBackendService(get(), get(), get(), get()) }
single<BackendService> { UdsBackendService(get(), get(), get(), get(), get()) }
single<TunnelImportService> { DefaultTunnelImportService(get()) }
}
+6
View File
@@ -0,0 +1,6 @@
#Fri Mar 27 04:08:22 EDT 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -248,7 +248,7 @@ fun App(uiState: AppUiState, viewModel: AppViewModel, toaster: ToasterState) {
entryProvider =
entryProvider {
currentTab.startRoute
entry<Route.Tunnels> { TunnelsScreen(viewModel) }
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Tunnel> {
val viewModel: TunnelViewModel =
koinViewModel(parameters = { parametersOf(it.id) })
@@ -30,7 +30,6 @@ import com.zaneschepke.wireguardautotunnel.desktop.ui.common.tooltip.CustomToolt
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.components.TunnelList
import com.zaneschepke.wireguardautotunnel.desktop.ui.sideeffects.AppSideEffect
import com.zaneschepke.wireguardautotunnel.desktop.util.FileUtils
import com.zaneschepke.wireguardautotunnel.desktop.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.desktop.viewmodel.TunnelsViewModel
import io.github.vinceglb.filekit.PlatformFile
import io.github.vinceglb.filekit.dialogs.FileKitMode
@@ -48,10 +47,9 @@ import org.orbitmvi.orbit.compose.collectSideEffect
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TunnelsScreen(appViewModel: AppViewModel, viewModel: TunnelsViewModel = koinViewModel()) {
fun TunnelsScreen(viewModel: TunnelsViewModel = koinViewModel()) {
val uiState by viewModel.collectAsState()
val appUiState by appViewModel.collectAsState()
var pendingDeleteIntent by remember { mutableStateOf<DeleteIntent?>(null) }
@@ -90,43 +88,48 @@ fun TunnelsScreen(appViewModel: AppViewModel, viewModel: TunnelsViewModel = koin
if (!uiState.isLoaded) return
val scope = rememberCoroutineScope()
val pickerLauncher = rememberFilePickerLauncher(
mode = FileKitMode.Single
) { platformFile: PlatformFile? ->
platformFile?.let { file ->
val ext = file.extension.lowercase()
val pickerLauncher =
rememberFilePickerLauncher(mode = FileKitMode.Single) { platformFile: PlatformFile? ->
platformFile?.let { file ->
val ext = file.extension.lowercase()
scope.launch(Dispatchers.Main.immediate) {
when (ext) {
FileUtils.CONF_FILE_EXTENSION -> {
runCatching {
val text = file.readString()
viewModel.onConfImport(text, file.nameWithoutExtension)
}.onFailure {
toaster.show(Toast(ToastType.Error, "Failed to read .conf file"))
scope.launch(Dispatchers.Main.immediate) {
when (ext) {
FileUtils.CONF_FILE_EXTENSION -> {
runCatching {
val text = file.readString()
viewModel.onConfImport(text, file.nameWithoutExtension)
}
.onFailure {
toaster.show(
Toast(ToastType.Error, "Failed to read .conf file")
)
}
}
}
FileUtils.ZIP_FILE_EXTENSION -> {
runCatching {
val bytes = file.readBytes()
val configMap = FileUtils.readConfigsFromZip(bytes)
viewModel.onMultiConfImport(configMap)
}.onFailure {
toaster.show(Toast(ToastType.Error, "Failed to read .zip archive"))
FileUtils.ZIP_FILE_EXTENSION -> {
runCatching {
val bytes = file.readBytes()
val configMap = FileUtils.readConfigsFromZip(bytes)
viewModel.onMultiConfImport(configMap)
}
.onFailure {
toaster.show(
Toast(ToastType.Error, "Failed to read .zip archive")
)
}
}
}
else -> {
toaster.show(
Toast(
type = ToastType.Warning,
message = "Only '.conf' or '.zip' files supported"
else -> {
toaster.show(
Toast(
type = ToastType.Warning,
message = "Only '.conf' or '.zip' files supported",
)
)
)
}
}
}
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
@@ -184,7 +187,6 @@ fun TunnelsScreen(appViewModel: AppViewModel, viewModel: TunnelsViewModel = koin
) {
TunnelList(
uiState = uiState,
tunnelStatuses = appUiState.tunnelStatuses,
startTunnel = viewModel::onStartTunnel,
stopTunnel = viewModel::onStopTunnel,
viewModel::onItemsReordered,
@@ -8,8 +8,7 @@ import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.WarningAmber
fun TunnelState.asColor(): Color {
return when (this) {
TunnelState.DOWN,
TunnelState.UNKNOWN -> Color.Gray
TunnelState.DOWN -> Color.Gray
TunnelState.HEALTHY -> HealthyGreen
TunnelState.HANDSHAKE_FAILURE -> ErrorRed
TunnelState.RESOLVING_DNS,
@@ -21,7 +20,6 @@ fun TunnelState.asColor(): Color {
fun TunnelState.asTooltipMessage(): String {
return when (this) {
TunnelState.DOWN,
TunnelState.UNKNOWN -> "Unknown"
TunnelState.STARTING -> "Starting"
TunnelState.STOPPING -> "Stopping"
TunnelState.HEALTHY -> "Healthy"
@@ -27,8 +27,6 @@ import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelState
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelStatus
import com.zaneschepke.wireguardautotunnel.desktop.ui.common.LocalNavController
import com.zaneschepke.wireguardautotunnel.desktop.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.desktop.ui.common.button.SwitchWithDivider
@@ -48,7 +46,6 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun TunnelList(
uiState: TunnelsUiState,
tunnelStatuses: List<TunnelStatus>,
startTunnel: (id: Long) -> Unit,
stopTunnel: (id: Long) -> Unit,
onReorder: (Int, Int) -> Unit,
@@ -68,18 +65,6 @@ fun TunnelList(
onReorder(from.index, to.index)
}
val tunnelIndicators by
remember(tunnelStatuses, uiState.tunnels) {
derivedStateOf {
uiState.tunnels.associate { tunnel ->
val state =
tunnelStatuses.firstOrNull { it.id == tunnel.id }?.state
?: TunnelState.UNKNOWN
tunnel.id to (state.asColor() to state.asTooltipMessage())
}
}
}
LaunchedEffect(reorderableState.isAnyItemDragging) {
if (!reorderableState.isAnyItemDragging) {
onReorderCompleted()
@@ -99,7 +84,7 @@ fun TunnelList(
}
.fillMaxSize(),
) {
if (uiState.tunnels.isEmpty()) {
if (uiState.tunnelItems.isEmpty()) {
item {
Box(
modifier = Modifier.fillMaxSize().padding(top = 80.dp),
@@ -111,9 +96,9 @@ fun TunnelList(
return@LazyColumn
}
items(uiState.tunnels, key = { it.id }) { tunnel ->
val isSelected = uiState.selectedTunnels.contains(tunnel)
ReorderableItem(reorderableState, key = tunnel.id) { isDragging ->
items(uiState.tunnelItems, key = { it.config.id }) { item ->
val isSelected = uiState.selectedTunnels.contains(item.config)
ReorderableItem(reorderableState, key = item.config.id) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
ContextMenuArea(
@@ -122,20 +107,20 @@ fun TunnelList(
if (!uiState.isSelectionMode) {
add(
ContextMenuItem("Details") {
navController.push(Route.Tunnel(tunnel.id))
navController.push(Route.Tunnel(item.config.id))
}
)
add(
ContextMenuItem("Delete") {
onDelete(DeleteIntent.Tunnel(tunnel))
onDelete(DeleteIntent.Tunnel(item.config))
}
)
add(
ContextMenuItem("Export") {
onExport(ExportIntent.Tunnel(tunnel))
onExport(ExportIntent.Tunnel(item.config))
}
)
add(ContextMenuItem("Select") { onSelected(tunnel) })
add(ContextMenuItem("Select") { onSelected(item.config) })
} else {
add(
ContextMenuItem("Delete selected") {
@@ -155,7 +140,7 @@ fun TunnelList(
}
) {
SurfaceRow(
title = tunnel.name,
title = item.config.name,
modifier =
Modifier.shadow(elevation)
.animateItem()
@@ -167,23 +152,22 @@ fun TunnelList(
.then(if (isDragging) Modifier.zIndex(1f) else Modifier),
onClick = {
if (!uiState.isSelectionMode) {
navController.push(Route.Tunnel(tunnel.id))
navController.push(Route.Tunnel(item.config.id))
}
},
leading = {
val tooltip = tunnelIndicators[tunnel.id]?.second
val indicatorColor = tunnelIndicators[tunnel.id]?.first
val item = uiState.tunnelItems.first { it.config.id == item.config.id }
@Composable
fun icon() {
Icon(
Icons.Rounded.Circle,
contentDescription = null,
tint = indicatorColor ?: TunnelState.UNKNOWN.asColor(),
tint = item.stateColor,
modifier = Modifier.size(14.dp),
)
}
if (tooltip != null) {
CustomTooltip(text = tooltip) { icon() }
if (item.tooltipMessage.isNotBlank()) {
CustomTooltip(text = item.tooltipMessage) { icon() }
} else {
icon()
}
@@ -192,9 +176,10 @@ fun TunnelList(
trailing = {
if (!uiState.isSelectionMode) {
SwitchWithDivider(
checked = tunnel.active,
checked = item.isRunning,
onClick = {
if (it) startTunnel(tunnel.id) else stopTunnel(tunnel.id)
if (it) startTunnel(item.config.id)
else stopTunnel(item.config.id)
},
)
}
@@ -15,19 +15,13 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class, FlowPreview::class)
@Composable
fun ConfigEditor(
rawConfig: String,
isEditable: Boolean,
onConfigChange: (String) -> Unit
) {
fun ConfigEditor(rawConfig: String, isEditable: Boolean, onConfigChange: (String) -> Unit) {
var textFieldValue by remember { mutableStateOf(TextFieldValue(rawConfig)) }
val verticalScrollState = rememberScrollState()
@@ -48,20 +42,22 @@ fun ConfigEditor(
.launchIn(this)
}
val scrollbarStyle = defaultScrollbarStyle().copy(
thickness = 10.dp,
unhoverColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.18f),
hoverColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.65f),
)
val scrollbarStyle =
defaultScrollbarStyle()
.copy(
thickness = 10.dp,
unhoverColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.18f),
hoverColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.65f),
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (isEditable) MaterialTheme.colorScheme.surfaceContainerLowest
else MaterialTheme.colorScheme.surface
)
.clipToBounds()
modifier =
Modifier.fillMaxSize()
.background(
if (isEditable) MaterialTheme.colorScheme.surfaceContainerLowest
else MaterialTheme.colorScheme.surface
)
.clipToBounds()
) {
BasicTextField(
value = textFieldValue,
@@ -70,17 +66,18 @@ fun ConfigEditor(
textFieldValue = newValue
}
},
modifier = Modifier
.fillMaxSize()
.verticalScroll(verticalScrollState)
.horizontalScroll(horizontalScrollState)
.padding(16.dp)
.padding(end = 26.dp),
textStyle = TextStyle(
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface,
fontFamily = FontFamily.Monospace,
),
modifier =
Modifier.fillMaxSize()
.verticalScroll(verticalScrollState)
.horizontalScroll(horizontalScrollState)
.padding(16.dp)
.padding(end = 26.dp),
textStyle =
TextStyle(
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface,
fontFamily = FontFamily.Monospace,
),
readOnly = !isEditable,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
visualTransformation = remember { ConfigVisualTransformation() },
@@ -23,7 +23,7 @@ class ConfigVisualTransformation : VisualTransformation {
builder.addStyle(
SpanStyle(color = Color(0xFFBB86FC), fontWeight = FontWeight.Bold),
it.range.first,
it.range.last + 1
it.range.last + 1,
)
}
@@ -32,7 +32,7 @@ class ConfigVisualTransformation : VisualTransformation {
builder.addStyle(
SpanStyle(color = Color(0xFF03DAC5)),
it.range.first,
it.range.last + 1
it.range.last + 1,
)
}
@@ -43,7 +43,7 @@ class ConfigVisualTransformation : VisualTransformation {
builder.addStyle(
SpanStyle(color = Color.Gray, fontStyle = FontStyle.Italic),
match.range.first,
match.range.first + trimmed.length
match.range.first + trimmed.length,
)
}
}
@@ -1,10 +1,26 @@
package com.zaneschepke.wireguardautotunnel.desktop.ui.state
import androidx.compose.ui.graphics.Color
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelState
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelStatus
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.components.asColor
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.components.asTooltipMessage
data class TunnelsUiState(
val tunnels: List<TunnelConfig> = emptyList(),
val tunnelItems: List<TunnelUiItem> = emptyList(),
val selectedTunnels: List<TunnelConfig> = emptyList(),
val isSelectionMode: Boolean = false,
val isLoaded: Boolean = false,
)
data class TunnelUiItem(val config: TunnelConfig, val status: TunnelStatus? = null) {
val isRunning: Boolean
get() = status?.state != TunnelState.DOWN && status != null
val stateColor: Color
get() = status?.state?.asColor() ?: TunnelState.DOWN.asColor()
val tooltipMessage: String
get() = status?.state?.asTooltipMessage() ?: ""
}
@@ -5,11 +5,13 @@ import com.dokar.sonner.ToastType
import com.zaneschepke.wireguardautotunnel.client.domain.error.ClientException
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.client.service.BackendService
import com.zaneschepke.wireguardautotunnel.client.service.TunnelImportService
import com.zaneschepke.wireguardautotunnel.client.service.TunnelService
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.DeleteIntent
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.ExportIntent
import com.zaneschepke.wireguardautotunnel.desktop.ui.sideeffects.AppSideEffect
import com.zaneschepke.wireguardautotunnel.desktop.ui.state.TunnelUiItem
import com.zaneschepke.wireguardautotunnel.desktop.ui.state.TunnelsUiState
import com.zaneschepke.wireguardautotunnel.desktop.util.FileUtils
import com.zaneschepke.wireguardautotunnel.desktop.util.asUserMessage
@@ -17,8 +19,6 @@ import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.dialogs.openFileSaver
import io.github.vinceglb.filekit.name
import io.github.vinceglb.filekit.write
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
@@ -26,31 +26,53 @@ class TunnelsViewModel(
private val tunnelRepository: TunnelRepository,
private val tunnelService: TunnelService,
private val tunnelImportService: TunnelImportService,
private val backendService: BackendService,
) : ContainerHost<TunnelsUiState, AppSideEffect>, ViewModel() {
override val container =
container<TunnelsUiState, AppSideEffect>(
TunnelsUiState(),
buildSettings = { repeatOnSubscribedStopTimeout = 5_000L },
) {
container<TunnelsUiState, AppSideEffect>(TunnelsUiState()) {
intent {
tunnelRepository.flow.collect { tunnels ->
reduce { state.copy(tunnels = tunnels, isLoaded = true) }
tunnelRepository.flow.collect { configs ->
reduce {
val currentItems = state.tunnelItems
val updatedItems =
configs.map { config ->
val existingStatus =
currentItems.firstOrNull { it.config.id == config.id }?.status
TunnelUiItem(config = config, status = existingStatus)
}
state.copy(tunnelItems = updatedItems, isLoaded = true)
}
}
}
intent {
backendService.statusFlow().collect { backendStatus ->
reduce {
val updatedItems =
state.tunnelItems.map { item ->
val newStatus =
backendStatus.activeTunnels.firstOrNull {
it.id == item.config.id
}
item.copy(status = newStatus)
}
state.copy(tunnelItems = updatedItems)
}
}
}
}
fun onItemsReordered(fromIndex: Int, toIndex: Int) = intent {
val list = state.tunnels.toMutableList()
val list = state.tunnelItems.toMutableList()
val item = list.removeAt(fromIndex)
list.add(toIndex, item)
reduce { state.copy(tunnels = list) }
reduce { state.copy(tunnelItems = list) }
}
fun onPersistReorder() = intent {
val updatedTunnels =
state.tunnels.mapIndexed { index, tunnel -> tunnel.copy(position = index) }
state.tunnelItems.mapIndexed { index, item -> item.config.copy(position = index) }
tunnelRepository.updateAll(updatedTunnels)
}
@@ -128,7 +150,12 @@ class TunnelsViewModel(
}
fun onSelectAll() = intent {
reduce { state.copy(isSelectionMode = true, selectedTunnels = state.tunnels) }
reduce {
state.copy(
isSelectionMode = true,
selectedTunnels = state.tunnelItems.map { it.config },
)
}
}
fun onDelete(intent: DeleteIntent) = intent {
@@ -14,7 +14,6 @@ fun Tunnel.State.toDto(): TunnelState =
is Tunnel.State.Up.Healthy -> TunnelState.HEALTHY
is Tunnel.State.Up.HandshakeFailure -> TunnelState.HANDSHAKE_FAILURE
is Tunnel.State.Up.ResolvingDns -> TunnelState.RESOLVING_DNS
is Tunnel.State.Up.Unknown -> TunnelState.UNKNOWN
is Tunnel.State.Stopping -> TunnelState.STOPPING
}
@@ -10,5 +10,4 @@ enum class TunnelState {
HEALTHY,
HANDSHAKE_FAILURE,
RESOLVING_DNS,
UNKNOWN,
}
@@ -68,44 +68,6 @@ class AmneziaBackend : Backend {
override suspend fun start(tunnel: Tunnel, config: String): Result<Unit> = runCatching {
log.i { "Start request for tunnel: ${tunnel.id}" }
val statusChannel = Channel<Int>(Channel.BUFFERED)
val statusCallback =
object : StatusCodeCallback {
override fun onTunnelStatusCode(handle: Int, statusCode: Int) {
log.v { "Native Callback - Handle: $handle, Code: $statusCode" }
statusChannel.trySend(statusCode)
}
}
statusCallbacks[tunnel.id] = statusCallback
val handle =
tunnelMutex.withLock {
if (tunnelHandles.containsKey(tunnel.id)) {
log.w { "Tunnel ${tunnel.id} already exists in handles map." }
throw BackendException.StateConflict("Tunnel ${tunnel.id} is already in use")
}
log.d { "Lock acquired. Invoking native turnOn..." }
val nativeHandle =
when (currentMode) {
Backend.Mode.Proxy -> tun.awgProxyTurnOn(config, statusCallback)
Backend.Mode.Userspace -> tun.awgTurnOn(config, statusCallback)
}
if (nativeHandle < 0) {
log.e { "Native turnOn failed: $nativeHandle" }
throw BackendException.InternalError(
"Tunnel failed with internal error code $nativeHandle"
)
}
tunnelHandles[tunnel.id] = nativeHandle
nativeHandle
}
log.i { "Tunnel ${tunnel.id} native initialization successful. Handle: $handle" }
tunnel.updateState(Tunnel.State.Starting)
_status.update {
it.copy(
@@ -114,12 +76,43 @@ class AmneziaBackend : Backend {
)
}
val statusChannel = Channel<Int>(Channel.BUFFERED)
val statusCallback =
object : StatusCodeCallback {
override fun onTunnelStatusCode(handle: Int, statusCode: Int) {
log.v { "Native Callback - Handle: $handle, Code: $statusCode" }
statusChannel.trySend(statusCode)
}
}
statusCallbacks[tunnel.id] = statusCallback
val handle =
tunnelMutex.withLock {
if (tunnelHandles.containsKey(tunnel.id)) {
throw BackendException.StateConflict("Tunnel ${tunnel.id} is already in use")
}
val nativeHandle =
when (currentMode) {
Backend.Mode.Proxy -> tun.awgProxyTurnOn(config, statusCallback)
Backend.Mode.Userspace -> tun.awgTurnOn(config, statusCallback)
}
if (nativeHandle < 0) {
throw BackendException.InternalError(
"Tunnel failed with internal error code $nativeHandle"
)
}
tunnelHandles[tunnel.id] = nativeHandle
nativeHandle
}
log.i { "Tunnel ${tunnel.id} native initialization successful. Handle: $handle" }
tunnelJobs[tunnel.id] =
backendScope.launch {
try {
statusChannel.consumeAsFlow().collect { statusCode ->
val tunnelState = mapStatusCodeToState(statusCode)
log.d { "Tunnel ${tunnel.id} status update: $statusCode -> $tunnelState" }
log.d { "Tunnel ${tunnel.id} status update: $statusCode $tunnelState" }
_status.update {
it.copy(
@@ -130,10 +123,7 @@ class AmneziaBackend : Backend {
}
tunnel.updateState(tunnelState)
}
} catch (e: Exception) {
log.e(e) { "Error in status flow for tunnel ${tunnel.id}" }
} finally {
log.i { "Status collector for tunnel ${tunnel.id} terminating." }
statusChannel.close()
cleanupTunnelState(tunnel.id)
tunnel.updateState(Tunnel.State.Down)
@@ -146,12 +136,10 @@ class AmneziaBackend : Backend {
val handle =
tunnelHandles[id]
?: run {
log.w { "Stop requested for $id but no handle found." }
return Result.failure(
BackendException.StateConflict("Tunnel $id is not active.")
)
}
?: return Result.failure(
BackendException.StateConflict("Tunnel $id is not active.")
)
_status.update { current ->
val key = current.activeTunnels.keys.firstOrNull { it.id == id }
if (key != null)
@@ -160,15 +148,12 @@ class AmneziaBackend : Backend {
}
tunnelMutex.withLock {
log.d { "Lock acquired for Stop. Calling native turnOff for handle: $handle" }
when (currentMode) {
Backend.Mode.Proxy -> tun.awgProxyTurnOff(handle)
Backend.Mode.Userspace -> tun.awgTurnOff(handle)
}
}
tunnelJobs[id]?.cancel()
log.i { "Stop command sent and job cancelled for tunnel $id" }
}
@@ -256,7 +241,6 @@ class AmneziaBackend : Backend {
0 -> Tunnel.State.Up.Healthy
1 -> Tunnel.State.Up.HandshakeFailure
2 -> Tunnel.State.Up.ResolvingDns
3 -> Tunnel.State.Up.Unknown
else -> Tunnel.State.Down
}
}
@@ -14,8 +14,6 @@ interface Tunnel {
data object ResolvingDns : Up()
data object HandshakeFailure : Up()
data object Unknown : Up()
}
data object Down : State