Browse Source

android: standby duty-cycle в фоне — defer ping/reconnect/probe через standby_wait

standby.h/c: duty-cycle ACTIVE/SLEEP, standby_wait() будит в burst сразу,
в SLEEP — до следующего burst; notify_network_activity для раннего пробуждения.
Интеграция (по флагу UTUN_HAVE_STANDBY, десктоп без изменений):
- conn_mgr: bg_ping/candidate_ping в SLEEP defer через standby_wait
- route_connectivity: probe_node откладывается (PROBE_STATUS_DEFERRED)
- topo_group_connect: пауза между попытками — активный режим 1с, фон standby_wait
- jni_bridge/ConfigProvider/NetworkActivityMonitor: set_active + сетевая активность
- chat_setting: standby_active_sec/sleep_sec/min_sleep_sec
+ юнит-тест test_standby (31 проверок)
topo_upd
evgeny 2 months ago
parent
commit
b203a48063
  1. 3
      .gitignore
  2. 3
      src/chat/chat_setting.c
  3. 7
      src/routing_layer/conn_mgr_core.c
  4. 17
      src/routing_layer/conn_mgr_monitor.c
  5. 1
      src/routing_layer/conn_mgr_priv.h
  6. 56
      src/routing_layer/route_connectivity.c
  7. 1
      src/routing_layer/topo_group.c
  8. 36
      src/routing_layer/topo_group_connect.c
  9. 3
      src/routing_layer/topo_group_connect.h
  10. 2
      src/routing_layer/topo_node.h
  11. 1
      src/utun_instance.h
  12. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  13. 12
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  14. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  15. 48
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkActivityMonitor.kt
  16. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt
  17. 28
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
  18. 22
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  19. 4
      tools/chatgui-android/jni_bridge/android_jni_bridge.h
  20. 2
      tools/chatgui-android/libutun_lite/CMakeLists.txt
  21. 7
      tools/chatgui-android/libutun_lite/instance_lite.c
  22. 386
      tools/chatgui-android/libutun_lite/standby.c
  23. 61
      tools/chatgui-android/libutun_lite/standby.h
  24. 7
      tools/chatgui-android/libutun_lite/tests/CMakeLists.txt
  25. 216
      tools/chatgui-android/libutun_lite/tests/test_standby.c
  26. 2
      tools/chatgui-android/libutun_lite/utun_sources.cmake

3
.gitignore vendored

@ -11,7 +11,8 @@ tools/chatgui/ffmpeg/ffmpeg_build/
*.a
*.so
*.so.*
utun
/utun
/src/utun
test_serialize
linux-x64-deb

3
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},

7
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;

17
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);

1
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;

56
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;

1
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++;

36
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; }

3
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.
*

2
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)

1
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;

3
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 */

12
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")

4
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?

48
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")
}
}

3
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()
}

28
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)

22
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(

4
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);

2
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)

7
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 <pthread.h>
#include <stdio.h>
#include <string.h>
@ -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;
}

386
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;
}

61
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 */

7
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)

216
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 <stdio.h>
#include <string.h>
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): still sleeping");
/* ── 13. notify в SLEEP после min_sleep → раннее пробуждение ── */
uasync_poll(g_ua, 2000); /* +200ms → прошло min_sleep(100ms) */
standby_notify_network_activity();
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;
}

2
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()

Loading…
Cancel
Save