Files
SimpleRemoter/common/logger.h
yuanyuanxiang 11e8b122fe Fix: Client log dialog stops updating past edit-control default limit
The Win32 EDIT control caps text at ~32K/64K chars by default, so the
client log dialog silently stopped accepting appends long before the
512KB truncation logic could run. Call SetLimitText(0) in both
CClientLog and CActivityDialog to lift the cap.

Also unify newline handling in Logger: each entry now ends with exactly
one '\n' at the source, so the in-memory ring-buffer dump (used by the
TCP log query) splits into lines correctly instead of relying on every
Mprintf caller ending its format with '\n'. writeToFile drops the extra
std::endl accordingly.

Additionally fix a CFont leak in CClientLog::OnInitDialog by moving the
font to a member.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 17:54:33 +02:00

418 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#ifdef _MSC_VER
#pragma warning(disable: 4996)
#endif
#ifdef _WIN32
#ifdef _WINDOWS
#include <afxwin.h>
#else
#include <windows.h>
#endif
#include "skCrypter.h"
#else
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#endif
#include <iostream>
#include <fstream>
#include <mutex>
#include <queue>
#include <string>
#include <thread>
#include <condition_variable>
#include <atomic>
#include <chrono>
#include <sstream>
#include <cstdarg>
#include <iomanip>
#include <algorithm>
#include <deque>
inline bool stringToBool(const std::string& str)
{
std::string lower = str;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
return (lower == "true" || lower == "1");
}
class Logger
{
public:
enum LogLevel {
InfoLevel, WarningLevel, ErrorLevel
};
// 单例模式
static Logger& getInstance()
{
static Logger instance;
if (instance.pid.empty()) {
#ifdef _WIN32
char buf[16] = {};
sprintf_s(buf, "%d", GetCurrentProcessId());
instance.pid = buf;
// SYSTEM | C:\Windows\Temp
// 普通用户 | C:\Users\用户名\AppData\Local\Temp
char logPath[MAX_PATH] = { 0 };
GetEnvironmentVariableA("TEMP", logPath, MAX_PATH);
instance.InitLogFile(logPath, instance.pid);
#ifdef _WINDOWS
instance.enable = true; // 主控日志默认打开
#else
char var[32] = {};
const char* name = skCrypt("ENABLE_LOG");
DWORD size = GetEnvironmentVariableA(name, var, sizeof(var));
instance.enable = stringToBool(var);
instance.log("logger.h", __LINE__, "GetEnvironmentVariable: %s=%s\n", name, var);
#endif
#else // Linux
char buf[16] = {};
snprintf(buf, sizeof(buf), "%d", (int)getpid());
instance.pid = buf;
// ~/.config/ghost/ 目录写日志
std::string logDir;
const char* xdg = getenv("XDG_CONFIG_HOME");
if (xdg && xdg[0]) {
logDir = std::string(xdg) + "/ghost";
} else {
const char* home = getenv("HOME");
if (home && home[0]) {
logDir = std::string(home) + "/.config/ghost";
} else {
logDir = "/tmp";
}
}
mkdir(logDir.c_str(), 0755); // 确保日志目录存在
instance.InitLogFile(logDir, instance.pid);
// daemon 模式默认打开日志,可通过 ENABLE_LOG=0 关闭
const char* envLog = getenv("ENABLE_LOG");
if (envLog) {
instance.enable = stringToBool(envLog);
} else {
instance.enable = true;
}
#endif
}
return instance;
}
// 禁止拷贝和赋值
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
// 设置日志文件名
void setLogFile(const std::string& filename)
{
std::lock_guard<std::mutex> lock(fileMutex);
logFileName = filename;
}
// 启用日志
void usingLog(bool b = true)
{
enable = b;
}
// 刷新日志队列(同步等待所有日志写入完成)
void flush()
{
// 等待队列清空
while (true) {
{
std::lock_guard<std::mutex> lock(queueMutex);
if (logQueue.empty()) break;
}
cv.notify_one();
#ifdef _WIN32
Sleep(1);
#else
usleep(1000);
#endif
}
}
// 写日志,支持 printf 格式化
void log(const char* file, int line, const char* format, ...)
{
va_list args;
va_start(args, format);
std::string message = formatString(format, args);
va_end(args);
auto timestamp = getCurrentTimestamp();
std::string id = pid.empty() ? "" : "[" + pid + "]";
std::string logEntry = file && line ?
id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message:
id + "[" + timestamp + "] " + message;
// 方案B在源头统一保证每条日志以单个 '\n' 结尾,文件与内存 ring buffer
// 共用同一份文本;写文件时不再额外追加换行,内存导出直接拼接即可正确分行。
while (!logEntry.empty() && (logEntry.back() == '\n' || logEntry.back() == '\r'))
logEntry.pop_back();
logEntry += '\n';
// Always record to in-memory ring buffer regardless of enable flag.
{
std::lock_guard<std::mutex> lock(m_memMutex);
m_memLog.push_back(logEntry);
m_totalCount++;
if (m_memLog.size() > kMemLogMax)
m_memLog.pop_front();
}
if (enable) {
if (running) {
std::lock_guard<std::mutex> lock(queueMutex);
logQueue.push(logEntry);
} else {
writeToFile(logEntry);
}
}
#ifdef _DEBUG
#ifndef _WINDOWS
printf("%s", logEntry.c_str());
#else
OutputDebugStringA(logEntry.c_str());
#endif
#endif
cv.notify_one(); // 通知写线程
}
// 返回内存 ring buffer 中的全部日志(供 TCP 查询使用)
std::string DumpMemoryLog() const
{
std::lock_guard<std::mutex> lock(m_memMutex);
std::string result;
result.reserve(m_memLog.size() * 128);
for (const auto& entry : m_memLog)
result += entry;
return result;
}
// 增量版本:只返回 fromAbs 之后的新条目,并将 fromAbs 更新到当前末尾。
// fromAbs 是调用方维护的绝对计数器(首次传 0 即得全量)。
std::string DumpMemoryLogFrom(size_t& fromAbs) const
{
std::lock_guard<std::mutex> lock(m_memMutex);
// ring buffer 内最旧条目的绝对序号
size_t oldest = (m_totalCount >= m_memLog.size()) ? m_totalCount - m_memLog.size() : 0;
size_t startIdx = (fromAbs <= oldest) ? 0 : (fromAbs - oldest);
std::string result;
for (size_t i = startIdx; i < m_memLog.size(); ++i)
result += m_memLog[i];
fromAbs = m_totalCount;
return result;
}
// 停止日志系统
void stop()
{
if (!running) return;
{
std::lock_guard<std::mutex> lock(queueMutex);
running = false; // 设置运行状态
}
cv.notify_one();
if (workerThread.joinable()) {
try {
workerThread.join();
} catch (const std::system_error& e) {
printf("Join failed: %s [%d]\n", e.what(), e.code().value());
}
}
#ifdef _WIN32
for (int i = 0; threadRun && i++ < 1000; Sleep(1));
#else
for (int i = 0; threadRun && i++ < 1000; usleep(1000));
#endif
}
private:
// 日志按月份起名
void InitLogFile(const std::string & dir, const std::string& pid)
{
time_t currentTime = time(nullptr);
tm* localTime = localtime(&currentTime);
char timeString[32];
strftime(timeString, sizeof(timeString), "%Y-%m", localTime);
char fileName[100];
#ifdef _WINDOWS
sprintf_s(fileName, "\\YAMA_%s_%s.txt", timeString, pid.c_str());
#elif defined(_WIN32)
sprintf_s(fileName, "\\log_%s_%s.txt", timeString, pid.c_str());
#else
snprintf(fileName, sizeof(fileName), "/log_%s_%s.txt", timeString, pid.c_str());
#endif
logFileName = dir + fileName;
}
std::string logFileName; // 日志文件名
bool enable; // 是否启用
bool threadRun; // 日志线程状态
std::queue<std::string> logQueue; // 日志队列
std::mutex queueMutex; // 队列互斥锁
std::condition_variable cv; // 条件变量
std::atomic<bool> running; // 是否运行
std::thread workerThread; // 后台线程
std::mutex fileMutex; // 文件写入锁
std::string pid; // 进程ID
static constexpr size_t kMemLogMax = 1000;
std::deque<std::string> m_memLog;
size_t m_totalCount = 0;
mutable std::mutex m_memMutex;
Logger() : enable(false), threadRun(false), running(true), workerThread(&Logger::processLogs, this) {}
~Logger()
{
stop();
}
// 后台线程处理日志
// 退出语义stop() 设 running=false 后,本线程必须把队列里**已入队**的日志
// 全部刷盘再退出。否则进程死亡前最后几条 Mprintf包括退出原因会丢失。
void processLogs()
{
threadRun = true;
while (true) {
std::unique_lock<std::mutex> lock(queueMutex);
cv.wait(lock, [this]() {
return !running || !logQueue.empty();
});
// drain不带 running 判断,确保 stop() 时残留条目也写完
while (!logQueue.empty()) {
std::string logEntry = logQueue.front();
logQueue.pop();
lock.unlock();
// 写入日志文件
writeToFile(logEntry);
lock.lock();
}
// 队列已空再决定要不要退出
if (!running) break;
}
threadRun = false;
}
// 写入文件
void writeToFile(const std::string& logEntry)
{
std::lock_guard<std::mutex> lock(fileMutex);
std::ofstream logFile(logFileName, std::ios::app);
if (logFile.is_open()) {
logFile << logEntry; // logEntry 已在 log() 中以单个 '\n' 结尾
}
}
// 获取当前时间戳
std::string getCurrentTimestamp()
{
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm;
#ifdef _WIN32
localtime_s(&tm, &in_time_t); // Windows 安全版本
#else
localtime_r(&in_time_t, &tm); // POSIX 安全版本
#endif
std::stringstream ss;
ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
return ss.str();
}
// 将日志级别转换为字符串
std::string logLevelToString(LogLevel level)
{
switch (level) {
case InfoLevel:
return "INFO";
case WarningLevel:
return "WARNING";
case ErrorLevel:
return "ERROR";
default:
return "UNKNOWN";
}
}
// 格式化字符串
std::string formatString(const char* format, va_list args)
{
char buffer[1024];
vsnprintf(buffer, sizeof(buffer), format, args);
return std::string(buffer);
}
};
inline const char* getFileName(const char* path)
{
const char* fileName = strrchr(path, '\\');
if (!fileName) {
fileName = strrchr(path, '/');
}
return fileName ? fileName + 1 : path;
}
#ifdef _WINDOWS
#ifdef _DEBUG
#define Mprintf(format, ...) TRACE(format, __VA_ARGS__)
#else
#define Mprintf(format, ...) Logger::getInstance().log(getFileName(__FILE__), __LINE__, format, __VA_ARGS__)
#endif
#elif defined(_WIN32)
#define Mprintf(format, ...) Logger::getInstance().log(getFileName((__FILE__)), __LINE__, (format), __VA_ARGS__)
#elif defined(__APPLE__)
// macOS: 使用 NSLog 输出到系统日志(可通过 Console.app 查看)
#ifdef Mprintf
#undef Mprintf
#endif
#ifdef __OBJC__
#define Mprintf(format, ...) NSLog(@"%@", [NSString stringWithFormat:@(format), ##__VA_ARGS__])
#else
#define Mprintf(format, ...) printf(format, ##__VA_ARGS__)
#endif
#elif defined(__ANDROID__)
#include <android/log.h>
#include <cstdarg>
#ifdef Mprintf
#undef Mprintf
#endif
void UseAndroidLog(void (*cb)(const char*));
void android_log(const char* fmt, ...);
#define Mprintf android_log
#else
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
#ifdef Mprintf
#undef Mprintf
#endif
#define Mprintf(format, ...) Logger::getInstance().log(getFileName(__FILE__), __LINE__, format, ##__VA_ARGS__)
#endif
inline void Log(const char* message)
{
return Logger::getInstance().log(NULL, 0, "%s", message);
}
inline void Logf(const char* file, int line, const char* format, ...)
{
va_list args;
va_start(args, format);
char message[1024];
vsnprintf(message, sizeof(message), format, args);
va_end(args);
return Logger::getInstance().log(getFileName(file), line, "%s", message);
}