From 5d1b8d26e7c739e08107fdcedb2937f0664073d1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 5 Oct 2026 21:19:19 +0200 Subject: [PATCH] Daemonize before creating core workers --- doc/service_lifecycle.md | 6 +++ src/member_files/member_files.c | 14 ++++++ src/utun.c | 72 +++++++++++++++++++++------ tests/Makefile.am | 8 +++ tests/test_daemon_worker.c | 86 +++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 tests/test_daemon_worker.c diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index d9bf10d5..bba6d4c3 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -1,5 +1,11 @@ # Жизненный цикл ядра и сервисов +CLI выполняет daemon fork до создания `UASYNC`, SQLite, сокетов и фоновых потоков. +Сервисы и их workers создаются уже в дочернем процессе. Родитель ждёт подтверждения +успешной инициализации и записи PID-файла; ошибка старта возвращается вызывающему. +`test_daemon_worker` проверяет настоящий запуск без `-f`, наличие дискового потока, +штатный SIGTERM и ненулевой статус launcher при ошибке PID-файла. + Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks останавливаемых модулей. GUI передаёт команды в этот поток. diff --git a/src/member_files/member_files.c b/src/member_files/member_files.c index ec114eab..116b058d 100644 --- a/src/member_files/member_files.c +++ b/src/member_files/member_files.c @@ -124,6 +124,10 @@ static int mf_send(struct member_files* f, uint64_t group, uint64_t peer, const int n = mf_encode(m, wire, sizeof(wire)); if (n < 0) return -1; int result = etcp_router_conn_send(rc, wire, (size_t)n, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); + if (!result) DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, + "member_files: tx id=%llu kind=%u cmd=%u group=%llx peer=%llx owner=%llx status=%u bytes=%d", + (unsigned long long)m->request_id, m->kind, m->command, (unsigned long long)group, + (unsigned long long)peer, (unsigned long long)m->owner, m->status, n); if (result) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: send failed group=%llu peer=%llu cmd=%u rc=%d", (unsigned long long)group, (unsigned long long)peer, m->command, result); return result ? -1 : 0; @@ -236,6 +240,9 @@ static void mf_server_ready(struct ll_queue* q, void* arg) { static void mf_server_work(void* arg) { struct mf_job* j = arg; + if (j->request.command) DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: disk start id=%llu cmd=%u group=%llx peer=%llx path=%s", + (unsigned long long)j->request.request_id, j->request.command, (unsigned long long)j->group, + (unsigned long long)j->peer, j->request.path); if (j->request.command) mf_storage_execute(j->files->storage, j->group, j->peer, &j->request, &j->response); else mf_storage_sweep(j->files->storage, get_time_tb()); } @@ -245,6 +252,8 @@ static void mf_server_done(void* arg, int error) { struct member_files* f = j->files; f->worker = NULL; if (f->closing || !j->request.command) { mf_job_free(j); if (!f->closing) mf_server_pump(f); return; } if (error) { j->response = j->request; j->response.kind = MF_RESPONSE; j->response.status = MF_BUSY; j->response.data_len = 0; } + DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: disk done id=%llu cmd=%u status=%u error=%d jobs=%u", + (unsigned long long)j->request.request_id, j->request.command, j->response.status, error, f->job_count); j->state = 2; mf_server_pump(f); } @@ -316,6 +325,8 @@ static void mf_server_pump(struct member_files* f) { j->response.data_len = 0; j->state = 2; mf_server_pump(f); return; } j->state = 1; f->worker = j; + if (j->request.command) DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: disk queued id=%llu cmd=%u jobs=%u", + (unsigned long long)j->request.request_id, j->request.command, f->job_count); mf_worker_submit(f->disk, mf_server_work, j, mf_server_done, j); } @@ -348,6 +359,9 @@ static void mf_tick(void* arg) { /* Общая обработка проверенного сетевого сообщения или команды внутри экземпляра. */ static void mf_dispatch(struct member_files* f, uint64_t group, uint64_t peer, const struct mf_message* message) { struct mf_message m = *message; + DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: rx id=%llu kind=%u cmd=%u group=%llx peer=%llx owner=%llx jobs=%u worker=%p", + (unsigned long long)m.request_id, m.kind, m.command, (unsigned long long)group, + (unsigned long long)peer, (unsigned long long)m.owner, f->job_count, (void*)f->worker); if (m.kind == MF_RESPONSE) { for (struct mf_pending* p = f->pending; p; p = p->next) { if (p->group != group || p->server != peer || p->request.request_id != m.request_id) continue; diff --git a/src/utun.c b/src/utun.c index 2819b8b7..2b42ecc7 100644 --- a/src/utun.c +++ b/src/utun.c @@ -179,28 +179,51 @@ static void remove_pidfile(const char *pidfile) { } // Daemonize process -static int daemonize(void) { +static int daemonize(int* ready_fd) { #ifdef _WIN32 // Windows doesn't support fork/setsid, just return success + (void)ready_fd; return 0; #else + int notify[2]; + if (pipe(notify) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Daemon startup pipe failed: %s", strerror(errno)); return -1; + } pid_t pid = fork(); if (pid < 0) { - perror("fork"); + int error = errno; close(notify[0]); close(notify[1]); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Daemon fork failed: %s", strerror(error)); return -1; } if (pid > 0) { - // Parent exits - exit(0); + /* Родитель сообщает успех только после инициализации дочернего процесса. */ + close(notify[1]); + uint8_t ready = 0; ssize_t count; + do { count = read(notify[0], &ready, sizeof(ready)); } while (count < 0 && errno == EINTR); + close(notify[0]); + if (count != 1 || ready != 1) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Daemon initialization failed; see log file"); + exit(count == 1 && ready == 1 ? 0 : 1); } + close(notify[0]); *ready_fd = notify[1]; // Child becomes session leader if (setsid() < 0) { - perror("setsid"); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Daemon setsid failed: %s", strerror(errno)); + close(*ready_fd); *ready_fd = -1; return -1; } - + + return 0; +#endif +} + +/* После вывода startup-информации, но до запуска потоков сервисов. */ +static int daemon_redirect_stdio(void) { +#ifdef _WIN32 + return 0; +#else + fflush(stdout); fflush(stderr); // Close standard file descriptors close(STDIN_FILENO); close(STDOUT_FILENO); @@ -277,6 +300,7 @@ void test_tmr(void* arg) { int main(int argc, char *argv[]) { cmd_args_t args; + int ready_fd = -1; parse_args(argc, argv, &args); if (args.help) { @@ -328,6 +352,11 @@ int main(int argc, char *argv[]) { } #endif + /* Fork до event loop, SQLite, сокетов и workers: фоновые потоки в ребёнка не переходят. */ + if (!args.foreground && daemonize(&ready_fd) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to daemonize before core initialization"); return 1; + } + srand(time(NULL)); // Create uasync instance struct UASYNC* ua = uasync_create(); @@ -365,6 +394,7 @@ int main(int argc, char *argv[]) { struct UTUN_INSTANCE *instance = utun_instance_create(ua, args.config_file); if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to create UTUN instance"); + uasync_destroy(ua, 0); main_ua = NULL; return 1; } @@ -373,11 +403,16 @@ int main(int argc, char *argv[]) { if (args.foreground) { print_config(instance->config); } + if (!args.foreground && daemon_redirect_stdio() < 0) { + utun_instance_destroy(instance); uasync_destroy(ua, 0); main_ua = NULL; + return 1; + } // Initialize all components (TUN, routing, connections) if (utun_instance_init(instance) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to initialize instance"); utun_instance_destroy(instance); + uasync_destroy(ua, 0); main_ua = NULL; return 1; } @@ -390,21 +425,28 @@ int main(int argc, char *argv[]) { signal(SIGHUP, signal_handler); #endif - // Daemonize if not in foreground - if (!args.foreground) { - if (daemonize() < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to daemonize"); - utun_instance_destroy(instance); - return 1; - } - } - // Write PID file (only if specified) if (args.pid_file && write_pidfile(args.pid_file) < 0) { utun_instance_destroy(instance); + uasync_destroy(ua, 0); main_ua = NULL; return 1; } +#ifndef _WIN32 + if (ready_fd >= 0) { + uint8_t ready = 1; ssize_t count; + do { count = write(ready_fd, &ready, sizeof(ready)); } while (count < 0 && errno == EINTR); + int error = errno; + close(ready_fd); ready_fd = -1; + if (count != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to notify daemon launcher: %s", strerror(error)); + utun_instance_destroy(instance); uasync_destroy(ua, 0); main_ua = NULL; + remove_pidfile(args.pid_file); return 1; + } + DEBUG_INFO(DEBUG_CATEGORY_SYS, "Daemon initialized pid=%ld; core and workers created after fork", (long)getpid()); + } +#endif + DEBUG_INFO(DEBUG_CATEGORY_SYS, "Run mainloop"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Run mainloop"); diff --git a/tests/Makefile.am b/tests/Makefile.am index 5ed27baf..db069f5b 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -74,6 +74,7 @@ check_PROGRAMS = \ test_services \ test_member_files \ test_member_files_network \ + test_daemon_worker \ test_group_exchange \ test_group_recovery \ test_group_connect \ @@ -725,6 +726,13 @@ test_member_files_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_member_files_network_SOURCES = test_member_files_network.c test_member_files_network_CFLAGS = $(test_services_CFLAGS) test_member_files_network_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_daemon_worker_SOURCES = test_daemon_worker.c +test_daemon_worker_CFLAGS = $(test_services_CFLAGS) -DUTUN_CLI_PATH='"$(abs_top_builddir)/src/utun"' +test_daemon_worker_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_daemon_worker_DEPENDENCIES = $(LIBUTUN) $(top_builddir)/src/utun + +$(top_builddir)/src/utun: + $(MAKE) -C $(top_builddir)/src utun # Граничные длины и checksum UDP/ICMP после восстановления пакета. test_proxy_packets_SOURCES = test_proxy_packets.c diff --git a/tests/test_daemon_worker.c b/tests/test_daemon_worker.c new file mode 100644 index 00000000..b2d0f853 --- /dev/null +++ b/tests/test_daemon_worker.c @@ -0,0 +1,86 @@ +/* Настоящий CLI без -f: daemon должен создать disk worker после fork, завершиться + * по SIGTERM и вернуть launcher ошибку, если PID-файл нельзя создать. Без настоящего TUN. */ +#include "../lib/platform_compat.h" +#include "../lib/u_async.h" +#include "../lib/debug_config.h" +#include "test_utils.h" +#include +#include +#ifdef __linux__ +#include +#include +#include +#include +extern char** environ; + +static int launch(const char* config, const char* pidfile, const char* log) { + char* argv[] = { UTUN_CLI_PATH, "-c", (char*)config, "-p", (char*)pidfile, "-l", (char*)log, + "-d", "sys=info,media=debug", NULL }; + pid_t launcher; + int error = posix_spawn(&launcher, UTUN_CLI_PATH, NULL, NULL, argv, environ); + if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: spawn failed error=%d", error); return -1; } + int status; + while (waitpid(launcher, &status, 0) < 0) { + if (errno == EINTR) continue; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: waitpid failed: %s", strerror(errno)); return -1; + } + return WIFEXITED(status) ? WEXITSTATUS(status) : -1; +} + +static int thread_count(pid_t pid) { + char path[128]; snprintf(path, sizeof(path), "/proc/%ld/task", (long)pid); + DIR* dir = opendir(path); + if (!dir) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: task listing failed: %s", strerror(errno)); return -1; } + int count = 0; struct dirent* entry; + while ((entry = readdir(dir))) if (entry->d_name[0] != '.') count++; + closedir(dir); return count; +} + +static int stop(pid_t pid, const char* pidfile) { + if (kill(pid, SIGTERM)) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: SIGTERM failed: %s", strerror(errno)); return -1; } + uint64_t deadline = get_time_tb() + 20000; + while (access(pidfile, F_OK) == 0 && get_time_tb() < deadline) poll(NULL, 0, 10); + if (access(pidfile, F_OK) == 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: shutdown stuck pid=%ld; killing own fixture", (long)pid); + if (kill(pid, SIGKILL)) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: fixture cleanup failed: %s", strerror(errno)); + return -1; + } + return 0; +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); + char dir[512] = "/tmp/utun_daemon_worker_XXXXXX"; + if (test_mkdtemp(dir)) return 1; + char config[768], pidfile[768], log[768], bad_pid[768]; + snprintf(config, sizeof(config), "%s/utun.conf", dir); snprintf(pidfile, sizeof(pidfile), "%s/daemon.pid", dir); + snprintf(log, sizeof(log), "%s/daemon.log", dir); snprintf(bad_pid, sizeof(bad_pid), "%s/missing/daemon.pid", dir); + FILE* f = fopen(config, "w"); if (!f) return 1; + fprintf(f, "[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" + "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" + "tun_ip=192.0.2.1\ntun_test_mode=1\n" + "[server:test]\naddr=127.0.0.1:0\ntype=private\n" + "[chatserver]\ndb_path=%s/db\nstorage_autoload=0\ngroup_autoconnect=0\n", dir); + if (fclose(f) || launch(config, pidfile, log)) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: startup failed log=%s", log); return 1; + } + f = fopen(pidfile, "r"); long value = 0; + if (!f || fscanf(f, "%ld", &value) != 1 || value <= 1) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: invalid/missing pidfile=%s", pidfile); if (f) fclose(f); return 1; + } + fclose(f); pid_t daemon_pid = (pid_t)value; + int threads = thread_count(daemon_pid); + int stopped = stop(daemon_pid, pidfile); + if (threads < 2 || stopped) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: worker lost or shutdown failed threads=%d stopped=%d log=%s", threads, stopped, log); + return 1; + } + if (launch(config, bad_pid, log) == 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "daemon test: launcher accepted failed child initialization log=%s", log); return 1; + } + printf("daemon worker: PASS threads=%d, graceful shutdown, startup errors propagate\n", threads); + return 0; +} +#else +int main(void) { printf("daemon worker: SKIP (requires Linux procfs)\n"); return 77; } +#endif