Skip to content

Don't revert removals on providing mod choice#4609

Merged
HebaruSan merged 1 commit into
KSP-CKAN:masterfrom
HebaruSan:fix/removal-revert-on-choice
May 7, 2026
Merged

Don't revert removals on providing mod choice#4609
HebaruSan merged 1 commit into
KSP-CKAN:masterfrom
HebaruSan:fix/removal-revert-on-choice

Conversation

@HebaruSan

@HebaruSan HebaruSan commented May 7, 2026

Copy link
Copy Markdown
Member

Problem

  1. Install ParallaxContinued
  2. Configure the Sol repo from Add Sol metadata as an official repository CKAN-meta#3383
  3. In one changeset, choose to remove ParallaxContinued and install Sol-Configs
  4. The install aborts on the conflict between ParallaxContinued and Sol's fork of that mod (easier to tell with Improve error messages for provide conflicts #4608 installed), after ParallaxContinued has already been uninstalled

Cause

Exactly one year ago, #4367 moved GUI's installation transaction from outside the for (bool resolvedAllProvidedMods = false; ... loop to inside of it.

When InstallList throws TooManyModsProvideKraken (as it does many times for Sol-Configs because it has a lot of providing mod choices), the try block inside the transaction catches and handles it (so the exception does not directly cause the transaction to be aborted), but we then exit the transaction's scope without finalizing it, which causes the transaction to abort, which restores the installed mods on disk and in the registry.

We then try to apply the changeset again, but the uninstallation step is skipped because the toUninstall.Clear(); line has already cleared the list of mods to uninstall after the first uninstallation step. This means that when InstallList is called, the conflicting mod (ParallaxContinued in the example) is still present on disk and in the registry.

The only workaround is to perform the removals separately in an earlier changeset.

The reason given for this change in #4367 was:

  • After that, I started getting some confusing errors System.Transactions.TransactionAbortedException: The transaction has aborted., which I do not think could be correct, but the scope of the main GUI installation transaction is adjusted to work around this.

This happens with the original transaction scope if the user clicks Retry in the failed downloads dialog:

System.Transactions.TransactionAbortedException: The transaction has aborted.
   at System.Transactions.TransactionStateAborted.CreateAbortingClone(InternalTransaction tx)
   at System.Transactions.DependentTransaction..ctor(IsolationLevel isoLevel, InternalTransaction internalTransaction, Boolean blocking)
   at System.Transactions.Transaction.DependentClone(DependentCloneOption cloneOption)
   at System.Transactions.TransactionScope.SetCurrent(Transaction newCurrent)
   at System.Transactions.TransactionScope.PushScope()
   at System.Transactions.TransactionScope..ctor(TransactionScopeOption scopeOption, TransactionOptions transactionOptions, TransactionScopeAsyncFlowOption asyncFlowOption)
   at CKAN.CkanTransaction.CreateTransactionScope()
   at CKAN.IO.ModuleInstaller.InstallList(IReadOnlyCollection`1 modules, RelationshipResolverOptions options, RegistryManager registry_manager, HashSet`1& possibleConfigOnlyDirs, InstalledFilesDeduplicator deduper, String userAgent, IDownloader downloader, ISet`1 autoInstalled, Boolean ConfirmPrompt)
   at CKAN.GUI.Main.InstallMods(Object sender, DoWorkEventArgs e)
   at CKAN.GUI.Wait.DoWork(Object sender, DoWorkEventArgs e)
   at System.ComponentModel.BackgroundWorker.WorkerThreadStart(Object argument)

This happens after the failed downloads kraken is (and must be) thrown within InstallList's transaction scope. TransactionScope does not support nesting in the intuitive sense that I thought it did (i.e., that a sub-transaction could be rolled back independently while its parent transaction continues on with further sub-transactions until they all commit together). Instead, aborting a child transaction aborts the parent, so this exception causes the main outer transaction to abort as well, so we can't use it to retry downloads.

(There is a way to get an independent "child" transaction using TransactionScopeOption.RequiresNew, but these do not revert if the child is completed and the parent is aborted, so that won't work for us.)

So we can't just have a loop outside the transaction scope, or we'll revert uninstallations every time there's a provides prompt. And we can't just have a loop inside the transaction scope, or we'll try to retry downloads with an aborted transaction. Hmm. ⚖️

Changes

  • Now we have two loops in MainInstall 👶🪓: One outside the transaction scope to retry after exceptions that abort the transaction, and another inside the transaction scope to retry after exceptions that do not abort the transaction.
  • If ModuleDownloadErrorsKraken is thrown, we catch it and retry via the outer loop (assuming the user chooses to retry), since the transaction has already been aborted
  • If TooManyModsProvideKraken is thrown, we catch it and retry via the inner loop (assuming the user picks a mod), since the transaction has not been aborted
  • If anything else is thrown, the transaction is aborted and control flows to the post-install cleanup handler
  • toUninstall.Clear(); is replaced with a new bool didUninstall variable that allows us to skip already-removed modules without losing track of which ones they were in case we need to re-do that step in the future
  • toInstall.Clear(); is replaced with a new bool didInstall variable that allows us to skip already-installed modules without losing track of which ones they were in case we need to re-do that step in the future

After these changes:

  • The providing mods prompt will no longer revert the registry or deleted files (because it will not abort the transaction)
  • Retrying download errors will still work, although since this aborts the transaction, a new transaction will be started and the mods to remove will be removed again
  • Removing a mod (e.g. ParallaxContinued) and replacing it with a conflicting mod with virtual dependencies (e.g. Sol-Configs) will work as expected

@HebaruSan HebaruSan added Bug Something is not working as intended Easy This is easy to fix GUI Issues affecting the interactive GUI labels May 7, 2026
@coveralls

This comment was marked as off-topic.

@HebaruSan HebaruSan added In progress We're still working on this and removed Easy This is easy to fix labels May 7, 2026
@HebaruSan HebaruSan marked this pull request as draft May 7, 2026 17:33
@HebaruSan

This comment was marked as resolved.

@HebaruSan HebaruSan force-pushed the fix/removal-revert-on-choice branch from 341589a to 909324a Compare May 7, 2026 18:42
@HebaruSan HebaruSan force-pushed the fix/removal-revert-on-choice branch from 909324a to 13ed64e Compare May 7, 2026 19:09
@HebaruSan HebaruSan marked this pull request as ready for review May 7, 2026 19:17
@HebaruSan HebaruSan removed the In progress We're still working on this label May 7, 2026
@HebaruSan HebaruSan merged commit 0b421e8 into KSP-CKAN:master May 7, 2026
9 checks passed
@HebaruSan HebaruSan deleted the fix/removal-revert-on-choice branch May 7, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working as intended GUI Issues affecting the interactive GUI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants