wip: STCP TCP chat integration — diagnostics, invite TCP fallback, test infra
This commit is contained in:
1 parent
e2d8f68c9a
commit
dfe9d36783
41 files changed
+2432
-294
No files matched your search
@@ -8,6 +8,7 @@ import java.nio.ByteOrder
|
||||
|
||||
const val EVT_NODEINFO_UPDATED = 16
|
||||
const val EVT_LINK_STATUS_CHANGED = 17
|
||||
const val EVT_LOCAL_SOCKETS = 19
|
||||
|
||||
data class NodeStatus(
|
||||
val nodeId: Long = 0,
|
||||
@@ -79,6 +80,14 @@ data class LinkStatus(val family: Int = 0, val address: String = "", val port: I
|
||||
data class MemberLinks(val connected: Boolean = false, val initialized: Boolean = false,
|
||||
val linksUp: Int = 0, val links: List<LinkStatus> = emptyList())
|
||||
|
||||
data class LocalSocketState(
|
||||
val sockId: Int = 0, val ifName: String = "", val bindIp: String = "",
|
||||
val port: Int = 0, val protocol: String = "", val state: Int = 0,
|
||||
val linksCount: Int = 0
|
||||
) {
|
||||
val stateLabel: String get() = when (state) { 2 -> "UP"; 1 -> "CONNECTING"; else -> "DOWN" }
|
||||
}
|
||||
|
||||
class ChatRepository {
|
||||
private val myNodeId: Long = NativeLib.getMyNodeId()
|
||||
|
||||
@@ -292,5 +301,26 @@ class ChatRepository {
|
||||
return MemberLinks()
|
||||
}
|
||||
|
||||
fun getLocalSockets(): List<LocalSocketState> {
|
||||
try {
|
||||
val arr = JSONArray(NativeLib.getLocalSockets())
|
||||
val list = mutableListOf<LocalSocketState>()
|
||||
for (i in 0 until arr.length()) {
|
||||
val obj = arr.getJSONObject(i)
|
||||
list.add(LocalSocketState(
|
||||
sockId = obj.optInt("si", 0),
|
||||
ifName = obj.optString("nm", ""),
|
||||
bindIp = obj.optString("ad", ""),
|
||||
port = obj.optInt("po", 0),
|
||||
protocol = obj.optString("pt", ""),
|
||||
state = obj.optInt("st", 0),
|
||||
linksCount = obj.optInt("lk", 0)
|
||||
))
|
||||
}
|
||||
return list
|
||||
} catch (e: Exception) { Log.w("utun-gui", "getLocalSockets error", e) }
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun close() {}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class ConfigProvider(private val context: Context) {
|
||||
private val chatCompressorRiseRate = intPreferencesKey("chat_compressor_rise_rate")
|
||||
private val chatMediaDownloadMaxPeers = intPreferencesKey("chat_media_download_max_peers")
|
||||
private val firstLaunchDone = booleanPreferencesKey("first_launch_done")
|
||||
private val connectionModeKey = stringPreferencesKey("connection_mode")
|
||||
|
||||
private fun keysFilePath(): String {
|
||||
val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath
|
||||
@@ -187,6 +188,16 @@ class ConfigProvider(private val context: Context) {
|
||||
context.dataStore.edit { prefs -> prefs[firstLaunchDone] = true }
|
||||
}
|
||||
|
||||
/* ── Connection mode ── */
|
||||
|
||||
fun getConnectionMode(): String = runBlocking {
|
||||
context.dataStore.data.first()[connectionModeKey] ?: "manual"
|
||||
}
|
||||
|
||||
suspend fun setConnectionMode(mode: String) {
|
||||
context.dataStore.edit { prefs -> prefs[connectionModeKey] = mode }
|
||||
}
|
||||
|
||||
/* ── Server entries ── */
|
||||
|
||||
suspend fun detectInterfaces(): List<ServerEntry> {
|
||||
@@ -364,6 +375,13 @@ class ConfigProvider(private val context: Context) {
|
||||
|
||||
/** Build full INI config text for NativeLib.start() */
|
||||
fun buildConfigText(): String {
|
||||
if (getConnectionMode() == "auto") {
|
||||
val detected = runBlocking { detectInterfaces() }
|
||||
LogManager.addLog("INFO", "CFG", "buildConfigText: auto mode, detected ${detected.size} interfaces" +
|
||||
detected.joinToString(", ") { "${it.ifName}=${it.protocol}" })
|
||||
saveServersToConfig(detected)
|
||||
return buildConfigFromServers(detected)
|
||||
}
|
||||
val cfgFile = File(configFilePath())
|
||||
val existing = if (cfgFile.exists()) loadServersFromConfig() else emptyList()
|
||||
LogManager.addLog("INFO", "CFG", "buildConfigText: exists=${cfgFile.exists()} srv=${existing.size}" +
|
||||
|
||||
@@ -86,6 +86,9 @@ object NativeLib {
|
||||
}
|
||||
fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}"
|
||||
|
||||
/* ── Local sockets (for auto-mode display) ── */
|
||||
fun getLocalSockets(): String = nativeGetLocalSockets() ?: "[]"
|
||||
|
||||
/* ── Voice recording ── */
|
||||
fun voiceStart(channelId: String): Boolean = nativeVoiceStart(channelId)
|
||||
fun voiceFeed(samples: ShortArray): Boolean = nativeVoiceFeed(samples)
|
||||
@@ -145,6 +148,9 @@ object NativeLib {
|
||||
private external fun nativeSetMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, storage: Boolean)
|
||||
private external fun nativeGetMemberLinks(nodeId: Long): String?
|
||||
|
||||
/* ── Local sockets JNI ── */
|
||||
private external fun nativeGetLocalSockets(): String?
|
||||
|
||||
/* ── Voice recording JNI ── */
|
||||
private external fun nativeVoiceStart(channelId: String): Boolean
|
||||
private external fun nativeVoiceFeed(samples: ShortArray): Boolean
|
||||
|
||||
+97
-47
@@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.utun.chat.ChatApplication
|
||||
import com.utun.chat.data.NativeLib
|
||||
import com.utun.chat.data.ServerEntry
|
||||
import com.utun.chat.data.LocalSocketState
|
||||
import com.utun.chat.viewmodel.ChatViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -107,6 +108,9 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
var catLevels by remember { mutableStateOf<Map<String, String>>(emptyMap()) }
|
||||
var servers by remember { mutableStateOf<List<ServerEntry>>(emptyList()) }
|
||||
var availableIfaces by remember { mutableStateOf<List<Pair<String, String>>>(emptyList()) }
|
||||
var connectionMode by remember { mutableStateOf("manual") }
|
||||
|
||||
val localSocketStates by vm.localSockets.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
nodeName = provider.getString("node.name") ?: provider.getDeviceName()
|
||||
@@ -125,9 +129,18 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
catLevels = if (stored.isNotEmpty()) stored.split(",").associate { it.split("=", limit = 2).let { (k, v) -> k to v } } else emptyMap()
|
||||
servers = provider.loadServersFromConfig()
|
||||
availableIfaces = provider.getAvailableInterfaces()
|
||||
connectionMode = provider.getConnectionMode()
|
||||
}
|
||||
|
||||
val save = { s: List<ServerEntry> -> onDirty(true); servers = s; provider.saveServersToConfig(s) }
|
||||
val switchMode = { mode: String ->
|
||||
connectionMode = mode; onDirty(true)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
provider.setConnectionMode(mode)
|
||||
NativeLib.restart(provider.getString("db.path") ?: "",
|
||||
provider.getInt("control.port"), provider.buildConfigText())
|
||||
}
|
||||
}
|
||||
val setDebug = { level: String ->
|
||||
onDirty(true); NativeLib.setDebugLevel("", level)
|
||||
scope.launch { provider.setValue("debug.console_level", level) }; debugLevel = level
|
||||
@@ -192,60 +205,97 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
/* Network Interfaces */
|
||||
/* ── Connection Mode ── */
|
||||
Text("Network Interfaces", style = MaterialTheme.typography.titleMedium)
|
||||
servers.forEachIndexed { idx, srv ->
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var ifExpanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(expanded = ifExpanded, onExpandedChange = { ifExpanded = it }, modifier = Modifier.weight(1f)) {
|
||||
OutlinedTextField(srv.ifName, {}, readOnly = true, singleLine = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(ifExpanded) },
|
||||
modifier = Modifier.menuAnchor().heightIn(min = 40.dp))
|
||||
ExposedDropdownMenu(ifExpanded, onDismissRequest = { ifExpanded = false }) {
|
||||
availableIfaces.forEach { (n, ip) ->
|
||||
DropdownMenuItem(text = { Text("$n ($ip)") }, onClick = {
|
||||
save(servers.toMutableList().also { it[idx] = it[idx].copy(ifName = n, bindIp = ip) }); ifExpanded = false
|
||||
})
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FilterChip(selected = connectionMode == "manual", onClick = { if (connectionMode != "manual") switchMode("manual") },
|
||||
label = { Text("Manual") })
|
||||
FilterChip(selected = connectionMode == "auto", onClick = { if (connectionMode != "auto") switchMode("auto") },
|
||||
label = { Text("Auto") })
|
||||
}
|
||||
|
||||
if (connectionMode == "auto") {
|
||||
/* Auto mode: read-only live socket display */
|
||||
if (localSocketStates.isEmpty()) {
|
||||
Text("No sockets detected", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
} else {
|
||||
localSocketStates.forEach { s ->
|
||||
val stateColor = when (s.state) { 2 -> Color(0xFF27AE60); 1 -> Color(0xFFF1C40F); else -> Color(0xFFE53935) }
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(s.ifName, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("${s.bindIp}:${s.port}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Surface(shape = MaterialTheme.shapes.extraSmall, color = stateColor,
|
||||
modifier = Modifier.size(10.dp)) {}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(s.stateLabel, style = MaterialTheme.typography.labelSmall, color = stateColor)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(s.protocol, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
if (s.linksCount > 0) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("${s.linksCount} links", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(srv.bindIp, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1, modifier = Modifier.width(105.dp))
|
||||
IconButton(onClick = { save(servers.toMutableList().also { it.removeAt(idx) }) }, modifier = Modifier.size(28.dp)) {
|
||||
Icon(Icons.Default.Close, "Remove", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var protoExpanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(expanded = protoExpanded, onExpandedChange = { protoExpanded = it }, modifier = Modifier.width(100.dp)) {
|
||||
OutlinedTextField(srv.protocol.uppercase(), {}, readOnly = true, singleLine = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(protoExpanded) },
|
||||
modifier = Modifier.menuAnchor().heightIn(min = 40.dp))
|
||||
ExposedDropdownMenu(protoExpanded, onDismissRequest = { protoExpanded = false }) {
|
||||
PROTOCOLS.forEach { p ->
|
||||
DropdownMenuItem(text = { Text(p.uppercase()) }, onClick = {
|
||||
save(servers.toMutableList().also { it[idx] = srv.copy(protocol = p) }); protoExpanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
var portText by remember(srv.id) { mutableStateOf(srv.port.toString()) }
|
||||
OutlinedTextField(portText, { v -> portText = v; val p = v.toIntOrNull(); if (p != null && p in 1..65535)
|
||||
save(servers.toMutableList().also { it[idx] = srv.copy(port = p) }) },
|
||||
singleLine = true, modifier = Modifier.width(80.dp).heightIn(min = 40.dp),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(2.dp))
|
||||
} else {
|
||||
/* Manual mode: existing editable server config */
|
||||
servers.forEachIndexed { idx, srv ->
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var ifExpanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(expanded = ifExpanded, onExpandedChange = { ifExpanded = it }, modifier = Modifier.weight(1f)) {
|
||||
OutlinedTextField(srv.ifName, {}, readOnly = true, singleLine = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(ifExpanded) },
|
||||
modifier = Modifier.menuAnchor().heightIn(min = 40.dp))
|
||||
ExposedDropdownMenu(ifExpanded, onDismissRequest = { ifExpanded = false }) {
|
||||
availableIfaces.forEach { (n, ip) ->
|
||||
DropdownMenuItem(text = { Text("$n ($ip)") }, onClick = {
|
||||
save(servers.toMutableList().also { it[idx] = it[idx].copy(ifName = n, bindIp = ip) }); ifExpanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(srv.bindIp, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1, modifier = Modifier.width(105.dp))
|
||||
IconButton(onClick = { save(servers.toMutableList().also { it.removeAt(idx) }) }, modifier = Modifier.size(28.dp)) {
|
||||
Icon(Icons.Default.Close, "Remove", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var protoExpanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(expanded = protoExpanded, onExpandedChange = { protoExpanded = it }, modifier = Modifier.width(100.dp)) {
|
||||
OutlinedTextField(srv.protocol.uppercase(), {}, readOnly = true, singleLine = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(protoExpanded) },
|
||||
modifier = Modifier.menuAnchor().heightIn(min = 40.dp))
|
||||
ExposedDropdownMenu(protoExpanded, onDismissRequest = { protoExpanded = false }) {
|
||||
PROTOCOLS.forEach { p ->
|
||||
DropdownMenuItem(text = { Text(p.uppercase()) }, onClick = {
|
||||
save(servers.toMutableList().also { it[idx] = srv.copy(protocol = p) }); protoExpanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
var portText by remember(srv.id) { mutableStateOf(srv.port.toString()) }
|
||||
OutlinedTextField(portText, { v -> portText = v; val p = v.toIntOrNull(); if (p != null && p in 1..65535)
|
||||
save(servers.toMutableList().also { it[idx] = srv.copy(port = p) }) },
|
||||
singleLine = true, modifier = Modifier.width(80.dp).heightIn(min = 40.dp),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(2.dp))
|
||||
}
|
||||
TextButton(onClick = {
|
||||
val iface = availableIfaces.firstOrNull() ?: ("lo" to "127.0.0.1")
|
||||
val rndPort = (2000..9999).random()
|
||||
save(servers + ServerEntry(UUID.randomUUID().toString(), iface.first, iface.second, "tcp", rndPort))
|
||||
}, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.Add, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(4.dp)); Text("Add interface") }
|
||||
}
|
||||
TextButton(onClick = {
|
||||
val iface = availableIfaces.firstOrNull() ?: ("lo" to "127.0.0.1")
|
||||
val rndPort = (2000..9999).random()
|
||||
save(servers + ServerEntry(UUID.randomUUID().toString(), iface.first, iface.second, "tcp", rndPort))
|
||||
}, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.Add, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(4.dp)); Text("Add interface") }
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
|
||||
@@ -65,6 +65,9 @@ class ChatViewModel : ViewModel() {
|
||||
private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap())
|
||||
val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses
|
||||
|
||||
private val _localSockets = MutableStateFlow<List<LocalSocketState>>(emptyList())
|
||||
val localSockets: StateFlow<List<LocalSocketState>> = _localSockets
|
||||
|
||||
val audioRecorder = AudioRecorderManager()
|
||||
|
||||
private val _isRecording = MutableStateFlow(false)
|
||||
@@ -143,6 +146,7 @@ class ChatViewModel : ViewModel() {
|
||||
dbReady = true
|
||||
LogManager.addLog("INFO", "VM", "DB ready")
|
||||
refreshChannels()
|
||||
refreshLocalSockets()
|
||||
}
|
||||
11 -> { /* KEYS_GENERATED: [pub_hex:64] */
|
||||
if (data != null && data.size >= 64) {
|
||||
@@ -209,6 +213,7 @@ class ChatViewModel : ViewModel() {
|
||||
val detail = _selectedMember.value
|
||||
if (detail != null && detail.nodeId == nodeId) refreshMemberLinks(nodeId)
|
||||
}
|
||||
19 -> { if (repo != null) refreshLocalSockets() } /* LOCAL_SOCKETS */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +306,13 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshLocalSockets() {
|
||||
val r = repo ?: return
|
||||
viewModelScope.launch {
|
||||
_localSockets.value = withContext(Dispatchers.IO) { r.getLocalSockets() }
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(text: String) {
|
||||
val r = repo ?: return
|
||||
val ch = _currentChannel.value ?: return
|
||||
@@ -490,6 +502,7 @@ class ChatViewModel : ViewModel() {
|
||||
_messages.value = emptyList()
|
||||
_currentChannel.value = null
|
||||
_generatingKeys.value = false
|
||||
_localSockets.value = emptyList()
|
||||
}
|
||||
|
||||
class Factory : ViewModelProvider.Factory {
|
||||
|
||||
@@ -1018,6 +1018,100 @@ char* utun_bridge_get_member_links_json(uint64_t node_id) {
|
||||
return u_strdup(buf);
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────
|
||||
* Local sockets state (for auto-mode UI display)
|
||||
* ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
#include "../../../lib/socket_compat.h"
|
||||
|
||||
char* utun_bridge_get_local_sockets_json(void) {
|
||||
if (!chat_core_is_initialized() || !g_cc.inst) return u_strdup("[]");
|
||||
struct UTUN_INSTANCE* inst = g_cc.inst;
|
||||
|
||||
/* count active TCP connections */
|
||||
int active_conns = 0, active_links = 0;
|
||||
if (inst->connections) {
|
||||
for (struct ll_entry* entry = inst->connections->head; entry; entry = entry->next) {
|
||||
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
|
||||
if (ce && ce->conn && ce->conn->transport_link && ce->conn->initialized) {
|
||||
active_conns++;
|
||||
active_links += ce->conn->links_up;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inst->tcp_connections) {
|
||||
for (struct ll_entry* entry = inst->tcp_connections->head; entry; entry = entry->next) {
|
||||
struct tcp_conn_entry* te = (struct tcp_conn_entry*)entry->data;
|
||||
if (te && te->etcp_conn && te->etcp_conn->initialized) {
|
||||
active_conns++;
|
||||
active_links += te->etcp_conn->links_up;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t cap = 4096, pos = 1;
|
||||
char* json = u_malloc(cap);
|
||||
if (!json) return u_strdup("[]");
|
||||
json[0] = '[';
|
||||
int first = 1;
|
||||
|
||||
/* TCP sockets */
|
||||
{ struct TCP_SOCKET* ts = inst->tcp_sockets;
|
||||
while (ts) {
|
||||
char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0;
|
||||
const struct sockaddr_storage* addr = ts->interface_addr.ss_family ? &ts->interface_addr : &ts->local_addr;
|
||||
if (addr->ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
|
||||
port = ntohs(sin->sin_port);
|
||||
inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str));
|
||||
} else if (addr->ss_family == AF_INET6) {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
|
||||
port = ntohs(sin6->sin6_port);
|
||||
inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str));
|
||||
}
|
||||
int state = active_conns > 0 ? 2 : 0;
|
||||
const char* sep = first ? "" : ","; first = 0;
|
||||
size_t needed = snprintf(NULL, 0, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"TCP\",\"st\":%d,\"lk\":%d}",
|
||||
sep, ts->sock_id, ts->name, ip_str, port, state, active_links);
|
||||
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
|
||||
if (pos + needed + 2 <= cap)
|
||||
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"TCP\",\"st\":%d,\"lk\":%d}",
|
||||
sep, ts->sock_id, ts->name, ip_str, port, state, active_links);
|
||||
ts = ts->next;
|
||||
}
|
||||
}
|
||||
|
||||
/* UDP sockets */
|
||||
{ struct ETCP_SOCKET* es = inst->etcp_sockets;
|
||||
while (es) {
|
||||
char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0;
|
||||
const struct sockaddr_storage* addr = es->interface_addr.ss_family ? &es->interface_addr : &es->local_addr;
|
||||
if (addr->ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
|
||||
port = ntohs(sin->sin_port);
|
||||
inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str));
|
||||
} else if (addr->ss_family == AF_INET6) {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
|
||||
port = ntohs(sin6->sin6_port);
|
||||
inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str));
|
||||
}
|
||||
int lk = es->links_queue ? queue_entry_count(es->links_queue) : 0;
|
||||
int state = lk > 0 ? 2 : 0;
|
||||
const char* sep = first ? "" : ","; first = 0;
|
||||
size_t needed = snprintf(NULL, 0, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lk\":%d}",
|
||||
sep, es->sock_id, es->name, ip_str, port, state, lk);
|
||||
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
|
||||
if (pos + needed + 2 <= cap)
|
||||
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lk\":%d}",
|
||||
sep, es->sock_id, es->name, ip_str, port, state, lk);
|
||||
es = es->next;
|
||||
}
|
||||
}
|
||||
|
||||
json[pos++] = ']'; json[pos] = '\0';
|
||||
return json;
|
||||
}
|
||||
|
||||
void utun_bridge_restart(const char* config_text) {
|
||||
bridge_log(BLEV_INFO, "bridge restart: posting to instance");
|
||||
#ifdef __ANDROID__
|
||||
@@ -1804,4 +1898,15 @@ JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMemberLinks
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── Local sockets JSON ── */
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetLocalSockets(
|
||||
JNIEnv* env, jobject thiz) {
|
||||
(void)thiz;
|
||||
char* json = utun_bridge_get_local_sockets_json();
|
||||
jstring result = (*env)->NewStringUTF(env, json ? json : "[]");
|
||||
u_free(json);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif /* __ANDROID__ */
|
||||
@@ -125,6 +125,10 @@ void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id,
|
||||
|
||||
char* utun_bridge_get_member_links_json(uint64_t node_id);
|
||||
|
||||
/* ── Local sockets state (for auto-mode display) ── */
|
||||
|
||||
char* utun_bridge_get_local_sockets_json(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "transport_layer/etcp_connections.h"
|
||||
#include "transport_layer/etcp.h"
|
||||
#include "../jni_bridge/android_udp_log.h"
|
||||
#include "../jni_bridge/android_jni_bridge.h"
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -169,6 +170,14 @@ static void nodeinfo_event_cb(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE*
|
||||
g_event_handler(CHAT_EVT_NODEINFO_UPDATED, data, sizeof(data));
|
||||
}
|
||||
|
||||
static void fire_local_sockets_event(void) {
|
||||
if (!g_event_handler) return;
|
||||
char* json = utun_bridge_get_local_sockets_json();
|
||||
if (!json) return;
|
||||
g_event_handler(CHAT_EVT_LOCAL_SOCKETS, (const uint8_t*)json, (int)strlen(json));
|
||||
u_free(json);
|
||||
}
|
||||
|
||||
static void on_link_status_changed(struct ETCP_CONN* conn, struct ETCP_LINK* link,
|
||||
int old_state, int old_status, void* arg) {
|
||||
(void)old_state; (void)old_status; (void)arg;
|
||||
@@ -181,6 +190,7 @@ static void on_link_status_changed(struct ETCP_CONN* conn, struct ETCP_LINK* lin
|
||||
data[11] = conn->links_up;
|
||||
data[12] = conn->initialized ? 1 : 0;
|
||||
g_event_handler(CHAT_EVT_LINK_STATUS_CHANGED, data, sizeof(data));
|
||||
fire_local_sockets_event();
|
||||
}
|
||||
|
||||
/* ── Crash handler ── */
|
||||
@@ -283,6 +293,7 @@ static void* instance_thread(void* arg) {
|
||||
|
||||
/* sockets are now created — sync my addresses to DB */
|
||||
chat_core_sync_my_addresses();
|
||||
fire_local_sockets_event();
|
||||
|
||||
/* Bind chat sync via etcp_router */
|
||||
etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL);
|
||||
@@ -358,6 +369,7 @@ static void* instance_thread(void* arg) {
|
||||
|
||||
if (utun_instance_init(g_inst) != 0) { IL_LOGE("poll exit: utun_instance_init failed"); break; }
|
||||
chat_core_sync_my_addresses();
|
||||
fire_local_sockets_event();
|
||||
if (g_inst->config->global.name[0]) chat_core_update_my_name(g_inst->config->global.name);
|
||||
etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL);
|
||||
utun_add_nodeinfo_cbk(g_inst, nodeinfo_event_cb, NULL);
|
||||
|
||||
@@ -34,7 +34,6 @@ function(utun_setup_sources LIB_DIR SRC_DIR CONFIG_DIR TOOLS_DIR)
|
||||
# ── Our added files ──
|
||||
set(_cfg_src
|
||||
"${CONFIG_DIR}/utun_config_api.c"
|
||||
"${CONFIG_DIR}/invite_link_c.c"
|
||||
"${CONFIG_DIR}/instance_lite.c"
|
||||
"${CONFIG_DIR}/voice_recorder.c"
|
||||
"${CONFIG_DIR}/attachment_sender.c"
|
||||
|
||||
Reference in new issue
Block a user