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 проверок)
This commit is contained in:
1 parent
1c3ec5b9f3
commit
dec16ff51e
26 files changed
+921
-14
No files matched your search
+2
-1
@@ -11,7 +11,8 @@ tools/chatgui/ffmpeg/ffmpeg_build/
|
||||
*.a
|
||||
*.so
|
||||
*.so.*
|
||||
utun
|
||||
/utun
|
||||
/src/utun
|
||||
test_serialize
|
||||
linux-x64-deb
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -29,3 +29,5 @@ target_link_libraries(utun_lite PUBLIC
|
||||
OpenSSL::Crypto
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
)
|
||||
|
||||
add_subdirectory(tests)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user