477 lines
17 KiB
C++
477 lines
17 KiB
C++
#include <QCoreApplication>
|
||
#include <QTimer>
|
||
#include <QDir>
|
||
#include <QFileInfo>
|
||
#include <QFile>
|
||
#include <QJsonObject>
|
||
#include <iostream>
|
||
#include <chrono>
|
||
|
||
#if defined(__linux__)
|
||
#include <signal.h>
|
||
#include <execinfo.h>
|
||
#include <unistd.h>
|
||
#include <sys/syscall.h>
|
||
#include <dlfcn.h>
|
||
#include <pthread.h>
|
||
#include <exception>
|
||
#include <typeinfo>
|
||
#include <cxxabi.h>
|
||
#include <thread>
|
||
#include <sstream>
|
||
#include <cstring>
|
||
#endif
|
||
|
||
#include "DroneScrewServerPresenter.h"
|
||
#include "DroneScrewZmqProtocol.h"
|
||
#include "Version.h"
|
||
#include "VrLog.h"
|
||
#include "VrError.h"
|
||
|
||
#if defined(__linux__)
|
||
// ========== exit() 拦截 ==========
|
||
// libff_media.so 内部线程在错误条件下会直接调用 exit() 导致整个进程崩溃。
|
||
// 我们拦截 exit() 调用:主线程的 exit 正常放行,子线程的 exit 改为只结束该线程。
|
||
|
||
namespace
|
||
{
|
||
// 真正的 exit 函数指针,启动时初始化
|
||
typedef void (*real_exit_t)(int);
|
||
real_exit_t g_real_exit = nullptr;
|
||
|
||
bool g_exitHookInstalled = false;
|
||
} // namespace
|
||
|
||
extern "C" void exit(int status)
|
||
{
|
||
// 递归保护:如果 exit hook 内部再次调用 exit,直接走 _exit
|
||
static thread_local bool g_inExitHook = false;
|
||
if (g_inExitHook)
|
||
_exit(status);
|
||
g_inExitHook = true;
|
||
|
||
// 首次调用时获取真正的 exit 地址
|
||
if (!g_real_exit)
|
||
{
|
||
g_real_exit = reinterpret_cast<real_exit_t>(dlsym(RTLD_NEXT, "exit"));
|
||
if (!g_real_exit)
|
||
_exit(status); // 极端情况:拿不到真 exit,直接用 _exit
|
||
}
|
||
|
||
// 判断是否为主线程(主线程的 tid == pid)
|
||
const pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
|
||
const pid_t pid = getpid();
|
||
|
||
// 获取线程 ID 字符串
|
||
std::ostringstream oss;
|
||
oss << std::this_thread::get_id();
|
||
|
||
if (tid == pid)
|
||
{
|
||
// 主线程调用 exit() — 正常退出流程
|
||
LOG_ERROR("========================================\n");
|
||
LOG_ERROR("[EXIT-HOOK] exit(%d) called from MAIN thread tid=%d, passing through to real exit\n",
|
||
status, static_cast<int>(tid));
|
||
LOG_ERROR("========================================\n");
|
||
g_real_exit(status);
|
||
}
|
||
else
|
||
{
|
||
// 子线程调用 exit() — 记录 backtrace 并仅结束该线程
|
||
LOG_ERROR("========================================\n");
|
||
LOG_ERROR("[EXIT-HOOK] exit(%d) called from NON-MAIN thread! tid=%d c++id=%s\n",
|
||
status, static_cast<int>(tid), oss.str().c_str());
|
||
LOG_ERROR("[EXIT-HOOK] Preventing process exit — terminating this thread only.\n");
|
||
|
||
// 打印调用栈
|
||
void* array[30];
|
||
const int size = backtrace(array, 30);
|
||
LOG_ERROR("[EXIT-HOOK-BACKTRACE] %d frames:\n", size);
|
||
char** messages = backtrace_symbols(array, size);
|
||
if (messages)
|
||
{
|
||
for (int i = 0; i < size; i++)
|
||
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
||
free(messages);
|
||
}
|
||
LOG_ERROR("========================================\n");
|
||
|
||
// 仅退出当前线程,进程继续运行
|
||
pthread_exit(nullptr);
|
||
}
|
||
}
|
||
|
||
namespace
|
||
{
|
||
QString procStatusValue(const char* key)
|
||
{
|
||
#if defined(__linux__)
|
||
QFile f(QStringLiteral("/proc/self/status"));
|
||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
|
||
return QString();
|
||
|
||
const QByteArray prefix = QByteArray(key) + ':';
|
||
while (!f.atEnd())
|
||
{
|
||
const QByteArray line = f.readLine().trimmed();
|
||
if (line.startsWith(prefix))
|
||
return QString::fromLatin1(line.mid(prefix.size()).trimmed());
|
||
}
|
||
#else
|
||
Q_UNUSED(key);
|
||
#endif
|
||
return QString();
|
||
}
|
||
|
||
#if defined(__linux__)
|
||
|
||
std::string getThreadIdStr()
|
||
{
|
||
std::ostringstream oss;
|
||
oss << std::this_thread::get_id();
|
||
return oss.str();
|
||
}
|
||
|
||
void terminateHandler()
|
||
{
|
||
LOG_ERROR("========================================\n");
|
||
LOG_ERROR("[TERMINATE] std::terminate() called! thread=%s\n", getThreadIdStr().c_str());
|
||
LOG_ERROR("========================================\n");
|
||
|
||
// 尝试获取未捕获的异常信息
|
||
auto eptr = std::current_exception();
|
||
if (eptr)
|
||
{
|
||
try
|
||
{
|
||
std::rethrow_exception(eptr);
|
||
}
|
||
catch (const std::exception& e)
|
||
{
|
||
int status = 0;
|
||
char* demangled = abi::__cxa_demangle(typeid(e).name(), nullptr, nullptr, &status);
|
||
LOG_ERROR("[TERMINATE] uncaught exception type: %s\n",
|
||
(status == 0 && demangled) ? demangled : typeid(e).name());
|
||
LOG_ERROR("[TERMINATE] what(): %s\n", e.what());
|
||
if (demangled) free(demangled);
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_ERROR("[TERMINATE] uncaught exception (unknown type, not std::exception)\n");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LOG_ERROR("[TERMINATE] no active exception (terminate called without exception)\n");
|
||
}
|
||
|
||
// 打印调用栈
|
||
void* array[30];
|
||
const int size = backtrace(array, 30);
|
||
LOG_ERROR("[TERMINATE-BACKTRACE] Stack trace (%d frames):\n", size);
|
||
|
||
char** messages = backtrace_symbols(array, size);
|
||
if (messages)
|
||
{
|
||
for (int i = 0; i < size; i++)
|
||
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
||
free(messages);
|
||
}
|
||
else
|
||
{
|
||
LOG_ERROR(" (backtrace_symbols failed)\n");
|
||
}
|
||
|
||
LOG_ERROR("========================================\n");
|
||
LOG_ERROR("[TERMINATE] aborting...\n");
|
||
LOG_ERROR("========================================\n");
|
||
|
||
// 刷新日志后调用默认 abort
|
||
std::abort();
|
||
}
|
||
|
||
void atexitHandler()
|
||
{
|
||
LOG_ERROR("========================================\n");
|
||
LOG_ERROR("[ATEXIT] Program is exiting normally!\n");
|
||
LOG_ERROR("========================================\n");
|
||
|
||
// 打印调用栈看是谁调用了 exit
|
||
void* array[30];
|
||
const int size = backtrace(array, 30);
|
||
LOG_ERROR("[ATEXIT-BACKTRACE] Exit call stack (%d frames):\n", size);
|
||
|
||
char** messages = backtrace_symbols(array, size);
|
||
if (messages)
|
||
{
|
||
for (int i = 0; i < size; i++)
|
||
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
||
free(messages);
|
||
}
|
||
else
|
||
{
|
||
LOG_ERROR(" (backtrace_symbols failed)\n");
|
||
}
|
||
|
||
LOG_ERROR("========================================\n");
|
||
}
|
||
|
||
void signalHandler(int sig)
|
||
{
|
||
const char* sigName = "UNKNOWN";
|
||
switch (sig)
|
||
{
|
||
case SIGSEGV: sigName = "SIGSEGV (Segmentation Fault)"; break;
|
||
case SIGABRT: sigName = "SIGABRT (Abort)"; break;
|
||
case SIGTERM: sigName = "SIGTERM (Termination)"; break;
|
||
case SIGINT: sigName = "SIGINT (Interrupt)"; break;
|
||
case SIGILL: sigName = "SIGILL (Illegal Instruction)"; break;
|
||
case SIGFPE: sigName = "SIGFPE (Floating Point Exception)"; break;
|
||
case SIGBUS: sigName = "SIGBUS (Bus Error)"; break;
|
||
case SIGPIPE: sigName = "SIGPIPE (Broken Pipe)"; break;
|
||
default: break;
|
||
}
|
||
|
||
LOG_ERROR("========================================\n");
|
||
LOG_ERROR("[SIGNAL] Caught signal %d: %s\n", sig, sigName);
|
||
LOG_ERROR("========================================\n");
|
||
|
||
// 打印调用栈
|
||
void* array[30];
|
||
const int size = backtrace(array, 30);
|
||
LOG_ERROR("[BACKTRACE] Stack trace (%d frames):\n", size);
|
||
|
||
char** messages = backtrace_symbols(array, size);
|
||
if (messages)
|
||
{
|
||
for (int i = 0; i < size; i++)
|
||
LOG_ERROR(" [%d] %s\n", i, messages[i]);
|
||
free(messages);
|
||
}
|
||
else
|
||
{
|
||
LOG_ERROR(" (backtrace_symbols failed)\n");
|
||
}
|
||
|
||
LOG_ERROR("========================================\n");
|
||
|
||
// 对于致命信号,调用默认处理器
|
||
signal(sig, SIG_DFL);
|
||
raise(sig);
|
||
}
|
||
#endif
|
||
|
||
}
|
||
|
||
int main(int argc, char* argv[])
|
||
{
|
||
QCoreApplication app(argc, argv);
|
||
app.setApplicationName(DRONESCREWSERVER_PRODUCT_NAME);
|
||
app.setApplicationVersion(DRONESCREWSERVER_VERSION_STRING);
|
||
app.setOrganizationName(DRONESCREWSERVER_COMPANY_NAME);
|
||
|
||
VrLogUtils::InitLog();
|
||
VrLogUtils::EnableTime(false);
|
||
|
||
#if defined(__linux__)
|
||
// 注册 std::terminate 处理器(捕获未处理 C++ 异常,优先级最高)
|
||
std::set_terminate(terminateHandler);
|
||
LOG_INFO("[TERMINATE] std::terminate handler registered, main thread=%s\n",
|
||
getThreadIdStr().c_str());
|
||
|
||
// 注册 atexit 处理器(捕获正常退出)
|
||
atexit(atexitHandler);
|
||
LOG_INFO("[ATEXIT] Exit handler registered\n");
|
||
|
||
// 注册信号处理器
|
||
signal(SIGSEGV, signalHandler); // 段错误
|
||
signal(SIGABRT, signalHandler); // 异常终止
|
||
signal(SIGTERM, signalHandler); // 终止信号
|
||
signal(SIGINT, signalHandler); // 中断信号 (Ctrl+C)
|
||
signal(SIGILL, signalHandler); // 非法指令
|
||
signal(SIGFPE, signalHandler); // 浮点异常
|
||
signal(SIGBUS, signalHandler); // 总线错误
|
||
signal(SIGPIPE, signalHandler); // 管道破裂
|
||
LOG_INFO("[SIGNAL] Signal handlers registered\n");
|
||
|
||
// 屏蔽 ffmedia 库的 WARN 日志(仅在 ERROR 时打印)
|
||
extern unsigned int ff_log_level;
|
||
ff_log_level = 0; // 0=ERROR only, 1=WARN
|
||
|
||
// exit() 拦截已在链接时生效(本可执行文件定义的 exit 符号优先级高于 libc)
|
||
// 子线程调用 exit() 时仅结束该线程,不会杀死整个进程
|
||
LOG_INFO("[EXIT-HOOK] exit() interceptor active: main-thread exit passes through, child-thread exit converts to pthread_exit\n");
|
||
#endif
|
||
|
||
LOG_DEBUG("===========================================\n");
|
||
LOG_INFO(" %s\n", DRONESCREWSERVER_PRODUCT_NAME);
|
||
LOG_INFO(" V%s_%s built %s %s\n", DRONESCREWSERVER_VERSION_STRING,
|
||
DRONESCREWSERVER_VERSION_BUILD, __DATE__, __TIME__);
|
||
LOG_DEBUG("===========================================\n");
|
||
|
||
qRegisterMetaType<DroneScrewResult>("DroneScrewResult");
|
||
qRegisterMetaType<DroneScrewAlgoParams>("DroneScrewAlgoParams");
|
||
qRegisterMetaType<MvsImageData>("MvsImageData");
|
||
|
||
DroneScrewServerPresenter presenter;
|
||
DroneScrewZmqProtocol zmq;
|
||
|
||
// 信号槽:协议 → presenter
|
||
QObject::connect(&zmq, &DroneScrewZmqProtocol::rawImagePublishEnabledRequested,
|
||
&presenter, &DroneScrewServerPresenter::setRawPubEnabled,
|
||
Qt::DirectConnection);
|
||
QObject::connect(&zmq, &DroneScrewZmqProtocol::detectModeRequested,
|
||
&presenter, &DroneScrewServerPresenter::setDetectMode,
|
||
Qt::DirectConnection);
|
||
QObject::connect(&zmq, &DroneScrewZmqProtocol::setExposureRequested,
|
||
&presenter, &DroneScrewServerPresenter::handleSetExposure);
|
||
QObject::connect(&zmq, &DroneScrewZmqProtocol::setGainRequested,
|
||
&presenter, &DroneScrewServerPresenter::handleSetGain);
|
||
QObject::connect(&zmq, &DroneScrewZmqProtocol::updateAlgoParamsRequested,
|
||
&presenter, &DroneScrewServerPresenter::handleUpdateAlgoParams);
|
||
|
||
// presenter → 协议(发布检测结果)
|
||
QObject::connect(&presenter, &DroneScrewServerPresenter::detectionResult,
|
||
&zmq, &DroneScrewZmqProtocol::publishDetectionResult);
|
||
|
||
// presenter → 协议(原始图像 PUB)— 同步连接,回调内立即拷贝完成
|
||
QObject::connect(&presenter, &DroneScrewServerPresenter::rawImageReady,
|
||
&zmq, &DroneScrewZmqProtocol::publishRawImage,
|
||
Qt::DirectConnection);
|
||
|
||
zmq.setSingleDetectionHandler([&presenter]() {
|
||
return presenter.runSingleDetection();
|
||
});
|
||
zmq.setStartWorkHandler([&presenter]() {
|
||
return presenter.startDetectionWork();
|
||
});
|
||
zmq.setStopWorkHandler([&presenter]() {
|
||
return presenter.stopDetectionWork();
|
||
});
|
||
zmq.setFinalDetectionResultProvider([&presenter](DroneScrewResult& result) {
|
||
return presenter.getLastFinalDetectionResult(result);
|
||
});
|
||
zmq.setStartLiveStreamHandler([&presenter]() {
|
||
return presenter.startLiveStream();
|
||
});
|
||
zmq.setStopLiveStreamHandler([&presenter]() {
|
||
return presenter.stopLiveStream();
|
||
});
|
||
zmq.setSwapCameraRolesHandler([&presenter]() {
|
||
return presenter.swapCameraRoles();
|
||
});
|
||
zmq.setServerInfoProvider([&presenter]() {
|
||
return presenter.getRuntimeInfo();
|
||
});
|
||
zmq.setCalibrationInfoProvider([&presenter]() {
|
||
return presenter.getCalibrationInfo();
|
||
});
|
||
|
||
// 配置文件 —— 运行时配置优先(runtime_config.xml 不会被部署覆盖),
|
||
// config/config.xml 仅作为出厂默认值。
|
||
const QString appDir = QCoreApplication::applicationDirPath();
|
||
const QString runtimeCfgPath = QDir(appDir).filePath("runtime_config.xml");
|
||
|
||
auto findDefaultConfig = [&]() -> QString {
|
||
const QString a = QDir(appDir).filePath("config/config.xml");
|
||
if (QFileInfo::exists(a)) return a;
|
||
const QString b = QDir::current().filePath("config/config.xml");
|
||
if (QFileInfo::exists(b)) return b;
|
||
return QString();
|
||
};
|
||
|
||
// 之后所有 saveConfiguration 调用都写到 runtime_config.xml
|
||
presenter.setConfigFilePath(runtimeCfgPath);
|
||
|
||
if (QFileInfo::exists(runtimeCfgPath))
|
||
{
|
||
// 正常路径:加载持久化的运行时配置
|
||
LOG_INFO("[CONFIG] loading runtime config: %s\n", runtimeCfgPath.toStdString().c_str());
|
||
if (!presenter.loadConfiguration(runtimeCfgPath))
|
||
{
|
||
LOG_WARN("[CONFIG] runtime config load failed, falling back to defaults\n");
|
||
const QString fallback = findDefaultConfig();
|
||
if (!fallback.isEmpty())
|
||
presenter.loadConfiguration(fallback);
|
||
// loadConfiguration 会覆盖 m_configFilePath,需要重新指回 runtime 路径
|
||
presenter.setConfigFilePath(runtimeCfgPath);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 首次部署:加载出厂默认配置,然后直接复制到 runtime_config.xml
|
||
const QString defaultCfg = findDefaultConfig();
|
||
if (!defaultCfg.isEmpty())
|
||
{
|
||
LOG_INFO("[CONFIG] first run, loading default: %s\n", defaultCfg.toStdString().c_str());
|
||
presenter.loadConfiguration(defaultCfg);
|
||
// loadConfiguration 会覆盖 m_configFilePath,需要重新指回 runtime 路径
|
||
presenter.setConfigFilePath(runtimeCfgPath);
|
||
|
||
// 直接复制默认配置文件到运行时位置,保留所有元素
|
||
if (QFile::copy(defaultCfg, runtimeCfgPath))
|
||
LOG_INFO("[CONFIG] default config copied to: %s\n", runtimeCfgPath.toStdString().c_str());
|
||
else
|
||
LOG_WARN("[CONFIG] failed to copy default config to: %s\n", runtimeCfgPath.toStdString().c_str());
|
||
}
|
||
else
|
||
{
|
||
LOG_INFO("[CONFIG] no config file found, using defaults\n");
|
||
}
|
||
}
|
||
|
||
// 初始化各子系统
|
||
presenter.initAll();
|
||
|
||
// 启动 ZMQ
|
||
zmq.setRtspUrl(presenter.getRtspUrl());
|
||
if (!zmq.startServer(presenter.getZmqControlPort(),
|
||
presenter.getZmqResultPort(),
|
||
presenter.getZmqRawImagePort()))
|
||
{
|
||
LOG_ERROR("ZMQ start fail\n");
|
||
return ERR_CODE(NET_ERR_CREAT_INIT);
|
||
}
|
||
|
||
// raw 图像发布由 start.mode 控制:pull_stream 关闭,subscribe 开启。
|
||
presenter.setRawPubEnabled(false);
|
||
|
||
LOG_INFO("=== DroneScrewServer started, ZMQ ctrl=%d result=%d raw=%d, RTSP=%s ===\n",
|
||
presenter.getZmqControlPort(),
|
||
presenter.getZmqResultPort(),
|
||
presenter.getZmqRawImagePort(),
|
||
presenter.getRtspUrl().toStdString().c_str());
|
||
|
||
const auto startedAt = std::chrono::steady_clock::now();
|
||
QTimer heartbeatTimer;
|
||
heartbeatTimer.setInterval(60000);
|
||
QObject::connect(&heartbeatTimer, &QTimer::timeout, [&presenter, startedAt]() {
|
||
const auto now = std::chrono::steady_clock::now();
|
||
const auto uptimeSec =
|
||
std::chrono::duration_cast<std::chrono::seconds>(now - startedAt).count();
|
||
const QJsonObject info = presenter.getRuntimeInfo();
|
||
const QString mode = info["mode"].toString();
|
||
const QString rss = procStatusValue("VmRSS");
|
||
const QString vmPeak = procStatusValue("VmPeak");
|
||
const QString vmSize = procStatusValue("VmSize");
|
||
const QString threads = procStatusValue("Threads");
|
||
LOG_INFO("[HEARTBEAT] uptime=%llds mode=%s rss=%s vmPeak=%s vmSize=%s threads=%s\n",
|
||
static_cast<long long>(uptimeSec),
|
||
mode.toStdString().c_str(),
|
||
rss.isEmpty() ? "N/A" : rss.toStdString().c_str(),
|
||
vmPeak.isEmpty() ? "N/A" : vmPeak.toStdString().c_str(),
|
||
vmSize.isEmpty() ? "N/A" : vmSize.toStdString().c_str(),
|
||
threads.isEmpty() ? "N/A" : threads.toStdString().c_str());
|
||
});
|
||
heartbeatTimer.start();
|
||
|
||
return app.exec();
|
||
}
|
||
#else
|
||
int main()
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
#endif
|