#### Brief overview of PR changes/additions
- Profile loading now only considers real `*.xml` saves: an empty
QSaveFile temporary left behind by a crash during a save can no longer
be loaded as "the profile", which made a profile open with its
connection settings intact but every trigger/script seemingly gone.
Affected profiles heal themselves on next load by falling back to the
newest real save.
- Packages that uninstall themselves from their own timer script or
event-handler script (a common auto-updater pattern) no longer free the
very objects still executing: `TimerUnit`/`ScriptUnit` uninstall now
defers deletion while `TTimer::execute()` / `Host::raiseEvent()` are on
the call stack, completing the #9337/#9383 fix that already covered
triggers/aliases/keys. Deferred timer deletes are flushed before the
queued post-uninstall save runs, so removed items cannot be serialized
back into the profile.
- `Host::saveProfile()`'s background module task no longer reads
`writers`/`saveFutures` concurrently with the main thread (data race in
the profile save path).
#### Motivation for adding to Mudlet
Fixes a real-world heap-corruption crash cluster (Sentry MUDLET-32 /
MUDLET-2S / MUDLET-48: `STATUS_HEAP_CORRUPTION` on 4.21.0/4.21.1, frames
touching lua51/Qt6Core/libpugixml, breadcrumbs showing package uninstall
activity around saves) and the profile data loss it caused.
#### Other info (issues closed, discussion etc)
Root cause of the crashes: #9111 (in the 4.20.1 → 4.21.0 window) changed
the `*Unit::uninstall()` methods from unregister-only to immediate
`delete`. A package script calling `uninstallPackage()` on its own
package then freed objects still on the call stack - use-after-free that
poisons the heap, typically detected slightly later during the
background save serialization (hence the pugixml/lua frames, aborts
mid-save, and zero-byte `....xml.XXXXXX` QSaveFile leftovers in
`current/`). #9383 fixed the trigger/alias/key cases; this completes
timers and scripts, which reproduce under ASan on current development
(heap-use-after-free in `Tree<TScript>::isActive()` /
`TTimer::execute()`).
Data-loss mechanism (generic): a crash mid-save leaves a 0-byte
QSaveFile temporary as the newest file in `current/`;
`mudlet::loadProfile()` picked the newest file of any name, tried to
load the empty temp, and the profile opened "gutted" (connection details
live in separate files and survived). Verified end-to-end with affected
profile data and covered by a synthetic regression test.
Both new functional tests fail on pre-fix code
(`PackageSelfUninstallTest` trips ASan heap-use-after-free;
`ProfileLoadTempFileTest` reproduces the data loss) and pass with the
fix; full functional suite green (24/24).
Known remaining (pre-existing) issue documented in-code at
`Host::pendingXmlSaveFutures()`: module writing still touches `writers`
from the background task for profiles that use modules; fixing that
properly means moving module serialization back to the main thread and
deserves its own PR.
**Test case:**
1. Create a package containing a timer or event-handler script that
calls `uninstallPackage()` on its own package, and let it fire - no
crash, package cleanly removed, next save does not resurrect it.
2. Simulate an interrupted save: place an empty file named like
`2026-01-01#12-00-00.xml.AbCdEf` in a profile's `current/` folder with
the newest timestamp - the profile still loads the newest real save with
all triggers intact, and the temporary no longer appears in Connect →
Options → Profile history.
3. `ctest -R "ProfileLoadTempFileTest|PackageSelfUninstallTest"` in an
ASan (default Debug) build.
Assisted-by: Claude:claude-fable-5
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>