Commit graph

1 commit

Author SHA1 Message Date
Vadim Peretokin
887b930e47
fix: module save reaching into the profile after it is destroyed (#9690)
#### Brief overview of PR changes/additions

- A profile save hands the modules that are set to sync to a thread pool
task and returns. Two closes wait for nothing - answering "No" to "Save
profile?", and any close that finds the main console already gone - so
the `Host` is destroyed with the write still going, and the write went
on reading it: its `XMLexport`, and then its name. The job now carries
its own copy of each module's document plus every path it needs, and
`writeModuleFiles()`/`updateModuleZip()` are static, so nothing it
touches belongs to the profile.
- Both save watchers are now owned by the profile that made them. They
were unparented, and the `deleteLater()` they are wired to needs an
event loop that is still running to be delivered - which on the way out
there is not, and for a destroyed `Host` there is no owner left either.
- The unpacked module folder is created before the write that goes into
it rather than after, so a module whose folder the user removed no
longer fails to write and then loses its stale XML from its archive with
no replacement going in.

#### Motivation for adding to Mudlet

Save a profile that has a module set to sync - Lua `saveProfile()`, the
editor's autosave, a package install - then close the profile without
saving. That is a crash or a half-rewritten `.mpackage` on the way out,
i.e. another "Mudlet crashed when I closed it". Same shape as #9653, and
it survived 850 sanitizer runs only because no test profile had a module
in it: with none installed the save's module list comes out empty and
the write returns at its first line, so the entire path was unreachable
in the fixtures.

#### Other info (issues closed, discussion etc)

Test case: install a module, `enableModuleSync()` it, `saveProfile()`,
then close the profile answering "No" - Mudlet exits cleanly and the
module still lands on disk and in its archive.

New `ModuleSaveTeardownTest` is the module coverage that was missing
anywhere in the tree - no C++ test installed a module at all. It holds
the thread pool so the write is provably still queued when the `Host` is
destroyed, then lets it run: without the fix that kills the run under
AddressSanitizer inside `Host::writeModuleFiles()`. The watcher half is
pinned by asserting the watchers are owned by the profile and gone with
it, because LSan cannot see this one (a pending `QFutureCallOutEvent`
keeps it reachable at exit) and the functional tests run with
`detect_leaks=0` anyway. `Package_spec.lua` gains a synced-module save
so the write also runs under the busted job's leak-checked ASan.

Assisted-by: Claude:claude-opus-5
2026-08-06 06:08:40 +02:00