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

KSP-CKAN / CKAN / 17904669173

22 Sep 2025 04:34AM UTC coverage: 75.604% (+1.2%) from 74.397%
17904669173

push

github

HebaruSan
Merge #4443 Report number of filtered files in install

5231 of 7236 branches covered (72.29%)

Branch coverage included in aggregate %.

192 of 218 new or added lines in 41 files covered. (88.07%)

35 existing lines in 7 files now uncovered.

11163 of 14448 relevant lines covered (77.26%)

1.58 hits per line

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

78.45
/Core/Registry/RegistryManager.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Diagnostics;
4
using System.IO;
5
using System.Linq;
6
using System.Text;
7
using System.ComponentModel;
8
using System.Reflection;
9
using System.Diagnostics.CodeAnalysis;
10

11
using ChinhDo.Transactions.FileManager;
12
using log4net;
13
using Newtonsoft.Json;
14

15
using CKAN.Configuration;
16
using CKAN.Versioning;
17

18
namespace CKAN
19
{
20
    public class RegistryManager : IDisposable
21
    {
22
        private static readonly Dictionary<string, RegistryManager> registryCache =
2✔
23
            new Dictionary<string, RegistryManager>();
24

25
        private static readonly ILog log = LogManager.GetLogger(typeof(RegistryManager));
2✔
26
        private readonly string path;
27
        public readonly string lockfilePath;
28
        private FileStream?   lockfileStream = null;
2✔
29
        private StreamWriter? lockfileWriter = null;
2✔
30

31
        private readonly GameInstance gameInstance;
32

33
        public Registry registry;
34

35
        /// <summary>
36
        /// If loading the registry failed, the parsing error text, else null.
37
        /// </summary>
38
        public string? previousCorruptedMessage;
39

40
        /// <summary>
41
        /// If loading the registry failed, the location to which we moved it, else null.
42
        /// </summary>
43
        public string? previousCorruptedPath;
44

45
        private static string InstanceRegistryLockPath(string ckanDirPath)
46
            => Path.Combine(ckanDirPath, "registry.locked");
2✔
47

48
        public static bool IsInstanceMaybeLocked(string ckanDirPath)
49
            => File.Exists(InstanceRegistryLockPath(ckanDirPath));
2✔
50

51
        // We require our constructor to be private so we can
52
        // enforce this being an instance (via Instance() above)
53
        private RegistryManager(string                          path,
2✔
54
                                GameInstance                    inst,
55
                                RepositoryDataManager           repoData,
56
                                IReadOnlyCollection<Repository> initialRepositories)
57
        {
2✔
58
            gameInstance = inst;
2✔
59

60
            this.path    = Path.Combine(path, "registry.json");
2✔
61
            lockfilePath = InstanceRegistryLockPath(path);
2✔
62

63
            // Create a lock for this registry, so we cannot touch it again.
64
            if (!GetLock())
2✔
65
            {
×
66
                log.DebugFormat("Unable to acquire registry lock: {0}", lockfilePath);
×
67
                throw new RegistryInUseKraken(lockfilePath);
×
68
            }
69

70
            try
71
            {
2✔
72
                Load(repoData, initialRepositories);
2✔
73
            }
2✔
74
            catch
2✔
75
            {
2✔
76
                // Clean up the lock file
77
                Dispose(false);
2✔
78
                throw;
2✔
79
            }
80

81
            // We don't cause an inconsistency error to stop the registry from being loaded,
82
            // because then the user can't do anything to correct it. However we're
83
            // sure as hell going to complain if we spot one!
84
            try
85
            {
2✔
86
                registry.CheckSanity();
2✔
87
            }
2✔
88
            catch (InconsistentKraken kraken)
×
89
            {
×
90
                // Only log an error for this if user-interactive,
91
                // automated tools do not care that no one picked a Scatterer config
92
                if (gameInstance.User.Headless)
×
93
                {
×
94
                    log.InfoFormat("Loaded registry with inconsistencies:\r\n\r\n{0}", kraken.Message);
×
95
                }
×
96
                else
97
                {
×
98
                    log.ErrorFormat("Loaded registry with inconsistencies:\r\n\r\n{0}", kraken.Message);
×
99
                }
×
100
            }
×
101
        }
2✔
102

103
        #region destruction
104

105
        // See http://stackoverflow.com/a/538238/19422 for an awesome explanation of
106
        // what's going on here.
107

108
        /// <summary>
109
        /// Releases all resource used by the <see cref="RegistryManager"/> object.
110
        /// </summary>
111
        /// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="RegistryManager"/>. The
112
        /// <see cref="Dispose()"/> method leaves the <see cref="RegistryManager"/> in an unusable state. After
113
        /// calling <see cref="Dispose()"/>, you must release all references to the <see cref="RegistryManager"/> so
114
        /// the garbage collector can reclaim the memory that the <see cref="RegistryManager"/> was occupying.</remarks>
115
        public void Dispose()
116
        {
2✔
117
            Dispose(true);
2✔
118
            GC.SuppressFinalize(this);
2✔
119
        }
2✔
120

121
        #pragma warning disable IDE0060
122
        protected void Dispose(bool safeToAlsoFreeManagedObjects)
123
        #pragma warning restore IDE0060
124
        {
2✔
125
            // Right now we just release our lock, and leave everything else
126
            // to the GC, but if we were implementing the full pattern we'd also
127
            // free managed (.NET core) objects when called with a true value here.
128

129
            ReleaseLock();
2✔
130
            var directory = gameInstance.CkanDir;
2✔
131
            if (!registryCache.ContainsKey(directory))
2✔
132
            {
2✔
133
                log.DebugFormat("Registry not in cache at {0}", directory);
2✔
134
                return;
2✔
135
            }
136

137
            log.DebugFormat("Dispose of registry at {0}", directory);
2✔
138
            registryCache.Remove(directory);
2✔
139
        }
2✔
140

141
        #endregion
142

143
        /// <summary>
144
        /// If the lock file exists, it contains the id of the owning process.
145
        /// If there is no process with that id, then the lock file is stale.
146
        /// If there IS a process with that id, there are two possibilities:
147
        ///   1. It's actually the CKAN process that owns the lock
148
        ///   2. It's some other process that got the same id by coincidence
149
        /// If #1, it's definitely not stale.
150
        /// If #2, it's stale, but we don't know that.
151
        /// Since we can't tell the difference between #1 and #2, we need to
152
        /// keep the lock file.
153
        /// If we encounter any other errors (permissions, corrupt file, etc.),
154
        /// then we need to keep the file.
155
        /// </summary>
156
        private void CheckStaleLock()
157
        {
2✔
158
            log.DebugFormat("Checking for stale lock file at {0}", lockfilePath);
2✔
159
            if (IsInstanceMaybeLocked(gameInstance.CkanDir))
2!
160
            {
×
161
                log.DebugFormat("Lock file found at {0}", lockfilePath);
×
162
                string contents;
163
                try
164
                {
×
165
                    contents = File.ReadAllText(lockfilePath);
×
166
                }
×
167
                catch
×
168
                {
×
169
                    // If we can't read the file, we can't check whether it's stale.
170
                    log.DebugFormat("Lock file unreadable at {0}", lockfilePath);
×
171
                    return;
×
172
                }
173
                log.DebugFormat("Lock file contents: {0}", contents);
×
174
                if (int.TryParse(contents, out int pid))
×
175
                {
×
176
                    // File contains a valid integer.
177
                    try
178
                    {
×
179
                        // Try to find the corresponding process.
180
                        log.DebugFormat("Looking for process with ID: {0}", pid);
×
181
                        Process.GetProcessById(pid);
×
182
                        // If no exception is thrown, then a process with this id
183
                        // is running, and it's not safe to delete the lock file.
184
                        // We are done.
185
                    }
×
186
                    catch (ArgumentException)
×
187
                    {
×
188
                        // ArgumentException means the process doesn't exist,
189
                        // so the lock file is stale and we can delete it.
190
                        try
191
                        {
×
192
                            log.DebugFormat("Deleting stale lock file at {0}", lockfilePath);
×
193
                            File.Delete(lockfilePath);
×
194
                        }
×
195
                        catch
×
196
                        {
×
197
                            // If we can't delete the file, then all this was for naught,
198
                            // but at least we haven't crashed.
199
                        }
×
200
                    }
×
201
                }
×
202
            }
×
203
        }
2✔
204

205
        /// <summary>
206
        /// Tries to lock the registry by creating a lock file.
207
        /// </summary>
208
        /// <returns><c>true</c>, if lock was gotten, <c>false</c> otherwise.</returns>
209
        public bool GetLock()
210
        {
2✔
211
            try
212
            {
2✔
213
                CheckStaleLock();
2✔
214

215
                log.DebugFormat("Trying to create lock file: {0}", lockfilePath);
2✔
216

217
                lockfileStream = new FileStream(lockfilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 512, FileOptions.DeleteOnClose);
2✔
218

219
                // Write the current process ID to the file.
220
                lockfileWriter = new StreamWriter(lockfileStream);
2✔
221
                lockfileWriter.Write(Process.GetCurrentProcess().Id);
2✔
222
                lockfileWriter.Flush();
2✔
223
                // The lock file is now locked and open.
224
                log.DebugFormat("Lock file created: {0}", lockfilePath);
2✔
225
            }
2✔
226
            catch (IOException)
×
227
            {
×
228
                log.DebugFormat("Failed to create lock file: {0}", lockfilePath);
×
229
                return false;
×
230
            }
231

232
            return true;
2✔
233
        }
2✔
234

235
        /// <summary>
236
        /// Release the lock by deleting the file, but only if we managed to create the file.
237
        /// </summary>
238
        public void ReleaseLock()
239
        {
2✔
240
            // We have to dispose our writer first, otherwise it cries when
241
            // it finds the stream is already disposed.
242
            if (lockfileWriter != null)
2✔
243
            {
2✔
244
                log.DebugFormat("Disposing of lock file writer at {0}", lockfilePath);
2✔
245
                lockfileWriter.Dispose();
2✔
246
                lockfileWriter = null;
2✔
247
            }
2✔
248

249
            // Disposing the writer also disposes the underlying stream,
250
            // but we're extra tidy just in case.
251
            if (lockfileStream != null)
2✔
252
            {
2✔
253
                log.DebugFormat("Disposing of lock file stream at {0}", lockfilePath);
2✔
254
                lockfileStream.Dispose();
2✔
255
                lockfileStream = null;
2✔
256
            }
2✔
257

258
        }
2✔
259

260
        /// <summary>
261
        /// Returns an instance of the registry manager for the game instance.
262
        /// The file `registry.json` is assumed.
263
        /// </summary>
264
        public static RegistryManager Instance(GameInstance                     inst,
265
                                               RepositoryDataManager            repoData,
266
                                               IReadOnlyCollection<Repository>? repositories = null)
267
        {
2✔
268
            string directory = inst.CkanDir;
2✔
269
            if (!registryCache.ContainsKey(directory))
2✔
270
            {
2✔
271
                log.DebugFormat("Preparing to load registry at {0}", directory);
2✔
272
                registryCache[directory] = new RegistryManager(directory, inst, repoData,
2✔
273
                                                               repositories ?? Array.Empty<Repository>());
274
            }
2✔
275

276
            return registryCache[directory];
2✔
277
        }
2✔
278

279
        public static void DisposeInstance(GameInstance inst)
280
        {
2✔
281
            if (registryCache.TryGetValue(inst.CkanDir, out RegistryManager? regMgr))
2✔
282
            {
2✔
283
                regMgr.Dispose();
2✔
284
            }
2✔
285
        }
2✔
286

287
        /// <summary>
288
        /// Call Dispose on all the registry managers in the cache.
289
        /// Useful for exiting without Dispose-related exceptions.
290
        /// Note that this also REMOVES these entries from the cache.
291
        /// </summary>
292
        public static void DisposeAll()
293
        {
2✔
294
            foreach (RegistryManager rm in new List<RegistryManager>(registryCache.Values))
5✔
295
            {
2✔
296
                rm.Dispose();
2✔
297
            }
2✔
298
        }
2✔
299

300
        [MemberNotNull(nameof(registry))]
301
        private void Load(RepositoryDataManager           repoData,
302
                          IReadOnlyCollection<Repository> repositories)
303
        {
2✔
304
            try
305
            {
2✔
306
                registry = LoadRegistry(gameInstance, repoData);
2✔
307
                AscertainDefaultRepo();
2✔
308
            }
2✔
309
            catch (IOException exc) when (exc is FileNotFoundException or DirectoryNotFoundException)
2✔
310
            {
2✔
311
                Create(repoData, repositories);
2✔
312
            }
2✔
313
            catch (RegistryVersionNotSupportedKraken kraken)
2✔
314
            {
2✔
315
                // Throw a new one with the full path, since Registry doesn't know it
316
                throw new RegistryVersionNotSupportedKraken(
2✔
317
                    kraken.requestVersion,
318
                    string.Format(Properties.Resources.RegistryManagerRegistryVersionNotSupported,
319
                                  Platform.FormatPath(path)));
320
            }
321
            catch (JsonException exc)
2✔
322
            {
2✔
323
                previousCorruptedMessage = exc.Message;
2✔
324
                previousCorruptedPath    = path + "_CORRUPTED_" + DateTime.Now.ToString("yyyyMMddHHmmss");
2✔
325
                log.ErrorFormat("{0} is corrupted, archiving to {1}: {2}",
2✔
326
                    path, previousCorruptedPath, previousCorruptedMessage);
327
                File.Move(path, previousCorruptedPath);
2✔
328
                Create(repoData, repositories);
2✔
329
            }
2✔
330
            catch (Exception ex)
×
331
            {
×
332
                log.ErrorFormat("Uncaught exception loading registry: {0}", ex.ToString());
×
333
                throw;
×
334
            }
335
        }
2✔
336

337
        public static Registry? ReadOnlyRegistry(GameInstance          inst,
338
                                                 RepositoryDataManager repoData)
339
            => Utilities.DefaultIfThrows(() => registryCache.TryGetValue(inst.CkanDir,
2!
340
                                                                         out RegistryManager? regMgr)
341
                                                   ? regMgr.registry
342
                                                   : LoadRegistry(inst, repoData));
343

344
        private static Registry LoadRegistry(GameInstance          inst,
345
                                             RepositoryDataManager repoData)
346
            => Registry.FromJson(inst, repoData,
2✔
347
                                 File.ReadAllText(Path.Combine(inst.CkanDir, "registry.json")));
348

349
        [MemberNotNull(nameof(registry))]
350
        private void Create(RepositoryDataManager   repoData,
351
                            IEnumerable<Repository> repositories)
352
        {
2✔
353
            log.InfoFormat("Creating new CKAN registry at {0}", path);
2✔
354
            registry = new Registry(repoData, repositories);
2✔
355
            AscertainDefaultRepo();
2✔
356
            ScanUnmanagedFiles();
2✔
357
        }
2✔
358

359
        private void AscertainDefaultRepo()
360
        {
2✔
361
            if (registry.Repositories.Count == 0)
2✔
362
            {
2✔
363
                log.InfoFormat("Fabricating repository: {0}", gameInstance.Game.DefaultRepositoryURL);
2✔
364
                var repo = Repository.DefaultGameRepo(gameInstance.Game);
2✔
365
                registry.RepositoriesSet(new SortedDictionary<string, Repository>
2✔
366
                {
367
                    { repo.name, repo }
368
                });
369
            }
2✔
370
        }
2✔
371

372
        private string Serialize()
373
        {
2✔
374
            StringBuilder sb = new StringBuilder();
2✔
375
            StringWriter sw = new StringWriter(sb);
2✔
376

377
            using (JsonTextWriter writer = new JsonTextWriter(sw))
2✔
378
            {
2✔
379
                writer.Formatting = Formatting.Indented;
2✔
380
                writer.Indentation = 0;
2✔
381

382
                JsonSerializer serializer = new JsonSerializer();
2✔
383
                serializer.Serialize(writer, registry);
2✔
384
            }
2✔
385

386
            return sw + Environment.NewLine;
2!
387
        }
2✔
388

389
        public void Save(bool enforce_consistency = true)
390
        {
2✔
391
            TxFileManager file_transaction = new TxFileManager();
2✔
392

393
            log.InfoFormat("Saving CKAN registry at {0}", path);
2✔
394

395
            if (enforce_consistency)
2✔
396
            {
2✔
397
                // No saving the registry unless it's in a sane state.
398
                registry.CheckSanity();
2✔
399
            }
2✔
400

401
            var directoryPath = Path.GetDirectoryName(path);
2✔
402

403
            if (directoryPath == null)
2!
404
            {
×
405
                log.ErrorFormat("Failed to save registry, invalid path: {0}", path);
×
406
                throw new DirectoryNotFoundKraken(path, string.Format(
×
407
                    Properties.Resources.RegistryManagerDirectoryNotFound, path));
408
            }
409

410
            if (!Directory.Exists(directoryPath))
2!
411
            {
×
412
                Directory.CreateDirectory(directoryPath);
×
413
            }
×
414

415
            file_transaction.WriteAllText(path, Serialize());
2✔
416

417
            if (!Directory.Exists(gameInstance.InstallHistoryDir))
2!
UNCOV
418
            {
×
NEW
419
                Directory.CreateDirectory(gameInstance.InstallHistoryDir);
×
420
            }
×
421
            ExportInstalled(new string[]
2✔
422
                            {
423
                                Path.Combine(directoryPath,
424
                                             LatestInstalledExportFilename()),
425
                                Path.Combine(gameInstance.InstallHistoryDir,
426
                                             HistoricInstalledExportFilename()),
427
                            },
428
                            false, true);
429
        }
2✔
430

431
        public string LatestInstalledExportFilename()
432
            => $"{Properties.Resources.RegistryManagerExportFilenamePrefix}-{gameInstance.SanitizedName}.ckan";
2✔
433
        private string HistoricInstalledExportFilename()
434
            => $"{Properties.Resources.RegistryManagerExportFilenamePrefix}-{gameInstance.SanitizedName}-{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.ckan";
2✔
435

436
        /// <summary>
437
        /// Save a custom .ckan file that contains all the currently
438
        /// installed mods as dependencies.
439
        /// </summary>
440
        /// <param name="paths">Desired locations of files to save</param>
441
        /// <param name="recommends">True to save the mods as recommended, false for depends</param>
442
        /// <param name="withVersions">True to include the mod versions in the file, false to omit them</param>
443
        private void ExportInstalled(IEnumerable<string> paths,
444
                                     bool                recommends,
445
                                     bool                withVersions)
446
        {
2✔
447
            var tx         = new TxFileManager();
2✔
448
            var serialized = SerializeCurrentInstall(recommends, withVersions);
2✔
449
            foreach (var path in paths)
5✔
450
            {
2✔
451
                tx.WriteAllText(path, serialized);
2✔
452
            }
2✔
453
        }
2✔
454

455
        private string SerializeCurrentInstall(bool recommends = false, bool with_versions = true)
456
            => GenerateModpack(recommends, with_versions).ToJson();
2✔
457

458
        /// <summary>
459
        /// Create a CkanModule object that represents the currently installed
460
        /// mod list as a metapackage.
461
        /// </summary>
462
        /// <param name="recommends">If true, put the mods in the recommends relationship, otherwise use depends</param>
463
        /// <param name="with_versions">If true, set the installed mod versions in the relationships</param>
464
        /// <returns>
465
        /// The CkanModule object
466
        /// </returns>
467
        public CkanModule GenerateModpack(bool recommends = false, bool with_versions = true)
468
        {
2✔
469
            string gameInstanceName = gameInstance.Name;
2✔
470
            string name      = string.Format(Properties.Resources.ModpackName, gameInstanceName);
2✔
471
            var    crit      = gameInstance.VersionCriteria();
2✔
472
            var    minAndMax = crit.MinAndMax;
2✔
473
            var module = new CkanModule(
2✔
474
                new ModuleVersion("v1.6"),
475
                Identifier.Sanitize(name),
476
                name,
477
                string.Format(Properties.Resources.RegistryManagerDefaultModpackAbstract, gameInstanceName),
478
                null,
479
                new List<string>()  { Environment.UserName   },
480
                new List<License>() { License.UnknownLicense },
481
                new ModuleVersion(DateTime.UtcNow.ToString("yyyy.MM.dd.hh.mm.ss")),
482
                null,
483
                ModuleKind.metapackage)
484
            {
485
                ksp_version_min       = minAndMax.Lower.AsInclusiveLower().WithoutBuild,
486
                ksp_version_max       = minAndMax.Upper.AsInclusiveUpper().WithoutBuild,
487
                download_content_type = typeof(CkanModule).GetTypeInfo()
488
                                            ?.GetDeclaredField("download_content_type")
489
                                            ?.GetCustomAttribute<DefaultValueAttribute>()
490
                                            ?.Value?.ToString(),
491
                release_date          = DateTime.Now,
492
            };
493

494
            var mods = registry.InstalledModules
2✔
495
                               .Where(inst => !inst.Module.IsDLC && !inst.AutoInstalled
2✔
496
                                              && IsAvailable(inst, gameInstance.StabilityToleranceConfig))
497
                               .Select(inst => inst.Module)
2✔
498
                               .ToHashSet();
499
            // Sort dependencies before dependers
500
            var resolver = new RelationshipResolver(mods, null,
2✔
501
                                                    RelationshipResolverOptions.ConflictsOpts(gameInstance.StabilityToleranceConfig),
502
                                                    registry, gameInstance.Game, gameInstance.VersionCriteria());
503
            var rels = resolver.ModList()
2✔
504
                               .Intersect(mods)
505
                               .Select(with_versions ? (Func<CkanModule, RelationshipDescriptor>)
506
                                                       RelationshipWithVersion
507
                                                     : RelationshipWithoutVersion)
508
                               .ToList();
509

510
            if (recommends)
2!
511
            {
×
512
                module.recommends = rels;
×
513
            }
×
514
            else
515
            {
2✔
516
                module.depends    = rels;
2✔
517
            }
2✔
518
            module.spec_version = SpecVersionAnalyzer.MinimumSpecVersion(module);
2✔
519

520
            return module;
2✔
521
        }
2✔
522

523
        private bool IsAvailable(InstalledModule inst, StabilityToleranceConfig stabilityTolerance)
524
        {
2✔
525
            try
526
            {
2✔
527
                var avail = registry.LatestAvailable(inst.identifier, stabilityTolerance, null);
2✔
528
                return true;
2✔
529
            }
530
            catch
2✔
531
            {
2✔
532
                // Skip unavailable modules (custom .ckan files)
533
                return false;
2✔
534
            }
535
        }
2✔
536

537
        private RelationshipDescriptor RelationshipWithVersion(CkanModule mod)
538
            => new ModuleRelationshipDescriptor()
2✔
539
            {
540
                name    = mod.identifier,
541
                version = mod.version,
542
            };
543

544
        private RelationshipDescriptor RelationshipWithoutVersion(CkanModule mod)
545
            => new ModuleRelationshipDescriptor()
2✔
546
            {
547
                name = mod.identifier,
548
            };
549

550
        /// <summary>
551
        /// Scans the game folder for DLL data and updates the registry.
552
        /// This operates as a transaction.
553
        /// </summary>
554
        /// <returns>
555
        /// True if found anything different, false if same as before
556
        /// </returns>
557
        public bool ScanUnmanagedFiles()
558
        {
2✔
559
            log.Info(Properties.Resources.GameInstanceScanning);
2✔
560
            using (var tx = CkanTransaction.CreateTransactionScope())
2✔
561
            {
2✔
562
                var modFolders = Enumerable.Repeat(gameInstance.Game.PrimaryModDirectoryRelative, 1)
2✔
563
                                           .Concat(gameInstance.Game.AlternateModDirectoriesRelative)
564
                                           .Select(mf => $"{mf}/")
2✔
565
                                           .ToArray();
566
                var stockFolders = gameInstance.Game.StockFolders
2✔
567
                                                    // Folders outside GameData won't be scanned
568
                                                    .Where(sf => modFolders.Any(mf => sf.StartsWith(mf)))
2✔
569
                                                    // Precalculate the full prefix once for all files
570
                                                    .Select(f => $"{f}/")
2✔
571
                                                    .ToArray();
572
                var dlls = modFolders.Select(gameInstance.ToAbsoluteGameDir)
2✔
573
                                     .Where(Directory.Exists)
574
                                     // EnumerateFiles is *case-sensitive* in its pattern, which causes
575
                                     // DLL files to be missed under Linux; we have to pick .dll, .DLL, or scanning
576
                                     // GameData *twice*.
577
                                     //
578
                                     // The least evil is to walk it once, and filter it ourselves.
579
                                     .SelectMany(absDir => Directory.EnumerateFiles(absDir, "*",
2✔
580
                                                                                    SearchOption.AllDirectories))
581
                                     .Where(file => file.EndsWith(".dll",
2✔
582
                                                                  StringComparison.CurrentCultureIgnoreCase))
583
                                     .Select(gameInstance.ToRelativeGameDir)
584
                                     .Where(relPath => !stockFolders.Any(f => relPath.StartsWith(f)))
2✔
585
                                     .GroupBy(gameInstance.DllPathToIdentifier)
586
                                     .OfType<IGrouping<string, string>>()
587
                                     .ToDictionary(grp => grp.Key,
2✔
588
                                                   grp => grp.First());
2✔
589
                log.DebugFormat("Registering DLLs: {0}", string.Join(", ", dlls.Values));
2✔
590
                var dllChanged = registry.SetDlls(dlls);
2✔
591

592
                var dlcChanged = ScanDlc();
2✔
593

594
                log.Debug("Scan completed, committing transaction");
2✔
595
                tx.Complete();
2✔
596

597
                return dllChanged || dlcChanged;
2✔
598
            }
599
        }
2✔
600

601
        /// <summary>
602
        /// Look for DLC installed in GameData
603
        /// </summary>
604
        /// <returns>
605
        /// True if not the same list as last scan, false otherwise
606
        /// </returns>
607
        public bool ScanDlc()
608
            => registry.SetDlcs(TestDlcScan(Path.Combine(gameInstance.CkanDir, "dlc"))
2✔
609
                                .Concat(WellKnownDlcScan())
610
                                .ToDictionary());
611

612
        private static IEnumerable<KeyValuePair<string, UnmanagedModuleVersion>> TestDlcScan(string dlcDir)
613
            => (Directory.Exists(dlcDir)
2✔
614
                       ? Directory.EnumerateFiles(dlcDir, "*.dlc",
615
                                                  SearchOption.TopDirectoryOnly)
616
                       : Enumerable.Empty<string>())
617
                   .Select(f => new KeyValuePair<string, UnmanagedModuleVersion>(
×
618
                       $"{Path.GetFileNameWithoutExtension(f)}-DLC",
619
                       new UnmanagedModuleVersion(File.ReadAllText(f).Trim())));
620

621
        private IEnumerable<KeyValuePair<string, UnmanagedModuleVersion>> WellKnownDlcScan()
622
            => gameInstance.Game.DlcDetectors
2✔
623
                .Select(d => d.IsInstalled(gameInstance, out string? identifier, out UnmanagedModuleVersion? version)
2!
624
                                 && identifier is not null && version is not null
625
                             ? new KeyValuePair<string, UnmanagedModuleVersion>(identifier, version)
626
                             : (KeyValuePair<string, UnmanagedModuleVersion>?)null)
627
                .OfType<KeyValuePair<string, UnmanagedModuleVersion>>();
628
    }
629
}
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

© 2025 Coveralls, Inc