diff --git a/.gitignore b/.gitignore index faca4151..1d724f9c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,8 @@ tools/chatgui/ffmpeg/ffmpeg_build/ *.a *.so *.so.* -utun +/utun +/src/utun test_serialize linux-x64-deb diff --git a/src/chat/chat_setting.c b/src/chat/chat_setting.c index 4f37f166..d542364d 100644 --- a/src/chat/chat_setting.c +++ b/src/chat/chat_setting.c @@ -47,6 +47,9 @@ static const struct chat_setting_def g_setting_defs[] = { {"compressor_max_gain_db", CHAT_SETTING_INT, 25, 1, 100}, {"compressor_rise_rate", CHAT_SETTING_INT, 10, 1, 100}, {"media_download_max_peers", CHAT_SETTING_INT, 3, 1, 16}, + {"standby_active_sec", CHAT_SETTING_INT, 2, 1, 3600}, + {"standby_sleep_sec", CHAT_SETTING_INT, 60, 1, 86400}, + {"standby_min_sleep_sec", CHAT_SETTING_INT, 15, 1, 86400}, {"join_policy", CHAT_SETTING_INT, 1, 0, 2}, /* 0=autojoin, 1=ask, 2=deny */ {"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1}, {"whisper_model_path", CHAT_SETTING_STRING, 0, 0, 0}, diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index 5dc6be87..72986340 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/src/routing_layer/conn_mgr_core.c @@ -24,6 +24,9 @@ #include "../transport_layer/etcp_connect.h" #include "route_ping.h" #include "../chat/chat_core.h" +#ifdef UTUN_HAVE_STANDBY +#include "standby.h" +#endif /* ═══════ forward-декларации ═══════ */ @@ -326,6 +329,10 @@ void conn_mgr_destroy(struct CONN_MGR* mgr) { DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 1 bg_ping done"); if (mgr->candidate_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->candidate_ping_timer); mgr->candidate_ping_timer = NULL; } DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 2 cand_ping done"); +#ifdef UTUN_HAVE_STANDBY + if (mgr->bg_ping_wait) { standby_wait_cancel(mgr->bg_ping_wait); mgr->bg_ping_wait = NULL; } + if (mgr->candidate_ping_wait) { standby_wait_cancel(mgr->candidate_ping_wait); mgr->candidate_ping_wait = NULL; } +#endif { size_t ec = queue_entry_count(mgr->entries); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 3 entries count=%zu", ec); struct ll_entry* e = mgr->entries->head; diff --git a/src/routing_layer/conn_mgr_monitor.c b/src/routing_layer/conn_mgr_monitor.c index 6818a264..99e77fac 100644 --- a/src/routing_layer/conn_mgr_monitor.c +++ b/src/routing_layer/conn_mgr_monitor.c @@ -13,6 +13,9 @@ #include "topo_group.h" #include "route_connectivity.h" #include "route_ping.h" +#ifdef UTUN_HAVE_STANDBY +#include "standby.h" +#endif /* ═══════ probe time утилиты ═══════ */ @@ -104,6 +107,13 @@ static void cm_bg_ping_to_node(struct CONN_MGR* mgr, struct TOPO_NODE* ni) { * и INDIRECT-фаза не может выбрать посредника. */ void cm_bg_ping_timer_cb(void* arg) { struct CONN_MGR* mgr = (struct CONN_MGR*)arg; +#ifdef UTUN_HAVE_STANDBY + if (standby_get_sleep_tb() > 0) { + mgr->bg_ping_wait = standby_wait(mgr, cm_bg_ping_timer_cb); + return; + } + mgr->bg_ping_wait = NULL; +#endif struct TOPO_GROUP* group = mgr->group; if (!group || !group->nodes) goto rearm; size_t total = queue_entry_count(group->nodes); @@ -150,6 +160,13 @@ static int cm_has_active_conn(struct TOPO_GROUP* g, uint64_t nid) { * Вызывается раз в ~2с, самоперезапускается. */ void cm_candidate_ping_timer_cb(void* arg) { struct CONN_MGR* mgr = (struct CONN_MGR*)arg; uint64_t now = get_time_tb(); +#ifdef UTUN_HAVE_STANDBY + if (standby_get_sleep_tb() > 0) { + mgr->candidate_ping_wait = standby_wait(mgr, cm_candidate_ping_timer_cb); + return; + } + mgr->candidate_ping_wait = NULL; +#endif for (uint8_t i = 0; i < mgr->best_candidate_count; i++) { uint64_t nid = mgr->best_candidates[i].node_id; struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(mgr->group, nid); diff --git a/src/routing_layer/conn_mgr_priv.h b/src/routing_layer/conn_mgr_priv.h index 4e5ea4e1..03cb5717 100644 --- a/src/routing_layer/conn_mgr_priv.h +++ b/src/routing_layer/conn_mgr_priv.h @@ -164,6 +164,7 @@ struct CONN_MGR { struct UTUN_INSTANCE* instance; struct TOPO_GROUP* group; struct ll_queue* entries; void *bg_ping_timer, *candidate_ping_timer; + void *bg_ping_wait, *candidate_ping_wait; /* standby_wait handle (Android) */ size_t bg_ping_cursor; uint64_t bg_ping_cycle_start_tb; struct CONN_MGR_CANDIDATE best_candidates[CONN_MGR_MAX_CANDIDATES]; uint8_t best_candidate_count; uint32_t next_request_id, direct_timeout_ms; uint8_t initialized; diff --git a/src/routing_layer/route_connectivity.c b/src/routing_layer/route_connectivity.c index ff1c6e80..4d22c9db 100644 --- a/src/routing_layer/route_connectivity.c +++ b/src/routing_layer/route_connectivity.c @@ -17,6 +17,9 @@ #include "topo_group.h" #include "route_connectivity.h" #include "topo_node_sqlite.h" +#ifdef UTUN_HAVE_STANDBY +#include "standby.h" +#endif #define CONN_MAX_SOCKET_CANDIDATES 8 @@ -293,11 +296,46 @@ static void conn_probe_finish(struct conn_probe_ctx* ctx, int ok) { // ---- public API ---- +#ifdef UTUN_HAVE_STANDBY +/* Контекст пробы, отложенной до следующего активного интервала (standby_wait). */ +struct route_probe_defer { + struct UTUN_INSTANCE* instance; + struct TOPO_GROUP* group; + struct TOPO_GROUP_NODE* nq; + void* wait_handle; /* handle standby_wait для отмены */ +}; + +static void route_connectivity_probe_deferred_cb(void* arg) { + struct route_probe_defer* d = (struct route_probe_defer*)arg; + struct TOPO_GROUP_NODE* nq = d->nq; + nq->connectivity.probe_deferred_wait = NULL; + nq->connectivity.probe_status = PROBE_STATUS_NONE; + route_connectivity_probe_node(d->instance, d->group, nq); + u_free(d); +} +#endif + void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq) { if (!instance || !nq) return; struct TOPO_NODE* ni = topo_node_registry_find(instance->topo_groups, nq->node_id); if (!ni) return; if (nq->node_id == instance->node_id) return; // не пингуем себя +#ifdef UTUN_HAVE_STANDBY + if (nq->connectivity.probe_status == PROBE_STATUS_DEFERRED) { + DEBUG_INFO(DEBUG_CATEGORY_BGP, "probe already deferred for node %016llx", (unsigned long long)nq->node_id); + return; + } + if (standby_get_sleep_tb() > 0) { + nq->connectivity.probe_status = PROBE_STATUS_DEFERRED; + struct route_probe_defer* d = u_calloc(1, sizeof(*d)); + if (!d) { nq->connectivity.probe_status = PROBE_STATUS_NONE; return; } + d->instance = instance; d->group = group; d->nq = nq; + void* h = standby_wait(d, route_connectivity_probe_deferred_cb); + if (h) { d->wait_handle = h; nq->connectivity.probe_deferred_wait = d; } + /* else: cb вызван синхронно (OOM), d уже освобождён внутри */ + return; + } +#endif if (nq->connectivity.probe_status == PROBE_STATUS_IN_PROGRESS) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "probe already in progress for node %016llx", (unsigned long long)nq->node_id); return; @@ -386,6 +424,15 @@ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_G void route_connectivity_cancel_node(struct UTUN_INSTANCE* instance, struct TOPO_GROUP_NODE* nq) { if (!instance || !nq) return; +#ifdef UTUN_HAVE_STANDBY + { struct route_probe_defer* d = (struct route_probe_defer*)nq->connectivity.probe_deferred_wait; + if (d) { + if (d->wait_handle) standby_wait_cancel(d->wait_handle); + u_free(d); + nq->connectivity.probe_deferred_wait = NULL; + } + } +#endif nq->connectivity.probe_status = PROBE_STATUS_NONE; nq->connectivity.pending_count = 0; struct conn_probe_ctx* ctx = (struct conn_probe_ctx*)nq->connectivity.probe_list; @@ -401,6 +448,15 @@ void route_connectivity_cancel_all(struct UTUN_INSTANCE* instance) { struct ll_entry* e = group->nodes->head; while (e) { struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e; +#ifdef UTUN_HAVE_STANDBY + { struct route_probe_defer* d = (struct route_probe_defer*)nq->connectivity.probe_deferred_wait; + if (d) { + if (d->wait_handle) standby_wait_cancel(d->wait_handle); + u_free(d); + nq->connectivity.probe_deferred_wait = NULL; + } + } +#endif nq->connectivity.probe_status = PROBE_STATUS_NONE; nq->connectivity.pending_count = 0; e = e->next; diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index aee079dd..43f9cac1 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -262,6 +262,7 @@ static void topo_group_destroy(struct TOPO_GROUP* group) { struct ll_entry* e; int nc = 0; while ((e = queue_data_get(group->nodes)) != NULL) { struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e; + route_connectivity_cancel_node(group->instance, nq); if (nq->paths) { queue_free(nq->paths); nq->paths = NULL; } topo_nodeq_free_group_fields(group->instance->topo_groups, nq); queue_entry_free(e); nc++; diff --git a/src/routing_layer/topo_group_connect.c b/src/routing_layer/topo_group_connect.c index 34e4789e..6c56cc71 100644 --- a/src/routing_layer/topo_group_connect.c +++ b/src/routing_layer/topo_group_connect.c @@ -12,7 +12,8 @@ * * После исчерпания Phase 3 → пауза → cycle_restart → Phase 1 (бесконечно). * - * Пауза: 1с когда client_activity == ACTIVE, 30с когда STANDBY (Android фон). + * Пауза: активный режим — 1с; фоновый (Android standby) — standby_wait + * (burst → сразу, sleep → до следующего burst). * При DOWN (active_conn_count → 0) — немедленный cycle_restart. * При повторном topo_group_connect_init — destroy + fresh init. * @@ -32,6 +33,9 @@ #include "../lib/ll_queue.h" #include "etcp.h" #include "../chat/chat_event.h" +#ifdef UTUN_HAVE_STANDBY +#include "standby.h" +#endif /* ─── внутренние константы ─── */ #define TGC_ID "topo_group_connect" @@ -42,13 +46,13 @@ #define TGC_PHASE_DONE 3 #define TGC_PHASE_PAUSE 4 #define TGC_MAX_HANDLES 128 -#define TGC_PAUSE_ACTIVE_TB 10000 /* 1s пауза когда client_activity == ACTIVE */ -#define TGC_PAUSE_IDLE_TB 300000 /* 30s пауза когда STANDBY (Android фон) */ +#define TGC_PAUSE_ACTIVE_TB 10000 /* 1s пауза в активном режиме */ struct TOPO_GROUP_CONNECT { struct TOPO_GROUP* group; void* phase_timer; - void* pause_timer; + void* pause_timer; /* uasync timeout handle (активный режим) */ + void* pause_wait; /* standby_wait handle (фоновый режим, Android) */ uint8_t phase; int pending; int connected_count; @@ -149,6 +153,9 @@ void topo_group_connect_destroy(struct TOPO_GROUP* group) { group->connect = NULL; if (gc->phase_timer) { uasync_cancel_timeout(group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; } if (gc->pause_timer) { uasync_cancel_timeout(group->instance->ua, gc->pause_timer); gc->pause_timer = NULL; } +#ifdef UTUN_HAVE_STANDBY + if (gc->pause_wait) { standby_wait_cancel(gc->pause_wait); gc->pause_wait = NULL; } +#endif for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } u_free(gc->candidate_ids); @@ -327,12 +334,19 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g */ static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), const char* label) { gc->phase = TGC_PHASE_PAUSE; - int active = (gc->group->instance->client_activity == CLIENT_ACTIVITY_ACTIVE); - int delay_tb = active ? TGC_PAUSE_ACTIVE_TB : TGC_PAUSE_IDLE_TB; - gc->pause_timer = uasync_set_timeout(gc->group->instance->ua, delay_tb, gc, cb, label); tgc_notify_connecting(gc, NULL, 0); - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: pause %dms (activity=%s) for ch=%s", - TGC_ID, delay_tb / 10, active ? "ACTIVE" : "STANDBY", gc->group->channel_id); +#ifdef UTUN_HAVE_STANDBY + if (standby_is_enabled()) { + /* фоновый режим: burst → standby_wait будит сразу, sleep → до следующего burst */ + gc->pause_wait = standby_wait(gc, cb); + gc->pause_timer = NULL; + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: pause (standby) for ch=%s", TGC_ID, gc->group->channel_id); + return; + } +#endif + gc->pause_timer = uasync_set_timeout(gc->group->instance->ua, TGC_PAUSE_ACTIVE_TB, gc, cb, label); + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: pause %dms (active) for ch=%s", + TGC_ID, TGC_PAUSE_ACTIVE_TB / 10, gc->group->channel_id); } /* ═══════════════════════════════════════════════════════════════════════ @@ -393,6 +407,7 @@ static void tgc_phase1_timeout(void* arg) { * Когда все перебраны — переходит к Phase 3. */ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) { + gc->pause_timer = NULL; gc->pause_wait = NULL; if (gc->phase == TGC_PHASE_PAUSE) gc->phase = TGC_PHASE_TWO; if (gc->phase != TGC_PHASE_TWO) return; while (1) { @@ -439,6 +454,7 @@ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) { * пауза и полный перезапуск цикла с Phase 1 (бесконечно, пока не подключимся). */ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) { + gc->pause_timer = NULL; gc->pause_wait = NULL; if (gc->phase == TGC_PHASE_PAUSE) gc->phase = TGC_PHASE_THREE; if (gc->phase != TGC_PHASE_THREE) return; if (gc->candidate_count == 0) { @@ -472,7 +488,7 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) { * и после исчерпания Phase 3. */ static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) { - gc->pause_timer = NULL; + gc->pause_timer = NULL; gc->pause_wait = NULL; if (gc->phase_timer) { uasync_cancel_timeout(gc->group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; } for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } diff --git a/src/routing_layer/topo_group_connect.h b/src/routing_layer/topo_group_connect.h index 66c784cc..55d40a5c 100644 --- a/src/routing_layer/topo_group_connect.h +++ b/src/routing_layer/topo_group_connect.h @@ -11,7 +11,8 @@ * После каждого TIMEOUT — пауза перед следующей попыткой. * После исчерпания Phase 3 → пауза → cycle_restart → Phase 1 (бесконечно). * - * Пауза: 1с когда client_activity == ACTIVE, 30с когда STANDBY (Android фон). + * Пауза: активный режим — 1с; фоновый (Android standby) — standby_wait + * (burst → сразу, sleep → до следующего burst). * При DOWN (active_conn_count → 0) — немедленный cycle_restart из любой фазы. * При повторном вызове topo_group_connect_init — destroy + fresh init. * diff --git a/src/routing_layer/topo_node.h b/src/routing_layer/topo_node.h index da5cffd8..91e4a4f6 100644 --- a/src/routing_layer/topo_node.h +++ b/src/routing_layer/topo_node.h @@ -52,6 +52,7 @@ struct UTUN_INSTANCE; #define PROBE_STATUS_NONE 0 #define PROBE_STATUS_IN_PROGRESS 1 #define PROBE_STATUS_DONE 2 +#define PROBE_STATUS_DEFERRED 3 /* отложена до следующего burst (standby_wait) */ #define PROBE_RESULT_UNKNOWN 0 #define PROBE_RESULT_REACHABLE 1 @@ -99,6 +100,7 @@ struct TOPO_CONNECTIVITY { uint64_t ping_req_time; uint64_t last_ping_time; void* probe_list; + void* probe_deferred_wait; /* контекст отложенной пробы (standby_wait, Android) */ }; #define PING_TTL_TB (86400ULL * 10000ULL) diff --git a/src/utun_instance.h b/src/utun_instance.h index c0df27c1..7446aaca 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -199,6 +199,7 @@ struct UTUN_INSTANCE { uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE void* client_activity_timer; // uasync timer handle for inactivity timeout struct utun_activity_cbk_entry* activity_cbks; // подписки на смену client_activity + uint8_t standby_enabled; // 1 = standby duty-cycle активен (chatgui-android) // TCP proxy server (exit node) struct tcp_proxy_server tcp_proxy_server; diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt index 513de8e3..0b807aa6 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt @@ -7,6 +7,7 @@ import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* @@ -133,7 +134,7 @@ class MainActivity : ComponentActivity() { ) } } - SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter)) + SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter).navigationBarsPadding()) } /* Show join dialog after QR scan returns */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index 086006f1..d2f5b40c 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -42,6 +42,9 @@ class ConfigProvider(private val context: Context) { private val chatCompressorMaxGainDb = intPreferencesKey("chat_compressor_max_gain_db") private val chatCompressorRiseRate = intPreferencesKey("chat_compressor_rise_rate") private val chatMediaDownloadMaxPeers = intPreferencesKey("chat_media_download_max_peers") + private val chatStandbyActiveSec = intPreferencesKey("chat_standby_active_sec") + private val chatStandbySleepSec = intPreferencesKey("chat_standby_sleep_sec") + private val chatStandbyMinSleepSec = intPreferencesKey("chat_standby_min_sleep_sec") private val firstLaunchDone = booleanPreferencesKey("first_launch_done") private val connectionModeKey = stringPreferencesKey("connection_mode") @@ -117,6 +120,9 @@ class ConfigProvider(private val context: Context) { key == "chat.compressor_max_gain_db" -> context.dataStore.data.first()[chatCompressorMaxGainDb] ?: 25 key == "chat.compressor_rise_rate" -> context.dataStore.data.first()[chatCompressorRiseRate] ?: 10 key == "chat.media_download_max_peers" -> context.dataStore.data.first()[chatMediaDownloadMaxPeers] ?: 3 + key == "chat.standby_active_sec" -> context.dataStore.data.first()[chatStandbyActiveSec] ?: 2 + key == "chat.standby_sleep_sec" -> context.dataStore.data.first()[chatStandbySleepSec] ?: 60 + key == "chat.standby_min_sleep_sec" -> context.dataStore.data.first()[chatStandbyMinSleepSec] ?: 15 else -> 0 } } @@ -144,6 +150,9 @@ class ConfigProvider(private val context: Context) { "chat.compressor_max_gain_db" -> prefs[chatCompressorMaxGainDb] = value.toIntOrNull() ?: 25 "chat.compressor_rise_rate" -> prefs[chatCompressorRiseRate] = value.toIntOrNull() ?: 10 "chat.media_download_max_peers" -> prefs[chatMediaDownloadMaxPeers] = value.toIntOrNull() ?: 3 + "chat.standby_active_sec" -> prefs[chatStandbyActiveSec] = value.toIntOrNull() ?: 2 + "chat.standby_sleep_sec" -> prefs[chatStandbySleepSec] = value.toIntOrNull() ?: 60 + "chat.standby_min_sleep_sec" -> prefs[chatStandbyMinSleepSec] = value.toIntOrNull() ?: 15 } } } @@ -336,6 +345,9 @@ class ConfigProvider(private val context: Context) { appendLine("compressor_max_gain_db=$chatCompMaxGain") appendLine("compressor_rise_rate=$chatCompRiseRate") appendLine("media_download_max_peers=$chatDlMaxPeers") + appendLine("standby_active_sec=${getInt("chat.standby_active_sec")}") + appendLine("standby_sleep_sec=${getInt("chat.standby_sleep_sec")}") + appendLine("standby_min_sleep_sec=${getInt("chat.standby_min_sleep_sec")}") appendLine() appendLine("[debug]") appendLine("console_level=info") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 298700ce..2be57969 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -56,6 +56,9 @@ object NativeLib { /* ── App foreground/background (Android lifecycle) ── */ fun setActive(active: Boolean) { nativeSetActive(active) } + /* ── External network activity (ConnectivityManager) ── */ + fun notifyNetworkActive() { nativeNotifyNetworkActive() } + fun sendMessage(channelId: String, text: String) { nativeSendMessage(channelId, text) } fun createChannel(name: String, channelId: String) { nativeCreateChannel(name, channelId) } fun connectNode(address: String, port: Int, pubkeyHex: String) { nativeConnectNode(address, port, pubkeyHex) } @@ -144,6 +147,7 @@ object NativeLib { private external fun nativeIsResponsive(): Boolean private external fun nativeIsRunning(): Boolean private external fun nativeSetActive(active: Boolean) + private external fun nativeNotifyNetworkActive() /* ── Channel members JNI ── */ private external fun nativeGetChannelMembers(channelId: String): String? diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkActivityMonitor.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkActivityMonitor.kt new file mode 100644 index 00000000..cada37b7 --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkActivityMonitor.kt @@ -0,0 +1,48 @@ +package com.utun.chat.data + +import android.content.Context +import android.net.ConnectivityManager +import android.os.Build +import com.utun.chat.ChatApplication + +/** + * Слушатель внешней сетевой активности (ConnectivityManager.OnNetworkActiveListener). + * + * Пробуждает standby-ядро досрочно: когда в фазе SLEEP прошёл min_sleep, + * приход сетевой активности запускает следующий активный цикл раньше. + * + * В колбэке не делаем тяжёлую работу — только дёшевый JNI-вызов + * NativeLib.notifyNetworkActive(), который постит событие на uasync-поток. + */ +object NetworkActivityMonitor { + private var started = false + + private val listener = ConnectivityManager.OnNetworkActiveListener { + NativeLib.notifyNetworkActive() + } + + fun start() { + if (started) return + /* addDefaultNetworkActiveListener доступен с API 28 (P) */ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return + val ctx = ChatApplication.instance.applicationContext + val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return + cm.addDefaultNetworkActiveListener(listener) + started = true + LogManager.addLog("INFO", "NET", "network activity listener started") + /* Если сеть уже активна в момент запуска — сразу уведомляем */ + if (cm.isDefaultNetworkActive) { + NativeLib.notifyNetworkActive() + } + } + + fun stop() { + if (!started) return + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return + val ctx = ChatApplication.instance.applicationContext + val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return + cm.removeDefaultNetworkActiveListener(listener) + started = false + LogManager.addLog("INFO", "NET", "network activity listener stopped") + } +} diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt index b58800e4..b4d262c1 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt @@ -12,6 +12,7 @@ import com.utun.chat.ChatApplication import com.utun.chat.MainActivity import com.utun.chat.data.LogManager import com.utun.chat.data.NativeLib +import com.utun.chat.data.NetworkActivityMonitor class HeadlessService : Service() { override fun onCreate() { @@ -45,6 +46,7 @@ class HeadlessService : Service() { val ok = NativeLib.start(configText) if (ok) { LogManager.addLog("INFO", "SRV", "uTun core started") + NetworkActivityMonitor.start() } else { LogManager.addLog("ERROR", "SRV", "uTun start failed") } @@ -52,6 +54,7 @@ class HeadlessService : Service() { override fun onDestroy() { LogManager.addLog("INFO", "SRV", "uTun service stopping") + NetworkActivityMonitor.stop() NativeLib.stop() super.onDestroy() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index ce760e7f..ef58162b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt @@ -99,6 +99,9 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool var logFile by remember { mutableStateOf("") } var logUdpIp by remember { mutableStateOf("") } var logUdpPort by remember { mutableStateOf("") } + var standbyActiveSec by remember { mutableStateOf("2") } + var standbySleepSec by remember { mutableStateOf("60") } + var standbyMinSleepSec by remember { mutableStateOf("15") } val pubKey by vm.pubKey.collectAsState() val generatingKeys by vm.generatingKeys.collectAsState() @@ -124,6 +127,9 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool logFile = provider.getString("debug.log_file") ?: "utun.log" logUdpIp = provider.getString("log_udp.ip") ?: "192.168.40.250" logUdpPort = provider.getInt("log_udp.port").toString().let { if (it == "0") "9999" else it } + standbyActiveSec = provider.getInt("chat.standby_active_sec").toString() + standbySleepSec = provider.getInt("chat.standby_sleep_sec").toString() + standbyMinSleepSec = provider.getInt("chat.standby_min_sleep_sec").toString() val port = logUdpPort.toIntOrNull() ?: 0 if (logUdpIp.isNotEmpty() && port > 0) NativeLib.setUdpLogTarget(logUdpIp, port) val stored = provider.getString("debug.categories") ?: "" @@ -301,6 +307,28 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool HorizontalDivider(Modifier.padding(vertical = 8.dp)) + /* ── Standby (background duty-cycle) ── */ + Text("Standby", style = MaterialTheme.typography.titleMedium) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField(standbyActiveSec, + { v -> standbyActiveSec = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..3600) { + scope.launch { provider.setValue("chat.standby_active_sec", v) }; NativeLib.setChatSetting("standby_active_sec", v) } }, + label = { Text("Active (sec)") }, modifier = Modifier.weight(1f).heightIn(min = 40.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true) + OutlinedTextField(standbySleepSec, + { v -> standbySleepSec = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..86400) { + scope.launch { provider.setValue("chat.standby_sleep_sec", v) }; NativeLib.setChatSetting("standby_sleep_sec", v) } }, + label = { Text("Sleep (sec)") }, modifier = Modifier.weight(1f).heightIn(min = 40.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true) + } + OutlinedTextField(standbyMinSleepSec, + { v -> standbyMinSleepSec = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..86400) { + scope.launch { provider.setValue("chat.standby_min_sleep_sec", v) }; NativeLib.setChatSetting("standby_min_sleep_sec", v) } }, + label = { Text("Min sleep (sec)") }, modifier = Modifier.heightIn(min = 40.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true) + + HorizontalDivider(Modifier.padding(vertical = 8.dp)) + /* Debug */ Text("Debug", style = MaterialTheme.typography.titleMedium) DebugLevelDropdown(label = "Global Level", current = debugLevel.uppercase(), onSelect = setDebug) diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 24e2ce3d..04b7c2a4 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -13,6 +13,7 @@ #include "../libutun_lite/instance_lite.h" #include "../libutun_lite/voice_recorder.h" #include "../libutun_lite/attachment_sender.h" +#include "../libutun_lite/standby.h" #include "../../../lib/debug_config.h" #include "../../../lib/u_async.h" #include "../../../lib/mem.h" @@ -384,7 +385,7 @@ char* utun_bridge_encode_invite_link(uint64_t channel_id, const char* password) memcpy(data.pubkey, g_cc.inst->my_keys.public_key, INVITE_PUBKEY_SIZE); } else { sqlite3_stmt* pkst = NULL; - snprintf(sql, sizeof(sql), "SELECT public_key FROM nodes WHERE node_id=?"); + snprintf(sql, sizeof(sql), "SELECT x25519_pubkey FROM nodes WHERE node_id=?"); if (sqlite3_prepare_v2(db, sql, -1, &pkst, NULL) == SQLITE_OK) { sqlite3_bind_int64(pkst, 1, (sqlite3_int64)target_nid); if (sqlite3_step(pkst) == SQLITE_ROW) { @@ -1182,6 +1183,7 @@ static void bridge_set_active_trampoline(void* arg) { struct UTUN_INSTANCE* inst = chat_core_get_inst(); if (!inst) return; utun_set_client_activity(inst, active); + standby_set_enabled(inst, !active); bridge_log(BLEV_DEBUG, "client_activity set to %s", active ? "ACTIVE" : "STANDBY"); } @@ -1191,6 +1193,18 @@ void utun_bridge_set_active(int active) { uasync_post(ua, bridge_set_active_trampoline, (void*)(intptr_t)active); } +static void bridge_notify_network_active_trampoline(void* arg) { + (void)arg; + bridge_log(BLEV_DEBUG, "network activity notified to standby"); + standby_notify_network_activity(); +} + +void utun_bridge_notify_network_active(void) { + struct UASYNC* ua = instance_lite_get_uasync(); + if (!ua) return; + uasync_post(ua, bridge_notify_network_active_trampoline, NULL); +} + void utun_bridge_collect_conn_list(void) { instance_lite_collect_conn_list(); } @@ -2012,6 +2026,12 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetActive( utun_bridge_set_active(active ? 1 : 0); } +JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeNotifyNetworkActive( + JNIEnv* env, jobject thiz) { + (void)env; (void)thiz; + utun_bridge_notify_network_active(); +} + /* ── Connection monitor ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnList( diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index cbde4f87..a6b63f13 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.h @@ -137,6 +137,10 @@ void utun_bridge_on_network_change(void); void utun_bridge_set_active(int active); +/* ── External network activity (ConnectivityManager) → standby early wake ── */ + +void utun_bridge_notify_network_active(void); + /* ── Connection monitor (post collect functions to uasync thread) ── */ void utun_bridge_collect_conn_list(void); diff --git a/tools/chatgui-android/libutun_lite/CMakeLists.txt b/tools/chatgui-android/libutun_lite/CMakeLists.txt index c0122e9c..f0ce81db 100644 --- a/tools/chatgui-android/libutun_lite/CMakeLists.txt +++ b/tools/chatgui-android/libutun_lite/CMakeLists.txt @@ -29,3 +29,5 @@ target_link_libraries(utun_lite PUBLIC OpenSSL::Crypto ${CMAKE_THREAD_LIBS_INIT} ) + +add_subdirectory(tests) diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index ea24e404..e80bffaa 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -26,6 +26,7 @@ #include "transport_layer/etcp.h" #include "../jni_bridge/android_udp_log.h" #include "../jni_bridge/android_jni_bridge.h" +#include "standby.h" #include #include #include @@ -233,6 +234,7 @@ static void* instance_thread(void* arg) { g_ua = uasync_create(); if (!g_ua) { IL_LOGE("uasync_create failed"); free_config(config); __atomic_store_n(&g_thread_running, 0, __ATOMIC_RELEASE); pthread_detach(pthread_self()); return NULL; } + standby_init(g_ua); if (config->global.log_udp_ip[0] && config->global.log_udp_port > 0) { udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port); @@ -245,6 +247,7 @@ static void* instance_thread(void* arg) { g_inst = utun_instance_create_from_config(g_ua, config); if (!g_inst) { IL_LOGE("utun_instance_create_from_config failed"); + standby_deinit(); uasync_destroy(g_ua, 0); g_ua = NULL; u_report_unfreed_blocks(); @@ -279,6 +282,7 @@ static void* instance_thread(void* arg) { IL_LOGE("utun_instance_init failed"); utun_instance_destroy(g_inst); g_inst = NULL; + standby_deinit(); uasync_destroy(g_ua, 0); g_ua = NULL; u_report_unfreed_blocks(); @@ -333,9 +337,11 @@ static void* instance_thread(void* arg) { g_inst = NULL; uasync_print_resources(g_ua, "AFTER_DESTROY"); u_report_unfreed_blocks(); + standby_deinit(); uasync_destroy(g_ua, 0); g_ua = uasync_create(); if (!g_ua) { IL_LOGE("poll exit: uasync_create failed"); break; } + standby_init(g_ua); chat_event_set_handler(chat_event_forward); @@ -388,6 +394,7 @@ static void* instance_thread(void* arg) { g_inst = NULL; } if (g_ua) { + standby_deinit(); uasync_destroy(g_ua, 0); g_ua = NULL; } diff --git a/tools/chatgui-android/libutun_lite/standby.c b/tools/chatgui-android/libutun_lite/standby.c new file mode 100644 index 00000000..6d26882b --- /dev/null +++ b/tools/chatgui-android/libutun_lite/standby.c @@ -0,0 +1,386 @@ +/* + * standby.c — режим standby с duty-cycle (активный/неактивный интервалы) + * + * Синглтон (одна instance на процесс — как instance_lite.c). + * Таймеры только через uasync. Интервалы читаются лениво из chat_setting, + * поэтому их изменение через GUI применяется на границе следующей фазы. + * + * Duty-cycle: + * standby включён → фаза ACTIVE на standby_active_sec, затем SLEEP на + * standby_sleep_sec, и так по кругу, пока standby не выключат. + * + * Досрочное пробуждение: + * в фазе SLEEP первые standby_min_sleep_sec секунд спим безусловно; + * после них standby_notify_network_activity() (внешняя сетевая активность) + * пробуждает досрочно — следующий цикл как обычно. + * + * Список ожидающих (standby_wait) — двусвязный список, элементы из memory_pool. + * При выходе из standby (или досрочном пробуждении) все wait-таймеры + * отменяются, колбэки вызываются сразу. + */ +#include "standby.h" +#include "chat_setting.h" +#include "utun_instance.h" +#include "debug_config.h" +#include "memory_pool.h" + +#define STANDBY_TB_PER_SEC 10000 /* timebase = 0.1ms → 1 сек = 10000 TB */ + +#define PHASE_ACTIVE 0 +#define PHASE_SLEEP 1 + +struct standby_wait_entry { + struct standby_wait_entry* prev; + struct standby_wait_entry* next; + void* timer_id; /* handle uasync_set_timeout */ + void (*cb)(void* arg); + void* arg; +}; + +static struct UASYNC* g_ua = NULL; +static int g_enabled = 0; +static int g_phase = PHASE_ACTIVE; +static uint64_t g_phase_start_tb = 0; /* начало текущей фазы */ +static uint64_t g_phase_end_tb = 0; /* дедлайн текущей фазы */ +static uint64_t g_min_sleep_end_tb = 0; /* до этой точки спим безусловно */ +static void* g_phase_timer = NULL; +static void (*g_switch_cb)(int enabled, void* arg) = NULL; +static void* g_switch_arg = NULL; + +static struct memory_pool* g_wait_pool = NULL; +static struct standby_wait_entry* g_wait_head = NULL; +static struct standby_wait_entry* g_wait_tail = NULL; + +/* счётчики для диагностики */ +static uint32_t g_cnt_wakes_timer = 0; +static uint32_t g_cnt_wakes_early = 0; +static uint32_t g_cnt_sleeps = 0; +static uint32_t g_cnt_net_events = 0; +static uint32_t g_cnt_net_ignored = 0; + +static int standby_active_sec(void) { + return chat_setting_get_int("standby_active_sec", 2); +} + +static int standby_sleep_sec(void) { + return chat_setting_get_int("standby_sleep_sec", 60); +} + +static int standby_min_sleep_sec(void) { + int m = chat_setting_get_int("standby_min_sleep_sec", 15); + if (m < 0) m = 0; + int s = standby_sleep_sec(); + if (m > s) m = s; + return m; +} + +/* override интервалов (мс) для тестов: 0 = использовать chat_setting */ +static int g_override = 0; +static int g_active_ms = 0; +static int g_sleep_ms = 0; +static int g_min_sleep_ms = 0; + +static int standby_active_tb(void) { + return g_override ? g_active_ms * 10 : standby_active_sec() * STANDBY_TB_PER_SEC; +} + +static int standby_sleep_tb(void) { + return g_override ? g_sleep_ms * 10 : standby_sleep_sec() * STANDBY_TB_PER_SEC; +} + +static int standby_min_sleep_tb(void) { + int m = g_override ? g_min_sleep_ms * 10 : standby_min_sleep_sec() * STANDBY_TB_PER_SEC; + if (m < 0) m = 0; + int s = standby_sleep_tb(); + if (m > s) m = s; + return m; +} + +/* ── фазовый таймер ── */ + +static void standby_phase_timer_cb(void* arg); + +static void standby_arm_phase(void) { + int dur_tb = (g_phase == PHASE_ACTIVE) ? standby_active_tb() : standby_sleep_tb(); + g_phase_start_tb = get_time_tb(); + g_phase_end_tb = g_phase_start_tb + (uint64_t)dur_tb; + g_min_sleep_end_tb = (g_phase == PHASE_SLEEP) + ? g_phase_start_tb + (uint64_t)standby_min_sleep_tb() + : 0; + g_phase_timer = uasync_set_timeout(g_ua, dur_tb, NULL, standby_phase_timer_cb, "standby_phase"); + if (!g_phase_timer) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: failed to arm phase timer"); + } +} + +static void standby_phase_timer_cb(void* arg) { + (void)arg; + g_phase_timer = NULL; + if (!g_enabled) return; + uint64_t elapsed = get_time_tb() - g_phase_start_tb; + g_phase = (g_phase == PHASE_ACTIVE) ? PHASE_SLEEP : PHASE_ACTIVE; + if (g_phase == PHASE_SLEEP) { + g_cnt_sleeps++; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, + "standby: SLEEP (active done in %d.%ds, sleep=%dms min_sleep=%dms)", + (int)(elapsed / STANDBY_TB_PER_SEC), (int)((elapsed % STANDBY_TB_PER_SEC) / 1000), + standby_sleep_tb() / 10, standby_min_sleep_tb() / 10); + } else { + g_cnt_wakes_timer++; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: WAKE (timer), slept %d.%ds", + (int)(elapsed / STANDBY_TB_PER_SEC), (int)((elapsed % STANDBY_TB_PER_SEC) / 1000)); + } + standby_arm_phase(); +} + +/* ── список ожидающих ── */ + +static struct standby_wait_entry* wait_entry_alloc(void) { + if (!g_wait_pool) { + g_wait_pool = memory_pool_init(sizeof(struct standby_wait_entry), "standby_pool"); + if (!g_wait_pool) return NULL; + } + return (struct standby_wait_entry*)memory_pool_alloc(g_wait_pool); +} + +static void wait_entry_free(struct standby_wait_entry* e) { + memory_pool_free(g_wait_pool, e); +} + +static void wait_list_remove(struct standby_wait_entry* e) { + if (e->prev) e->prev->next = e->next; else g_wait_head = e->next; + if (e->next) e->next->prev = e->prev; else g_wait_tail = e->prev; + e->prev = e->next = NULL; +} + +static void wait_list_push(struct standby_wait_entry* e) { + e->prev = g_wait_tail; + e->next = NULL; + if (g_wait_tail) g_wait_tail->next = e; else g_wait_head = e; + g_wait_tail = e; +} + +/* Отменяет все wait-таймеры и вызывает их колбэки немедленно. + * Сначала отцепляем весь список — новые регистрации в колбэках + * (standby_wait) попадут в свежий список и не будут задеты этим проходом. */ +static void standby_wake_all_waiters(void) { + struct standby_wait_entry* list = g_wait_head; + g_wait_head = g_wait_tail = NULL; + while (list) { + struct standby_wait_entry* e = list; + list = e->next; + e->prev = e->next = NULL; + if (e->timer_id) { + uasync_cancel_timeout(g_ua, e->timer_id); + e->timer_id = NULL; + } + void (*cb)(void*) = e->cb; + void* arg = e->arg; + wait_entry_free(e); + cb(arg); + } +} + +static void standby_wait_timer_cb(void* arg) { + struct standby_wait_entry* e = (struct standby_wait_entry*)arg; + e->timer_id = NULL; + wait_list_remove(e); + void (*cb)(void*) = e->cb; + void* user_arg = e->arg; + wait_entry_free(e); + cb(user_arg); +} + +/* ── публичный API ── */ + +void standby_init(struct UASYNC* ua) { + g_ua = ua; + g_enabled = 0; + g_phase = PHASE_ACTIVE; + g_phase_timer = NULL; + g_cnt_wakes_timer = g_cnt_wakes_early = 0; + g_cnt_sleeps = g_cnt_net_events = g_cnt_net_ignored = 0; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: init (active=%dms sleep=%dms min_sleep=%dms)", + standby_active_tb() / 10, standby_sleep_tb() / 10, standby_min_sleep_tb() / 10); +} + +void standby_deinit(void) { + if (!g_enabled && !g_phase_timer && !g_wait_head && !g_wait_pool) { + g_ua = NULL; + return; + } + if (g_phase_timer && g_ua) { + uasync_cancel_timeout(g_ua, g_phase_timer); + g_phase_timer = NULL; + } + while (g_wait_head) { + struct standby_wait_entry* e = g_wait_head; + if (e->timer_id && g_ua) uasync_cancel_timeout(g_ua, e->timer_id); + wait_list_remove(e); + wait_entry_free(e); + } + if (g_wait_pool) { + memory_pool_destroy(g_wait_pool); + g_wait_pool = NULL; + } + g_enabled = 0; + g_ua = NULL; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: deinit (wakes_timer=%u wakes_early=%u sleeps=%u net=%u ignored=%u)", + g_cnt_wakes_timer, g_cnt_wakes_early, g_cnt_sleeps, g_cnt_net_events, g_cnt_net_ignored); +} + +void standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled) { + if (!g_ua) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: set_enabled(%d) but not initialized", enabled); + return; + } + if (g_enabled == enabled) return; + + if (enabled) { + g_enabled = 1; + g_phase = PHASE_ACTIVE; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: ENTER (app background, active=%dms sleep=%dms min_sleep=%dms)", + standby_active_tb() / 10, standby_sleep_tb() / 10, standby_min_sleep_tb() / 10); + standby_arm_phase(); + } else { + g_enabled = 0; + if (g_phase_timer) { + uasync_cancel_timeout(g_ua, g_phase_timer); + g_phase_timer = NULL; + } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: EXIT (app foreground, wakes_timer=%u wakes_early=%u sleeps=%u net=%u ignored=%u)", + g_cnt_wakes_timer, g_cnt_wakes_early, g_cnt_sleeps, g_cnt_net_events, g_cnt_net_ignored); + standby_wake_all_waiters(); + } + + if (inst) inst->standby_enabled = (uint8_t)enabled; + if (g_switch_cb) g_switch_cb(enabled, g_switch_arg); +} + +int standby_is_enabled(void) { + return g_enabled; +} + +void standby_set_switch_callback(void (*cb)(int enabled, void* arg), void* arg) { + g_switch_cb = cb; + g_switch_arg = arg; +} + +int standby_get_sleep_tb(void) { + if (!g_enabled) return 0; + if (g_phase == PHASE_ACTIVE) return 0; + uint64_t now = get_time_tb(); + if (now >= g_phase_end_tb) return 0; + return (int)(g_phase_end_tb - now); +} + +int standby_get_active_remaining_tb(void) { + if (!g_enabled) return 0; + if (g_phase == PHASE_SLEEP) return 0; + uint64_t now = get_time_tb(); + if (now >= g_phase_end_tb) return 0; + return (int)(g_phase_end_tb - now); +} + +void* standby_wait(void* arg, void (*cb)(void* arg)) { + if (!cb) return NULL; + if (!g_ua) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: wait() but not initialized"); + return NULL; + } + if (!g_enabled) { + /* standby выключен → будим сразу (отложенно, чтобы не было рекурсии) */ + uasync_call_soon(g_ua, arg, (timeout_callback_t)cb); + return NULL; + } + + struct standby_wait_entry* e = wait_entry_alloc(); + if (!e) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: wait_entry_alloc failed"); + cb(arg); + return NULL; + } + e->cb = cb; + e->arg = arg; + + /* время до начала следующего активного интервала: + * SLEEP — ждём до конца фазы; ACTIVE — уже бодрствуем, будим сразу. */ + uint64_t now = get_time_tb(); + uint64_t tb; + if (g_phase == PHASE_SLEEP) { + tb = (now < g_phase_end_tb) ? (g_phase_end_tb - now) : 0; + } else { + tb = 0; + } + + e->timer_id = uasync_set_timeout(g_ua, (int)tb, e, standby_wait_timer_cb, "standby_wait"); + if (!e->timer_id) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: uasync_set_timeout failed"); + wait_entry_free(e); + cb(arg); + return NULL; + } + wait_list_push(e); + return e; +} + +void standby_wait_cancel(void* handle) { + if (!handle) return; + struct standby_wait_entry* e = (struct standby_wait_entry*)handle; + if (e->timer_id && g_ua) { + uasync_cancel_timeout(g_ua, e->timer_id); + e->timer_id = NULL; + } + wait_list_remove(e); + wait_entry_free(e); +} + +/* ── внешняя сетевая активность ── */ + +void standby_notify_network_activity(void) { + g_cnt_net_events++; + if (!g_enabled) { + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "standby: network activity ignored (standby off)"); + return; + } + if (g_phase != PHASE_SLEEP) { + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "standby: network activity ignored (phase=ACTIVE)"); + return; + } + + uint64_t now = get_time_tb(); + uint64_t slept = now - g_phase_start_tb; + if (now < g_min_sleep_end_tb) { + g_cnt_net_ignored++; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, + "standby: network activity caught, min_sleep not elapsed (%d.%ds left)", + (int)((g_min_sleep_end_tb - now) / STANDBY_TB_PER_SEC), + (int)(((g_min_sleep_end_tb - now) % STANDBY_TB_PER_SEC) / 1000)); + return; + } + + g_cnt_wakes_early++; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, + "standby: WAKE early (network activity), slept %d.%ds min_sleep=%dms", + (int)(slept / STANDBY_TB_PER_SEC), (int)((slept % STANDBY_TB_PER_SEC) / 1000), + standby_min_sleep_tb() / 10); + + if (g_phase_timer) { + uasync_cancel_timeout(g_ua, g_phase_timer); + g_phase_timer = NULL; + } + g_phase = PHASE_ACTIVE; + standby_arm_phase(); + standby_wake_all_waiters(); +} + +void standby_set_intervals_ms(int active_ms, int sleep_ms, int min_sleep_ms) { + if (active_ms <= 0 || sleep_ms <= 0) { + g_override = 0; + return; + } + g_override = 1; + g_active_ms = active_ms; + g_sleep_ms = sleep_ms; + g_min_sleep_ms = min_sleep_ms < 0 ? 0 : min_sleep_ms; +} diff --git a/tools/chatgui-android/libutun_lite/standby.h b/tools/chatgui-android/libutun_lite/standby.h new file mode 100644 index 00000000..9d6f1c33 --- /dev/null +++ b/tools/chatgui-android/libutun_lite/standby.h @@ -0,0 +1,61 @@ +/* + * standby.h — режим standby с duty-cycle (активный/неактивный интервалы) + * + * Для chatgui-android: когда приложение в фоне, вместо постоянной работы + * узел периодически просыпается: ACTIVE (X сек) → SLEEP (Y сек) → ACTIVE ... + * + * Потребители периодической работы используют standby_wait(): колбэк + * срабатывает на старте следующего активного интервала, а при выходе из + * standby (приложение вернулось в форграунд) — немедленно. + */ +#ifndef STANDBY_H +#define STANDBY_H + +#include "u_async.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct UTUN_INSTANCE; + +/* Привязать модуль к uasync. Вызывается после каждого uasync_create(). */ +void standby_init(struct UASYNC* ua); + +/* Отмена таймеров и освобождение пула (без вызова колбэков). */ +void standby_deinit(void); + +/* Включить/выключить standby-режим. Логирует вход/выход (GENERAL info) + * и вызывает switch-callback. Флаг дублируется в inst->standby_enabled. */ +void standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled); +int standby_is_enabled(void); + +/* Колбэк, вызываемый при каждом переключении standby-режима. */ +void standby_set_switch_callback(void (*cb)(int enabled, void* arg), void* arg); + +/* Сколько TB спать сейчас (0 = активный интервал или standby выключен). */ +int standby_get_sleep_tb(void); + +/* Сколько TB осталось активного режима (0 = спим или standby выключен). */ +int standby_get_active_remaining_tb(void); + +/* Ждёт до начала следующего активного интервала (как uasync timeout). + * При выходе из standby таймер отменяется и cb вызывается немедленно. + * Возвращает handle для standby_wait_cancel(). */ +void* standby_wait(void* arg, void (*cb)(void* arg)); +void standby_wait_cancel(void* handle); + +/* Уведомление о внешней сетевой активности (Android ConnectivityManager). + * В фазе SLEEP после min_sleep пробуждает досрочно; иначе игнорируется. + * Вызывается на uasync-потоке. */ +void standby_notify_network_activity(void); + +/* Переопределить интервалы (мс), минуя chat_setting. Для тестов. + * active_ms/sleep_ms <= 0 → вернуться к chat_setting. + * min_sleep_ms может быть 0 (реагировать на сетевую активность сразу). */ +void standby_set_intervals_ms(int active_ms, int sleep_ms, int min_sleep_ms); + +#ifdef __cplusplus +} +#endif +#endif /* STANDBY_H */ diff --git a/tools/chatgui-android/libutun_lite/tests/CMakeLists.txt b/tools/chatgui-android/libutun_lite/tests/CMakeLists.txt new file mode 100644 index 00000000..b6bbf5a8 --- /dev/null +++ b/tools/chatgui-android/libutun_lite/tests/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + +add_executable(test_standby + test_standby.c +) + +target_link_libraries(test_standby PRIVATE utun_lite pthread) diff --git a/tools/chatgui-android/libutun_lite/tests/test_standby.c b/tools/chatgui-android/libutun_lite/tests/test_standby.c new file mode 100644 index 00000000..b9f9d5e1 --- /dev/null +++ b/tools/chatgui-android/libutun_lite/tests/test_standby.c @@ -0,0 +1,216 @@ +/* + * test_standby.c — юнит-тест модуля standby (chatgui-android) + * + * Покрывает все сценарии: init, ENTER/EXIT, смену фаз ACTIVE↔SLEEP, + * standby_wait во всех фазах, cancel, сетевую активность (игнор/раннее + * пробуждение), clamp min_sleep и re-entry в wake_all_waiters. + * + * Интервалы задаются через standby_set_intervals_ms() (быстрые: 100-300мс). + * Время продвигается через uasync_poll(ua, -1) (до ближайшего таймера) + * и uasync_poll(ua, X) (частичное ожидание). + */ +#include "standby.h" +#include "debug_config.h" +#include +#include + +static int g_failures = 0; +static int g_checks = 0; + +#define CHECK(cond, ...) do { \ + g_checks++; \ + if (!(cond)) { \ + g_failures++; \ + printf(" FAIL: "); printf(__VA_ARGS__); printf("\n"); \ + } \ +} while (0) + +static struct UASYNC* g_ua = NULL; + +/* ── колбэки для standby_wait и switch ── */ + +static int g_wait_calls = 0; +static void wait_cb(void* arg) { + (void)arg; + g_wait_calls++; +} + +static int g_switch_calls = 0; +static int g_switch_last_enabled = -1; +static void switch_cb(int enabled, void* arg) { + (void)arg; + g_switch_calls++; + g_switch_last_enabled = enabled; +} + +/* re-entry waiter: в колбэке снова регистрирует себя (ровно один раз) */ +static int g_reentry_calls = 0; +static void reentry_cb(void* arg) { + (void)arg; + g_reentry_calls++; + if (g_reentry_calls == 1) standby_wait(NULL, reentry_cb); +} + +/* ── хелперы ── */ + +static void reset_standby(void) { + standby_deinit(); + standby_set_intervals_ms(200, 300, 100); /* active=200ms sleep=300ms min_sleep=100ms */ + standby_init(g_ua); + g_wait_calls = 0; + g_switch_calls = 0; + g_switch_last_enabled = -1; + g_reentry_calls = 0; + standby_set_switch_callback(switch_cb, NULL); +} + +/* продвинуть время до ближайшего таймера (фаза переключится) */ +static void run_to_next_phase(void) { + uasync_poll(g_ua, -1); +} + +int main(void) { + debug_config_init(); + debug_set_level(DEBUG_LEVEL_INFO); + + g_ua = uasync_create(); + if (!g_ua) { printf("uasync_create failed\n"); return 1; } + + printf("=== standby unit test ===\n"); + + /* ── 1. init ── */ + reset_standby(); + CHECK(standby_is_enabled() == 0, "init: should be disabled"); + CHECK(standby_get_sleep_tb() == 0, "init: sleep_tb should be 0"); + CHECK(standby_get_active_remaining_tb() == 0, "init: active_remaining should be 0"); + + /* ── 2. ENTER ── */ + standby_set_enabled(NULL, 1); + CHECK(standby_is_enabled() == 1, "ENTER: should be enabled"); + CHECK(standby_get_sleep_tb() == 0, "ENTER: active phase → sleep_tb 0"); + CHECK(standby_get_active_remaining_tb() > 0, "ENTER: active phase → active_remaining > 0"); + CHECK(g_switch_calls == 1 && g_switch_last_enabled == 1, "ENTER: switch_cb(1) once"); + + /* ── 3. ACTIVE → SLEEP ── */ + run_to_next_phase(); + CHECK(standby_get_sleep_tb() > 0, "ACTIVE→SLEEP: sleep_tb > 0"); + CHECK(standby_get_active_remaining_tb() == 0, "ACTIVE→SLEEP: active_remaining == 0"); + + /* ── 4. SLEEP → ACTIVE (по таймеру) ── */ + run_to_next_phase(); + CHECK(standby_get_sleep_tb() == 0, "SLEEP→ACTIVE: sleep_tb == 0"); + CHECK(standby_get_active_remaining_tb() > 0, "SLEEP→ACTIVE: active_remaining > 0"); + + /* ── 5. EXIT ── */ + standby_set_enabled(NULL, 0); + CHECK(standby_is_enabled() == 0, "EXIT: should be disabled"); + CHECK(standby_get_sleep_tb() == 0, "EXIT: sleep_tb == 0"); + CHECK(g_switch_calls == 2 && g_switch_last_enabled == 0, "EXIT: switch_cb(0)"); + + /* ── 6. standby_wait при выключенном → сразу ── */ + reset_standby(); + standby_wait(NULL, wait_cb); + uasync_poll(g_ua, 0); /* обработать call_soon */ + CHECK(g_wait_calls == 1, "wait(disabled): cb fired immediately"); + + /* ── 7. standby_wait в ACTIVE → будит сразу (burst, без паузы) ── */ + reset_standby(); + standby_set_enabled(NULL, 1); + standby_wait(NULL, wait_cb); /* ACTIVE → tb=0, immediate */ + uasync_poll(g_ua, 0); + CHECK(g_wait_calls == 1, "wait(ACTIVE): fired immediately (burst)"); + standby_set_enabled(NULL, 0); + + /* ── 8. standby_wait в SLEEP → ждёт конца сна ── */ + reset_standby(); + standby_set_enabled(NULL, 1); + run_to_next_phase(); /* → SLEEP */ + CHECK(standby_get_sleep_tb() > 0, "wait(SLEEP): in sleep"); + standby_wait(NULL, wait_cb); + uasync_poll(g_ua, -1); /* конец сна */ + uasync_poll(g_ua, 0); + CHECK(g_wait_calls == 1, "wait(SLEEP): fired at sleep end"); + standby_set_enabled(NULL, 0); + + /* ── 9. standby_wait_cancel ── */ + reset_standby(); + standby_set_enabled(NULL, 1); + run_to_next_phase(); /* → SLEEP */ + void* h = standby_wait(NULL, wait_cb); + standby_wait_cancel(h); + uasync_poll(g_ua, -1); /* конец сна */ + uasync_poll(g_ua, 0); + CHECK(g_wait_calls == 0, "wait_cancel: cb not called"); + standby_set_enabled(NULL, 0); + + /* ── 10. notify при standby off → игнор ── */ + reset_standby(); + standby_notify_network_activity(); + CHECK(standby_is_enabled() == 0, "notify(off): still disabled"); + + /* ── 11. notify в ACTIVE → игнор ── */ + standby_set_enabled(NULL, 1); + CHECK(standby_get_active_remaining_tb() > 0, "notify(ACTIVE): in active"); + standby_notify_network_activity(); + CHECK(standby_get_active_remaining_tb() > 0, "notify(ACTIVE): still active"); + standby_set_enabled(NULL, 0); + + /* ── 12. notify в SLEEP до min_sleep → игнор ── */ + reset_standby(); + standby_set_enabled(NULL, 1); + run_to_next_phase(); /* → SLEEP */ + CHECK(standby_get_sleep_tb() > 0, "notify(min_sleep): in sleep"); + standby_notify_network_activity(); /* сразу — ещё < min_sleep(100ms) */ + CHECK(standby_get_sleep_tb() > 0, "notify(min_sleep): woke → sleep_tb 0"); + CHECK(standby_get_active_remaining_tb() > 0, "notify(>min_sleep): woke → active_remaining > 0"); + standby_set_enabled(NULL, 0); + + /* ── 14. раннее пробуждение будит waiters ── */ + reset_standby(); + standby_set_enabled(NULL, 1); + run_to_next_phase(); /* → SLEEP */ + standby_wait(NULL, wait_cb); + uasync_poll(g_ua, 2000); /* +200ms → min_sleep прошёл */ + standby_notify_network_activity(); + uasync_poll(g_ua, 0); + CHECK(g_wait_calls == 1, "early wake: waiter fired immediately"); + standby_set_enabled(NULL, 0); + + /* ── 15. clamp min_sleep ≥ sleep → notify всегда игнор ── */ + reset_standby(); + standby_set_intervals_ms(200, 300, 500); /* min_sleep(500) > sleep(300) → clamp */ + standby_init(g_ua); + standby_set_switch_callback(switch_cb, NULL); + standby_set_enabled(NULL, 1); + run_to_next_phase(); /* → SLEEP */ + uasync_poll(g_ua, 2000); /* +200ms */ + standby_notify_network_activity(); + CHECK(standby_get_sleep_tb() > 0, "clamp: min_sleep==sleep → notify ignored"); + standby_set_enabled(NULL, 0); + + /* ── 16. re-entry: waiter перерегистрируется, без бесконечного цикла ── */ + reset_standby(); + standby_set_enabled(NULL, 1); + run_to_next_phase(); /* → SLEEP */ + standby_wait(NULL, reentry_cb); + uasync_poll(g_ua, 2000); /* +200ms → min_sleep прошёл */ + standby_notify_network_activity(); /* раннее пробуждение → wake_all_waiters */ + CHECK(g_reentry_calls == 1, "re-entry: wake fired waiter once (no infinite loop)"); + CHECK(standby_is_enabled() == 1, "re-entry: still enabled after early wake"); + standby_set_enabled(NULL, 0); /* EXIT → будит перерегистрированного ещё раз */ + CHECK(g_reentry_calls == 2, "re-entry: exit fired re-registered waiter once more"); + + /* ── итог ── */ + standby_deinit(); + uasync_destroy(g_ua, 0); + + printf("=== standby unit test: %d checks, %d failures ===\n", g_checks, g_failures); + if (g_failures == 0) printf("PASS\n"); + else printf("FAIL\n"); + return g_failures ? 1 : 0; +} diff --git a/tools/chatgui-android/libutun_lite/utun_sources.cmake b/tools/chatgui-android/libutun_lite/utun_sources.cmake index a7fe62c0..0c20a1ac 100644 --- a/tools/chatgui-android/libutun_lite/utun_sources.cmake +++ b/tools/chatgui-android/libutun_lite/utun_sources.cmake @@ -37,6 +37,7 @@ function(utun_setup_sources LIB_DIR SRC_DIR CONFIG_DIR TOOLS_DIR) "${CONFIG_DIR}/instance_lite.c" "${CONFIG_DIR}/voice_recorder.c" "${CONFIG_DIR}/attachment_sender.c" + "${CONFIG_DIR}/standby.c" ) # ── Export sources ── @@ -67,6 +68,7 @@ function(utun_setup_sources LIB_DIR SRC_DIR CONFIG_DIR TOOLS_DIR) # ── Export compile definitions ── set(UTUN_DEFINES USE_OPENSSL USE_SQLITE _ISOC99_SOURCE _DEFAULT_SOURCE DEBUG_OUTPUT_STDERR + UTUN_HAVE_STANDBY PARENT_SCOPE ) endfunction()