chat: BGP-driven address sync, member identity verify, join_sig fix
- Адреса узлов вынесены из wire-протокола member_sync/merkle/join/welcome в BGP/topo_group: node_addresses теперь пишет только topo_node_sqlite_addrs_put (источник истины ni->v4/v6_addrs), классификация config_type/nat_type в sockmeta - Жёсткая анти-подделка: node_id=derive(x25519), ed25519 сверяется с доверенной таблицей nodes, NODEINFO node_id/pubkey mismatch отклоняется как forgery - Единые member_sync_build_join_msg (112Б) и member_sync_build_update_msg: устранён баг _verify_join_sig (join_ts по смещению 80 вместо 104, длина 104→112), который рушил приём мемберов; все 7 мест подписи/проверки join_sig через хелпер - Единое обновление своего мембера chat_core_update_my_member (вместо sync_my_addresses) - Android standby: пауза таймеров (broadcast/db_sync/merkle_sync/chat_sync) через standby_wait - name[16] → name[MAX_CONN_NAME_LEN] с truncation-warning в парсере - conn_mgr: etcp_connect_cancel_by_node (use-after-free при отмене invite), промоушен conn в ncd после успешного invite - Android GUI: foreground-gating опросов, сохранение портов интерфейсов, фикс udp-log flush - chatgui: персистентный db_path; мелкие DEBUG-логи epoll/pool
This commit is contained in:
1 parent
c07bd385ca
commit
5c2501b4f3
34 files changed
+808
-882
No files matched your search
@@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import com.utun.chat.data.Channel
|
||||
import com.utun.chat.data.InviteData
|
||||
import com.utun.chat.data.LogManager
|
||||
@@ -36,6 +37,13 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory())
|
||||
|
||||
/* Pause GUI polling (health-check, recording/playback timers) when backgrounded,
|
||||
refresh statuses on resume. */
|
||||
LifecycleStartEffect(Unit) {
|
||||
vm.setForeground(true)
|
||||
onStopOrDispose { vm.setForeground(false) }
|
||||
}
|
||||
|
||||
var screen by remember { mutableStateOf(Screen.ChannelList) }
|
||||
var qrResult by remember { mutableStateOf<String?>(null) }
|
||||
var showJoinDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -209,7 +209,7 @@ class ConfigProvider(private val context: Context) {
|
||||
|
||||
/* ── Server entries ── */
|
||||
|
||||
suspend fun detectInterfaces(): List<ServerEntry> {
|
||||
suspend fun detectInterfaces(saved: List<ServerEntry> = emptyList()): List<ServerEntry> {
|
||||
val servers = mutableListOf<ServerEntry>()
|
||||
val seen = mutableSetOf<String>()
|
||||
try {
|
||||
@@ -224,14 +224,18 @@ class ConfigProvider(private val context: Context) {
|
||||
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr.hostAddress ?: "") -> addr.hostAddress?.let { it.split("%")[0] }
|
||||
else -> null
|
||||
} ?: continue
|
||||
val key = "${ni.name}/${if (ip.contains(":")) "v6" else "v4"}"
|
||||
val isV6 = ip.contains(":")
|
||||
val key = "${ni.name}/${if (isV6) "v6" else "v4"}"
|
||||
if (!seen.add(key)) continue
|
||||
val savedPort = saved.firstOrNull {
|
||||
it.ifName == ni.name && it.bindIp.contains(":") == isV6
|
||||
}?.port
|
||||
servers.add(ServerEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
ifName = ni.name,
|
||||
bindIp = ip,
|
||||
protocol = "tcp",
|
||||
port = ThreadLocalRandom.current().nextInt(2000, 10000)
|
||||
port = savedPort ?: ThreadLocalRandom.current().nextInt(2000, 10000)
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -395,7 +399,9 @@ class ConfigProvider(private val context: Context) {
|
||||
/** Build full INI config text for NativeLib.start() */
|
||||
fun buildConfigText(): String {
|
||||
if (getConnectionMode() == "auto") {
|
||||
val detected = runBlocking { detectInterfaces() }
|
||||
val cfgFile = File(configFilePath())
|
||||
val saved = if (cfgFile.exists()) loadServersFromConfig() else emptyList()
|
||||
val detected = runBlocking { detectInterfaces(saved) }
|
||||
LogManager.addLog("INFO", "CFG", "buildConfigText: auto mode, detected ${detected.size} interfaces" +
|
||||
detected.joinToString(", ") { "${it.ifName}=${it.protocol}" })
|
||||
saveServersToConfig(detected)
|
||||
|
||||
@@ -86,6 +86,20 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
@Volatile private var realPlaybackElapsedMs = 0
|
||||
|
||||
private val _foreground = MutableStateFlow(false)
|
||||
|
||||
fun setForeground(fg: Boolean) {
|
||||
_foreground.value = fg
|
||||
if (fg) {
|
||||
_isStuck.value = false
|
||||
if (dbReady && repo != null) {
|
||||
refreshChannels()
|
||||
refreshLocalSockets()
|
||||
_currentChannel.value?.let { refreshMessages(it.id); refreshMembers(it.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
AppEventHandler.events.collect { (type, data) -> handleEvent(type, data) }
|
||||
@@ -104,37 +118,43 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
while (isActive) {
|
||||
delay(3000)
|
||||
if (!dbReady || !NativeLib.isReady()) { _isStuck.value = true; continue }
|
||||
NativeLib.ping()
|
||||
delay(100)
|
||||
_isStuck.value = !NativeLib.isResponsive()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
while (isActive) {
|
||||
if (_isRecording.value) {
|
||||
_recordingDurationMs.value = (System.currentTimeMillis() - recordingStartTimeMs).toInt()
|
||||
_foreground.collectLatest { fg ->
|
||||
while (fg && isActive) {
|
||||
delay(3000)
|
||||
if (!dbReady || !NativeLib.isReady()) { _isStuck.value = true; continue }
|
||||
NativeLib.ping()
|
||||
delay(100)
|
||||
_isStuck.value = !NativeLib.isResponsive()
|
||||
}
|
||||
delay(80)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
while (isActive) {
|
||||
delay(50)
|
||||
val s = _playbackState.value
|
||||
if (s.playing && !s.paused) {
|
||||
val real = realPlaybackElapsedMs
|
||||
val targetMs = if (real >= s.durMs && s.durMs > 0) s.durMs else real
|
||||
val newMs = (s.elapsedMs + (targetMs - s.elapsedMs) * 0.3f).toInt().coerceIn(0, s.durMs)
|
||||
val newProg = if (s.durMs > 0) newMs.toFloat() / s.durMs else 0f
|
||||
_foreground.collectLatest { fg ->
|
||||
while (fg && isActive) {
|
||||
if (_isRecording.value) {
|
||||
_recordingDurationMs.value = (System.currentTimeMillis() - recordingStartTimeMs).toInt()
|
||||
}
|
||||
delay(80)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_foreground.collectLatest { fg ->
|
||||
while (fg && isActive) {
|
||||
delay(50)
|
||||
val s = _playbackState.value
|
||||
if (s.playing && !s.paused) {
|
||||
val real = realPlaybackElapsedMs
|
||||
val targetMs = if (real >= s.durMs && s.durMs > 0) s.durMs else real
|
||||
val newMs = (s.elapsedMs + (targetMs - s.elapsedMs) * 0.3f).toInt().coerceIn(0, s.durMs)
|
||||
val newProg = if (s.durMs > 0) newMs.toFloat() / s.durMs else 0f
|
||||
|
||||
if (real >= s.durMs && s.durMs > 0 && newMs >= s.durMs - 30) {
|
||||
_playbackState.value = PlaybackState()
|
||||
realPlaybackElapsedMs = 0
|
||||
} else {
|
||||
_playbackState.value = s.copy(elapsedMs = newMs, progress = newProg)
|
||||
if (real >= s.durMs && s.durMs > 0 && newMs >= s.durMs - 30) {
|
||||
_playbackState.value = PlaybackState()
|
||||
realPlaybackElapsedMs = 0
|
||||
} else {
|
||||
_playbackState.value = s.copy(elapsedMs = newMs, progress = newProg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user