Preserve confirmed settings and manual socket choices after failures

This commit is contained in:
evgeny committed 2026-10-29 13:35:19 +01:00
1 parent de907c697b
commit 299518e866
2 files changed
+39 -30

No files matched your search

@@ -152,15 +152,6 @@ class ConfigProvider(private val context: Context) {
prefs[radioCompressorMaxGainDbKey] ?: 15, prefs[chatCompressorRiseRate] ?: 6)
}.distinctUntilChanged()
private fun keysFilePath(): String {
val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath
return "$dir/keys.conf"
}
fun deleteKeysFile() {
File(keysFilePath()).delete()
}
private fun readKeysFromFile(f: File): Pair<String, String>? {
if (!f.exists()) return null
try {
@@ -394,6 +385,10 @@ class ConfigProvider(private val context: Context) {
}
suspend fun setValue(key: String, value: String) {
require(value.none { it == '\r' || it == '\n' || it == '\u0000' }) {
LogManager.addLog("ERROR", "CFG", "Setting rejected: control characters key=$key")
"Configuration value must fit one line"
}
context.dataStore.edit { prefs ->
when (key) {
"gui.utun_enabled" -> prefs[guiUtunEnabled] = if (value == "1" || value == "yes" || value == "true") 1 else 0
@@ -435,8 +430,13 @@ class ConfigProvider(private val context: Context) {
"radio.capture_stereo" -> prefs[radioCaptureStereoKey] = value.toIntOrNull() ?: 0
"radio.compressor_enabled" -> prefs[radioCompressorEnabledKey] = value.toIntOrNull() ?: 1
"radio.compressor_max_gain_db" -> prefs[radioCompressorMaxGainDbKey] = value.toIntOrNull() ?: 15
else -> {
LogManager.addLog("ERROR", "CFG", "Unknown setting rejected key=$key")
throw IllegalArgumentException("Unknown configuration key: $key")
}
}
}
LogManager.addLog("INFO", "CFG", "Setting saved key=$key")
}
private fun radioAutoSettings(prefs: Preferences) = RadioAutoSettings(
@@ -500,12 +500,8 @@ class ConfigProvider(private val context: Context) {
LogManager.addLog("INFO", "AudioSettings", "Saved: $settings")
}
suspend fun generateKeys() {
/* Keys are generated in C (instance_lite via ensure_keys).
* They are saved back via NativeLib.start() → nativeGetGeneratedKeys(). */
}
private companion object {
private val ipv6FlagsWarning = java.util.concurrent.atomic.AtomicBoolean(false)
/** Accept only global unicast (2000::/3) and unique local (fc00::/7) IPv6. */
fun isUsableV6(addr: Inet6Address): Boolean {
if (addr.isLoopbackAddress || addr.isLinkLocalAddress || addr.isMulticastAddress) return false
@@ -516,17 +512,16 @@ class ConfigProvider(private val context: Context) {
}
// 0x20 = IFA_F_TEMPORARY в /proc/net/if_inet6
fun isTemporaryV6(ifName: String, ip: String): Boolean {
val hexAddr = ip.split("%")[0].replace(":", "").uppercase().padStart(32, '0')
fun isTemporaryV6(ifName: String, address: Inet6Address): Boolean {
return try {
File("/proc/net/if_inet6").useLines { lines ->
lines.any { line ->
val parts = line.trim().split("\\s+".toRegex(), limit = 6)
parts.size >= 6 && parts[0] == hexAddr && parts[5] == ifName &&
parts[4].toIntOrNull(16)?.let { (it and 0x20) != 0 } == true
}
temporaryIpv6Address(ifName, address.address, lines)
}
} catch (_: Exception) { false }
} catch (e: Exception) {
if (ipv6FlagsWarning.compareAndSet(false, true)) LogManager.addLog("WARN", "CFG",
"IPv6 temporary flags unavailable; keeping usable IPv6 addresses: ${e.javaClass.simpleName}: ${e.message}")
false
}
}
}
@@ -553,7 +548,12 @@ class ConfigProvider(private val context: Context) {
}
suspend fun setConnectionMode(mode: String) {
require(mode == "auto" || mode == "manual") {
LogManager.addLog("ERROR", "CFG", "Invalid connection mode rejected: $mode")
"Unknown connection mode"
}
context.dataStore.edit { prefs -> prefs[connectionModeKey] = mode }
LogManager.addLog("INFO", "CFG", "Connection mode saved: $mode")
}
fun getAutoSocketProtocols(): AutoSocketProtocols = runBlocking {
@@ -664,6 +664,7 @@ class ConfigProvider(private val context: Context) {
LogManager.addLog("INFO", "PTT", "Volume PTT enabled=$enabled")
} catch (e: IOException) {
LogManager.addLog("ERROR", "PTT", "Cannot save volume PTT enabled=$enabled: $e")
throw e
}
}
@@ -683,9 +684,10 @@ class ConfigProvider(private val context: Context) {
}
suspend fun setRadioOverlay(enabled: Boolean) {
context.dataStore.edit { prefs -> prefs[radioOverlayKey] = enabled }
radioOverlayCached = enabled
radioOverlayLoaded = true
context.dataStore.edit { prefs -> prefs[radioOverlayKey] = enabled }
LogManager.addLog("INFO", "radio", "Overlay setting saved enabled=$enabled")
}
@Volatile private var radioMicSourceCached = 0
@@ -700,9 +702,10 @@ class ConfigProvider(private val context: Context) {
}
suspend fun setRadioMicSource(source: Int) {
context.dataStore.edit { prefs -> prefs[radioMicSourceKey] = source }
radioMicSourceCached = source
radioMicSourceLoaded = true
context.dataStore.edit { prefs -> prefs[radioMicSourceKey] = source }
LogManager.addLog("INFO", "radio", "Microphone source saved source=$source")
}
@Volatile private var radioCaptureStereoCached = 0
@@ -717,9 +720,10 @@ class ConfigProvider(private val context: Context) {
}
suspend fun setRadioCaptureStereo(enabled: Boolean) {
context.dataStore.edit { prefs -> prefs[radioCaptureStereoKey] = if (enabled) 1 else 0 }
radioCaptureStereoCached = if (enabled) 1 else 0
radioCaptureStereoLoaded = true
context.dataStore.edit { prefs -> prefs[radioCaptureStereoKey] = if (enabled) 1 else 0 }
LogManager.addLog("INFO", "radio", "Stereo capture setting saved enabled=$enabled")
}
/* ── Server entries ── */
@@ -730,14 +734,14 @@ class ConfigProvider(private val context: Context) {
val seen = mutableSetOf<String>()
try {
val nis = NetworkInterface.getNetworkInterfaces()
while (nis.hasMoreElements()) {
while (nis != null && nis.hasMoreElements()) {
val ni = nis.nextElement()
if (!ni.isUp || ni.isLoopback) continue
for (a in ni.interfaceAddresses) {
val addr = a.address
val ip = when {
addr is Inet4Address -> addr.hostAddress
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr.hostAddress ?: "") -> addr.hostAddress?.let { it.split("%")[0] }
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr) -> addr.hostAddress?.substringBefore('%')
else -> null
} ?: continue
val isV6 = ip.contains(":")
@@ -748,6 +752,7 @@ class ConfigProvider(private val context: Context) {
}
} catch (e: Exception) {
LogManager.addLog("ERROR", "CFG", "detectInterfaces failed: ${e.message}")
throw e // Частичный снимок не должен заменять работающую конфигурацию.
}
return servers
}
@@ -940,14 +945,14 @@ class ConfigProvider(private val context: Context) {
val list = mutableListOf<Pair<String, String>>()
try {
val nis = NetworkInterface.getNetworkInterfaces()
while (nis.hasMoreElements()) {
while (nis != null && nis.hasMoreElements()) {
val ni = nis.nextElement()
if (!ni.isUp || ni.isLoopback) continue
for (a in ni.interfaceAddresses) {
val addr = a.address
val ip = when {
addr is Inet4Address -> addr.hostAddress
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr.hostAddress ?: "") -> addr.hostAddress?.let { it.split("%")[0] }
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr) -> addr.hostAddress?.substringBefore('%')
else -> null
} ?: continue
list.add(ni.name to ip)
@@ -971,7 +976,7 @@ class ConfigProvider(private val context: Context) {
val stored = loadServers(cfgFile)
val saved = stored.active + stored.inactive
val mode = snapshot.preferences[connectionModeKey] ?: "auto"
val active = if (mode == "auto" || stored.active.isEmpty()) detectInterfaces(saved, protocols) else stored.active
val active = if (mode == "auto") detectInterfaces(saved, protocols) else stored.active
val activeKeys = active.map { it.socketKey }.toSet()
val inactive = saved.filter { it.socketKey !in activeKeys }.distinctBy { it.socketKey }
LogManager.addLog("INFO", "CFG", "buildConfigText: mode=$mode protocols=${protocols.configValue} " +
@@ -126,6 +126,10 @@ socket=info
```
Секции `[server:<имя>]` с addr/type/transport/netif строятся из интерфейсов устройства.
В manual пустой список остаётся пустым; автоматическое обнаружение не включает отключённые пользователем сокеты.
Ошибка перечисления интерфейсов отклоняет новый снимок до замены файла и сохраняет прежний runtime config.
Флаг временного IPv6 сопоставляется по 16 байтам адреса с /proc; недоступность /proc на Android
логируется один раз и оставляет пригодные IPv6-адреса. Кэши overlay/microphone/stereo меняются после DataStore commit.
NetworkChangeMonitor передаёт новый INI в updateSockets: uasync согласует сокеты через
`auto_socket_reload_config`, сохраняя ядро/чат. В auto_sockets=android общий
`auto_socket_on_network_change()` не запускает scan: нужен именно reload из Kotlin.