< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Utilities.AssemblyAutoLoader
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Utilities/AssemblyAutoLoader.cs
Tag: Kestrun/Kestrun@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
65%
Covered lines: 42
Uncovered lines: 22
Coverable lines: 64
Total lines: 206
Line coverage: 65.6%
Branch coverage
63%
Covered branches: 33
Total branches: 52
Branch coverage: 63.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
PreloadAll(...)95%202094.73%
ResolveFromSearchDirs(...)0%210140%
SafeLoad(...)50%12860%
Clear(...)100%1010100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Utilities/AssemblyAutoLoader.cs

#LineLine coverage
 1//  File: AssemblyAutoLoader.cs
 2//  Namespace: Kestrun.Utilities   (choose any namespace that suits you)
 3
 4using System.Reflection;
 5using System.Runtime.Loader;
 6
 7namespace Kestrun.Utilities;
 8
 9/// <summary>
 10///  Registers one or more folders that contain private assemblies and makes
 11///  sure every DLL in those folders is available to PowerShell / scripts.
 12///  Call <see cref="PreloadAll"/> **once** at startup (or from PowerShell)
 13///  and forget about “could not load assembly …” errors.
 14/// </summary>
 15public static class AssemblyAutoLoader
 16{
 117    private static readonly HashSet<string> _searchDirs =
 118        new(StringComparer.OrdinalIgnoreCase);
 19
 20    private static bool _hookInstalled;
 21    private static bool _verbose;
 22
 23#if NET9_0_OR_GREATER
 124    private static readonly Lock _gate = new();
 25#else
 26    private static readonly object _gate = new();
 27#endif
 28
 29    /// <summary>
 30    ///  Scans the supplied directories, loads every DLL that isn’t already
 31    ///  loaded, and installs an <c>AssemblyResolve</c> hook so that any
 32    ///  later requests are resolved automatically.
 33    /// </summary>
 34    /// <param name="verbose">
 35    ///  If <see langword="true"/>, outputs diagnostic information to the console.
 36    /// </param>
 37    /// <param name="directories">
 38    ///  One or more absolute paths (they may be repeated; duplicates ignored).
 39    /// </param>
 40    /// <remarks>
 41    ///  You can call this more than once — new folders are merged into the
 42    ///  internal set, previously scanned ones are skipped.
 43    /// </remarks>
 44    public static void PreloadAll(bool verbose = false, params string[] directories)
 45    {
 1246        if (directories is null || directories.Length == 0)
 47        {
 148            throw new ArgumentException("At least one folder is required.", nameof(directories));
 49        }
 50
 51        lock (_gate)
 52        {
 1153            _verbose = verbose;
 54            // Remember new folders
 4655            foreach (var dir in directories.Where(Directory.Exists))
 56            {
 1257                if (_verbose)
 58                {
 59                    // Use Console.WriteLine for simplicity, or use your logging framework
 160                    Console.WriteLine($"Adding search directory: {dir}");
 61                }
 62
 1263                if (_searchDirs.Contains(dir))
 64                {
 65                    continue; // skip duplicates
 66                }
 67
 768                _ = _searchDirs.Add(Path.GetFullPath(dir));
 69            }
 70
 71            // Install the resolve hook once
 1172            if (!_hookInstalled)
 73            {
 674                if (_verbose)
 75                {
 76                    // Use Console.WriteLine for simplicity, or use your logging framework
 177                    Console.WriteLine("Installing AssemblyResolve hook for Kestrun.Utilities");
 78                }
 79                // This will be called whenever the runtime fails to find an assembly
 680                AppDomain.CurrentDomain.AssemblyResolve += ResolveFromSearchDirs;
 681                _hookInstalled = true;
 82            }
 83
 84            // Pre-load everything so types are immediately available
 4885            foreach (var dir in _searchDirs)
 86            {
 3287                foreach (var dll in Directory.GetFiles(dir, "*.dll"))
 88                {
 389                    if (_verbose)
 90                    {
 091                        Console.WriteLine($"Pre-loading assembly: {dll}");
 92                    }
 93
 394                    _ = SafeLoad(dll);
 95                }
 96            }
 97        }
 1198    }
 99
 100    // ---------------- helpers ----------------
 101
 102    private static Assembly? ResolveFromSearchDirs(object? sender, ResolveEventArgs args)
 103    {
 0104        if (args is null || string.IsNullOrEmpty(args.Name))
 105        {
 0106            if (_verbose)
 107            {
 0108                Console.WriteLine("ResolveFromSearchDirs called with null or empty name.");
 109            }
 110
 0111            return null; // let the runtime continue searching
 112        }
 0113        var shortName = new AssemblyName(args.Name).Name + ".dll";
 0114        if (_verbose)
 115        {
 0116            Console.WriteLine($"Resolving assembly: {shortName}");
 117        }
 118
 0119        foreach (var dir in _searchDirs)
 120        {
 0121            var candidate = Path.Combine(dir, shortName);
 0122            if (File.Exists(candidate))
 123            {
 0124                if (_verbose)
 125                {
 0126                    Console.WriteLine($"Resolving assembly: {candidate}");
 127                }
 128
 0129                return SafeLoad(candidate);
 130            }
 131        }
 0132        return null; // let the runtime continue searching
 0133    }
 134
 135    private static Assembly? SafeLoad(string path)
 136    {
 3137        var name = Path.GetFileNameWithoutExtension(path);
 138
 139        // Skip if already loaded
 3140        if (AppDomain.CurrentDomain.GetAssemblies()
 761141              .Any(a => string.Equals(a.GetName().Name, name,
 761142                                      StringComparison.OrdinalIgnoreCase)))
 143        {
 1144            if (_verbose)
 145            {
 0146                Console.WriteLine($"Assembly '{name}' is already loaded, skipping: {path}");
 147            }
 148            // Return null to indicate no new assembly was loaded
 1149            return null;
 150        }
 151
 152        try
 153        {
 2154            if (_verbose)
 155            {
 0156                Console.WriteLine($"Loading assembly: {path}");
 157            }
 158            // Load the assembly from the specified path (Core-friendly)
 2159            return AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(path));
 160        }
 0161        catch
 162        {
 0163            if (_verbose)
 164            {
 0165                Console.WriteLine($"Failed to load assembly: {path}");
 166            }
 167            // Swallow – we don’t block startup because of one bad DLL
 0168            return null;
 169        }
 2170    }
 171    /// <summary>
 172    /// Detaches the <c>AssemblyResolve</c> hook and, optionally, clears the
 173    /// list of search-directories.  Call this at the end of a runspace or
 174    /// when the application no longer needs dynamic resolution.
 175    /// </summary>
 176    /// <param name="clearSearchDirs">
 177    /// <see langword="true"/> ⇒ also forget the registered folders.
 178    /// Leave it <see langword="false"/> if you want to keep the list so a
 179    /// later <c>PreloadAll()</c> call can reuse it without re-scanning.
 180    /// </param>
 181    public static void Clear(bool clearSearchDirs = false)
 13182    {
 183        lock (_gate)
 184        {
 13185            if (_hookInstalled)
 186            {
 6187                AppDomain.CurrentDomain.AssemblyResolve -= ResolveFromSearchDirs;
 6188                _hookInstalled = false;
 189
 6190                if (_verbose)
 191                {
 1192                    Console.WriteLine("AssemblyResolve hook removed.");
 193                }
 194            }
 195
 13196            if (clearSearchDirs && _searchDirs.Count > 0)
 197            {
 5198                _searchDirs.Clear();
 5199                if (_verbose)
 200                {
 1201                    Console.WriteLine("Search-directory list cleared.");
 202                }
 203            }
 13204        }
 13205    }
 206}