Preserve confirmed settings and manual socket choices after failures
This commit is contained in:
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.
|
||||
|
||||
Reference in new issue
Block a user