Implements steps 1-3 of discussion #644 (deliberately not steps 4/5 --
no signal handler / crash flush, no diagnostics UI; see below).
## Step 1 -- fix the existing logger (bugs, no new behavior)
- One QFile handle held open for the whole session under a mutex,
instead of opening and closing the log file on every single message.
- The log directory and the session's date-stamped filename are
resolved exactly once, in the new QetLogger::init() called explicitly
from main() immediately before qInstallMessageHandler() -- not
recomputed per message, so a session that runs past midnight now
stays in one file instead of silently splitting.
- Age-based retention now uses lastModified() instead of lastRead():
opening a log to attach it to a bug report no longer resets its
retention clock.
- stderr and file output both encode UTF-8 explicitly (toUtf8()),
replacing stderr's toLocal8Bit() and the file stream's previously
Qt5/Qt6-inconsistent default encoding.
## Step 2 -- size-capped rotation + hardening
- The previously-unbounded daily file is now capped at 2 MiB and
rotated (kMaxFileBytes/kRotationKeep in QetLogger), keeping
<date>.log plus <date>.1.log .. <date>.4.log; oldest is dropped.
- Each message is truncated to 4 KB with a "...[truncated N bytes]"
marker before it reaches the ring or the file.
- Control characters (newlines, tabs, other non-printables) in message
content are escaped, since much of what QET logs is externally
controlled (file paths, element names, font strings out of a .qet
file) -- left unescaped, an embedded '\n' could forge log lines.
- The log file is refused if a symlink already exists at that path,
and is created/rotated owner-read/write only.
## Step 3 -- in-memory ring buffer
- LogRing (sources/logging/logring.h) is a fixed-capacity, always-on
ring of the last 4096 log lines, preallocated once at construction
(4096 * 512 B = 2 MiB) so append() never allocates. Entries are
stored as plain pre-formatted bytes in fixed-size slots -- the shape
discussion #644 specifies so a *future* crash handler could dump it
with nothing but write(2), even though no such handler exists yet.
Thread-safe via a plain QMutex (the lock-free requirement in the
discussion applies specifically to a signal-handler read path, which
this step doesn't add).
## Escape hatch
QET_LOG_DISABLE=1 in the environment at startup bypasses all of the
above -- no ring, no file, no rotation -- falling back to a minimal,
self-contained stderr passthrough that doesn't share any code with the
new formatting/sanitization path, so it stays usable even if that path
is what's misbehaving.
## Deliberately not included (per the discussion's own phasing)
- No signal handler / crash-time ring flush (step 4) -- the discussion
flags this as the highest-risk piece, explicitly meant to land last
and behind its own switch once the rest is proven.
- No diagnostics export UI (step 5).
- No log categories, session header, repeat collapsing or rate
limiting -- listed under "best practices worth building in", not
part of steps 1-3.
## Testing
Built clean, no new warnings.
Verified with real runs (QT_QPA_PLATFORM=offscreen, isolated HOME):
- Log file created at the expected dataDir()/YYYYMMDD.log path, mode
0600.
- A full startup's worth of real messages (translations, MachineInfo's
system dump, collection loading) written correctly; every one of the
231 lines in one run starts with a proper timestamp -- confirmed the
sanitizer correctly escapes the raw embedded newlines/tabs in
MachineInfo's multi-line CPU/GPU description fields into visible
\n/\t sequences rather than letting them fragment the log.
- QET_LOG_DISABLE=1: zero log files created, stderr still worked via
the independent legacy path.
- Rotation: pre-filled a log to just under the 2 MiB cap, ran a normal
session, confirmed it rotated to <date>.1.log (still 0600) with a
byte-clean split (no truncated/duplicated line at the boundary) and
a fresh <date>.log picked up from the next line.