567 lines
12 KiB
C++
567 lines
12 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "UnsyncLog.h"
|
|
#include "UnsyncCore.h"
|
|
#include "UnsyncError.h"
|
|
#include "UnsyncFile.h"
|
|
#include "UnsyncUtil.h"
|
|
#include "UnsyncVersion.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <vector>
|
|
#include <chrono>
|
|
|
|
UNSYNC_THIRD_PARTY_INCLUDES_START
|
|
#include <fmt/format.h>
|
|
#include <fmt/chrono.h>
|
|
UNSYNC_THIRD_PARTY_INCLUDES_END
|
|
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
# include <Dbghelp.h>
|
|
# include <Windows.h>
|
|
# pragma comment(lib, "Dbghelp.lib")
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
|
|
namespace unsync {
|
|
|
|
static constexpr size_t TimestampStringSize = sizeof("YYYY-MM-DDTHH:MM:SS.sssZ");
|
|
using FTimestampString = fmt::basic_memory_buffer<char, TimestampStringSize>;
|
|
static FTimestampString FormatTimestamp(std::chrono::system_clock::time_point Timestamp);
|
|
static FTimestampString FormatWindowsFileTime(uint64 Ticks);
|
|
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
bool
|
|
IsDebuggerPresent()
|
|
{
|
|
return ::IsDebuggerPresent();
|
|
}
|
|
#else
|
|
bool
|
|
IsDebuggerPresent()
|
|
{
|
|
return false; // TODO
|
|
}
|
|
#endif
|
|
|
|
thread_local bool GLogVerbose = false;
|
|
bool GLogVeryVerbose = false;
|
|
bool GLogSilent = false;
|
|
bool GBreakOnError = IsDebuggerPresent();
|
|
bool GBreakOnWarning = false;
|
|
thread_local uint32 GLogIndent = 0;
|
|
bool GLogProgress = false;
|
|
bool GLogMachineReadable = false;
|
|
|
|
std::mutex GLogMutex;
|
|
|
|
std::atomic<uint32> GLogThreadIndexCounter;
|
|
thread_local uint32 GLogThreadIndex = ~0u;
|
|
|
|
FTimePoint GNextFlushTime = TimePointNow();
|
|
|
|
std::vector<std::wstring> GCommandLine;
|
|
|
|
void
|
|
LogSaveCommandLineUtf8(int Argc, char** Argv)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
for (int i = 0; i < Argc; ++i)
|
|
{
|
|
GCommandLine.push_back(ConvertUtf8ToWide(Argv[i]));
|
|
}
|
|
}
|
|
|
|
static FILE*
|
|
GetLogStream(ELogLevel LogLevel)
|
|
{
|
|
// If machine readable log format is enabled, *all* diagnostic logging is sent to stderr.
|
|
// Otherwise, only errors and warnings are sent to stderr, while normal diagnostics are sent to stdout.
|
|
|
|
if (GLogMachineReadable)
|
|
{
|
|
return (LogLevel == ELogLevel::MachineReadable) ? stdout : stderr;
|
|
}
|
|
else
|
|
{
|
|
return (LogLevel >= ELogLevel::Info) ? stdout : stderr;
|
|
}
|
|
}
|
|
|
|
static void
|
|
LogConditionalFlush(FILE* Stream)
|
|
{
|
|
FTimePoint NextFlushTime = GNextFlushTime;
|
|
FTimePoint CurrentTime = TimePointNow();
|
|
if (CurrentTime > NextFlushTime || GLogProgress)
|
|
{
|
|
GNextFlushTime = CurrentTime + std::chrono::milliseconds(1000);
|
|
fflush(Stream);
|
|
}
|
|
}
|
|
|
|
static void
|
|
LogConditionalFlush(ELogLevel LogLevel)
|
|
{
|
|
LogConditionalFlush(GetLogStream(LogLevel));
|
|
}
|
|
|
|
static uint32
|
|
GetLogThreadIndex()
|
|
{
|
|
if (GLogThreadIndex == ~0u)
|
|
{
|
|
GLogThreadIndex = GLogThreadIndexCounter++;
|
|
}
|
|
return GLogThreadIndex;
|
|
}
|
|
|
|
struct FLogFile
|
|
{
|
|
FLogFile(const wchar_t* InFilename) : Filename(InFilename)
|
|
{
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
Handle = _wfopen(InFilename, L"wt, ccs=UNICODE");
|
|
#else
|
|
std::string FilenameUtf8 = ConvertWideToUtf8(std::wstring_view(InFilename));
|
|
Handle = fopen(FilenameUtf8.c_str(), "wt");
|
|
#endif
|
|
}
|
|
|
|
~FLogFile()
|
|
{
|
|
if (Handle)
|
|
{
|
|
fclose(Handle);
|
|
}
|
|
}
|
|
|
|
std::wstring Filename;
|
|
FILE* Handle = nullptr;
|
|
};
|
|
|
|
std::unique_ptr<FLogFile> GLogFile;
|
|
|
|
static void
|
|
RotateLogs(const FPath& LogFilename)
|
|
{
|
|
static const FPath ExpectedSuffix(".log");
|
|
|
|
std::error_code ErrorCode;
|
|
FFileAttributes ExistingAttrib = GetFileAttrib(LogFilename);
|
|
|
|
if (!LogFilename.has_parent_path() || !ExistingAttrib.bValid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FTimestampString TimestampString = FormatWindowsFileTime(ExistingAttrib.Mtime);
|
|
std::string BackupExtensionStr = TimestampString.data();
|
|
std::replace_if(
|
|
BackupExtensionStr.begin(),
|
|
BackupExtensionStr.end(),
|
|
[](char C) { return C == ':' || C == '.'; },
|
|
'-');
|
|
BackupExtensionStr += ".log";
|
|
|
|
FPath BackupExtension = FPath(BackupExtensionStr);
|
|
FPath BackupLogFilename = LogFilename;
|
|
BackupLogFilename.replace_extension(BackupExtension);
|
|
|
|
FileRename(LogFilename, BackupLogFilename, ErrorCode);
|
|
|
|
FPath LogDirectory = LogFilename.parent_path();
|
|
|
|
FPathFilterCallback Filter = [](const FPath& CandidatePath)
|
|
{
|
|
return CandidatePath.has_extension() && CandidatePath.native().ends_with(ExpectedSuffix.native());
|
|
};
|
|
|
|
DeleteOldFilesInDirectory(LogDirectory, 10 /*files to keep*/, true /*allow in dry run*/, Filter);
|
|
}
|
|
|
|
void
|
|
LogSetFileInternal(const wchar_t* Filename)
|
|
{
|
|
GLogFile = {};
|
|
|
|
if (Filename)
|
|
{
|
|
GLogFile = std::make_unique<FLogFile>(Filename);
|
|
}
|
|
}
|
|
|
|
std::vector<std::unique_ptr<FLogFile>> GLogFileStack;
|
|
|
|
FLogFileScope::FLogFileScope(const wchar_t* Filename)
|
|
{
|
|
std::wstring CommandLine;
|
|
|
|
RotateLogs(FPath(Filename));
|
|
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
GLogFileStack.push_back(std::move(GLogFile));
|
|
LogSetFileInternal(Filename);
|
|
|
|
for (const std::wstring& Arg : GCommandLine)
|
|
{
|
|
if (!CommandLine.empty())
|
|
{
|
|
CommandLine += ' ';
|
|
}
|
|
CommandLine += Arg;
|
|
}
|
|
}
|
|
|
|
UNSYNC_VERBOSE2(L"UNSYNC v%hs started logging to file '%ls'", GetVersionString().c_str(), Filename);
|
|
|
|
if (!CommandLine.empty())
|
|
{
|
|
UNSYNC_VERBOSE2(L"Command line: %ls", CommandLine.c_str());
|
|
}
|
|
}
|
|
|
|
FLogFileScope::~FLogFileScope()
|
|
{
|
|
UNSYNC_VERBOSE2(L"Finished logging to file");
|
|
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
std::swap(GLogFile, GLogFileStack.back());
|
|
GLogFileStack.pop_back();
|
|
}
|
|
|
|
void
|
|
LogProgress(const wchar_t* ItemName, uint64 Current, uint64 Total)
|
|
{
|
|
if (!GLogProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
if (ItemName == nullptr)
|
|
{
|
|
ItemName = L"*";
|
|
}
|
|
wprintf(L"@progress [%ls] %llu / %llu\n", ItemName, Current, Total);
|
|
|
|
LogConditionalFlush(stdout); // progress is always reported to stdout
|
|
}
|
|
|
|
void
|
|
LogStatus(const wchar_t* InItemName, const wchar_t* Status)
|
|
{
|
|
if (!GLogProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
|
|
const wchar_t* ItemName = InItemName ? InItemName : L"*";
|
|
wprintf(L"@status [%ls] %ls\n", ItemName, Status);
|
|
|
|
if (InItemName)
|
|
{
|
|
LogConditionalFlush(stdout); // status is always reported to stdout
|
|
}
|
|
else
|
|
{
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
void
|
|
LogFlush()
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
fflush(stdout);
|
|
fflush(stderr);
|
|
if (GLogFile && GLogFile->Handle)
|
|
{
|
|
FILE* F = GLogFile->Handle;
|
|
fflush(F);
|
|
}
|
|
}
|
|
|
|
static FTimestampString
|
|
FormatTimestamp(std::chrono::system_clock::time_point Timestamp)
|
|
{
|
|
auto TimestampMilliseconds = std::chrono::time_point_cast<std::chrono::milliseconds>(Timestamp);
|
|
auto TimestampSeconds = std::chrono::time_point_cast<std::chrono::seconds>(TimestampMilliseconds);
|
|
auto DeltaMilliseconds = (TimestampMilliseconds - TimestampSeconds).count();
|
|
std::tm UtcTime = fmt::gmtime(std::chrono::system_clock::to_time_t(TimestampSeconds));
|
|
|
|
FTimestampString Result;
|
|
fmt::format_to(std::back_inserter(Result), "{:%FT%T}.{:03}Z", UtcTime, DeltaMilliseconds);
|
|
Result.push_back(0);
|
|
|
|
UNSYNC_ASSERT(Result.size() == TimestampStringSize);
|
|
|
|
return Result;
|
|
}
|
|
|
|
static FTimestampString
|
|
FormatWindowsFileTime(uint64 Ticks)
|
|
{
|
|
std::filesystem::file_time_type FileTime = FromWindowsFileTime(Ticks);
|
|
return FormatTimestamp(SystemTimeFromFileTime(FileTime));
|
|
}
|
|
|
|
void
|
|
LogPrintf(ELogLevel Level, const wchar_t* Str, ...)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
|
|
const auto Timestamp = std::chrono::system_clock::now();
|
|
|
|
bool bShouldIndent = false;
|
|
const wchar_t* Prefix = nullptr;
|
|
|
|
const uint32 ThreadIndex = GetLogThreadIndex();
|
|
|
|
bool bShouldOutputThreadIndex = false;
|
|
|
|
if (Level == ELogLevel::Error)
|
|
{
|
|
Prefix = L"ERROR: ";
|
|
bShouldOutputThreadIndex = ThreadIndex != 0;
|
|
}
|
|
else if (Level == ELogLevel::Warning)
|
|
{
|
|
Prefix = L"WARNING: ";
|
|
bShouldOutputThreadIndex = ThreadIndex != 0;
|
|
}
|
|
else if (GLogIndent)
|
|
{
|
|
// Indent the log only for info/debug/trace levels
|
|
bShouldIndent = true;
|
|
}
|
|
|
|
ELogLevel MaxDisplayLevel = ELogLevel::Info;
|
|
|
|
if (GLogSilent)
|
|
{
|
|
MaxDisplayLevel = ELogLevel::Warning;
|
|
}
|
|
else if (GLogVerbose)
|
|
{
|
|
if (GLogVeryVerbose)
|
|
{
|
|
MaxDisplayLevel = ELogLevel::Trace;
|
|
}
|
|
else
|
|
{
|
|
MaxDisplayLevel = ELogLevel::Debug;
|
|
}
|
|
}
|
|
|
|
FILE* LogStream = GetLogStream(Level);
|
|
|
|
if (Level <= MaxDisplayLevel || Level == ELogLevel::MachineReadable)
|
|
{
|
|
if (Prefix)
|
|
{
|
|
fwprintf(LogStream, Prefix);
|
|
}
|
|
|
|
if (bShouldOutputThreadIndex)
|
|
{
|
|
fwprintf(LogStream, L"[Thread %u] ", ThreadIndex);
|
|
}
|
|
|
|
if (bShouldIndent)
|
|
{
|
|
fwprintf(LogStream, L"%*c", GLogIndent, L' ');
|
|
}
|
|
|
|
va_list Va;
|
|
va_start(Va, Str);
|
|
vfwprintf(LogStream, Str, Va);
|
|
va_end(Va);
|
|
|
|
LogConditionalFlush(Level);
|
|
}
|
|
|
|
if (GLogFile && GLogFile->Handle)
|
|
{
|
|
FILE* LogFileStream = GLogFile->Handle;
|
|
|
|
FTimestampString TimestampString = FormatTimestamp(Timestamp);
|
|
|
|
fwprintf(LogFileStream, L"[%hs] ", TimestampString.data());
|
|
|
|
fwprintf(LogFileStream, L"[%3u] ", ThreadIndex);
|
|
|
|
switch (Level)
|
|
{
|
|
case ELogLevel::Error:
|
|
fwprintf(LogFileStream, L"[ERROR] ");
|
|
break;
|
|
case ELogLevel::Warning:
|
|
fwprintf(LogFileStream, L"[WARN] ");
|
|
break;
|
|
case ELogLevel::Info:
|
|
fwprintf(LogFileStream, L"[INFO] ");
|
|
break;
|
|
case ELogLevel::Debug:
|
|
fwprintf(LogFileStream, L"[DEBUG] ");
|
|
break;
|
|
case ELogLevel::Trace:
|
|
fwprintf(LogFileStream, L"[TRACE] ");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
va_list Va;
|
|
va_start(Va, Str);
|
|
vfwprintf(LogFileStream, Str, Va);
|
|
va_end(Va);
|
|
|
|
if (Level == ELogLevel::Error || Level == ELogLevel::Warning)
|
|
{
|
|
fflush(LogFileStream);
|
|
}
|
|
}
|
|
}
|
|
|
|
extern const char* HttpStatusToString(int32 Code);
|
|
void
|
|
LogError(const FError& E, std::wstring ExtraContext)
|
|
{
|
|
const char* ErrorKindStr = nullptr;
|
|
const char* ErrorDescStr = nullptr;
|
|
switch (E.Kind)
|
|
{
|
|
default:
|
|
case EErrorKind::Unknown:
|
|
ErrorKindStr = "Unknown";
|
|
break;
|
|
case EErrorKind::Http:
|
|
ErrorDescStr = HttpStatusToString(E.Code);
|
|
ErrorKindStr = "HTTP";
|
|
break;
|
|
case EErrorKind::System:
|
|
ErrorKindStr = "System";
|
|
break;
|
|
case EErrorKind::App:
|
|
ErrorKindStr = "Application";
|
|
break;
|
|
}
|
|
|
|
const bool bHaveContext = !E.Context.empty();
|
|
const bool bHaveExtraContext = !ExtraContext.empty();
|
|
|
|
if (ErrorDescStr)
|
|
{
|
|
LogPrintf(ELogLevel::Error,
|
|
L"%ls%hs%hs code: %d (%hs).%ls%ls\n",
|
|
bHaveExtraContext ? ExtraContext.c_str() : L"",
|
|
bHaveExtraContext ? ": " : "",
|
|
ErrorKindStr,
|
|
E.Code,
|
|
ErrorDescStr,
|
|
bHaveContext ? L" Context: " : L"",
|
|
E.Context.empty() ? L"" : E.Context.c_str());
|
|
}
|
|
else
|
|
{
|
|
LogPrintf(ELogLevel::Error,
|
|
L"%ls%hs%hs code: %d.%ls%ls\n",
|
|
bHaveExtraContext ? ExtraContext.c_str() : L"",
|
|
bHaveExtraContext ? ": " : "",
|
|
ErrorKindStr,
|
|
E.Code,
|
|
bHaveContext ? L" Context: " : L"",
|
|
E.Context.empty() ? L"" : E.Context.c_str());
|
|
}
|
|
}
|
|
|
|
static FPath GCrashDumpPath;
|
|
void
|
|
SetCrashDumpPath(const FPath& Path)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
GCrashDumpPath = Path;
|
|
}
|
|
|
|
bool
|
|
get_crash_dump_path(FPath& OutPath)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(GLogMutex);
|
|
if (GCrashDumpPath.empty())
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
OutPath = GCrashDumpPath;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool
|
|
LogWriteCrashDump(void* InExceptionPointers)
|
|
{
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
|
|
_EXCEPTION_POINTERS* ExceptionPointers = (_EXCEPTION_POINTERS*)InExceptionPointers;
|
|
|
|
FPath CrashDumpPath;
|
|
if (!get_crash_dump_path(CrashDumpPath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
wchar_t DumpFilename[4096] = {};
|
|
uint64 Timestamp = TimePointNow().time_since_epoch().count();
|
|
swprintf_s(DumpFilename, L"unsync-%016llX.dmp", Timestamp);
|
|
FPath CrashDumpFilename = CrashDumpPath / FPath(DumpFilename);
|
|
|
|
LogPrintf(ELogLevel::Info, L"!!! Writing crash dump to '%ls'\n", CrashDumpFilename.wstring().c_str());
|
|
|
|
MINIDUMP_EXCEPTION_INFORMATION ExceptionInfo;
|
|
ExceptionInfo.ThreadId = GetCurrentThreadId();
|
|
ExceptionInfo.ExceptionPointers = ExceptionPointers;
|
|
ExceptionInfo.ClientPointers = true;
|
|
|
|
HANDLE DumpFile =
|
|
CreateFileW(CrashDumpFilename.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
|
|
if (DumpFile != INVALID_HANDLE_VALUE)
|
|
{
|
|
const BOOL Ok =
|
|
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), DumpFile, MiniDumpWithDataSegs, &ExceptionInfo, nullptr, nullptr);
|
|
|
|
if (Ok)
|
|
{
|
|
LogPrintf(ELogLevel::Info, L"!!! Crash dump file written.\n");
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
LogPrintf(ELogLevel::Error, L"!!! Failed to generate crash dump. %hs\n", FormatSystemErrorMessage(GetLastError()).c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogPrintf(ELogLevel::Error, L"!!! Failed to open output file. %hs\n", FormatSystemErrorMessage(GetLastError()).c_str());
|
|
}
|
|
|
|
LogPrintf(ELogLevel::Error, L"!!! Failed to write dump file.\n");
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
|
|
return false;
|
|
}
|
|
|
|
FLogFlushScope::~FLogFlushScope()
|
|
{
|
|
LogFlush();
|
|
}
|
|
|
|
} // namespace unsync
|