Skip to content

Download manager refactor and bug fixes#2372

Open
Al12rs wants to merge 12 commits intomasterfrom
al/downloadManager-refactor
Open

Download manager refactor and bug fixes#2372
Al12rs wants to merge 12 commits intomasterfrom
al/downloadManager-refactor

Conversation

@Al12rs
Copy link
Copy Markdown
Member

@Al12rs Al12rs commented Apr 18, 2026

Summary

Refactor of the downloads view-model plumbing plus fixes for several long-standing bugs. Best reviewed commit-by-commit.

Changes

  • Model refresh plumbing: ModelResetGuard RAII helper replaces scattered aboutToUpdate()/update(int) calls; row notifications centralised in setState; directory watcher encapsulated in DirWatcherManager.
  • addNXMDownload dedup check: stray comma operator was discarding the game-name comparison; all three of game/mod/file IDs now participate.
  • Lost finished() signal on fast downloads: hoisted the "Download again?" prompt out of startDownload into addDownload so startDownload is straight-line setup with no event-loop pumping or re-entrancy. finished() is connected last and dispatched manually if the reply already finished (Qt does not emit to a later connection). Pending-queue entry is cleaned up on the cancel branch.
  • DownloadInfo::createFromMeta leak: allocation moved past the early-return checks so path-mismatch and hidden-skip paths no longer leak.
  • getDownloadFileName: suffix-collision branch now uses the sanitised basename like the initial path.

Status

This incorporates some of the fixes from #2363.

I'll mark this as ready, will handle the move to stable monotonically increasing download ids in a separate PRs that builds on top of this one.

Al12rs and others added 8 commits April 18, 2026 16:59
QFileSystemWatcher suppression currently relies on public static start/end
methods and a static counter. Seven call sites pair them raw, one of them
outside the class. Any exception between a pair permanently disables the
watcher, and the static counter implies a singleton DownloadManager.

A new DirWatcherManager owns the watcher, the counter (now an instance
member), and the filtering. The only way to suspend is an RAII Guard
obtained via a scopedGuard() factory. All raw pairs migrate to guards. A
TODO flags the existing processEvents() in the dtor as a known reentrancy
hazard worth replacing later.
Replace the fragile two-signal protocol with a refcounted RAII
ModelResetGuard. Split update(int) into aboutToResetModel/modelReset
(guard only) and rowChanged(int); notifyRowChanged() is suppressed while
a reset is active.

Fixes "beginResetModel without endResetModel" warnings from three sites
in downloadFinished/removeDownload that were pairing reset with a row
update. removePending only opens a guard when an actual match is removed.
setState emits notifyRowChanged itself, uses indexByInfo (-1 when
untracked), and re-looks up the row at each use so reply->abort() and
plugin callbacks that re-enter and erase info don't produce stale
signals.

Remove the trailing emit loop from createMetaFile and the now-redundant
notifyRowChanged calls scattered after setState. Add the two missing
emits in restoreDownload (after m_Hidden) and metaDataChanged (after
rename). Guard downloadFinished with a top-level DirWatcherGuard to
prevent filesystem events from its writes racing with model updates.
The game-name comparison result was discarded by the comma operator,
so the dedup only matched modId/fileId across all games.
Hoist the file-exists prompt out of startDownload so setup is straight-line.
Connect finished() last and dispatch manually if the reply already finished.
Move the allocation past the early-return checks so path-mismatch and
hidden-skip paths no longer leak a fresh DownloadInfo.
The collision-avoidance branch was using the raw baseName, so invalid
characters sanitized out of the initial path leaked into the suffixed one.
@JonathanFeenstra
Copy link
Copy Markdown
Member

Draft, still investigating better handling of download IDs, currently it is possible to get conflicting ids. I don't like the random generation offered in the other PR though.

Agreed. Could we use a mutex to ensure no duplicate download IDs are generated when using m_ActiveDownloads.size(), or increment a std::atomic<int> to generate them?

@Al12rs
Copy link
Copy Markdown
Member Author

Al12rs commented Apr 18, 2026

Draft, still investigating better handling of download IDs, currently it is possible to get conflicting ids. I don't like the random generation offered in the other PR though.

Agreed. Could we use a mutex to ensure no duplicate download IDs are generated when using m_ActiveDownloads.size(), or increment a std::atomic<int> to generate them?

The download manager runs on a single thread, so in theory a simple increasing counter should work. But I need to check if there are assumptions about what the id represents (e.g. index inside arrays or stuff like that.

@mick-lue
Copy link
Copy Markdown
Contributor

So the download ID is not really used in a way where random IDs would break something, as far as I followed all the code paths.
Not using random IDs would be neat of course.
A theory I haven't completely tested regarding duplicate download IDs was that there is a race condition when a download finishes (and gets removed from the active downloads) while another download is about to start.
Also, the 2 functions returning download IDs where previously using m_ActiveDownloads.size() and m_ActiveDownloads.size() - 1, which could be an issue

@Al12rs
Copy link
Copy Markdown
Member Author

Al12rs commented Apr 18, 2026

So the download ID is not really used in a way where random IDs would break something, as far as I followed all the code paths. Not using random IDs would be neat of course. A theory I haven't completely tested regarding duplicate download IDs was that there is a race condition when a download finishes (and gets removed from the active downloads) while another download is about to start. Also, the 2 functions returning download IDs where previously using m_ActiveDownloads.size() and m_ActiveDownloads.size() - 1, which could be an issue

Yeah, I think that race could happen, but a monotonically increasing Id should prevent any issues.

m_AlphabeticalTranslation was written but never read; drop it along with
refreshAlphabeticalTranslation, ByName, and the LessThanWrapper helper.
Comment thread src/downloadmanager.cpp Outdated
Comment thread src/downloadmanager.cpp
Comment thread src/downloadmanager.cpp
@Al12rs Al12rs marked this pull request as ready for review April 19, 2026 08:13
@Al12rs Al12rs requested review from Holt59, Silarn and mick-lue April 19, 2026 08:34
Copy link
Copy Markdown
Contributor

@mick-lue mick-lue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 minor comments, otherwise LGTM

Comment thread src/downloadmanager.cpp Outdated
Comment thread src/downloadmanager.cpp
Al12rs added 2 commits April 19, 2026 18:23
Moves the ModelResetGuard out of the try-catch so it also wraps the
refreshList() call below. Without this, one reset fires when the guard
destructs at the end of the try block and another fires from
refreshList's own guard, producing two resets where one is sufficient.
openMetaFile creates the .meta file via QSettings when one does not
exist; the disk write fires directoryChanged and triggers a spurious
refreshList. Wrap it in a DirWatcherManager::Guard like the other
meta-file editing paths.
@Silarn
Copy link
Copy Markdown
Member

Silarn commented Apr 23, 2026

I'm going to rerun the build just to make sure the Qt 6.11.0 change doesn't cause problems, but I doubt it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants