diff --git a/CloudUtils/Inc/LaserDataLoader.h b/CloudUtils/Inc/LaserDataLoader.h index 7121bb6..2c2f108 100644 --- a/CloudUtils/Inc/LaserDataLoader.h +++ b/CloudUtils/Inc/LaserDataLoader.h @@ -1,24 +1,24 @@ -#ifndef LASER_DATA_LOADER_H -#define LASER_DATA_LOADER_H - -#include -#include +#ifndef LASER_DATA_LOADER_H +#define LASER_DATA_LOADER_H + +#include +#include #include #include "VZNL_Types.h" #include "VrError.h" #include "PointCloudXYZRange.h" - -// 保存进度回调接口 -class ISaveProgressCallback -{ -public: - virtual ~ISaveProgressCallback() = default; - // 进度回调,progress 范围 0.0~1.0 - virtual void onSaveProgress(float progress) = 0; - // 返回 true 取消保存 - virtual bool isSaveCancelled() const { return false; } -}; - + +// 保存进度回调接口 +class ISaveProgressCallback +{ +public: + virtual ~ISaveProgressCallback() = default; + // 进度回调,progress 范围 0.0~1.0 + virtual void onSaveProgress(float progress) = 0; + // 返回 true 取消保存 + virtual bool isSaveCancelled() const { return false; } +}; + // 范围过滤后的源扫描线布局,仅在对应回调期间使用。 struct LaserLineLoadInfo { @@ -43,10 +43,10 @@ public: using LaserLoadFinishedCallback = std::function; - LaserDataLoader(); - ~LaserDataLoader(); - - // 从文件加载激光扫描数据 - 统一接口,根据文件格式自动判断数据类型 + LaserDataLoader(); + ~LaserDataLoader(); + + // 从文件加载激光扫描数据 - 统一接口,根据文件格式自动判断数据类型 int LoadLaserScanData(const std::string& fileName, std::vector>& laserLines, int& lineNum, @@ -82,38 +82,49 @@ public: const LaserLineLoadIndexedCallback& lineCallback, const LaserLoadFinishedCallback& finishedCallback, const PointCloudXYZRange& range); - - // 保存激光扫描数据到文件 - 统一接口,支持两种类型的数据 - int SaveLaserScanData(const std::string& fileName, - const std::vector>& laserLines, - int lineNum, - float scanSpeed, - int maxTimeStamp, - int clockPerSecond); - - int DebugSaveLaser(std::string fileName, std::vector> xyzData); - int DebugSaveLaser(std::string fileName, std::vector> xyzData, - ISaveProgressCallback* callback); - - // 释放统一格式数据内存 - void FreeLaserScanData(std::vector>& laserLines); - - // 释放转换后的SVzNL3DLaserLine数据内存 - void FreeConvertedData(std::vector& xyzData); - - // 释放转换后的SVzNLXYZRGBDLaserLine数据内存 - void FreeConvertedData(std::vector& rgbdData); - - // 转换统一格式数据为SVzNL3DLaserLine格式 - int ConvertToSVzNL3DLaserLine(const std::vector>& unifiedData, - std::vector& xyzData); - - // 转换统一格式数据为SVzNLXYZRGBDLaserLine格式 - int ConvertToSVzNLXYZRGBDLaserLine(const std::vector>& unifiedData, - std::vector& rgbdData); - - // 转换统一格式数据为std::vector>格式 - // validPointCount: 可选输出参数,返回有效点个数(xyz全非0的点) + + // 保存激光扫描数据到文件 - 统一接口,支持两种类型的数据 + // lineNum 参数保留以兼容调用方,文件行数始终使用 laserLines.size()。 + int SaveLaserScanData(const std::string& fileName, + const std::vector>& laserLines, + int lineNum, + float scanSpeed, + int maxTimeStamp, + int clockPerSecond); + + // 保存兼容旧加载器的 .dat:每行最多4096点,仅保留XYZ和双目坐标,不保存颜色/强度。 + // lineNum 参数保留以兼容调用方,文件行数始终使用 laserLines.size()。 + // 旧格式帧号为有符号32位、时间戳为无符号32位;加载后统一为Position,二维坐标取整。 + int SaveLaserScanDataBinary(const std::string& fileName, + const std::vector>& laserLines, + int lineNum, + float scanSpeed, + int maxTimeStamp, + int clockPerSecond); + + int DebugSaveLaser(std::string fileName, std::vector> xyzData); + int DebugSaveLaser(std::string fileName, std::vector> xyzData, + ISaveProgressCallback* callback); + + // 释放统一格式数据内存 + void FreeLaserScanData(std::vector>& laserLines); + + // 释放转换后的SVzNL3DLaserLine数据内存 + void FreeConvertedData(std::vector& xyzData); + + // 释放转换后的SVzNLXYZRGBDLaserLine数据内存 + void FreeConvertedData(std::vector& rgbdData); + + // 转换统一格式数据为SVzNL3DLaserLine格式 + int ConvertToSVzNL3DLaserLine(const std::vector>& unifiedData, + std::vector& xyzData); + + // 转换统一格式数据为SVzNLXYZRGBDLaserLine格式 + int ConvertToSVzNLXYZRGBDLaserLine(const std::vector>& unifiedData, + std::vector& rgbdData); + + // 转换统一格式数据为std::vector>格式 + // validPointCount: 可选输出参数,返回有效点个数(xyz全非0的点) int ConvertToSVzNL3DPosition(const std::vector>& unifiedData, std::vector>& scanLines, size_t* validPointCount = nullptr); @@ -131,19 +142,19 @@ public: // 保留旧名称,兼容已有调用方。 int LoadPolySegments(const std::string& fileName, std::vector>& polyLines); - - // 获取最后的错误信息 - std::string GetLastError() const { return m_lastError; } - -private: + + // 获取最后的错误信息 + std::string GetLastError() const { return m_lastError; } + +private: // 读取XYZ格式的激光数据 int _ParseLaserScanPoint(const std::string& data, SVzNL3DPosition& sData, SVzNL2DPosition& s2DData); int _ParseLaserScanPoint(const std::string& data, SVzNL3DPosition& sData, SVzNL2DPositionF& s2DData); // 读取RGBD格式的激光数据 - 新增RGBD支持 - int _ParseLaserScanPoint(const std::string& data, SVzNLPointXYZRGBA& sData, SVzNL2DLRPoint& s2DData); - - // 获取激光数据类型 + int _ParseLaserScanPoint(const std::string& data, SVzNLPointXYZRGBA& sData, SVzNL2DLRPoint& s2DData); + + // 获取激光数据类型 int _GetLaserType(const std::string& fileName, EVzResultDataType& eDataType); // Read binary .dat laser scan data. @@ -162,7 +173,7 @@ private: const PointCloudXYZRange& range); std::string m_lastError; - static const int VZ_LASER_LINE_PT_MAX_NUM = 4096; -}; - -#endif // LASER_DATA_LOADER_H + static const int VZ_LASER_LINE_PT_MAX_NUM = 4096; +}; + +#endif // LASER_DATA_LOADER_H diff --git a/CloudUtils/Src/LaserDataLoader.cpp b/CloudUtils/Src/LaserDataLoader.cpp index 2109408..81a087e 100644 --- a/CloudUtils/Src/LaserDataLoader.cpp +++ b/CloudUtils/Src/LaserDataLoader.cpp @@ -10,9 +10,11 @@ #include #include #include +#include #include #include #include +#include #include #include "VrLog.h" @@ -97,6 +99,7 @@ struct DatLineScan3DData }; static_assert(sizeof(DatLaserScan3DLinesHeader) == 40, "Unexpected dat header layout"); +static_assert(sizeof(DatLineScan3DData) == 16, "Unexpected dat line layout"); static_assert(sizeof(Dat3DFeaturePoint) == 112, "Unexpected dat feature point layout"); template @@ -989,11 +992,15 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, { LOG_INFO("Saving to file: %s\n", fileName.c_str()); - if (laserLines.empty() || lineNum <= 0) { + if (laserLines.empty() || + laserLines.size() > static_cast((std::numeric_limits::max)())) + { m_lastError = "Invalid input parameters for saving unified data"; LOG_ERROR("Invalid parameters for saving unified laser data\n"); return ERR_CODE(DEV_ARG_INVAILD); } + // 以实际写入的扫描线数为准,避免过滤或首尾裁剪后沿用旧行数。 + lineNum = static_cast(laserLines.size()); try { std::ofstream sw(fileName); @@ -1115,6 +1122,12 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, return ERR_CODE(FILE_ERR_WRITE); } sw.close(); + // 关闭文件时也可能提交写入失败,不能让调用方误以为现场已保存完整。 + if (!sw.good()) + { + m_lastError = "Failed while closing file: " + fileName; + return ERR_CODE(FILE_ERR_WRITE); + } return SUCCESS; } catch (const std::exception& e) { @@ -1124,6 +1137,177 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, } } +int LaserDataLoader::SaveLaserScanDataBinary( + const std::string& fileName, + const std::vector>& laserLines, + int lineNum, + float scanSpeed, + int maxTimeStamp, + int clockPerSecond) +{ + LOG_INFO("Saving binary laser scan data to file: %s\n", fileName.c_str()); + if (laserLines.empty() || + laserLines.size() > static_cast((std::numeric_limits::max)())) + { + m_lastError = "Invalid input parameters for binary laser data"; + return ERR_CODE(DEV_ARG_INVAILD); + } + // 与文本格式一致,中间全零线仍占一行,不按有效点或原始帧号计数。 + lineNum = static_cast(laserLines.size()); + + // 打开目标文件前校验全部扫描线,避免无效输入覆盖已有文件。 + size_t maxPointCount = 0; + for (const auto& linePair : laserLines) + { + const SVzLaserLineData& line = linePair.second; + if (line.nPointCount < 0 || line.nPointCount > VZ_LASER_LINE_PT_MAX_NUM) + { + m_lastError = "Point count outside binary laser line limit"; + return ERR_CODE(DATA_ERR_LEN); + } + if (linePair.first != keResultDataType_Position && + linePair.first != keResultDataType_PositionF && + linePair.first != keResultDataType_PointXYZRGBA && + linePair.first != keResultDataType_PointXYZI) + { + m_lastError = "Unsupported laser data type for binary storage"; + return ERR_CODE(DEV_ARG_INVAILD); + } + if (line.nPointCount > 0 && !line.p3DPoint) + { + m_lastError = "Missing 3D data for binary laser line"; + return ERR_CODE(DEV_ARG_INVAILD); + } + if (line.llFrameIdx > static_cast((std::numeric_limits::max)()) || + line.llTimeStamp > (std::numeric_limits::max)()) + { + m_lastError = "Frame index or timestamp exceeds legacy binary format range"; + return ERR_CODE(DEV_ARG_INVAILD); + } + maxPointCount = std::max(maxPointCount, static_cast(line.nPointCount)); + } + + // 复用单行缓冲区,减少逐点写入调用,内存用量不随扫描行数增长。 + std::vector features(maxPointCount); + std::ofstream outputFile(fileName, std::ios::binary | std::ios::trunc); + if (!outputFile.is_open()) + { + m_lastError = "Cannot open file for binary writing: " + fileName; + return ERR_CODE(FILE_ERR_WRITE); + } + + DatLaserScan3DLinesHeader header = {}; + header.adjusted = 1; + header.scanLineNum = lineNum; + header.lineV = static_cast(scanSpeed); + header.clock = clockPerSecond; + header.maxTimeStamp = static_cast(std::max(0, maxTimeStamp)); + outputFile.write(reinterpret_cast(&header), sizeof(header)); + if (!outputFile.good()) + { + m_lastError = "Failed while writing binary header: " + fileName; + return ERR_CODE(FILE_ERR_WRITE); + } + + for (const auto& linePair : laserLines) + { + const SVzLaserLineData& line = linePair.second; + const int pointCount = line.nPointCount; + DatLineScan3DData lineHeader = {}; + lineHeader.lineIndex = static_cast(line.llFrameIdx); + lineHeader.timeStamp = static_cast(line.llTimeStamp); + lineHeader.lineStepping = line.dStep; + char lineRecord[40] = {}; + std::memcpy(lineRecord, &lineHeader, sizeof(lineHeader)); + outputFile.write(lineRecord, sizeof(lineRecord)); + + outputFile.write(reinterpret_cast(&pointCount), sizeof(pointCount)); + const SVzNL3DPosition* positionPoints = nullptr; + const SVzNLPointXYZRGBA* rgbaPoints = nullptr; + const SVzNLPointXYZI* intensityPoints = nullptr; + if (linePair.first == keResultDataType_Position || + linePair.first == keResultDataType_PositionF) + { + positionPoints = static_cast(line.p3DPoint); + } + else if (linePair.first == keResultDataType_PointXYZRGBA) + { + rgbaPoints = static_cast(line.p3DPoint); + } + else if (linePair.first == keResultDataType_PointXYZI) + { + intensityPoints = static_cast(line.p3DPoint); + } + for (int index = 0; index < pointCount; ++index) + { + Dat3DFeaturePoint& feature = features[index]; + feature = Dat3DFeaturePoint(); + feature.index = index; + feature.flag = 1; + if (positionPoints) + { + feature.eye.X = positionPoints[index].pt3D.x; + feature.eye.Y = positionPoints[index].pt3D.y; + feature.eye.Z = positionPoints[index].pt3D.z; + } + else if (rgbaPoints) + { + feature.eye.X = rgbaPoints[index].x; + feature.eye.Y = rgbaPoints[index].y; + feature.eye.Z = rgbaPoints[index].z; + } + else if (intensityPoints) + { + feature.eye.X = intensityPoints[index].x; + feature.eye.Y = intensityPoints[index].y; + feature.eye.Z = intensityPoints[index].z; + } + if (line.p2DPoint && linePair.first == keResultDataType_Position) + { + const SVzNL2DPosition* points2D = static_cast(line.p2DPoint); + feature.stereo.x = points2D[index].ptLeft2D.x; + feature.stereo.y = points2D[index].ptLeft2D.y; + feature.stereoRight.x = points2D[index].ptRight2D.x; + feature.stereoRight.y = points2D[index].ptRight2D.y; + } + else if (line.p2DPoint && linePair.first == keResultDataType_PositionF) + { + const SVzNL2DPositionF* points2D = static_cast(line.p2DPoint); + feature.stereo.x = points2D[index].ptLeft2D.x; + feature.stereo.y = points2D[index].ptLeft2D.y; + feature.stereoRight.x = points2D[index].ptRight2D.x; + feature.stereoRight.y = points2D[index].ptRight2D.y; + } + else if (line.p2DPoint) + { + const SVzNL2DLRPoint* points2D = static_cast(line.p2DPoint); + feature.stereo.x = points2D[index].sLeft.x; + feature.stereo.y = points2D[index].sLeft.y; + feature.stereoRight.x = points2D[index].sRight.x; + feature.stereoRight.y = points2D[index].sRight.y; + } + } + if (pointCount > 0) + { + outputFile.write(reinterpret_cast(features.data()), + static_cast(pointCount * sizeof(Dat3DFeaturePoint))); + } + if (!outputFile.good()) + { + m_lastError = "Failed while writing binary laser line: " + fileName; + return ERR_CODE(FILE_ERR_WRITE); + } + } + + outputFile.close(); + if (!outputFile.good()) + { + m_lastError = "Failed while writing binary file: " + fileName; + return ERR_CODE(FILE_ERR_WRITE); + } + return SUCCESS; +} + int LaserDataLoader::DebugSaveLaser(std::string fileName, std::vector > xyzData) { return DebugSaveLaser(std::move(fileName), std::move(xyzData), nullptr); @@ -1566,9 +1750,8 @@ int LaserDataLoader::ConvertToSVzNLPositionD( } } - if (!scanLine.empty()) { - scanLines.push_back(scanLine); - } + // 中间零点数扫描线也必须占位,转换后的行序与过滤及保存结果一致。 + scanLines.push_back(std::move(scanLine)); } if (scanLines.empty()) { diff --git a/CloudView3D/Src/PointCloudGLWidget.cpp b/CloudView3D/Src/PointCloudGLWidget.cpp index 82dbd4e..ddc85dc 100644 --- a/CloudView3D/Src/PointCloudGLWidget.cpp +++ b/CloudView3D/Src/PointCloudGLWidget.cpp @@ -404,8 +404,9 @@ void PointCloudGLWidget::repositionOverlayControls() const int y = qMax(0, (widgetHeight - buttonHeight * 2 - gap) / 2); m_zoomInButton->move(x, y); m_zoomOutButton->move(x, y + buttonHeight + gap); - } -} + } + +} void PointCloudGLWidget::updateViewAngleDisplay() @@ -3005,14 +3006,7 @@ void PointCloudGLWidget::drawPosePoints() } // 从视图矩阵取相机右/上方向,把轴线拉成面向屏幕的窄带,避免 GLES2 线宽被钳成 1 像素。 - const QMatrix4x4 invView = m_view.inverted(); - const QVector3D camRight = invView.column(0).toVector3D().normalized(); - const QVector3D camUp = invView.column(1).toVector3D().normalized(); - const float pixelWorld = m_distance * - std::tan(qDegreesToRadians(22.5f)) / - static_cast(qMax(1, height() / 2)); - - auto applyEulerRotation = [this](QMatrix4x4& poseLocal, const PosePoint& pose) + auto applyEulerRotation = [this](QMatrix4x4& poseLocal, const PosePoint& pose) { switch (m_eulerRotationOrder) { @@ -3049,45 +3043,32 @@ void PointCloudGLWidget::drawPosePoints() } }; - auto appendWorld = [](QVector& dst, const QVector3D& p) - { - dst.append(p.x()); - dst.append(p.y()); - dst.append(p.z()); - }; - - auto appendAxisBand = [&](QVector& positions, - QVector& colors, - const QVector3D& start, - const QVector3D& end, - const QVector3D& color, - float halfWidth) - { - QVector3D side = QVector3D::crossProduct(end - start, camUp); - if (side.lengthSquared() < 1.0e-12f) - { - side = QVector3D::crossProduct(end - start, camRight); - } - if (!side.isNull()) - { - side.normalize(); - side *= halfWidth; - } - const QVector3D s0 = start - side; - const QVector3D s1 = start + side; - const QVector3D e0 = end - side; - const QVector3D e1 = end + side; - const QVector3D quad[6] = {s0, e0, e1, s0, e1, s1}; - for (const QVector3D& p : quad) - { - appendWorld(positions, p); - appendWorld(colors, color); - } - }; - - for (const auto& pose : m_posePoints) - { - QMatrix4x4 poseLocal; + QVector positions; + QVector colors; + positions.reserve(m_posePoints.size() * 18); + colors.reserve(m_posePoints.size() * 18); + + const auto appendAxis = [&](const QVector3D& start, + const QVector3D& end, + const QVector3D& color) + { + positions.append(start.x()); + positions.append(start.y()); + positions.append(start.z()); + positions.append(end.x()); + positions.append(end.y()); + positions.append(end.z()); + for (int i = 0; i < 2; ++i) + { + colors.append(color.x()); + colors.append(color.y()); + colors.append(color.z()); + } + }; + + for (const PosePoint& pose : m_posePoints) + { + QMatrix4x4 poseLocal; poseLocal.setToIdentity(); poseLocal.translate(pose.x, pose.y, pose.z); applyEulerRotation(poseLocal, pose); @@ -3097,40 +3078,18 @@ void PointCloudGLWidget::drawPosePoints() const QVector3D xEnd = poseLocal.map(QVector3D(axisLength, 0.0f, 0.0f)); const QVector3D yEnd = poseLocal.map(QVector3D(0.0f, axisLength, 0.0f)); const QVector3D zEnd = poseLocal.map(QVector3D(0.0f, 0.0f, axisLength)); - const float halfWidth = qMax(axisLength * 0.02f, pixelWorld * 2.0f); - - QVector bandPositions; - QVector bandColors; - bandPositions.reserve(54); - bandColors.reserve(54); - appendAxisBand(bandPositions, bandColors, origin, xEnd, QVector3D(1.0f, 0.0f, 0.0f), halfWidth); - appendAxisBand(bandPositions, bandColors, origin, yEnd, QVector3D(0.0f, 1.0f, 0.0f), halfWidth); - appendAxisBand(bandPositions, bandColors, origin, zEnd, QVector3D(0.0f, 0.0f, 1.0f), halfWidth); - - // 关闭深度测试,避免姿态轴被周围点云挡住。 - drawPrimitives(m_mvp, - bandPositions.constData(), - bandPositions.size() / 3, - GL_TRIANGLES, - bandColors.constData(), - QVector3D(1.0f, 1.0f, 1.0f), - 1.0f, - 1.0f, - false); - - const float tipPositions[] = { - origin.x(), origin.y(), origin.z(), - xEnd.x(), xEnd.y(), xEnd.z(), - yEnd.x(), yEnd.y(), yEnd.z(), - zEnd.x(), zEnd.y(), zEnd.z() - }; - const float tipColors[] = { - 1.0f, 1.0f, 1.0f, - 1.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 1.0f - }; - drawPrimitives(m_mvp, tipPositions, 4, GL_POINTS, tipColors, - QVector3D(1.0f, 1.0f, 1.0f), 10.0f, 1.0f, false); - } -} + appendAxis(origin, xEnd, QVector3D(1.0f, 0.0f, 0.0f)); + appendAxis(origin, yEnd, QVector3D(0.0f, 1.0f, 0.0f)); + appendAxis(origin, zEnd, QVector3D(0.0f, 0.0f, 1.0f)); + + } + drawPrimitives(m_mvp, + positions.constData(), + positions.size() / 3, + GL_LINES, + colors.constData(), + QVector3D(1.0f, 1.0f, 1.0f), + 1.0f, + 1.0f, + false); +} diff --git a/VrUtils/Inc/IVrUtils.h b/VrUtils/Inc/IVrUtils.h index 200f088..8ceebbc 100644 --- a/VrUtils/Inc/IVrUtils.h +++ b/VrUtils/Inc/IVrUtils.h @@ -7,14 +7,22 @@ #include "VrMD5Utils.h" #include "VrStringUtils.h" #include "VrNumUtils.h" -#include "VrLog.h" +#include "VrLog.h" +#include "VrPathUtils.h" +#include "VrCrashHandler.h" #include "VrShellUtils.h" #include "VrNetUtils.h" #include "VrDebugTime.h" #include "VrNTPUtils.h" #include "../crc/checksum.h" -#include "../ini/SimpleIni.h" - +#include "../ini/SimpleIni.h" + +namespace VrUtilsDetail +{ + // 静态库调用方保留处理器对象文件,安装过程不经过日志初始化。 + static VrCrashHandler::AutoInitializer crashHandlerAutoInitializer; +} + #ifndef M_PI #define M_PI 3.14159265358979323846 #endif diff --git a/VrUtils/Inc/VrCrashHandler.h b/VrUtils/Inc/VrCrashHandler.h new file mode 100644 index 0000000..0afdaac --- /dev/null +++ b/VrUtils/Inc/VrCrashHandler.h @@ -0,0 +1,28 @@ +#ifndef VR_CRASH_HANDLER_H +#define VR_CRASH_HANDLER_H + +#include + +namespace VrCrashHandler +{ + // 启动阶段独立初始化,多个入口只执行一次。 + void Initialize(); + // 正常上下文调用,创建目录并安装进程级处理器。路径为 UTF-8。 + // 首次空路径使用 VrPathUtils 的日志目录/crash;重复空路径调用保留当前目录。 + bool Enable(const std::string& outputDirectory = std::string()); + // 在启动/配置阶段调用;恢复安装前的处理器,不删除已保存文件。 + void Disable(); + bool IsEnabled(); + std::string GetOutputDirectory(); + + class AutoInitializer + { + public: + AutoInitializer() + { + Initialize(); + } + }; +} + +#endif diff --git a/VrUtils/Inc/VrLog.h b/VrUtils/Inc/VrLog.h index 2fe4128..cc0e3d8 100644 --- a/VrUtils/Inc/VrLog.h +++ b/VrUtils/Inc/VrLog.h @@ -2,11 +2,8 @@ #define __VR_LOG__H_ #include -#include - -#ifdef _WIN32 -#pragma comment(lib, "Shell32.lib") // VrLog 需要 SHGetFolderPathA -#endif // _WIN32 +#include +#include #ifdef _WIN32 #define VR_UTILS_API @@ -61,8 +58,9 @@ enum VrLogType KELOGTYPE_All = 0xff, }; -namespace VrLogUtils -{ +namespace VrLogUtils +{ + /// 返回与日志文件相同的目录规则,供崩溃文件等诊断数据复用。 /// 初始化log VR_UTILS_API void InitLog(); diff --git a/VrUtils/Inc/VrPathUtils.h b/VrUtils/Inc/VrPathUtils.h new file mode 100644 index 0000000..461f106 --- /dev/null +++ b/VrUtils/Inc/VrPathUtils.h @@ -0,0 +1,11 @@ +#ifndef VR_PATH_UTILS_H +#define VR_PATH_UTILS_H + +#include + +namespace VrPathUtils +{ + std::string GetLogDirectory(); +} + +#endif diff --git a/VrUtils/Src/VrCrashHandler.cpp b/VrUtils/Src/VrCrashHandler.cpp new file mode 100644 index 0000000..9f0850d --- /dev/null +++ b/VrUtils/Src/VrCrashHandler.cpp @@ -0,0 +1,997 @@ +#include "VrCrashHandler.h" +#include "VrPathUtils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#elif defined(__linux__) +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__GLIBC__) +#include +#endif +#endif + +namespace +{ +#if defined(_WIN32) || defined(__linux__) + const int maxFrames = 64; + + struct OutputTarget + { + std::string directory; +#ifdef _WIN32 + std::wstring directoryPath; +#endif + }; + + // 回调只读不可变快照,不获取配置锁。旧快照保留到进程结束,避免切换目录时悬空。 + std::atomic outputTarget{nullptr}; + std::atomic_flag handlingCrash = ATOMIC_FLAG_INIT; + +#ifdef _WIN32 + using NativeFile = HANDLE; + const NativeFile invalidFile = INVALID_HANDLE_VALUE; +#else + using NativeFile = int; + const NativeFile invalidFile = -1; +#endif + + void writeBytes(NativeFile file, const char* data, size_t size) + { + while (size > 0) + { +#ifdef _WIN32 + DWORD written = 0; + if (!WriteFile(file, data, static_cast(size), &written, nullptr) || written == 0) + { + return; + } +#else + const ssize_t written = write(file, data, size); + if (written < 0 && errno == EINTR) + { + continue; + } + if (written <= 0) + { + return; + } +#endif + data += written; + size -= static_cast(written); + } + } + + void writeText(NativeFile file, const char* text) + { + size_t size = 0; + while (text[size] != '\0') + { + ++size; + } + writeBytes(file, text, size); + } + + // 信号路径不用 printf/iostream 或日志库,避免堆分配和 stdio/日志锁重入。 + void writeNumber(NativeFile file, const char* label, uint64_t value, unsigned int base = 10) + { + char buffer[32]; + char* end = buffer + sizeof(buffer); + char* begin = end; + do + { + *--begin = "0123456789abcdef"[value % base]; + value /= base; + } while (value != 0); + writeText(file, label); + if (base == 16) + { + writeText(file, "0x"); + } + writeBytes(file, begin, static_cast(end - begin)); + writeText(file, "\n"); + } + +#ifdef _WIN32 + struct DebugFunctions + { + HMODULE library = nullptr; + decltype(&SymInitialize) initialize = nullptr; + decltype(&SymSetOptions) setOptions = nullptr; + decltype(&SymFromAddr) fromAddress = nullptr; + decltype(&SymGetLineFromAddr64) getLine = nullptr; + decltype(&SymFunctionTableAccess64) functionTable = nullptr; + decltype(&SymGetModuleBase64) moduleBase = nullptr; + decltype(&StackWalk64) walk = nullptr; + decltype(&MiniDumpWriteDump) dump = nullptr; + bool symbolsReady = false; + }; + + // 正常初始化阶段加载 DbgHelp,静态库的调用方无需额外链接 dbghelp.lib。 + DebugFunctions debugFunctions; + + void prepareUnwinder() + { + if (debugFunctions.library) + { + return; + } + wchar_t systemPath[MAX_PATH] = {}; + const UINT length = GetSystemDirectoryW(systemPath, MAX_PATH); + if (length == 0 || length >= MAX_PATH - 13) + { + return; + } + const std::wstring libraryPath = std::wstring(systemPath) + L"\\dbghelp.dll"; + debugFunctions.library = LoadLibraryW(libraryPath.c_str()); + if (!debugFunctions.library) + { + return; + } +#define VR_LOAD_DEBUG_FUNCTION(member, name) \ + debugFunctions.member = reinterpret_cast(GetProcAddress(debugFunctions.library, name)) + VR_LOAD_DEBUG_FUNCTION(initialize, "SymInitialize"); + VR_LOAD_DEBUG_FUNCTION(setOptions, "SymSetOptions"); + VR_LOAD_DEBUG_FUNCTION(fromAddress, "SymFromAddr"); + VR_LOAD_DEBUG_FUNCTION(getLine, "SymGetLineFromAddr64"); + VR_LOAD_DEBUG_FUNCTION(functionTable, "SymFunctionTableAccess64"); + VR_LOAD_DEBUG_FUNCTION(moduleBase, "SymGetModuleBase64"); + VR_LOAD_DEBUG_FUNCTION(walk, "StackWalk64"); + VR_LOAD_DEBUG_FUNCTION(dump, "MiniDumpWriteDump"); +#undef VR_LOAD_DEBUG_FUNCTION + if (debugFunctions.setOptions) + { + debugFunctions.setOptions(SYMOPT_UNDNAME | SYMOPT_LOAD_LINES | SYMOPT_DEFERRED_LOADS | SYMOPT_FAIL_CRITICAL_ERRORS); + } + if (debugFunctions.initialize) + { + debugFunctions.symbolsReady = debugFunctions.initialize(GetCurrentProcess(), nullptr, TRUE) != FALSE; + } + } + + void writeWindowsFrame(NativeFile file, DWORD64 address) + { + writeNumber(file, " pc=", address, 16); + MEMORY_BASIC_INFORMATION memory = {}; + if (VirtualQuery(reinterpret_cast(static_cast(address)), &memory, sizeof(memory))) + { + writeNumber(file, " module_offset=", address - reinterpret_cast(memory.AllocationBase), 16); + wchar_t modulePath[MAX_PATH] = {}; + if (GetModuleFileNameW(static_cast(memory.AllocationBase), modulePath, MAX_PATH)) + { + char utf8Path[MAX_PATH * 3] = {}; + if (WideCharToMultiByte(CP_UTF8, 0, modulePath, -1, utf8Path, sizeof(utf8Path), nullptr, nullptr)) + { + writeText(file, " module="); + writeText(file, utf8Path); + writeText(file, "\n"); + } + } + } + if (!debugFunctions.symbolsReady) + { + return; + } + alignas(SYMBOL_INFO) char symbolBuffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME] = {}; + SYMBOL_INFO* symbol = reinterpret_cast(symbolBuffer); + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = MAX_SYM_NAME; + DWORD64 displacement = 0; + if (debugFunctions.fromAddress && debugFunctions.fromAddress(GetCurrentProcess(), address, &displacement, symbol)) + { + writeText(file, " symbol="); + writeText(file, symbol->Name); + writeNumber(file, "+", displacement, 16); + } + IMAGEHLP_LINE64 line = {}; + line.SizeOfStruct = sizeof(line); + DWORD lineDisplacement = 0; + if (debugFunctions.getLine && debugFunctions.getLine(GetCurrentProcess(), address, &lineDisplacement, &line)) + { + writeText(file, " source="); + writeText(file, line.FileName); + writeNumber(file, ":", line.LineNumber); + } + } + + void writeWindowsExceptionName(NativeFile file, DWORD code) + { + const char* name = "unknown"; + switch (code) + { + case EXCEPTION_ACCESS_VIOLATION: name = "EXCEPTION_ACCESS_VIOLATION"; break; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: name = "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; break; + case EXCEPTION_DATATYPE_MISALIGNMENT: name = "EXCEPTION_DATATYPE_MISALIGNMENT"; break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: name = "EXCEPTION_FLT_DIVIDE_BY_ZERO"; break; + case EXCEPTION_ILLEGAL_INSTRUCTION: name = "EXCEPTION_ILLEGAL_INSTRUCTION"; break; + case EXCEPTION_IN_PAGE_ERROR: name = "EXCEPTION_IN_PAGE_ERROR"; break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: name = "EXCEPTION_INT_DIVIDE_BY_ZERO"; break; + case EXCEPTION_STACK_OVERFLOW: name = "EXCEPTION_STACK_OVERFLOW"; break; + case 0xC0000374U: name = "STATUS_HEAP_CORRUPTION"; break; + case 0xC0000409U: name = "STATUS_STACK_BUFFER_OVERRUN"; break; + default: break; + } + writeText(file, "exception_name="); + writeText(file, name); + writeText(file, "\n"); + } + + void writeWindowsRegisters(NativeFile file, const CONTEXT& context) + { + writeText(file, "Registers:\n"); +#if defined(_M_X64) || defined(__x86_64__) + writeNumber(file, " rip=", context.Rip, 16); + writeNumber(file, " rsp=", context.Rsp, 16); + writeNumber(file, " rbp=", context.Rbp, 16); + writeNumber(file, " rflags=", context.EFlags, 16); + writeNumber(file, " rax=", context.Rax, 16); + writeNumber(file, " rbx=", context.Rbx, 16); + writeNumber(file, " rcx=", context.Rcx, 16); + writeNumber(file, " rdx=", context.Rdx, 16); + writeNumber(file, " rsi=", context.Rsi, 16); + writeNumber(file, " rdi=", context.Rdi, 16); + writeNumber(file, " r8=", context.R8, 16); + writeNumber(file, " r9=", context.R9, 16); +#elif defined(_M_IX86) || defined(__i386__) + writeNumber(file, " eip=", context.Eip, 16); + writeNumber(file, " esp=", context.Esp, 16); + writeNumber(file, " ebp=", context.Ebp, 16); + writeNumber(file, " eflags=", context.EFlags, 16); + writeNumber(file, " eax=", context.Eax, 16); + writeNumber(file, " ebx=", context.Ebx, 16); + writeNumber(file, " ecx=", context.Ecx, 16); + writeNumber(file, " edx=", context.Edx, 16); + writeNumber(file, " esi=", context.Esi, 16); + writeNumber(file, " edi=", context.Edi, 16); +#elif defined(_M_ARM64) || defined(__aarch64__) + writeNumber(file, " pc=", context.Pc, 16); + writeNumber(file, " sp=", context.Sp, 16); + writeNumber(file, " fp=", context.Fp, 16); + writeNumber(file, " lr=", context.Lr, 16); + for (int index = 0; index < 31; ++index) + { + char label[8] = {}; + label[0] = ' '; label[1] = 'x'; + if (index >= 10) + { + label[2] = static_cast('0' + index / 10); + label[3] = static_cast('0' + index % 10); + label[4] = '='; + } + else + { + label[2] = static_cast('0' + index); + label[3] = '='; + } + writeNumber(file, label, context.X[index], 16); + } + writeNumber(file, " cpsr=", context.Cpsr, 16); +#elif defined(_M_ARM) || defined(__arm__) + writeNumber(file, " pc=", context.Pc, 16); + writeNumber(file, " sp=", context.Sp, 16); + writeNumber(file, " fp=", context.R11, 16); + writeNumber(file, " lr=", context.Lr, 16); + writeNumber(file, " r0=", context.R0, 16); + writeNumber(file, " r1=", context.R1, 16); + writeNumber(file, " r2=", context.R2, 16); + writeNumber(file, " r3=", context.R3, 16); + writeNumber(file, " r12=", context.R12, 16); + writeNumber(file, " cpsr=", context.Cpsr, 16); +#endif + } + + void writeWindowsStack(NativeFile file, CONTEXT context) + { + STACKFRAME64 frame = {}; + DWORD machine = 0; +#if defined(_M_X64) || defined(__x86_64__) + machine = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = context.Rip; + frame.AddrFrame.Offset = context.Rbp; + frame.AddrStack.Offset = context.Rsp; +#elif defined(_M_IX86) || defined(__i386__) + machine = IMAGE_FILE_MACHINE_I386; + frame.AddrPC.Offset = context.Eip; + frame.AddrFrame.Offset = context.Ebp; + frame.AddrStack.Offset = context.Esp; +#elif defined(_M_ARM64) || defined(__aarch64__) + machine = IMAGE_FILE_MACHINE_ARM64; + frame.AddrPC.Offset = context.Pc; + frame.AddrFrame.Offset = context.Fp; + frame.AddrStack.Offset = context.Sp; +#endif + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Mode = AddrModeFlat; + writeText(file, "Faulting thread stack (frame 0 is the captured PC):\n"); + for (int index = 0; index < maxFrames && frame.AddrPC.Offset != 0; ++index) + { + writeNumber(file, "frame=", index); + writeWindowsFrame(file, frame.AddrPC.Offset); + if (!debugFunctions.symbolsReady || !debugFunctions.walk || !debugFunctions.functionTable || !debugFunctions.moduleBase) + { + break; + } + const DWORD64 previousPc = frame.AddrPC.Offset; + const DWORD64 previousSp = frame.AddrStack.Offset; + if (!debugFunctions.walk(machine, GetCurrentProcess(), GetCurrentThread(), &frame, &context, + nullptr, debugFunctions.functionTable, debugFunctions.moduleBase, nullptr) || + (frame.AddrPC.Offset == previousPc && frame.AddrStack.Offset == previousSp)) + { + break; + } + } + } + + void writeWindowsArchitecture(NativeFile file) + { +#if defined(_M_ARM64) || defined(__aarch64__) + writeText(file, "architecture=arm64\n"); +#elif defined(_M_ARM) || defined(__arm__) + writeText(file, "architecture=arm32\n"); +#elif defined(_M_X64) || defined(__x86_64__) + writeText(file, "architecture=x64\n"); +#elif defined(_M_IX86) || defined(__i386__) + writeText(file, "architecture=x86\n"); +#else + writeText(file, "architecture=unknown\n"); +#endif + } + + void writeWindowsReport(EXCEPTION_POINTERS* exceptionInfo, int signalNumber) + { + if (handlingCrash.test_and_set()) + { + TerminateProcess(GetCurrentProcess(), 1); + return; + } + OutputTarget* target = outputTarget.load(std::memory_order_acquire); + if (!target) + { + return; + } + SYSTEMTIME localTime = {}; + GetLocalTime(&localTime); + wchar_t stackPath[32768] = {}; + wchar_t dumpPath[32768] = {}; + const int stackPathLength = _snwprintf_s(stackPath, _countof(stackPath), _TRUNCATE, + L"%s\\crash_stack_%04u%02u%02u_%02u%02u%02u_%03u.txt", + target->directoryPath.c_str(), localTime.wYear, localTime.wMonth, localTime.wDay, + localTime.wHour, localTime.wMinute, localTime.wSecond, localTime.wMilliseconds); + const int dumpPathLength = _snwprintf_s(dumpPath, _countof(dumpPath), _TRUNCATE, + L"%s\\crash_%04u%02u%02u_%02u%02u%02u_%03u.dmp", + target->directoryPath.c_str(), localTime.wYear, localTime.wMonth, localTime.wDay, + localTime.wHour, localTime.wMinute, localTime.wSecond, localTime.wMilliseconds); + if (stackPathLength <= 0 || dumpPathLength <= 0) + { + return; + } + const NativeFile file = CreateFileW(stackPath, GENERIC_WRITE, FILE_SHARE_READ, + nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == invalidFile) + { + return; + } + writeText(file, "VrUtils crash report\n"); + writeWindowsArchitecture(file); + writeNumber(file, "pid=", GetCurrentProcessId()); + writeNumber(file, "tid=", GetCurrentThreadId()); + FILETIME time = {}; + GetSystemTimeAsFileTime(&time); + const uint64_t ticks = (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; + writeNumber(file, "unix_time=", (ticks - 116444736000000000ULL) / 10000000ULL); + writeNumber(file, "signal=", signalNumber); + const EXCEPTION_RECORD* record = exceptionInfo ? exceptionInfo->ExceptionRecord : nullptr; + const CONTEXT* capturedContext = exceptionInfo ? exceptionInfo->ContextRecord : nullptr; + if (record) + { + writeNumber(file, "exception_code=", record->ExceptionCode, 16); + writeNumber(file, "exception_address=", reinterpret_cast(record->ExceptionAddress), 16); + writeWindowsExceptionName(file, record->ExceptionCode); + if (record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && record->NumberParameters >= 2) + { + writeText(file, "access_type="); + writeText(file, record->ExceptionInformation[0] == 0 ? "read" : + (record->ExceptionInformation[0] == 1 ? "write" : "execute")); + writeText(file, "\n"); + writeNumber(file, "access_address=", record->ExceptionInformation[1], 16); + } + if (record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR && record->NumberParameters >= 3) + { + writeNumber(file, "in_page_status=", record->ExceptionInformation[0], 16); + writeNumber(file, "in_page_address=", record->ExceptionInformation[1], 16); + } + } + else + { + writeText(file, "exception_code=unavailable\n"); + writeText(file, "exception_address=unavailable\n"); + } + // 先提交异常头,再尽力生成 dump 和展开栈;展开器自身异常时仍有最低限度现场。 + FlushFileBuffers(file); + if (debugFunctions.dump && exceptionInfo) + { + const HANDLE dumpFile = CreateFileW(dumpPath, GENERIC_WRITE, FILE_SHARE_READ, + nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (dumpFile != INVALID_HANDLE_VALUE) + { + MINIDUMP_EXCEPTION_INFORMATION information = {}; + information.ThreadId = GetCurrentThreadId(); + information.ExceptionPointers = exceptionInfo; + information.ClientPointers = FALSE; + const BOOL success = debugFunctions.dump(GetCurrentProcess(), GetCurrentProcessId(), dumpFile, + static_cast(MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules), &information, nullptr, nullptr); + writeNumber(file, "minidump_written=", success ? 1 : 0); + FlushFileBuffers(dumpFile); + CloseHandle(dumpFile); + } + } + if (capturedContext) + { + writeWindowsRegisters(file, *capturedContext); + writeText(file, "Faulting instruction:\n"); + const DWORD64 faultAddress = record && record->ExceptionAddress + ? reinterpret_cast(record->ExceptionAddress) + : 0; + if (faultAddress != 0) + { + writeWindowsFrame(file, faultAddress); + } + writeWindowsStack(file, *capturedContext); + } + else + { + writeText(file, "Faulting thread stack unavailable\n"); + } + FlushFileBuffers(file); + CloseHandle(file); + } + + bool isFatalWindowsException(DWORD code) + { + switch (code) + { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + case EXCEPTION_DATATYPE_MISALIGNMENT: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_INT_OVERFLOW: + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + case EXCEPTION_STACK_OVERFLOW: + return true; + default: + return false; + } + } + + LONG CALLBACK windowsVectoredExceptionHandler(EXCEPTION_POINTERS* exceptionInfo) + { + if (exceptionInfo && exceptionInfo->ExceptionRecord && + isFatalWindowsException(exceptionInfo->ExceptionRecord->ExceptionCode)) + { + writeWindowsReport(exceptionInfo, 0); + } + return EXCEPTION_CONTINUE_SEARCH; + } + + LONG WINAPI windowsExceptionHandler(EXCEPTION_POINTERS* exceptionInfo) + { + writeWindowsReport(exceptionInfo, 0); + return EXCEPTION_EXECUTE_HANDLER; + } + + void windowsSignalHandler(int signalNumber) + { + CONTEXT context = {}; + RtlCaptureContext(&context); + EXCEPTION_RECORD record = {}; + record.ExceptionCode = 0xe0000000U | static_cast(signalNumber); + EXCEPTION_POINTERS information = {&record, &context}; + writeWindowsReport(&information, signalNumber); + TerminateProcess(GetCurrentProcess(), static_cast(128 + signalNumber)); + } + + const int fatalSignals[] = {SIGABRT, SIGSEGV, SIGILL, SIGFPE}; + using SignalHandler = void (*)(int); +#else + void prepareUnwinder() + { +#if defined(__GLIBC__) + // 提前加载 libgcc 展开器,减少首次崩溃时的动态加载/内存分配。 + void* frames[1]; + backtrace(frames, 1); +#endif + } + + void writeLinuxContext(NativeFile file, void* rawContext) + { + if (!rawContext) + { + return; + } + const ucontext_t* context = static_cast(rawContext); +#if defined(__x86_64__) + writeNumber(file, "fault_pc=", context->uc_mcontext.gregs[REG_RIP], 16); + writeNumber(file, "fault_sp=", context->uc_mcontext.gregs[REG_RSP], 16); +#elif defined(__i386__) + writeNumber(file, "fault_pc=", context->uc_mcontext.gregs[REG_EIP], 16); + writeNumber(file, "fault_sp=", context->uc_mcontext.gregs[REG_ESP], 16); +#elif defined(__aarch64__) + writeNumber(file, "fault_pc=", context->uc_mcontext.pc, 16); + writeNumber(file, "fault_sp=", context->uc_mcontext.sp, 16); + writeNumber(file, "fault_lr=", context->uc_mcontext.regs[30], 16); + writeNumber(file, "fault_fp=", context->uc_mcontext.regs[29], 16); + writeNumber(file, "fault_x0=", context->uc_mcontext.regs[0], 16); + writeNumber(file, "fault_x1=", context->uc_mcontext.regs[1], 16); + writeNumber(file, "fault_x2=", context->uc_mcontext.regs[2], 16); + writeNumber(file, "fault_x3=", context->uc_mcontext.regs[3], 16); + writeNumber(file, "fault_x4=", context->uc_mcontext.regs[4], 16); + writeNumber(file, "fault_x5=", context->uc_mcontext.regs[5], 16); + writeNumber(file, "fault_x6=", context->uc_mcontext.regs[6], 16); + writeNumber(file, "fault_x7=", context->uc_mcontext.regs[7], 16); + writeNumber(file, "fault_pstate=", context->uc_mcontext.pstate, 16); +#elif defined(__arm__) + writeNumber(file, "fault_pc=", context->uc_mcontext.arm_pc, 16); + writeNumber(file, "fault_sp=", context->uc_mcontext.arm_sp, 16); + writeNumber(file, "fault_lr=", context->uc_mcontext.arm_lr, 16); + writeNumber(file, "fault_fp=", context->uc_mcontext.arm_fp, 16); + writeNumber(file, "fault_r0=", context->uc_mcontext.arm_r0, 16); + writeNumber(file, "fault_r1=", context->uc_mcontext.arm_r1, 16); + writeNumber(file, "fault_r2=", context->uc_mcontext.arm_r2, 16); + writeNumber(file, "fault_r3=", context->uc_mcontext.arm_r3, 16); + writeNumber(file, "fault_r12=", context->uc_mcontext.arm_ip, 16); + writeNumber(file, "fault_cpsr=", context->uc_mcontext.arm_cpsr, 16); +#endif + } + + void writeLinuxArchitecture(NativeFile file) + { +#if defined(__aarch64__) + writeText(file, "architecture=arm64\n"); +#elif defined(__arm__) + writeText(file, "architecture=arm32\n"); +#elif defined(__x86_64__) + writeText(file, "architecture=x64\n"); +#elif defined(__i386__) + writeText(file, "architecture=x86\n"); +#else + writeText(file, "architecture=unknown\n"); +#endif + } + + void writeLinuxFaultFrames(NativeFile file, void* rawContext) + { + if (!rawContext) + { + return; + } + const ucontext_t* context = static_cast(rawContext); + writeText(file, "Fault registers used for symbol lookup:\n"); +#if defined(__aarch64__) + writeNumber(file, " fault_frame_pc=", context->uc_mcontext.pc, 16); + writeNumber(file, " fault_frame_lr=", context->uc_mcontext.regs[30], 16); + writeNumber(file, " fault_frame_fp=", context->uc_mcontext.regs[29], 16); +#elif defined(__arm__) + writeNumber(file, " fault_frame_pc=", context->uc_mcontext.arm_pc, 16); + writeNumber(file, " fault_frame_lr=", context->uc_mcontext.arm_lr, 16); + writeNumber(file, " fault_frame_fp=", context->uc_mcontext.arm_fp, 16); +#elif defined(__x86_64__) + writeNumber(file, " fault_frame_pc=", context->uc_mcontext.gregs[REG_RIP], 16); +#elif defined(__i386__) + writeNumber(file, " fault_frame_pc=", context->uc_mcontext.gregs[REG_EIP], 16); +#endif + } + + void linuxSignalHandler(int signalNumber, siginfo_t* information, void* context) + { + if (handlingCrash.test_and_set()) + { + _exit(128 + signalNumber); + } + OutputTarget* target = outputTarget.load(std::memory_order_acquire); + char stackPath[PATH_MAX] = {}; + struct timespec time = {}; + clock_gettime(CLOCK_REALTIME, &time); + struct tm localTime = {}; + localtime_r(&time.tv_sec, &localTime); + if (target) + { + std::snprintf(stackPath, sizeof(stackPath), + "%s/crash_stack_%04d%02d%02d_%02d%02d%02d_%03ld.txt", + target->directory.c_str(), localTime.tm_year + 1900, localTime.tm_mon + 1, + localTime.tm_mday, localTime.tm_hour, localTime.tm_min, localTime.tm_sec, + time.tv_nsec / 1000000L); + } + NativeFile file = target ? open(stackPath, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) : invalidFile; + const bool ownsFile = file != invalidFile; + if (!ownsFile) + { + file = STDERR_FILENO; + } + writeText(file, "VrUtils crash report\n"); + writeLinuxArchitecture(file); + writeNumber(file, "pid=", getpid()); + writeNumber(file, "tid=", static_cast(syscall(SYS_gettid))); + writeNumber(file, "unix_time=", static_cast(time.tv_sec)); + writeNumber(file, "signal=", signalNumber); + if (information) + { + writeNumber(file, "si_code_unsigned=", static_cast(information->si_code)); + if (information->si_code > 0 && signalNumber != SIGABRT) + { + writeNumber(file, "fault_address=", reinterpret_cast(information->si_addr), 16); + } + } + writeLinuxContext(file, context); + writeLinuxFaultFrames(file, context); + writeText(file, "Faulting instruction address is fault_pc above; resolve it against the maps below.\n"); + + // 路径、加载地址和文件偏移用于离线解析 PIE/共享库中的地址。 + writeText(file, "Memory maps (/proc/self/maps):\n"); + const int maps = open("/proc/self/maps", O_RDONLY | O_CLOEXEC); + if (maps >= 0) + { + char buffer[4096]; + ssize_t size = 0; + while ((size = read(maps, buffer, sizeof(buffer))) > 0) + { + writeBytes(file, buffer, static_cast(size)); + } + close(maps); + } + if (ownsFile) + { + fsync(file); + } + writeText(file, "Faulting thread backtrace (includes signal trampoline/handler):\n"); +#if defined(__GLIBC__) + // backtrace 并非 POSIX 异步信号安全接口,只尽力展开;原始 PC 和映射已先写入。 + void* frames[maxFrames]; + const int count = backtrace(frames, maxFrames); + for (int index = 0; index < count; ++index) + { + writeNumber(file, "frame=", index); + writeNumber(file, " pc=", reinterpret_cast(frames[index]), 16); + } + backtrace_symbols_fd(frames, count, file); +#else + writeText(file, "Unwinder unavailable on this libc; use fault_pc and memory maps.\n"); +#endif + if (ownsFile) + { + fsync(file); + close(file); + } + // 恢复默认动作并给当前故障线程重发信号,保留 core dump 和信号退出状态。 + struct sigaction action = {}; + action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); + sigaction(signalNumber, &action, nullptr); + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, signalNumber); + sigprocmask(SIG_UNBLOCK, &mask, nullptr); + raise(signalNumber); + _exit(128 + signalNumber); + } + + const int fatalSignals[] = {SIGSEGV, SIGABRT, SIGILL, SIGFPE, SIGBUS}; +#endif + + void terminateHandler() + { + // 未捕获 C++ 异常最终进入 SIGABRT,保留 terminate 调用链。 + std::abort(); + } + + struct HandlerState + { + std::mutex mutex; + bool enabled = false; + std::terminate_handler previousTerminate = nullptr; + std::vector> targets; +#ifdef _WIN32 + LPTOP_LEVEL_EXCEPTION_FILTER previousException = nullptr; + PVOID vectoredExceptionHandler = nullptr; + SignalHandler previousSignals[sizeof(fatalSignals) / sizeof(fatalSignals[0])] = {}; +#else + struct sigaction previousSignals[sizeof(fatalSignals) / sizeof(fatalSignals[0])] = {}; +#endif + }; + + HandlerState& handlerState() + { + // 不参与静态析构,程序退出阶段崩溃时路径和处理器仍有效。 + static HandlerState* state = new HandlerState; + return *state; + } + +#ifdef _WIN32 + std::wstring fromUtf8(const std::string& text) + { + const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.c_str(), -1, nullptr, 0); + if (count <= 0) + { + return std::wstring(); + } + std::wstring result(static_cast(count), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.c_str(), -1, &result[0], count); + result.resize(static_cast(count - 1)); + return result; + } + + bool createDirectories(const std::wstring& path) + { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES) + { + return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + } + const size_t slash = path.find_last_of(L"\\/"); + if (slash != std::wstring::npos && slash > 0 && !createDirectories(path.substr(0, slash))) + { + return false; + } + return CreateDirectoryW(path.c_str(), nullptr) != FALSE || GetLastError() == ERROR_ALREADY_EXISTS; + } +#else + bool createDirectories(const std::string& path) + { + struct stat information = {}; + if (stat(path.c_str(), &information) == 0) + { + return S_ISDIR(information.st_mode); + } + const size_t slash = path.find_last_of('/'); + if (slash != std::string::npos && slash > 0 && !createDirectories(path.substr(0, slash))) + { + return false; + } + return mkdir(path.c_str(), 0700) == 0 || errno == EEXIST; + } +#endif + + std::unique_ptr prepareTarget(const std::string& directory) + { + if (directory.find('\0') != std::string::npos) + { + return nullptr; + } + std::unique_ptr target(new OutputTarget); +#ifdef _WIN32 + const std::wstring requested = fromUtf8(directory); + wchar_t absolutePath[32768] = {}; + const DWORD length = GetFullPathNameW(requested.c_str(), 32768, absolutePath, nullptr); + if (requested.empty() || length == 0 || length >= 32768 || !createDirectories(absolutePath)) + { + return nullptr; + } + target->directoryPath = absolutePath; + const int count = WideCharToMultiByte(CP_UTF8, 0, absolutePath, -1, nullptr, 0, nullptr, nullptr); + if (count <= 0) + { + return nullptr; + } + target->directory.resize(static_cast(count)); + WideCharToMultiByte(CP_UTF8, 0, absolutePath, -1, &target->directory[0], count, nullptr, nullptr); + target->directory.resize(static_cast(count - 1)); +#else + if (!createDirectories(directory)) + { + return nullptr; + } + char* absolutePath = realpath(directory.c_str(), nullptr); + if (!absolutePath) + { + return nullptr; + } + target->directory = absolutePath; + free(absolutePath); +#endif + return target; + } +#endif +} + +void VrCrashHandler::Initialize() +{ + // 多个入口只安装一次,后续主动关闭不会被重复初始化重新开启。 + static const bool initialized = []() + { + // Lambda has no implicit this pointer; qualify the public function explicitly. + const bool enabled = VrCrashHandler::Enable(); + if (!enabled) + { + std::fputs("[VrUtils] failed to enable crash reporting\n", stderr); + } + return enabled; + }(); + static_cast(initialized); +} + +bool VrCrashHandler::Enable(const std::string& outputDirectory) +{ +#if defined(_WIN32) || defined(__linux__) + // 回调中的原子访问必须无锁,不能在信号打断时进入库内部互斥量。 + if (!outputTarget.is_lock_free()) + { + return false; + } + HandlerState& state = handlerState(); + std::lock_guard lock(state.mutex); + OutputTarget* target = outputTarget.load(); + const std::string directory = outputDirectory.empty() + ? (target ? target->directory : VrPathUtils::GetLogDirectory() + "/crash") : outputDirectory; + if (!target || directory != target->directory) + { + std::unique_ptr prepared = prepareTarget(directory); + if (!prepared) + { + return false; + } + target = prepared.get(); + state.targets.push_back(std::move(prepared)); + } + if (state.enabled) + { + outputTarget.store(target, std::memory_order_release); + return true; + } + prepareUnwinder(); + outputTarget.store(target, std::memory_order_release); + handlingCrash.clear(); + size_t installed = 0; + for (; installed < sizeof(fatalSignals) / sizeof(fatalSignals[0]); ++installed) + { +#ifdef _WIN32 + state.previousSignals[installed] = std::signal(fatalSignals[installed], windowsSignalHandler); + if (state.previousSignals[installed] == SIG_ERR) + { + break; + } +#else + struct sigaction action = {}; + action.sa_sigaction = linuxSignalHandler; + sigemptyset(&action.sa_mask); + action.sa_flags = SA_SIGINFO | SA_RESTART; + if (sigaction(fatalSignals[installed], &action, &state.previousSignals[installed]) != 0) + { + break; + } +#endif + } + if (installed != sizeof(fatalSignals) / sizeof(fatalSignals[0])) + { + while (installed > 0) + { + --installed; +#ifdef _WIN32 + std::signal(fatalSignals[installed], state.previousSignals[installed]); +#else + sigaction(fatalSignals[installed], &state.previousSignals[installed], nullptr); +#endif + } + return false; + } +#ifdef _WIN32 + state.vectoredExceptionHandler = AddVectoredExceptionHandler(1, windowsVectoredExceptionHandler); + if (!state.vectoredExceptionHandler) + { + while (installed > 0) + { + --installed; + std::signal(fatalSignals[installed], state.previousSignals[installed]); + } + return false; + } + state.previousException = SetUnhandledExceptionFilter(windowsExceptionHandler); +#endif + state.previousTerminate = std::set_terminate(terminateHandler); + state.enabled = true; + return true; +#else + (void)outputDirectory; + return false; +#endif +} + +void VrCrashHandler::Disable() +{ +#if defined(_WIN32) || defined(__linux__) + HandlerState& state = handlerState(); + std::lock_guard lock(state.mutex); + if (!state.enabled) + { + return; + } + if (std::get_terminate() == terminateHandler) + { + std::set_terminate(state.previousTerminate); + } +#ifdef _WIN32 + if (state.vectoredExceptionHandler) + { + RemoveVectoredExceptionHandler(state.vectoredExceptionHandler); + state.vectoredExceptionHandler = nullptr; + } + const auto currentException = SetUnhandledExceptionFilter(state.previousException); + if (currentException != windowsExceptionHandler) + { + SetUnhandledExceptionFilter(currentException); + } +#endif + for (size_t index = 0; index < sizeof(fatalSignals) / sizeof(fatalSignals[0]); ++index) + { + // 若第三方后来接管了处理器,不覆盖它的配置。 +#ifdef _WIN32 + const auto current = std::signal(fatalSignals[index], state.previousSignals[index]); + if (current != windowsSignalHandler && current != SIG_ERR) + { + std::signal(fatalSignals[index], current); + } +#else + struct sigaction current = {}; + if (sigaction(fatalSignals[index], nullptr, ¤t) == 0 && + (current.sa_flags & SA_SIGINFO) && current.sa_sigaction == linuxSignalHandler) + { + sigaction(fatalSignals[index], &state.previousSignals[index], nullptr); + } +#endif + } + state.enabled = false; +#endif +} + +bool VrCrashHandler::IsEnabled() +{ +#if defined(_WIN32) || defined(__linux__) + HandlerState& state = handlerState(); + std::lock_guard lock(state.mutex); + return state.enabled; +#else + return false; +#endif +} + +std::string VrCrashHandler::GetOutputDirectory() +{ +#if defined(_WIN32) || defined(__linux__) + OutputTarget* target = outputTarget.load(std::memory_order_acquire); + return target ? target->directory : std::string(); +#else + return std::string(); +#endif +} + +namespace +{ + // 对象文件被链接后独立自动启用,不依赖 VrLog 的初始化顺序。 + VrCrashHandler::AutoInitializer crashHandlerAutoInitializer; +} diff --git a/VrUtils/Src/VrLog.cpp b/VrUtils/Src/VrLog.cpp index 65ff378..d04745e 100644 --- a/VrUtils/Src/VrLog.cpp +++ b/VrUtils/Src/VrLog.cpp @@ -1,12 +1,12 @@ -#include "VrLog.h" +#include "VrLog.h" -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -16,12 +16,12 @@ #include #include -#include "VrFileUtils.h" +#include "VrFileUtils.h" +#include "VrPathUtils.h" #ifdef _WIN32 #include #include -#include #define _WINSOCK_DEPRECATED_NO_WARNINGS #include #pragma comment(lib,"Ws2_32.lib") @@ -120,58 +120,6 @@ static inline const char* TruncateFilePath(const char* filePath) { #endif // __ANROID__ ///******************************************************************************************************************/ -// 动态获取日志路径(包含应用名称) -static std::string GetLogPath() -{ - std::string appName = "YJApp"; // 默认应用名称 - - // 尝试从进程路径获取应用名称 -#ifdef _WIN32 - // 获取用户文档目录 - - // 如果无法获取用户目录,则使用原来的方式 - char exePath[MAX_PATH]; - if (GetModuleFileNameA(NULL, exePath, MAX_PATH) != 0) { - std::string fullPath(exePath); - size_t pos = fullPath.find_last_of("\\"); - if (pos != std::string::npos) { - std::string fileName = fullPath.substr(pos + 1); - size_t dotPos = fileName.find_last_of("."); - if (dotPos != std::string::npos) { - appName = fileName.substr(0, dotPos); - } - } - } - - char userPath[MAX_PATH]; - if (SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, userPath) == S_OK) { - std::string userDir(userPath); - return userDir + "\\" + appName + "\\Log"; - } - - return ".\\" + appName + "\\Log"; -#else - // Linux/Android平台 - char exePath[1024]; - ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); - if (len != -1) { - exePath[len] = '\0'; - std::string fullPath(exePath); - size_t pos = fullPath.find_last_of("/"); - if (pos != std::string::npos) { - appName = fullPath.substr(pos + 1); - } - } - -#ifdef __ANDROID__ - return "/sdcard/" + appName + "/Log"; -#else - return appName + "/Log"; -#endif -#endif -} - - // 自动初始化和清理类,确保程序启动时初始化日志,退出时清理日志资源 class VrLogAutoCleaner { public: @@ -188,8 +136,8 @@ public: static VrLogAutoCleaner g_logAutoCleaner; // 实际执行初始化的内部函数 -static void DoInitLog() -{ +static void DoInitLog() +{ #ifdef _WIN32 // 设置控制台代码页为 UTF-8,解决中文乱码 SetConsoleOutputCP(CP_UTF8); @@ -199,7 +147,7 @@ static void DoInitLog() #endif // if file exist then load file config - std::string logPath = GetLogPath(); + std::string logPath = VrPathUtils::GetLogDirectory(); std::string logConfig = logPath + PATH_SEP + LOG_CONFIG_FILE; if (CVrFileUtils::IsFileExist(logConfig.c_str())) { @@ -308,15 +256,15 @@ void VrLogUtils::EchoLog(VrLogLevel eLogLevel, const char* sFilePath, const int } // load log info - va_list args; - va_start(args, sFormat); - char szLogInfo[1024] = { 0 }; -#ifdef _WIN32 - vsnprintf_s(szLogInfo, sizeof(szLogInfo), _TRUNCATE, sFormat, args); -#else - vsnprintf(szLogInfo, sizeof(szLogInfo), sFormat, args); -#endif - va_end(args); + va_list args; + va_start(args, sFormat); + char szLogInfo[1024] = { 0 }; +#ifdef _WIN32 + vsnprintf_s(szLogInfo, sizeof(szLogInfo), _TRUNCATE, sFormat, args); +#else + vsnprintf(szLogInfo, sizeof(szLogInfo), sFormat, args); +#endif + va_end(args); log4cpp::Category& root = log4cpp::Category::getRoot(); switch (eLogLevel) diff --git a/VrUtils/Src/VrPathUtils.cpp b/VrUtils/Src/VrPathUtils.cpp new file mode 100644 index 0000000..ee679ff --- /dev/null +++ b/VrUtils/Src/VrPathUtils.cpp @@ -0,0 +1,55 @@ +#include "VrPathUtils.h" + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "Shell32.lib") +#else +#include +#endif + +namespace VrPathUtils +{ +std::string GetLogDirectory() +{ + std::string appName = "YJApp"; +#ifdef _WIN32 + char exePath[MAX_PATH] = {}; + if (GetModuleFileNameA(nullptr, exePath, MAX_PATH) != 0) + { + const std::string fullPath(exePath); + const size_t separator = fullPath.find_last_of("\\"); + if (separator != std::string::npos) + { + const std::string fileName = fullPath.substr(separator + 1); + const size_t dot = fileName.find_last_of('.'); + appName = dot == std::string::npos ? fileName : fileName.substr(0, dot); + } + } + char userPath[MAX_PATH] = {}; + if (SHGetFolderPathA(nullptr, CSIDL_PERSONAL, nullptr, SHGFP_TYPE_CURRENT, userPath) == S_OK) + { + return std::string(userPath) + "\\" + appName + "\\Log"; + } + return ".\\" + appName + "\\Log"; +#else + char exePath[1024] = {}; + const ssize_t length = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (length > 0) + { + exePath[length] = '\0'; + const std::string fullPath(exePath); + const size_t separator = fullPath.find_last_of('/'); + if (separator != std::string::npos && separator + 1 < fullPath.size()) + { + appName = fullPath.substr(separator + 1); + } + } +#ifdef __ANDROID__ + return "/sdcard/" + appName + "/Log"; +#else + return appName + "/Log"; +#endif +#endif +} +} diff --git a/VrUtils/VrUtils.pro b/VrUtils/VrUtils.pro index 8a2d81b..540647f 100644 --- a/VrUtils/VrUtils.pro +++ b/VrUtils/VrUtils.pro @@ -22,7 +22,9 @@ unix:!macx { win32-msvc*:QMAKE_CXXFLAGS += /D_HAS_AUTO_PTR_ETC=1 HEADERS += \ - Inc/IVrUtils.h \ + Inc/IVrUtils.h \ + Inc/VrCrashHandler.h \ + Inc/VrPathUtils.h \ Inc/VrDateUtils.h \ Inc/VrFileUtils.h \ Inc/VrLog.h \ @@ -97,7 +99,9 @@ HEADERS += \ jsoncpp/json/value.h \ jsoncpp/json/writer.h -SOURCES += \ +SOURCES += \ + Src/VrCrashHandler.cpp \ + Src/VrPathUtils.cpp \ Src/VrDateUtils.cpp \ Src/VrFileUtils.cpp \ Src/VrLog.cpp \