|
|
@@ -13,89 +13,92 @@
|
|
|
* 全局变量
|
|
|
* ============================================================ */
|
|
|
static AppConfig g_config;
|
|
|
-static int g_running = 1;
|
|
|
+
|
|
|
+/* [FIX] g_running 由信号处理函数写、主循环读, 必须是
|
|
|
+ * volatile sig_atomic_t, 否则优化下主循环可能永不退出。 */
|
|
|
+static volatile sig_atomic_t g_running = 1;
|
|
|
+
|
|
|
+/* [FIX] 后台线程句柄提升为文件作用域, 退出时统一 join,
|
|
|
+ * 修复原代码线程从未回收导致的退出竞态/崩溃。 */
|
|
|
+static pthread_t g_heartbeat_thread;
|
|
|
+static pthread_t g_sim_thread;
|
|
|
+static int g_heartbeat_started = 0;
|
|
|
+static int g_sim_started = 0;
|
|
|
+
|
|
|
+/* [FIX] 统一 PID 文件路径: 原代码写入用 PROJECT_NAME(含空格),
|
|
|
+ * 删除用 isup_client.pid, 二者不一致导致 PID 文件残留。 */
|
|
|
+#define PID_FILE_NAME "isup_client.pid"
|
|
|
|
|
|
/* ============================================================
|
|
|
* 回调函数实现
|
|
|
* ============================================================ */
|
|
|
|
|
|
-/**
|
|
|
- * @brief 设备注册回调
|
|
|
- *
|
|
|
- * 当设备注册/上线/下线时,CMS服务器会调用此回调函数。
|
|
|
- *
|
|
|
- * 参数说明:
|
|
|
- * iUserID: 用户ID(登录成功后返回的ID)
|
|
|
- * dwDataType: 数据类型
|
|
|
- * - DEV_REG_TYPE_ON: 设备上线
|
|
|
- * - DEV_REG_TYPE_OFF: 设备下线
|
|
|
- * - DEV_REG_TYPE_AUTH: 设备认证
|
|
|
- * - DEV_REG_TYPE_SESSIONKEY: 会话密钥
|
|
|
- * - DEV_REG_TYPE_ADDRESS_CHANGED: 地址变更
|
|
|
- * pOutBuffer: 输出缓冲区(包含设备注册信息)
|
|
|
- * dwOutLen: 输出缓冲区长度
|
|
|
- * pInBuffer: 输入缓冲区
|
|
|
- * dwInLen: 输入缓冲区长度
|
|
|
- * pUser: 用户自定义数据
|
|
|
- */
|
|
|
static void device_register_callback(int iUserID, int dwDataType,
|
|
|
void* pOutBuffer, int dwOutLen,
|
|
|
void* pInBuffer, int dwInLen,
|
|
|
void* pUser) {
|
|
|
(void)pOutBuffer; (void)dwOutLen; (void)pInBuffer; (void)dwInLen; (void)pUser;
|
|
|
-
|
|
|
+
|
|
|
char timebuf[64];
|
|
|
get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
-
|
|
|
+
|
|
|
switch (dwDataType) {
|
|
|
case DEV_REG_TYPE_ON:
|
|
|
LOG_INFO("[%s] Device ONLINE: UserID=%d", timebuf, iUserID);
|
|
|
LOG_INFO(" -> Device registered successfully, can now receive streams");
|
|
|
-
|
|
|
- /* 设备上线后自动开始推流(如果配置了自动推流) */
|
|
|
+
|
|
|
if (g_config.rtmp.auto_start && g_config.rtmp.enable) {
|
|
|
LOG_INFO(" -> Auto starting RTMP push...");
|
|
|
- /* 这里可以创建流会话并开始推流 */
|
|
|
+ StreamSession* sess = stream_create_session(
|
|
|
+ g_config.cms.device_id, 1, STREAM_TYPE_MAIN, CODEC_TYPE_H264,
|
|
|
+ g_config.sim.width, g_config.sim.height,
|
|
|
+ g_config.sim.fps, g_config.sim.bitrate);
|
|
|
+ if (sess != NULL) {
|
|
|
+ char rtmp_url[512];
|
|
|
+ snprintf(rtmp_url, sizeof(rtmp_url), "rtmp://%s:%d/%s/%s",
|
|
|
+ g_config.rtmp.rtmp_server, g_config.rtmp.rtmp_port,
|
|
|
+ g_config.rtmp.app_name, g_config.rtmp.stream_key);
|
|
|
+ if (stream_start_push(sess, rtmp_url, NULL, NULL) != SUCCESS) {
|
|
|
+ LOG_WARN(" -> Auto start push failed");
|
|
|
+ stream_destroy_session(sess);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
break;
|
|
|
-
|
|
|
+
|
|
|
case DEV_REG_TYPE_OFF:
|
|
|
LOG_INFO("[%s] Device OFFLINE: UserID=%d", timebuf, iUserID);
|
|
|
- /* 设备下线,停止相关流 */
|
|
|
stream_stop_all();
|
|
|
break;
|
|
|
-
|
|
|
+
|
|
|
case DEV_REG_TYPE_AUTH:
|
|
|
LOG_INFO("[%s] Device AUTH: UserID=%d", timebuf, iUserID);
|
|
|
break;
|
|
|
-
|
|
|
+
|
|
|
case DEV_REG_TYPE_SESSIONKEY:
|
|
|
LOG_INFO("[%s] Session Key: UserID=%d", timebuf, iUserID);
|
|
|
break;
|
|
|
-
|
|
|
+
|
|
|
case DEV_REG_TYPE_ADDRESS_CHANGED:
|
|
|
LOG_INFO("[%s] Address Changed: UserID=%d", timebuf, iUserID);
|
|
|
break;
|
|
|
-
|
|
|
+
|
|
|
case DEV_REG_TYPE_HEARTBEAT:
|
|
|
LOG_INFO("[%s] Heartbeat: UserID=%d", timebuf, iUserID);
|
|
|
break;
|
|
|
-
|
|
|
+
|
|
|
default:
|
|
|
LOG_INFO("[%s] Unknown event type: %d, UserID=%d", timebuf, dwDataType, iUserID);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 设备状态回调
|
|
|
- */
|
|
|
static void device_status_callback(const char* device_id, DeviceStatus status, void* user_data) {
|
|
|
(void)user_data;
|
|
|
-
|
|
|
+
|
|
|
char timebuf[64];
|
|
|
get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
-
|
|
|
+
|
|
|
const char* status_str = "UNKNOWN";
|
|
|
switch (status) {
|
|
|
case DEVICE_STATUS_OFFLINE: status_str = "OFFLINE"; break;
|
|
|
@@ -104,21 +107,17 @@ static void device_status_callback(const char* device_id, DeviceStatus status, v
|
|
|
case DEVICE_STATUS_STREAMING: status_str = "STREAMING"; break;
|
|
|
case DEVICE_STATUS_PLAYBACK: status_str = "PLAYBACK"; break;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
LOG_INFO("[%s] Device status changed: %s -> %s", timebuf, device_id, status_str);
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 报警数据回调
|
|
|
- */
|
|
|
static void alarm_data_callback(AlarmInfo* alarm, void* user_data) {
|
|
|
(void)user_data;
|
|
|
-
|
|
|
if (alarm == NULL) return;
|
|
|
-
|
|
|
+
|
|
|
char timebuf[64];
|
|
|
get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
-
|
|
|
+
|
|
|
const char* type_str = "UNKNOWN";
|
|
|
switch (alarm->type) {
|
|
|
case ALARM_TYPE_MOVEMENT: type_str = "MOVEMENT_DETECTION"; break;
|
|
|
@@ -138,117 +137,79 @@ static void alarm_data_callback(AlarmInfo* alarm, void* user_data) {
|
|
|
case ALARM_TYPE_THERMAL: type_str = "THERMAL"; break;
|
|
|
default: type_str = "CUSTOM"; break;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
LOG_WARN("[%s] ALARM: Type=%s, Device=%s, Channel=%d, Level=%d, Desc=%s",
|
|
|
timebuf, type_str, alarm->device_id, alarm->channel,
|
|
|
alarm->level, alarm->description);
|
|
|
-
|
|
|
- /* 将报警信息添加到未确认队列 */
|
|
|
- alarm_acknowledge(alarm->alarm_id, alarm->device_id);
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 报警处理回调(在业务线程中调用)
|
|
|
- */
|
|
|
static void alarm_process_callback(AlarmInfo* alarm, void* user_data) {
|
|
|
(void)user_data;
|
|
|
-
|
|
|
if (alarm == NULL) return;
|
|
|
-
|
|
|
+
|
|
|
char timebuf[64];
|
|
|
get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
-
|
|
|
+
|
|
|
LOG_INFO("[%s] Alarm processed: Device=%s, Type=%d, Desc=%s",
|
|
|
timebuf, alarm->device_id, alarm->type, alarm->description);
|
|
|
-
|
|
|
- /* 这里可以执行业务逻辑:
|
|
|
- * 1. 保存报警快照到数据库
|
|
|
- * 2. 发送通知(邮件/短信/飞书等)
|
|
|
- * 3. 触发联动设备(如打开灯光、录像等)
|
|
|
- * 4. 写入日志系统
|
|
|
- */
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 信号处理函数
|
|
|
- */
|
|
|
+/* ============================================================
|
|
|
+ * [FIX] 信号处理: 只置标志位, 不调用任何非 async-signal-safe
|
|
|
+ * 函数(原实现调用 fprintf/localtime, 属未定义行为)。
|
|
|
+ * 致命信号(SIGSEGV/SIGABRT)不再安装自定义处理, 保留默认行为。
|
|
|
+ * ============================================================ */
|
|
|
static void signal_handler(int sig) {
|
|
|
- char timebuf[64];
|
|
|
- get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
-
|
|
|
- const char* sig_name = "UNKNOWN";
|
|
|
- switch (sig) {
|
|
|
- case SIGINT: sig_name = "SIGINT"; break;
|
|
|
- case SIGTERM: sig_name = "SIGTERM"; break;
|
|
|
- case SIGSEGV: sig_name = "SIGSEGV"; break;
|
|
|
- case SIGABRT: sig_name = "SIGABRT"; break;
|
|
|
- }
|
|
|
-
|
|
|
- LOG_WARN("[%s] Received signal: %s, shutting down...", timebuf, sig_name);
|
|
|
+ (void)sig;
|
|
|
g_running = 0;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 模拟图片生成(用于测试)
|
|
|
- *
|
|
|
- * 在实际部署中,视频流来自海康设备。
|
|
|
- * 在开发测试阶段,可以使用本函数生成模拟视频帧。
|
|
|
- */
|
|
|
static void* sim_image_thread_func(void* arg) {
|
|
|
(void)arg;
|
|
|
-
|
|
|
LOG_INFO("Simulated image thread started");
|
|
|
-
|
|
|
+
|
|
|
+ /* [FIX] 防御性: fps<=0 会导致 1000/fps 除零崩溃, 兜底为 25 */
|
|
|
+ int fps = g_config.sim.fps;
|
|
|
+ if (fps <= 0) fps = SIM_FPS;
|
|
|
+
|
|
|
int frame_count = 0;
|
|
|
while (g_running) {
|
|
|
- /* 在实际实现中,这里会:
|
|
|
- * 1. 从图片目录读取测试图片
|
|
|
- * 2. 使用FFmpeg/libswscale将图片编码为H.264帧
|
|
|
- * 3. 将帧封装为PS流
|
|
|
- * 4. 通过流会话发送
|
|
|
- */
|
|
|
-
|
|
|
frame_count++;
|
|
|
-
|
|
|
if (frame_count % 100 == 0) {
|
|
|
char timebuf[64];
|
|
|
get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
LOG_INFO("[%s] Simulated frames generated: %d", timebuf, frame_count);
|
|
|
}
|
|
|
-
|
|
|
- /* 按照配置的帧率发送 */
|
|
|
- int sleep_ms = 1000 / g_config.sim.fps;
|
|
|
+ int sleep_ms = 1000 / fps;
|
|
|
+ if (sleep_ms < 1) sleep_ms = 1;
|
|
|
sleep_milliseconds(sleep_ms);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
LOG_INFO("Simulated image thread stopped (total frames: %d)", frame_count);
|
|
|
return NULL;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 心跳保活线程
|
|
|
- */
|
|
|
static void* heartbeat_thread_func(void* arg) {
|
|
|
(void)arg;
|
|
|
-
|
|
|
LOG_INFO("Heartbeat thread started");
|
|
|
-
|
|
|
+
|
|
|
while (g_running) {
|
|
|
- sleep_milliseconds(HEARTBEAT_INTERVAL * 1000);
|
|
|
-
|
|
|
+ for (int i = 0; i < HEARTBEAT_INTERVAL && g_running; i++) {
|
|
|
+ sleep_milliseconds(1000);
|
|
|
+ }
|
|
|
if (g_running) {
|
|
|
- /* 发送心跳包保持设备在线 */
|
|
|
- /* 在实际实现中,遍历所有已注册设备发送心跳 */
|
|
|
+ char devices[MAX_CHANNEL_NUM][MAX_DEVICE_ID];
|
|
|
+ int n = platform_get_online_devices(devices, MAX_CHANNEL_NUM);
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
+ platform_send_heartbeat(i);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
LOG_INFO("Heartbeat thread stopped");
|
|
|
return NULL;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 打印使用说明
|
|
|
- */
|
|
|
static void print_usage(const char* progname) {
|
|
|
printf("Usage: %s [options]\n", progname);
|
|
|
printf("Options:\n");
|
|
|
@@ -256,39 +217,81 @@ static void print_usage(const char* progname) {
|
|
|
printf(" -d Run as daemon\n");
|
|
|
printf(" -h Show this help\n");
|
|
|
printf(" -v Show version\n");
|
|
|
- printf(" -test Run self-test mode\n");
|
|
|
- printf("\n");
|
|
|
+ printf(" -test Run self-test mode\n\n");
|
|
|
printf("Version: %s %s\n", PROJECT_NAME, PROJECT_VERSION);
|
|
|
printf("Build: %s\n", PROJECT_BUILD_DATE);
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * @brief 打印版本信息
|
|
|
- */
|
|
|
static void print_version(void) {
|
|
|
printf("%s v%s\n", PROJECT_NAME, PROJECT_VERSION);
|
|
|
printf("Build date: %s\n", PROJECT_BUILD_DATE);
|
|
|
- printf("Target: Linux %s\n", "x86_64");
|
|
|
+ printf("Target: Linux x86_64\n");
|
|
|
+}
|
|
|
+
|
|
|
+/* [FIX] 自测流程: 注册->推流->回放->停止->登出, 验证核心链路无崩溃 */
|
|
|
+static int run_self_test(void) {
|
|
|
+ LOG_INFO("===== SELF TEST START =====");
|
|
|
+
|
|
|
+ int uid = platform_device_register(g_config.cms.server_ip, g_config.cms.server_port,
|
|
|
+ g_config.cms.device_id, g_config.cms.protocol_version,
|
|
|
+ g_config.cms.encrypt_key, NULL);
|
|
|
+ if (uid < 0) {
|
|
|
+ LOG_ERROR("Self-test: device register failed");
|
|
|
+ return FAILURE;
|
|
|
+ }
|
|
|
+
|
|
|
+ StreamSession* sess = stream_create_session(
|
|
|
+ g_config.cms.device_id, 1, STREAM_TYPE_SIM, CODEC_TYPE_H264,
|
|
|
+ g_config.sim.width, g_config.sim.height, g_config.sim.fps, g_config.sim.bitrate);
|
|
|
+ if (sess == NULL) {
|
|
|
+ LOG_ERROR("Self-test: create stream session failed");
|
|
|
+ platform_device_logout(uid);
|
|
|
+ return FAILURE;
|
|
|
+ }
|
|
|
+
|
|
|
+ char rtmp_url[512];
|
|
|
+ snprintf(rtmp_url, sizeof(rtmp_url), "rtmp://%s:%d/%s/%s",
|
|
|
+ g_config.rtmp.rtmp_server, g_config.rtmp.rtmp_port,
|
|
|
+ g_config.rtmp.app_name, g_config.rtmp.stream_key);
|
|
|
+ if (stream_start_push(sess, rtmp_url, NULL, NULL) != SUCCESS) {
|
|
|
+ LOG_WARN("Self-test: start push failed");
|
|
|
+ }
|
|
|
+
|
|
|
+ time_t now = time(NULL);
|
|
|
+ PlaybackSession* pb = playback_start(g_config.cms.device_id, 1,
|
|
|
+ now - 3600, now, 1, NULL, NULL, NULL);
|
|
|
+ if (pb != NULL) {
|
|
|
+ playback_pause(pb);
|
|
|
+ playback_resume(pb);
|
|
|
+ playback_stop(pb);
|
|
|
+ }
|
|
|
+
|
|
|
+ stream_stop_push(sess);
|
|
|
+ stream_destroy_session(sess);
|
|
|
+ platform_device_logout(uid);
|
|
|
+
|
|
|
+ int total = 0, unack = 0, today = 0;
|
|
|
+ alarm_get_stats(&total, &unack, &today);
|
|
|
+ LOG_INFO("Self-test stats: total=%d unack=%d today=%d", total, unack, today);
|
|
|
+
|
|
|
+ LOG_INFO("===== SELF TEST PASSED =====");
|
|
|
+ return SUCCESS;
|
|
|
}
|
|
|
|
|
|
-/* ============================================================
|
|
|
- * 主函数
|
|
|
- * ============================================================ */
|
|
|
int main(int argc, char* argv[]) {
|
|
|
char timebuf[64];
|
|
|
get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
-
|
|
|
+
|
|
|
printf("==================================================\n");
|
|
|
printf(" %s v%s\n", PROJECT_NAME, PROJECT_VERSION);
|
|
|
printf(" Build: %s\n", PROJECT_BUILD_DATE);
|
|
|
printf(" Started: %s\n", timebuf);
|
|
|
printf("==================================================\n");
|
|
|
-
|
|
|
- /* 命令行参数解析 */
|
|
|
+
|
|
|
const char* config_file = "./config.ini";
|
|
|
int daemon_mode = 0;
|
|
|
int test_mode = 0;
|
|
|
-
|
|
|
+
|
|
|
for (int i = 1; i < argc; i++) {
|
|
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
|
|
print_usage(argv[0]);
|
|
|
@@ -308,31 +311,30 @@ int main(int argc, char* argv[]) {
|
|
|
return 1;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /* 初始化配置 */
|
|
|
+
|
|
|
config_init_default(&g_config);
|
|
|
g_config.daemon_mode = daemon_mode;
|
|
|
-
|
|
|
- /* 加载配置文件 */
|
|
|
+
|
|
|
if (config_load(&g_config, config_file) != SUCCESS) {
|
|
|
LOG_WARN("Failed to load config file: %s, using defaults", config_file);
|
|
|
}
|
|
|
-
|
|
|
- /* 打印配置 */
|
|
|
+
|
|
|
config_print(&g_config);
|
|
|
-
|
|
|
- /* 创建必要目录 */
|
|
|
+
|
|
|
ensure_directory("./logs");
|
|
|
- ensure_directory("./sim_images");
|
|
|
+ ensure_directory(g_config.sim.image_dir);
|
|
|
ensure_directory(g_config.playback.save_path);
|
|
|
-
|
|
|
- /* 注册信号处理 */
|
|
|
- signal(SIGINT, signal_handler);
|
|
|
- signal(SIGTERM, signal_handler);
|
|
|
- signal(SIGSEGV, signal_handler);
|
|
|
- signal(SIGABRT, signal_handler);
|
|
|
-
|
|
|
- /* 守护进程模式 */
|
|
|
+
|
|
|
+ /* [FIX] sigaction + SA_RESTART, 仅 INT/TERM */
|
|
|
+ struct sigaction sa;
|
|
|
+ memset(&sa, 0, sizeof(sa));
|
|
|
+ sa.sa_handler = signal_handler;
|
|
|
+ sigemptyset(&sa.sa_mask);
|
|
|
+ sa.sa_flags = SA_RESTART;
|
|
|
+ sigaction(SIGINT, &sa, NULL);
|
|
|
+ sigaction(SIGTERM, &sa, NULL);
|
|
|
+ signal(SIGPIPE, SIG_IGN);
|
|
|
+
|
|
|
if (daemon_mode) {
|
|
|
if (daemon(1, 1) != 0) {
|
|
|
LOG_ERROR("Failed to start as daemon: %s", strerror(errno));
|
|
|
@@ -340,130 +342,118 @@ int main(int argc, char* argv[]) {
|
|
|
}
|
|
|
LOG_INFO("Started as daemon");
|
|
|
}
|
|
|
-
|
|
|
- /* 写入PID文件 */
|
|
|
- char pid_path[MAX_PATH_SIZE];
|
|
|
- snprintf(pid_path, sizeof(pid_path), "./logs/%s.pid", PROJECT_NAME);
|
|
|
- FILE* pid_fp = fopen(pid_path, "w");
|
|
|
+
|
|
|
+ snprintf(g_config.pid_file, sizeof(g_config.pid_file), "./logs/%s", PID_FILE_NAME);
|
|
|
+ FILE* pid_fp = fopen(g_config.pid_file, "w");
|
|
|
if (pid_fp) {
|
|
|
- fprintf(pid_fp, "%d", getpid());
|
|
|
+ fprintf(pid_fp, "%d\n", (int)getpid());
|
|
|
fclose(pid_fp);
|
|
|
}
|
|
|
-
|
|
|
- /* ===== 初始化各模块 ===== */
|
|
|
-
|
|
|
- /* 1. 初始化平台注册模块(CMS) */
|
|
|
+
|
|
|
+ int platform_ok = 0, stream_ok = 0, playback_ok = 0, alarm_ok = 0, cms_listening = 0, alarm_listening = 0;
|
|
|
+
|
|
|
if (platform_cms_init() != SUCCESS) {
|
|
|
LOG_ERROR("Failed to initialize platform module");
|
|
|
- goto cleanup;
|
|
|
+ goto teardown;
|
|
|
}
|
|
|
-
|
|
|
- /* 2. 初始化流媒体模块 */
|
|
|
+ platform_ok = 1;
|
|
|
+
|
|
|
if (stream_init() != SUCCESS) {
|
|
|
LOG_ERROR("Failed to initialize stream module");
|
|
|
- goto cleanup_platform;
|
|
|
+ goto teardown;
|
|
|
}
|
|
|
-
|
|
|
- /* 3. 初始化回放模块 */
|
|
|
+ stream_ok = 1;
|
|
|
+
|
|
|
if (playback_init() != SUCCESS) {
|
|
|
LOG_ERROR("Failed to initialize playback module");
|
|
|
- goto cleanup_stream;
|
|
|
+ goto teardown;
|
|
|
}
|
|
|
-
|
|
|
- /* 4. 初始化报警模块 */
|
|
|
+ playback_ok = 1;
|
|
|
+
|
|
|
if (alarm_init() != SUCCESS) {
|
|
|
LOG_ERROR("Failed to initialize alarm module");
|
|
|
- goto cleanup_playback;
|
|
|
+ goto teardown;
|
|
|
}
|
|
|
-
|
|
|
- /* ===== 启动各服务 ===== */
|
|
|
-
|
|
|
- /* 1. 启动CMS监听(接收设备注册) */
|
|
|
+ alarm_ok = 1;
|
|
|
+
|
|
|
if (g_config.cms.enable) {
|
|
|
if (platform_cms_start_listen("0.0.0.0", g_config.cms.server_port,
|
|
|
- device_register_callback, NULL) != SUCCESS) {
|
|
|
- LOG_ERROR("Failed to start CMS listen");
|
|
|
- } else {
|
|
|
+ device_register_callback, NULL) == SUCCESS) {
|
|
|
+ cms_listening = 1;
|
|
|
LOG_INFO("CMS listen started on port %d", g_config.cms.server_port);
|
|
|
+ } else {
|
|
|
+ LOG_ERROR("Failed to start CMS listen");
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /* 2. 注册设备状态回调 */
|
|
|
+
|
|
|
platform_register_status_callback(device_status_callback, NULL);
|
|
|
-
|
|
|
- /* 3. 启动报警监听 */
|
|
|
+
|
|
|
if (g_config.alarm.enable) {
|
|
|
if (alarm_start_listen("0.0.0.0", g_config.alarm.listen_port,
|
|
|
- alarm_data_callback, alarm_process_callback, NULL) != SUCCESS) {
|
|
|
- LOG_WARN("Failed to start alarm listen");
|
|
|
- } else {
|
|
|
+ alarm_data_callback, alarm_process_callback, NULL) == SUCCESS) {
|
|
|
+ alarm_listening = 1;
|
|
|
LOG_INFO("Alarm listen started on port %d", g_config.alarm.listen_port);
|
|
|
+ } else {
|
|
|
+ LOG_WARN("Failed to start alarm listen");
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /* 4. 启动心跳保活线程 */
|
|
|
- pthread_t heartbeat_thread;
|
|
|
- if (pthread_create(&heartbeat_thread, NULL, heartbeat_thread_func, NULL) != 0) {
|
|
|
+
|
|
|
+ if (pthread_create(&g_heartbeat_thread, NULL, heartbeat_thread_func, NULL) == 0) {
|
|
|
+ g_heartbeat_started = 1;
|
|
|
+ } else {
|
|
|
LOG_WARN("Failed to create heartbeat thread");
|
|
|
}
|
|
|
-
|
|
|
- /* 5. 模拟模式: 启动模拟图片生成线程 */
|
|
|
+
|
|
|
if (g_config.sim.enable) {
|
|
|
LOG_INFO("Simulation mode enabled, starting simulated stream generator...");
|
|
|
- pthread_t sim_thread;
|
|
|
- if (pthread_create(&sim_thread, NULL, sim_image_thread_func, NULL) != 0) {
|
|
|
+ if (pthread_create(&g_sim_thread, NULL, sim_image_thread_func, NULL) == 0) {
|
|
|
+ g_sim_started = 1;
|
|
|
+ } else {
|
|
|
LOG_WARN("Failed to create simulation thread");
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /* ===== 主循环 ===== */
|
|
|
- LOG_INFO("Main loop started, press Ctrl+C to stop");
|
|
|
-
|
|
|
- int loop_count = 0;
|
|
|
- while (g_running) {
|
|
|
- sleep_milliseconds(1000);
|
|
|
- loop_count++;
|
|
|
-
|
|
|
- /* 定期打印状态 */
|
|
|
- if (loop_count % 60 == 0) {
|
|
|
- int total, unack, today;
|
|
|
- alarm_get_stats(&total, &unack, &today);
|
|
|
-
|
|
|
- get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
- LOG_INFO("[%s] Status: TotalAlarms=%d, Unack=%d, Today=%d",
|
|
|
- timebuf, total, unack, today);
|
|
|
+
|
|
|
+ if (test_mode) {
|
|
|
+ int rc = run_self_test();
|
|
|
+ g_running = 0;
|
|
|
+ if (rc != SUCCESS) LOG_ERROR("Self-test failed");
|
|
|
+ } else {
|
|
|
+ LOG_INFO("Main loop started, press Ctrl+C to stop");
|
|
|
+ int loop_count = 0;
|
|
|
+ while (g_running) {
|
|
|
+ sleep_milliseconds(1000);
|
|
|
+ loop_count++;
|
|
|
+ if (loop_count % 60 == 0) {
|
|
|
+ int total = 0, unack = 0, today = 0;
|
|
|
+ alarm_get_stats(&total, &unack, &today);
|
|
|
+ get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
+ LOG_INFO("[%s] Status: TotalAlarms=%d, Unack=%d, Today=%d",
|
|
|
+ timebuf, total, unack, today);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /* ===== 清理 ===== */
|
|
|
-cleanup_alarm:
|
|
|
- alarm_stop_listen();
|
|
|
- alarm_cleanup();
|
|
|
-
|
|
|
-cleanup_playback:
|
|
|
- playback_cleanup();
|
|
|
-
|
|
|
-cleanup_stream:
|
|
|
- stream_cleanup();
|
|
|
-
|
|
|
-cleanup_platform:
|
|
|
- platform_cms_cleanup();
|
|
|
-
|
|
|
- /* 清理PID文件 */
|
|
|
- unlink("./logs/isup_client.pid");
|
|
|
-
|
|
|
+
|
|
|
+teardown:
|
|
|
+ /* [FIX] 先 join 后台线程, 再停监听, 最后逆序清理, 消除退出竞态 */
|
|
|
+ g_running = 0;
|
|
|
+
|
|
|
+ if (g_sim_started) { pthread_join(g_sim_thread, NULL); g_sim_started = 0; }
|
|
|
+ if (g_heartbeat_started) { pthread_join(g_heartbeat_thread, NULL); g_heartbeat_started = 0; }
|
|
|
+
|
|
|
+ if (alarm_listening) { alarm_stop_listen(); alarm_listening = 0; }
|
|
|
+ if (cms_listening) { platform_cms_stop_listen(); cms_listening = 0; }
|
|
|
+
|
|
|
+ if (alarm_ok) { alarm_cleanup(); alarm_ok = 0; }
|
|
|
+ if (playback_ok) { playback_cleanup(); playback_ok = 0; }
|
|
|
+ if (stream_ok) { stream_cleanup(); stream_ok = 0; }
|
|
|
+ if (platform_ok) { platform_cms_cleanup(); platform_ok = 0; }
|
|
|
+
|
|
|
+ if (g_config.pid_file[0]) unlink(g_config.pid_file);
|
|
|
+
|
|
|
get_timestamp(timebuf, sizeof(timebuf), NULL);
|
|
|
printf("==================================================\n");
|
|
|
printf(" %s stopped: %s\n", PROJECT_NAME, timebuf);
|
|
|
printf("==================================================\n");
|
|
|
-
|
|
|
+
|
|
|
return 0;
|
|
|
-
|
|
|
- /* unreachable */
|
|
|
-cleanup_stream:
|
|
|
- stream_cleanup();
|
|
|
-cleanup_platform:
|
|
|
- platform_cms_cleanup();
|
|
|
- unlink("./logs/isup_client.pid");
|
|
|
- return 1;
|
|
|
}
|