// io_utils.cpp - 通用 IO 工具实现 #include "io_utils.h" #include "config.h" #include #include #include #include #include #include #include #include // stb single-header JPEG 编码器(实现仅保留在本文件,全工程只定义一次) #define STB_IMAGE_WRITE_IMPLEMENTATION #include "PAVO2_SDK/include/stb_image_write.h" #ifdef _WIN32 #include // _mkdir #include #include #include #include #else #include #include #include #include #endif std::string trim_str(const std::string& s) { size_t b = s.find_first_not_of(" \t\r\n"); if (b == std::string::npos) return ""; size_t e = s.find_last_not_of(" \t\r\n"); return s.substr(b, e - b + 1); } std::string gbk_to_utf8(const std::string& s) { #ifdef _WIN32 if (s.empty()) return s; int wlen = MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), nullptr, 0); if (wlen <= 0) return s; std::wstring wbuf(wlen, L'\0'); MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), &wbuf[0], wlen); int ulen = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, nullptr, 0, nullptr, nullptr); if (ulen <= 0) return s; std::string ubuf(ulen, '\0'); WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, &ubuf[0], ulen, nullptr, nullptr); return ubuf; #else return s; #endif } #ifdef _WIN32 static bool mkdir_one(const std::string& path) { return _mkdir(path.c_str()) == 0 || errno == EEXIST; } #else static bool mkdir_one(const std::string& path) { return mkdir(path.c_str(), 0777) == 0 || errno == EEXIST; } #endif bool make_dirs(const std::string& path) { if (path.empty()) return false; std::string cur; size_t pos = 0; while (pos <= path.size()) { size_t slash = path.find_first_of("/\\", pos); cur += path.substr(pos, (slash == std::string::npos ? path.size() : slash) - pos); if (!cur.empty() && cur != ".") { if (!mkdir_one(cur) && errno != EEXIST) return false; } if (slash == std::string::npos) break; cur += '/'; pos = slash + 1; } return true; } std::string make_task_dir(const std::string& task_id) { std::string base = "data"; make_dirs(base); time_t rawtime = std::time(nullptr); struct tm tmv; #ifdef _WIN32 localtime_s(&tmv, &rawtime); // std::localtime 返回静态 tm,多线程并发会互相踩踏 #else localtime_r(&rawtime, &tmv); #endif struct tm* t = &tmv; char date_buf[16], time_buf[16]; std::strftime(date_buf, sizeof(date_buf), "%Y%m%d", t); std::strftime(time_buf, sizeof(time_buf), "%H%M%S", t); std::string day_dir = base + "/" + date_buf; make_dirs(day_dir); std::string task_dir = day_dir + "/" + time_buf; if (!task_id.empty()) task_dir += "_" + task_id; else task_dir += "_"; make_dirs(task_dir); return task_dir; } // Windows 递归删除目录(含只读文件),直接用 API,不弹 cmd 黑窗 #ifdef _WIN32 static bool remove_dir_recursive(const std::wstring& dir) { std::wstring pattern = dir + L"\\*"; WIN32_FIND_DATAW fd; HANDLE h = FindFirstFileW(pattern.c_str(), &fd); if (h == INVALID_HANDLE_VALUE) return RemoveDirectoryW(dir.c_str()) != 0; do { std::wstring name = fd.cFileName; if (name == L"." || name == L"..") continue; std::wstring full = dir + L"\\" + name; if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { remove_dir_recursive(full); // 子目录删不掉不致命,继续删其它 } else { DWORD attrs = GetFileAttributesW(full.c_str()); if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_READONLY)) SetFileAttributesW(full.c_str(), attrs & ~FILE_ATTRIBUTE_READONLY); DeleteFileW(full.c_str()); } } while (FindNextFileW(h, &fd)); FindClose(h); return RemoveDirectoryW(dir.c_str()) != 0; } #endif // 清理 data/日期 下早于 cfg.log.retain_days 天的过期日期目录(保留最近 N 天) // 在程序启动与每日 01:00 调用。目录名 YYYYMMDD 为纯数字,字典序即日期序。 void clean_expired_log_dirs() { std::cout << "[清理] 定时检查过期日志目录 (保留最近 " << cfg.log.retain_days << " 天)" << std::endl; if (cfg.log.retain_days <= 0) { std::cout << "[清理] 已禁用(cfg.log.retain_days<=0), 跳过" << std::endl; return; } #ifdef _WIN32 // 截止日期 = 今天 - cfg.log.retain_days 天(用文件时间做减法,避免 localtime/mktime // 修改共享 tm 缓冲区,与 make_task_dir 等其它线程并发调用时产生数据竞争) SYSTEMTIME st; GetLocalTime(&st); FILETIME ft; SystemTimeToFileTime(&st, &ft); ULONGLONG t = ((ULONGLONG)ft.dwHighDateTime << 32) | ft.dwLowDateTime; t -= (ULONGLONG)cfg.log.retain_days * 86400ULL * 10000000ULL; // 100ns 单位 ft.dwHighDateTime = (DWORD)(t >> 32); ft.dwLowDateTime = (DWORD)t; FileTimeToSystemTime(&ft, &st); char cutoff[16]; std::snprintf(cutoff, sizeof(cutoff), "%04u%02u%02u", st.wYear, st.wMonth, st.wDay); const std::string base = "data"; const std::wstring wbase(base.begin(), base.end()); WIN32_FIND_DATAW fd; HANDLE h = FindFirstFileW((wbase + L"\\*").c_str(), &fd); if (h == INVALID_HANDLE_VALUE) { std::cout << "[清理] data 目录不存在, 跳过" << std::endl; return; } int removed = 0; do { if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue; std::wstring wname = fd.cFileName; if (wname.size() != 8) continue; // 仅处理 YYYYMMDD bool is_num = true; for (wchar_t c : wname) if (c < L'0' || c > L'9') { is_num = false; break; } if (!is_num) continue; std::string name(wname.begin(), wname.end()); // 纯数字 ASCII,无损转换 if (name < cutoff) { // 早于截止日期才删 std::wstring full = wbase + L"\\" + wname; if (remove_dir_recursive(full)) { std::cout << "[清理] 已删除过期日志目录: data/" << name << std::endl; ++removed; } else std::cout << "[清理] 删除失败(可能正被占用): data/" << name << std::endl; } } while (FindNextFileW(h, &fd)); FindClose(h); std::cout << "[清理] 完成: 本次共删除 " << removed << " 个过期目录" << std::endl; #endif } void save_pcd_ascii(const std::string& filename, const std::vector& xs, const std::vector& ys, const std::vector& zs) { size_t n = xs.size(); std::ostringstream oss; oss << "# .PCD v0.7 - Point Cloud Data file format\n" << "VERSION 0.7\n" << "FIELDS x y z\n" << "SIZE 4 4 4\n" << "TYPE F F F\n" << "COUNT 1 1 1\n" << "WIDTH " << n << "\n" << "HEIGHT 1\n" << "VIEWPOINT 0 0 0 1 0 0 0\n" << "POINTS " << n << "\n" << "DATA ascii\n" << std::fixed << std::setprecision(1); for (size_t i = 0; i < n; i++) { oss << xs[i] << " " << ys[i] << " " << zs[i] << "\n"; } std::string buf = oss.str(); std::ofstream f(filename, std::ios::binary); f.write(buf.data(), (std::streamsize)buf.size()); f.close(); } bool save_jpg_gray(const std::string& filename, int width, int height, const std::vector& pixels, int quality) { std::vector rgb(width * height * 3); for (int i = 0; i < width * height; i++) { uint8_t v = pixels[i]; rgb[i * 3 + 0] = v; rgb[i * 3 + 1] = v; rgb[i * 3 + 2] = v; } int ret = stbi_write_jpg(filename.c_str(), width, height, 3, rgb.data(), quality); return ret != 0; } // ==================== 运行日志镜像:控制台 -> data/当天/run.log ==================== // 设计:用自定义 streambuf 接管 std::cout/std::cerr,按行同时写控制台与文件。 // - 线程安全(TcpServer/采集线程/后台保存线程都会打印) // - 跨天自动切换到新的 data/YYYYMMDD/run.log // - 对象常驻不释放,避免与 std::cout 的静态析构顺序冲突 namespace { // 当前日期字符串 YYYYMMDD(线程安全) static std::string log_today() { time_t raw = std::time(nullptr); struct tm tmv; #ifdef _WIN32 localtime_s(&tmv, &raw); #else localtime_r(&raw, &tmv); #endif char buf[16]; std::strftime(buf, sizeof(buf), "%Y%m%d", &tmv); return buf; } // 日志文件句柄(跨天滚动 + 线程安全) class RunLog { public: static RunLog& instance() { static RunLog* p = new RunLog(); return *p; } // 常驻 void write(const char* data, std::size_t n) { if (n == 0) return; std::lock_guard lk(mtx_); ensure_open_locked(); if (file_.is_open()) file_.write(data, (std::streamsize)n); } void flush() { std::lock_guard lk(mtx_); if (file_.is_open()) file_.flush(); } void close() { std::lock_guard lk(mtx_); if (file_.is_open()) { file_.flush(); file_.close(); } } private: RunLog() {} void ensure_open_locked() { std::string day = log_today(); if (file_.is_open() && day == day_) return; file_.close(); std::string dir = "data/" + day; make_dirs(dir); file_.open(dir + "/run.log", std::ios::app | std::ios::binary); day_ = day; } std::mutex mtx_; std::ofstream file_; std::string day_; }; // 同时写控制台(原 rdbuf)与 run.log 的 streambuf,按行落盘 class TeeBuf : public std::streambuf { public: explicit TeeBuf(std::streambuf* console) : console_(console) {} protected: int overflow(int ch) { std::lock_guard lk(mtx_); if (ch != EOF) { buf_.push_back((char)ch); if (ch == '\n') flush_locked(); } return ch; } std::streamsize xsputn(const char* s, std::streamsize n) { std::lock_guard lk(mtx_); buf_.append(s, (std::size_t)n); std::size_t pos = buf_.find_last_of('\n'); if (pos != std::string::npos) { out_locked(buf_.data(), pos + 1); // 只输出到最后一个换行 buf_.erase(0, pos + 1); } return n; } int sync() { std::lock_guard lk(mtx_); flush_locked(); if (console_) console_->pubsync(); return 0; } private: void flush_locked() { if (!buf_.empty()) { out_locked(buf_.data(), buf_.size()); buf_.clear(); } RunLog::instance().flush(); } void out_locked(const char* data, std::size_t n) { if (console_) console_->sputn(data, (std::streamsize)n); RunLog::instance().write(data, n); } std::streambuf* console_; std::string buf_; std::mutex mtx_; }; static TeeBuf* g_cout_tee = nullptr; static TeeBuf* g_cerr_tee = nullptr; static std::streambuf* g_cout_old = nullptr; static std::streambuf* g_cerr_old = nullptr; } // namespace void start_run_log() { if (g_cout_tee) return; // 已启动,幂等 std::cout.flush(); std::cerr.flush(); g_cout_old = std::cout.rdbuf(); g_cerr_old = std::cerr.rdbuf(); g_cout_tee = new TeeBuf(g_cout_old); // 常驻,不释放 g_cerr_tee = new TeeBuf(g_cerr_old); std::cout.rdbuf(g_cout_tee); std::cerr.rdbuf(g_cerr_tee); } void stop_run_log() { if (!g_cout_tee) return; std::cout.flush(); std::cerr.flush(); std::cout.rdbuf(g_cout_old); // 还原,避免静态析构期再进日志 std::cerr.rdbuf(g_cerr_old); RunLog::instance().close(); }