• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 15833572481

23 Jun 2025 07:42PM UTC coverage: 42.239% (+0.1%) from 42.099%
15833572481

push

github

HebaruSan
Merge #4398 Exception handling revamp, parallel multi-host inflation

3882 of 9479 branches covered (40.95%)

Branch coverage included in aggregate %.

48 of 137 new or added lines in 30 files covered. (35.04%)

12 existing lines in 6 files now uncovered.

8334 of 19442 relevant lines covered (42.87%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/GUI/Main/MainInstall.cs
1
using System;
2
using System.IO;
3
using System.Collections.Generic;
4
using System.ComponentModel;
5
using System.Linq;
6
using System.Transactions;
7
using System.Threading;
8
#if NET5_0_OR_GREATER
9
using System.Runtime.Versioning;
10
#endif
11

12
using Autofac;
13

14
using CKAN.IO;
15
using CKAN.Extensions;
16
using CKAN.GUI.Attributes;
17
using CKAN.Configuration;
18

19
// Don't warn if we use our own obsolete properties
20
#pragma warning disable 0618
21

22
namespace CKAN.GUI
23
{
24
    using InstallResult = Tuple<bool, List<ModChange>>;
25

26
    /// <summary>
27
    /// Type expected by InstallMods in DoWorkEventArgs.Argument
28
    /// Not a `using` because it's used by other files
29
    /// </summary>
30
    #if NET5_0_OR_GREATER
31
    [SupportedOSPlatform("windows")]
32
    #endif
33
    public class InstallArgument : Tuple<List<ModChange>, RelationshipResolverOptions>
34
    {
35
        public InstallArgument(List<ModChange> changes, RelationshipResolverOptions options)
36
            : base(changes, options)
×
37
        { }
×
38
    }
39

40
    public partial class Main
41
    {
42
        /// <summary>
43
        /// Initiate the GUI installer flow for one specific module
44
        /// </summary>
45
        /// <param name="registry">Reference to the registry</param>
46
        /// <param name="module">Module to install</param>
47
        public void InstallModuleDriver(IRegistryQuerier registry, IEnumerable<CkanModule> modules)
48
        {
×
49
            if (CurrentInstance != null)
×
50
            {
×
51
                try
52
                {
×
53
                    DisableMainWindow();
×
54
                    var userChangeSet = new List<ModChange>();
×
55
                    foreach (var module in modules)
×
56
                    {
×
57
                        var installed = registry.InstalledModule(module.identifier);
×
58
                        if (installed != null)
×
59
                        {
×
60
                            // Already installed, remove it first
61
                            userChangeSet.Add(new ModChange(installed.Module, GUIModChangeType.Remove,
×
62
                                                            ServiceLocator.Container.Resolve<IConfiguration>()));
63
                        }
×
64
                        // Install the selected mod
65
                        userChangeSet.Add(new ModChange(module, GUIModChangeType.Install,
×
66
                                                        ServiceLocator.Container.Resolve<IConfiguration>()));
67
                    }
×
68
                    if (userChangeSet.Count > 0)
×
69
                    {
×
70
                        // Resolve the provides relationships in the dependencies
71
                        Wait.StartWaiting(InstallMods, PostInstallMods, true,
×
72
                            new InstallArgument(userChangeSet,
73
                                                RelationshipResolverOptions.DependsOnlyOpts(CurrentInstance.StabilityToleranceConfig)));
74
                    }
×
75
                }
×
76
                catch
×
77
                {
×
78
                    // If we failed, do the clean-up normally done by PostInstallMods.
79
                    EnableMainWindow();
×
80
                    HideWaitDialog();
×
81
                }
×
82
            }
×
83
        }
×
84

85
        [ForbidGUICalls]
86
        private void InstallMods(object? sender, DoWorkEventArgs? e)
87
        {
×
88
            bool canceled = false;
×
89
            if (CurrentInstance != null
×
90
                && Manager.Cache != null
91
                && e?.Argument is (List<ModChange> changes, RelationshipResolverOptions options))
92
            {
×
93
                var cancelTokenSrc = new CancellationTokenSource();
×
94
                Wait.OnCancel += () =>
×
95
                {
×
96
                    canceled = true;
×
97
                    cancelTokenSrc.Cancel();
×
98
                };
×
99

100
                var registry_manager = RegistryManager.Instance(CurrentInstance, repoData);
×
101
                var registry = registry_manager.registry;
×
102
                var stabilityTolerance = CurrentInstance.StabilityToleranceConfig;
×
103
                var installer = new ModuleInstaller(CurrentInstance, Manager.Cache,
×
104
                                                    ServiceLocator.Container.Resolve<IConfiguration>(),
105
                                                    currentUser, cancelTokenSrc.Token);
106
                // Avoid accumulating multiple event handlers
107
                installer.OneComplete     -= OnModInstalled;
×
108
                installer.InstallProgress -= OnModInstalling;
×
109
                installer.OneComplete     += OnModInstalled;
×
110
                installer.InstallProgress += OnModInstalling;
×
111
                installer.RemoveProgress  -= OnModRemoving;
×
112
                installer.RemoveProgress  += OnModRemoving;
×
113

114
                // this will be the final list of mods we want to install
115
                var toInstall   = new List<CkanModule>();
×
116
                var toUninstall = new HashSet<CkanModule>();
×
117
                var toUpgrade   = new HashSet<CkanModule>();
×
118

119
                // Check whether we need an explicit Remove call for auto-removals.
120
                // If there's an Upgrade or a user-initiated Remove, they'll take care of it.
121
                var needRemoveForAuto = changes.All(ch => ch.ChangeType == GUIModChangeType.Install
×
122
                                                          || ch.IsAutoRemoval);
123

124
                // First compose sets of what the user wants installed, upgraded, and removed.
125
                foreach (ModChange change in changes)
×
126
                {
×
127
                    switch (change.ChangeType)
×
128
                    {
129
                        case GUIModChangeType.Remove:
130
                            // Let Upgrade and Remove handle auto-removals to avoid cascade-removal of depending mods.
131
                            // Unless auto-removal is the ONLY thing in the changeset, in which case
132
                            // filtering these out would give us a completely empty changeset.
133
                            if (needRemoveForAuto || !change.IsAutoRemoval)
×
134
                            {
×
135
                                toUninstall.Add(change.Mod);
×
136
                            }
×
137
                            break;
×
138
                        case GUIModChangeType.Update:
139
                            toUpgrade.Add(change is ModUpgrade mu ? mu.targetMod
×
140
                                                                  : change.Mod);
141
                            break;
×
142
                        case GUIModChangeType.Install:
143
                            toInstall.Add(change.Mod);
×
144
                            break;
×
145
                        case GUIModChangeType.Replace:
146
                            var repl = registry.GetReplacement(change.Mod, stabilityTolerance, CurrentInstance.VersionCriteria());
×
147
                            if (repl != null)
×
148
                            {
×
149
                                toUninstall.Add(repl.ToReplace);
×
150
                                if (!toInstall.Contains(repl.ReplaceWith))
×
151
                                {
×
152
                                    toInstall.Add(repl.ReplaceWith);
×
153
                                }
×
154
                            }
×
155
                            break;
×
156
                    }
157
                }
×
158

159
                Util.Invoke(this, () => UseWaitCursor = true);
×
160
                try
161
                {
×
162
                    var sourceModules = changes.Where(ch => ch.ChangeType is GUIModChangeType.Install
×
163
                                                                          or GUIModChangeType.Update)
164
                                               .Select(ch => ch.Mod)
×
165
                                               .ToHashSet();
166
                    var shown = new HashSet<CkanModule>();
×
167
                    // Prompt for recommendations and suggestions, if any
168
                    var labels = ModuleLabelList.ModuleLabels
×
169
                                                .LabelsFor(CurrentInstance.Name)
170
                                                .ToList();
171
                    var coreConfig = ServiceLocator.Container.Resolve<IConfiguration>();
×
172
                    while (ModuleInstaller.FindRecommendations(
×
173
                        CurrentInstance, sourceModules, toInstall, shown, registry,
174
                        out Dictionary<CkanModule, Tuple<bool, List<string>>> recommendations,
175
                        out Dictionary<CkanModule, List<string>> suggestions,
176
                        out Dictionary<CkanModule, HashSet<string>> supporters)
177
                        && configuration != null)
178
                    {
×
179
                        tabController.ShowTab(ChooseRecommendedModsTabPage.Name, 3);
×
180
                        ChooseRecommendedMods.LoadRecommendations(
×
181
                            registry, toInstall, toUninstall,
182
                            CurrentInstance.VersionCriteria(), Manager.Cache,
183
                            CurrentInstance.game, labels, coreConfig, configuration,
184
                            recommendations, suggestions, supporters);
185
                        tabController.SetTabLock(true);
×
186
                        shown.UnionWith(recommendations.Keys);
×
187
                        shown.UnionWith(suggestions.Keys);
×
188
                        shown.UnionWith(supporters.Keys);
×
189
                        Util.Invoke(this, () => UseWaitCursor = false);
×
190

191
                        var result = ChooseRecommendedMods.Wait();
×
192

193
                        tabController.SetTabLock(false);
×
194
                        tabController.HideTab(ChooseRecommendedModsTabPage.Name);
×
195
                        if (result == null)
×
196
                        {
×
197
                            e.Result = new InstallResult(false, changes);
×
198
                            throw new CancelledActionKraken();
×
199
                        }
200
                        else
201
                        {
×
202
                            sourceModules.UnionWith(result);
×
203
                            toInstall = toInstall.Concat(result).Distinct().ToList();
×
204
                        }
×
205
                    }
×
206
                }
×
207
                finally
208
                {
×
209
                    // Make sure the progress tab always shows up with a normal cursor even if an exception is thrown
210
                    Util.Invoke(this, () => UseWaitCursor = false);
×
211
                    ShowWaitDialog();
×
212
                }
×
213

214
                // Now let's make all our changes.
215
                Util.Invoke(this, () =>
×
216
                {
×
217
                    // Need to be on the GUI thread to get the translated string
218
                    tabController.RenameTab(WaitTabPage.Name, Properties.Resources.MainInstallWaitTitle);
×
219
                });
×
220
                tabController.SetTabLock(true);
×
221

222
                var downloader = new NetAsyncModulesDownloader(currentUser, Manager.Cache, userAgent,
×
223
                                                               cancelTokenSrc.Token);
224
                downloader.DownloadProgress += OnModDownloading;
×
225
                downloader.StoreProgress    += OnModValidating;
×
226

227
                if (Manager.Instances.Count > 1)
×
228
                {
×
229
                    currentUser.RaiseMessage(Properties.Resources.MainInstallDeduplicateScanning);
×
230
                }
×
231
                var deduper = new InstalledFilesDeduplicator(CurrentInstance,
×
232
                                                             Manager.Instances.Values,
233
                                                             repoData);
234

235
                HashSet<string>? possibleConfigOnlyDirs = null;
×
236

237
                // Checks if all actions were successful
238
                // Uninstall/installs/upgrades until every list is empty
239
                // If the queue is NOT empty, resolvedAllProvidedMods is false until the action is done
240
                for (bool resolvedAllProvidedMods = false; !resolvedAllProvidedMods;)
×
241
                {
×
242
                    // Treat whole changeset as atomic
243
                    using (TransactionScope transaction = CkanTransaction.CreateTransactionScope())
×
244
                    {
×
245
                        try
246
                        {
×
247
                            e.Result = new InstallResult(false, changes);
×
248
                            if (!canceled && toUninstall.Count > 0)
×
249
                            {
×
250
                                installer.UninstallList(toUninstall.Select(m => m.identifier),
×
251
                                    ref possibleConfigOnlyDirs, registry_manager, false, toInstall);
252
                                toUninstall.Clear();
×
253
                            }
×
254
                            if (!canceled && toInstall.Count > 0)
×
255
                            {
×
256
                                installer.InstallList(toInstall, options, registry_manager, ref possibleConfigOnlyDirs,
×
257
                                                      deduper, userAgent, downloader, false);
258
                                toInstall.Clear();
×
259
                            }
×
260
                            if (!canceled && toUpgrade.Count > 0)
×
261
                            {
×
262
                                installer.Upgrade(toUpgrade, downloader, ref possibleConfigOnlyDirs, registry_manager,
×
263
                                                  deduper, true, false);
264
                                toUpgrade.Clear();
×
265
                            }
×
266
                            if (canceled)
×
267
                            {
×
268
                                e.Result = new InstallResult(false, changes);
×
269
                                throw new CancelledActionKraken();
×
270
                            }
271
                            transaction.Complete();
×
272
                            resolvedAllProvidedMods = true;
×
273
                        }
×
274
                        catch (ModuleDownloadErrorsKraken k)
×
275
                        {
×
276
                            // Get full changeset (toInstall only includes user's selections, not dependencies)
277
                            var crit = CurrentInstance.VersionCriteria();
×
278
                            var fullChangeset = new RelationshipResolver(
×
279
                                toInstall.Concat(toUpgrade), toUninstall, options, registry, CurrentInstance.game, crit
280
                            ).ModList().ToList();
281
                            DownloadsFailedDialog? dfd = null;
×
282
                            Util.Invoke(this, () =>
×
283
                            {
×
284
                                dfd = new DownloadsFailedDialog(
×
285
                                    Properties.Resources.ModDownloadsFailedMessage,
286
                                    Properties.Resources.ModDownloadsFailedColHdr,
287
                                    Properties.Resources.ModDownloadsFailedAbortBtn,
288
                                    k.Exceptions.Select(kvp => new KeyValuePair<object[], Exception>(
×
289
                                        fullChangeset.Where(m => m.download == kvp.Key.download).ToArray(),
×
290
                                        kvp.Value)),
291
                                    (m1, m2) => (m1 as CkanModule)?.download == (m2 as CkanModule)?.download);
×
292
                                 dfd.ShowDialog(this);
×
293
                            });
×
294
                            var skip  = (dfd?.Wait()?.OfType<CkanModule>() ?? Enumerable.Empty<CkanModule>())
×
295
                                                     .ToArray();
296
                            var abort = dfd?.Abort ?? false;
×
297
                            dfd?.Dispose();
×
298
                            if (abort)
×
299
                            {
×
300
                                canceled = true;
×
301
                                e.Result = new InstallResult(false, changes);
×
302
                                throw new CancelledActionKraken();
×
303
                            }
304

305
                            if (skip.Length > 0)
×
306
                            {
×
307
                                // Remove mods from changeset that user chose to skip
308
                                // and any mods depending on them
309
                                var dependers = Registry.FindReverseDependencies(
×
310
                                        skip.Select(s => s.identifier).ToList(),
×
311
                                        null,
312
                                        registry.InstalledModules.Select(im => im.Module)
×
313
                                                                 .Concat(fullChangeset)
314
                                                                 .ToArray(),
315
                                        registry.InstalledDlls, registry.InstalledDlc,
316
                                        // Consider virtual dependencies satisfied so user can make a new choice if they skip
317
                                        rel => rel.LatestAvailableWithProvides(registry, stabilityTolerance, crit).Count > 1)
×
318
                                    .ToHashSet();
319
                                toInstall.RemoveAll(m => dependers.Contains(m.identifier));
×
320
                            }
×
321

322
                            // Now we loop back around again
323
                        }
×
324
                        catch (TooManyModsProvideKraken k)
×
325
                        {
×
326
                            // Prompt user to choose which mod to use
327
                            tabController.ShowTab(ChooseProvidedModsTabPage.Name, 3);
×
328
                            Util.Invoke(this, () => StatusProgress.Visible = false);
×
329
                            var repoData = ServiceLocator.Container.Resolve<RepositoryDataManager>();
×
330
                            ChooseProvidedMods.LoadProviders(
×
331
                                k.Message,
332
                                k.modules.OrderByDescending(m => repoData.GetDownloadCount(registry.Repositories.Values,
×
333
                                                                                           m.identifier)
334
                                                                 ?? 0)
335
                                         .ThenByDescending(m => m.identifier == k.requested)
×
336
                                         .ThenBy(m => m.name)
×
337
                                         .ToList(),
338
                                Manager.Cache,
339
                                ServiceLocator.Container.Resolve<IConfiguration>());
340
                            tabController.SetTabLock(true);
×
341
                            var chosen = ChooseProvidedMods.Wait();
×
342
                            // Close the selection prompt
343
                            tabController.SetTabLock(false);
×
344
                            tabController.HideTab(ChooseProvidedModsTabPage.Name);
×
345
                            if (chosen != null)
×
346
                            {
×
347
                                // User picked a mod, queue it up for installation
348
                                toInstall.Add(chosen);
×
349
                                // DON'T return so we can loop around and try the above InstallList call again
350
                                tabController.ShowTab(WaitTabPage.Name);
×
351
                                Util.Invoke(this, () => StatusProgress.Visible = true);
×
352
                            }
×
353
                            else
354
                            {
×
355
                                e.Result = new InstallResult(false, changes);
×
356
                                throw new CancelledActionKraken();
×
357
                            }
358
                        }
×
359
                    }
×
360
                }
×
361
                HandlePossibleConfigOnlyDirs(registry, possibleConfigOnlyDirs);
×
362
                e.Result = new InstallResult(true, changes);
×
363
            }
×
364
        }
×
365

366
        [ForbidGUICalls]
367
        private void HandlePossibleConfigOnlyDirs(Registry registry, HashSet<string>? possibleConfigOnlyDirs)
368
        {
×
369
            if (CurrentInstance != null && possibleConfigOnlyDirs != null)
×
370
            {
×
371
                // Check again for registered files, since we may
372
                // just have installed or upgraded some
373
                possibleConfigOnlyDirs.RemoveWhere(
×
374
                    d => !Directory.Exists(d)
×
375
                         || Directory.EnumerateFileSystemEntries(d, "*", SearchOption.AllDirectories)
376
                                     .Select(absF => CurrentInstance.ToRelativeGameDir(absF))
×
377
                                     .Any(relF => registry.FileOwner(relF) != null));
×
378
                if (possibleConfigOnlyDirs.Count > 0)
×
379
                {
×
380
                    Util.Invoke(this, () => StatusLabel.ToolTipText = StatusLabel.Text = "");
×
381
                    tabController.ShowTab(DeleteDirectoriesTabPage.Name, 4);
×
382
                    tabController.SetTabLock(true);
×
383

384
                    DeleteDirectories.LoadDirs(CurrentInstance, possibleConfigOnlyDirs);
×
385

386
                    // Wait here for the GUI process to finish dealing with the user
387
                    if (DeleteDirectories.Wait(out HashSet<string>? toDelete))
×
388
                    {
×
389
                        foreach (string dir in toDelete)
×
390
                        {
×
391
                            try
392
                            {
×
393
                                Directory.Delete(dir, true);
×
394
                            }
×
395
                            catch
×
396
                            {
×
397
                                // Don't worry if it doesn't work, just keep going
398
                            }
×
399
                        }
×
400
                    }
×
401

402
                    tabController.ShowTab(WaitTabPage.Name);
×
403
                    tabController.HideTab(DeleteDirectoriesTabPage.Name);
×
404
                    tabController.SetTabLock(false);
×
405
                }
×
406
            }
×
407
        }
×
408

409
        /// <summary>
410
        /// React to data received for a module
411
        /// </summary>
412
        /// <param name="mod">The module that is being downloaded</param>
413
        /// <param name="remaining">Number of bytes left to download</param>
414
        /// <param name="total">Number of bytes in complete download</param>
415
        public void OnModDownloading(CkanModule mod, long remaining, long total)
416
        {
×
417
            if (total > 0)
×
418
            {
×
419
                Wait.SetProgress(string.Format(Properties.Resources.Downloading,
×
420
                                               mod.name),
421
                                 remaining, total);
422
            }
×
423
        }
×
424

425
        private void OnModValidating(CkanModule mod, long remaining, long total)
426
        {
×
427
            if (total > 0)
×
428
            {
×
429
                Wait.SetProgress(string.Format(Properties.Resources.ValidatingDownload,
×
430
                                               mod.name),
431
                                 remaining, total);
432
            }
×
433
        }
×
434

435
        private void OnModInstalling(CkanModule mod, long remaining, long total)
436
        {
×
437
            if (total > 0)
×
438
            {
×
439
                Wait.SetProgress(string.Format(Properties.Resources.MainInstallInstallingMod,
×
440
                                               mod.name),
441
                                 remaining, total);
442
            }
×
443
        }
×
444

445
        private void OnModRemoving(InstalledModule instMod, long remaining, long total)
446
        {
×
447
            if (total > 0)
×
448
            {
×
449
                Wait.SetProgress(string.Format(Properties.Resources.MainInstallRemovingMod,
×
450
                                               instMod.Module.name),
451
                                 remaining, total);
452
            }
×
453
        }
×
454

455
        private void OnModInstalled(CkanModule mod)
456
        {
×
457
            LabelsAfterInstall(mod);
×
458
        }
×
459

460
        private void PostInstallMods(object? sender, RunWorkerCompletedEventArgs? e)
461
        {
×
462
            if (e?.Error != null)
×
463
            {
×
464
                switch (e.Error)
×
465
                {
466
                    case CancelledActionKraken exc:
467
                        // User already knows they cancelled, get out
468
                        EnableMainWindow();
×
469
                        HideWaitDialog();
×
470
                        break;
×
471

472
                    case RequestThrottledKraken exc:
473
                        string msg = exc.Message;
×
474
                        currentUser.RaiseMessage("{0}", msg);
×
475
                        if (configuration != null && CurrentInstance != null
×
476
                            && YesNoDialog(string.Format(Properties.Resources.MainInstallOpenSettingsPrompt, msg),
477
                                Properties.Resources.MainInstallOpenSettings,
478
                                Properties.Resources.MainInstallNo))
479
                        {
×
480
                            // Launch the URL describing this host's throttling practices, if any
481
                            if (exc.infoUrl != null)
×
482
                            {
×
483
                                Utilities.ProcessStartURL(exc.infoUrl.ToString());
×
484
                            }
×
485
                            // Now pretend they clicked the menu option for the settings
486
                            Enabled = false;
×
487
                            new SettingsDialog(ServiceLocator.Container.Resolve<IConfiguration>(),
×
488
                                               configuration,
489
                                               RegistryManager.Instance(CurrentInstance, repoData),
490
                                               updater,
491
                                               currentUser,
492
                                               userAgent)
493
                                .ShowDialog(this);
494
                            Enabled = true;
×
495
                        }
×
496
                        break;
×
497

498
                    case TransactionalKraken exc:
499
                        // Thrown when the Registry tries to enlist with multiple different transactions
500
                        // Want to see the stack trace for this one
501
                        currentUser.RaiseMessage("{0}", exc.ToString());
×
502
                        currentUser.RaiseError("{0}", exc.ToString());
×
503
                        break;
×
504

505
                    case Kraken kraken:
506
                        // Show nice message for mod problems
NEW
507
                        currentUser.RaiseMessage("{0}", kraken.Message);
×
NEW
508
                        currentUser.RaiseMessage(Properties.Resources.MainInstallGameDataReverted);
×
UNCOV
509
                        break;
×
510

511
                    default:
512
                        // Show stack trace for code problems
513
                        currentUser.RaiseMessage("{0}", e.Error.ToString());
×
NEW
514
                        currentUser.RaiseMessage(Properties.Resources.MainInstallGameDataReverted);
×
UNCOV
515
                        break;
×
516
                }
517

518
                Wait.RetryEnabled = true;
×
519
                FailWaitDialog(Properties.Resources.MainInstallErrorInstalling,
×
520
                               Properties.Resources.MainInstallKnownError,
521
                               Properties.Resources.MainInstallFailed);
522
            }
×
523
            // The Result property throws if InstallMods threw (!!!)
NEW
524
            else if (e?.Result is (bool, List<ModChange>))
×
525
            {
×
526
                currentUser.RaiseMessage(Properties.Resources.MainInstallSuccess);
×
527
                // Rebuilds the list of GUIMods
528
                RefreshModList(false);
×
529
            }
×
530
        }
×
531
    }
532
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc