Release v0.2.0 with incident diagnostics and in-app log viewer

This commit is contained in:
2026-10-28 01:38:30 +02:00
parent 076210dc9c
commit cf18a0a3c5
15 changed files with 678 additions and 27 deletions
+100 -4
View File
@@ -527,16 +527,112 @@ std::wstring lastError(const std::wstring &context)
{
return context + L" (Windows error " + std::to_wstring(GetLastError()) + L")";
}
std::wstring diagnosticText(const std::wstring &text)
{
// Escape UTF-16 code units so invisible characters and malformed surrogate pairs
// can be reproduced without introducing fake log lines. Bound large selections.
std::wstring out = L"\"";
constexpr wchar_t hex[] = L"0123456789abcdef";
const auto count = std::min<size_t>(text.size(), 512);
for (size_t i = 0; i < count; ++i)
{
auto c = static_cast<unsigned short>(text[i]);
if (c == L'"' || c == L'\\')
{
out += L'\\';
out += c;
}
else if (c < 32 || c >= 127)
{
out += L"\\u";
for (int shift = 12; shift >= 0; shift -= 4)
out += hex[(c >> shift) & 15];
}
else
out += c;
}
return out + L"\" units=" + std::to_wstring(text.size()) +
L" truncated=" + std::to_wstring(count != text.size());
}
namespace
{
std::recursive_mutex logGate;
bool loggingEnabled = false;
DiagnosticBuffer diagnosticBuffer;
}
std::wstring DiagnosticBuffer::record(const std::wstring &message, bool problem, ULONGLONG now)
{
while (!entries.empty() && (entries.size() >= 40 || now - entries.front().first > 30000))
entries.pop_front();
entries.emplace_back(now, message);
if (!problem)
return {};
std::wstring out = L"Diagnostic incident (preceding context, up to 30 seconds):";
for (const auto &[at, entry] : entries)
out += L"\n ageMs=" + std::to_wstring(now - at) + L" " + entry;
entries.clear();
return out;
}
void setLoggingEnabled(bool enabled)
{
std::lock_guard lock(logGate);
loggingEnabled = enabled;
if (!enabled)
diagnosticBuffer.clear();
}
std::wstring readDiagnosticLog(bool previous)
{
std::lock_guard lock(logGate);
auto path = dataDirectory() / (previous ? L"kswitcher.log.1" : L"kswitcher.log");
if (!std::filesystem::exists(path))
return previous ? L"No previous log is available yet." : L"No log yet. Entries will appear when a problem is detected.";
auto bytes = readFile(path, 2 * 1024 * 1024);
auto text = wide(std::string(bytes.begin(), bytes.end()));
std::wstring display;
for (size_t i = 0; i < text.size(); ++i)
{
if (text[i] == L'\n' && (i == 0 || text[i - 1] != L'\r'))
display += L'\r';
display += text[i];
}
return display.empty() ? L"The log is empty." : display;
}
void clearDiagnosticLog(bool previous)
{
// Serialize clearing with writes and rotation. Never delete the other log.
std::lock_guard lock(logGate);
auto path = dataDirectory() / (previous ? L"kswitcher.log.1" : L"kswitcher.log");
std::filesystem::remove(path);
if (!previous)
diagnosticBuffer.clear();
}
void diagnosticEvent(const std::wstring &message, bool problem)
{
std::lock_guard lock(logGate);
if (!loggingEnabled)
return;
auto incident = diagnosticBuffer.record(message, problem, GetTickCount64());
if (!incident.empty())
log(incident);
}
void log(const std::wstring &message)
{
static std::mutex gate;
std::lock_guard lock(gate);
std::lock_guard lock(logGate);
if (!loggingEnabled)
return;
try
{
auto path = dataDirectory() / L"kswitcher.log";
std::error_code ec;
if (std::filesystem::file_size(path, ec) > 256 * 1024)
std::filesystem::remove(path, ec);
auto size = std::filesystem::file_size(path, ec);
if (!ec && size > 256 * 1024)
{
auto previous = path;
previous += L".1";
std::filesystem::remove(previous, ec);
ec.clear();
std::filesystem::rename(path, previous, ec);
}
std::ofstream f(path, std::ios::app);
SYSTEMTIME t;
GetLocalTime(&t);