544 lines
19 KiB
C++
Raw Normal View History

2026-06-26 17:55:15 +08:00
#include <QCoreApplication>
#include <QTimer>
#include <QDir>
#include <QFileInfo>
#include <QFile>
#include <QJsonObject>
2026-07-11 15:28:36 +08:00
#include <QStringList>
2026-06-26 17:55:15 +08:00
#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;
2026-07-11 15:28:36 +08:00
QString simulationImageDirArg;
bool simulationArgSpecified = false;
const QStringList args = QCoreApplication::arguments();
for (int i = 1; i < args.size(); ++i)
{
const QString arg = args.at(i);
if (arg == QStringLiteral("--simulate-dir") ||
arg == QStringLiteral("--sim-dir"))
{
simulationArgSpecified = true;
if (i + 1 >= args.size())
{
LOG_ERROR("[SIM] missing directory after %s\n",
arg.toStdString().c_str());
return ERR_CODE(FILE_ERR_NOEXIST);
}
simulationImageDirArg = args.at(++i);
continue;
}
const QString simulateDirPrefix = QStringLiteral("--simulate-dir=");
const QString simDirPrefix = QStringLiteral("--sim-dir=");
if (arg.startsWith(simulateDirPrefix))
{
simulationArgSpecified = true;
simulationImageDirArg = arg.mid(simulateDirPrefix.size());
continue;
}
if (arg.startsWith(simDirPrefix))
{
simulationArgSpecified = true;
simulationImageDirArg = arg.mid(simDirPrefix.size());
continue;
}
if (!arg.startsWith(QStringLiteral("-")) && !simulationArgSpecified)
{
simulationArgSpecified = true;
simulationImageDirArg = arg;
}
}
2026-06-26 17:55:15 +08:00
// 信号槽:协议 → 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();
});
2026-07-03 14:19:06 +08:00
zmq.setFinalDetectionResultProvider([&presenter](DroneScrewResult& result) {
return presenter.getLastFinalDetectionResult(result);
});
2026-06-26 17:55:15 +08:00
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");
}
}
// 初始化各子系统
2026-07-11 15:28:36 +08:00
if (simulationArgSpecified)
{
const QString trimmed = simulationImageDirArg.trimmed();
if (trimmed.isEmpty())
{
LOG_ERROR("[SIM] simulation image directory is empty\n");
return ERR_CODE(FILE_ERR_NOEXIST);
}
const QFileInfo inputInfo(trimmed);
const QString absolutePath = inputInfo.isAbsolute()
? inputInfo.absoluteFilePath()
: QDir::current().absoluteFilePath(trimmed);
const QFileInfo simulationDirInfo(absolutePath);
if (!simulationDirInfo.exists() || !simulationDirInfo.isDir())
{
LOG_ERROR("[SIM] simulation image directory does not exist: %s\n",
absolutePath.toStdString().c_str());
return ERR_CODE(FILE_ERR_NOEXIST);
}
presenter.setSimulationImageDir(simulationDirInfo.absoluteFilePath());
}
2026-06-26 17:55:15 +08:00
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