mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-27 12:34:14 +02:00
remove-doxygen-lfs
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8c1b5206a |
Address review on #905: offered-list semantics, dump ordering, FreeBSD, legacy dumps
Five things raised in review, plus tests for the parts that were only described in prose. clearPendingCrashDump() did not do what its comment said. It called pendingCrashDumpFiles() again at clear time, so it deleted whatever was in the directory then, not what had been offered. The offer sits inside a modal dialog that stays open as long as the user reads it, and SingleApplication keys its socket on the binary path, so a second QElectroTech build running alongside is a separate process that can crash and write a dump in that window. Re-listing deleted that dump unseen -- the exact failure this change exists to fix. The list is now taken once in QETApp::checkCrashDump() and passed to both pendingCrashDumpContents() and clearPendingCrashDump(). The ring is now written before the backtrace. backtrace() unwinds through libgcc, which calls dl_iterate_phdr and takes the loader lock; warming it in install() removes the allocation but not the lock. Crashing inside dlopen() (Qt plugin loading), or on a corrupted stack, could therefore hang or re-fault the handler at the backtrace and lose the ring with it. Order is now header, signal, ring, backtrace, so the cheapest and most valuable part is already on disk before anything that can block. The class comment claimed the handler takes no locks; that was not strictly true and now says so. QET_CRASH_BACKTRACE comes from find_package(Backtrace) rather than __has_include(<execinfo.h>). The header exists on FreeBSD but backtrace() lives in libexecinfo there, so the probe compiled and the link failed. A crash_dump.log left by a pre-#905 version is migrated into crashes/ at startup, named from its own mtime. Otherwise upgrading stranded it: the new code never looks at that path, so the dump from the crash that prompted the upgrade would sit there unoffered forever. Also from the review: dumps are capped at the 10 newest, so a crash loop cannot fill the log directory before any dialog is shown; crashDumpDir() no longer creates the directory as a side effect of a const getter (ensureCrashDumpDir() does that for the callers that write); and redact() now masks an AppImage's per-run /tmp/.mount_XXXXXX prefix, which backtrace_symbols_fd() writes into every frame. Two test executables, both of which were checked to fail against the behaviour they replace: - tst_crashhandler covers CrashHandler::formatInt(), which had no coverage at all despite running only inside a signal handler, where nothing can assert: zero, negatives, INT_MIN (negated through unsigned, since -INT_MIN is UB), INT_MAX, truncation and a zero-sized buffer, each checked against a sentinel-filled buffer so a write past the reported length fails. - tst_crashdumps covers the bookkeeping: ordering, empty dumps, the exclusion of this run's own path, the cap, concatenation of every offered dump, that clearing deletes only what was offered, and what redact() masks. qetlogger.cpp needs exactly one symbol from the application, QETApp::dataDir(), which the test supplies itself. Not addressed here: the timestamp in crash_<timestamp>_<pid> is the launch time, not the crash time -- correct as observed, and the commit message that implied otherwise was the thing that was wrong. Resolvable QET frames for AppImage/Flatpak/Snap/Debian need -rdynamic and archived debug symbols, which is a packaging discussion, not this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0646f9ca4f |
Harden the crash reporter: keep every dump, and say what crashed
Three weaknesses, all visible in ChuckNr11's report on #898 -- "the report appeared only once despite there being 10 or more crashes". One dump per run instead of one per install ------------------------------------------- crashDumpPath() was a single fixed crash_dump.log, and the handler opens it O_TRUNC, so each crash destroyed the evidence from the one before. Ten crashes left one dump. Dumps now go to a crashes/ directory named crash_<timestamp>_<pid>.log, and every pending one is offered together, newest first, with a banner saying how many there are. A crash that repeats is exactly the case where the earlier dumps matter, because the difference between them is the evidence. The name is built in normal context and handed to CrashHandler::install(), which copies it into a preallocated buffer as before -- the handler still writes to one fixed path, so its no-allocation invariant is untouched. The dump now says which signal fired ------------------------------------ The header is built once at install(), so every dump looked identical no matter what killed the process -- and SIGSEGV and SIGABRT point at very different bugs. Written with an async-signal-safe integer formatter into a stack buffer, since snprintf is not on the POSIX safe list. ...and where it was ------------------- The ring said what the program was doing; nothing said where it died. The dump now carries a backtrace. backtrace() is warmed once in install() so its first-call lazy resolution cannot allocate inside the handler, and backtrace_symbols_fd() writes straight to the fd -- unlike backtrace_symbols(), which mallocs and must never be used here. Guarded on __has_include(<execinfo.h>) so platforms without it are unaffected. QET's own frames currently resolve as offsets rather than names, since the binary does not export its dynamic symbols. They are still resolvable offline: the header records the exact git SHA. Building with -rdynamic would give names directly, but that is a build-flag decision for its own change. Deliberately unchanged: the four invariants in crashhandler.h. Nothing added here allocates, blocks, takes a lock, or swallows the crash. Verified: three consecutive SIGSEGVs now leave three separate dumps, each carrying "Signal: 11" and a backtrace with resolved Qt frames; launching afterwards offers all three in one dialog, newest first, and clears them once shown. ctest 8/8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
da3a976b60 |
Add an event-loop responsiveness watchdog (discussion #644 follow-up)
QetLogger (discussion #644, steps 1-3) captures whatever an explicit qDebug()/qInfo()/qWarning() call already decided to report. Most of a session -- painting, dragging, a slow synchronous operation -- produces no log output at all, so a silent multi-second gap in the log is indistinguishable from the user simply not doing anything. That gap came up directly: investigating a user-reported "the program lagged" required inferring stalls from timestamp gaps between unrelated log lines, which can't tell a real freeze apart from normal idle time. EventLoopWatchdog closes that gap directly instead of inferring it. A QTimer::PreciseTimer repeating tick (every 50ms) measures the *actual* elapsed time since the previous tick via QElapsedTimer (monotonic, unaffected by system clock/NTP adjustments). Qt does not queue up missed fires for a normal repeating timer, so if the main thread is blocked for 600ms, the timer fires once as soon as the loop frees up, with ~600ms measured since the last tick -- that gap is the stall, measured at its source. Only logs (via the existing qWarning() path, so it reuses QetLogger's file/ring/rotation with no new plumbing) when a tick is late by more than 200ms, so a healthy session produces zero output from this class, in keeping with QetLogger's bounded-log design. Same QET_WATCHDOG_DISABLE=1 escape-hatch convention as QetLogger's own QET_LOG_DISABLE=1. Deliberately not included: attributing a stall to what caused it. This tells you a stall happened and how long -- pairing that timestamp with gdb attached to a running session (as used for the CLI hang, PR #661) is still how you get from "it stalled" to a root cause. Stacked on #647 (feature-diagnostic-logging-crash) for QetLogger/ qWarning() plumbing this depends on -- diff includes its commits until that merges. Verified against the compiled binary, not just read: temporarily injected a QThread::msleep(600) via a one-shot QTimer 2s after startup, confirmed the exact expected warning ("EventLoopWatchdog: main thread stalled for 620 ms") at the right severity through the real qWarning()/QetLogger path, then removed the test hook and reconfirmed a normal run produces no output from this class at all. |
||
|
|
5dec36cb29 |
Add crash-time ring flush and a diagnostics export UI (discussion #644, steps 4-5)
Stacked on the steps 1-3 branch (feature-diagnostic-logging, PR #646). Kept as its own PR rather than folded into that one, matching the discussion's own framing: step 4 is explicitly "the highest-risk piece ... lands last, behind its own switch." ## Step 4 -- crash-time ring flush (CrashHandler) Installs a handler for SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (POSIX) / SetUnhandledExceptionFilter (Windows) that flushes the in-memory ring to a fixed crash_dump.log before the process dies. This required reworking LogRing (step 3) to be genuinely lock-free, not just mutex-protected: a signal handler that blocks on a lock the crashing thread (or another thread) already holds turns a clean crash into a hang -- no ring dump *and* no core dump, worse than doing nothing. append() now claims a slot with a single atomic fetch-add; dumpToFd() reads the preallocated entries directly and writes them with write(2) only, looping on EINTR/short writes. Accepted tradeoff: at most one entry can be read torn if a crash lands mid-append into that exact slot -- documented in logring.h, and the alternative (a seqlock to detect and retry) wasn't judged worth the complexity for that window. Other invariants implemented per the discussion: - sigaltstack with a static 64 KiB buffer, SA_ONSTACK -- a stack- overflow SIGSEGV has no usable stack for a handler without one. - Nothing under the actual handler touches Qt, QString or the allocator: the dump path and a small header (version/git/OS/Qt) are precomputed into fixed char buffers by install(), which runs once at startup in normal context. - Atomic test-and-set so only the first crash writes a dump; a second concurrent/nested fault goes straight to restore-and-re-raise. - After writing, the handler restores SIG_DFL and re-raises (POSIX) / returns EXCEPTION_CONTINUE_SEARCH (Windows) so the OS's own crash path -- core dump, Windows Error Reporting -- still runs. A handler that "fixed" the crash by swallowing the signal would destroy exactly the post-mortem evidence this whole design exists to preserve. Tested in this environment: POSIX/Linux only, all five signals. Sent each directly to a running process and confirmed (a) crash_dump.log is written with the correct header and ring contents, mode 0600, and (b) the process still terminates via the signal with the kernel's own "core dumped" flag set (exit code 128+signal, confirmed for all five). The Windows path is implemented per the discussion's guidance but is untested -- no Windows build available in this sandbox. ## Step 5 -- getting the data back out - QETApp::checkCrashDump(), called from checkBackupFiles() only when there's no stale project file to recover this run (so the two prompts never both show, per the discussion), offers an unretrieved crash dump via DiagnosticsReportDialog and then deletes it regardless of the user's choice -- offered exactly once. - A new "Aide > Enregistrer un rapport de diagnostic..." action (QETMainWindow) builds the same kind of report from the *current* session (QetLogger::buildDiagnosticsReport(): header + this session's log file) for a manual "attach this to a bug report" flow, not tied to a crash. - Both go through QetLogger::redact() before ever reaching the user: the one redaction implemented is a literal replace of the home directory with "~", since an absolute path under it leaks the account name. The discussion's fancier "optionally redact project filenames too" isn't attempted -- reliably telling a project path apart from arbitrary log text is a much fuzzier problem than a literal prefix match. - DiagnosticsReportDialog shows the full (already-redacted) content before saving, per the discussion: "the user is about to attach this to a public tracker." Verified in a real GUI session (Xvfb): triggered a SIGSEGV, relaunched, confirmed the crash-report dialog appears with the right header/content, confirmed it does not reappear on a second relaunch, and confirmed the manual "Save report" action produces a correctly-formatted report and saves it to a chosen path. Built clean, no new warnings. ## Build systems Registered in both: cmake/qet_compilation_vars.cmake, and qelectrotech.pro. The .pro needed explicit globs for the new sources/logging/ui/ subfolder -- sources/logging/*.{h,cpp} was already globbed, but unlike the other ui/ subfolders that one had no entry of its own, so diagnosticsreportdialog.{h,cpp} would not have been built under qmake. |
||
|
|
ff812f221a |
Rework diagnostic logging: fix the file writer, add rotation and a ring buffer
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. |