| | 1 | | using System.Diagnostics; |
| | 2 | | using System.Reflection; |
| | 3 | | using Serilog; |
| | 4 | | namespace Kestrun.Utilities; |
| | 5 | |
|
| | 6 | | /// <summary> |
| | 7 | | /// Utility class to locate the Kestrun PowerShell module. |
| | 8 | | /// It searches for the module in both development and production environments. |
| | 9 | | /// </summary> |
| | 10 | | public static class PowerShellModuleLocator |
| | 11 | | { |
| | 12 | | /// <summary> |
| | 13 | | /// Retrieves the PowerShell module paths using pwsh. |
| | 14 | | /// This method executes a PowerShell command to get the PSModulePath environment variable, |
| | 15 | | /// splits it by the path separator, and returns the individual paths as an array. |
| | 16 | | /// </summary> |
| | 17 | | /// <returns>Array of PowerShell module paths.</returns> |
| | 18 | | private static string[] GetPSModulePathsViaPwsh() |
| | 19 | | { |
| | 20 | | try |
| | 21 | | { |
| 0 | 22 | | var psi = new ProcessStartInfo |
| 0 | 23 | | { |
| 0 | 24 | | FileName = "pwsh", |
| 0 | 25 | | Arguments = "-NoProfile -Command \"$env:PSModulePath -split [IO.Path]::PathSeparator\"", |
| 0 | 26 | | RedirectStandardOutput = true, |
| 0 | 27 | | RedirectStandardError = true, |
| 0 | 28 | | UseShellExecute = false, |
| 0 | 29 | | CreateNoWindow = true |
| 0 | 30 | | }; |
| | 31 | |
|
| 0 | 32 | | using var proc = Process.Start(psi); |
| 0 | 33 | | if (proc == null) |
| | 34 | | { |
| 0 | 35 | | Log.Error("❌ Failed to start pwsh process."); |
| 0 | 36 | | return []; |
| | 37 | | } |
| | 38 | |
|
| 0 | 39 | | var output = proc.StandardOutput.ReadToEnd(); |
| 0 | 40 | | var error = proc.StandardError.ReadToEnd(); |
| | 41 | |
|
| 0 | 42 | | proc.WaitForExit(); |
| | 43 | |
|
| 0 | 44 | | if (proc.ExitCode != 0) |
| | 45 | | { |
| 0 | 46 | | Log.Error("❌ pwsh exited with code {ExitCode}. Error:\n{Error}", proc.ExitCode, error); |
| 0 | 47 | | return []; |
| | 48 | | } |
| | 49 | |
|
| 0 | 50 | | return output |
| 0 | 51 | | .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); |
| | 52 | | } |
| 0 | 53 | | catch (Exception ex) |
| | 54 | | { |
| 0 | 55 | | Log.Error("⚠️ Exception during pwsh invocation: {Message}", ex.Message); |
| 0 | 56 | | return []; |
| | 57 | | } |
| 0 | 58 | | } |
| | 59 | |
|
| | 60 | | /// <summary> |
| | 61 | | /// Locates the Kestrun module path. |
| | 62 | | /// It first attempts to find the module in the development environment by searching upwards from the current direct |
| | 63 | | /// If not found, it will then check the production environment using PowerShell. |
| | 64 | | /// </summary> |
| | 65 | | /// <returns>The full path to the Kestrun module if found, otherwise null.</returns> |
| | 66 | | public static string? LocateKestrunModule() |
| | 67 | | { |
| | 68 | | // 1. Try development search |
| 117 | 69 | | var asm = Assembly.GetExecutingAssembly(); |
| 117 | 70 | | var dllPath = asm.Location; |
| | 71 | | // Get full InformationalVersion |
| 117 | 72 | | var fullVersion = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion; |
| | 73 | |
|
| | 74 | | // Strip build metadata if present (everything after and including '+') |
| 117 | 75 | | var semver = fullVersion?.Split('+')[0]; |
| | 76 | |
|
| 117 | 77 | | var devPath = FindFileUpwards(Path.GetDirectoryName(dllPath)!, Path.Combine("src", "PowerShell", "Kestrun", "Kes |
| | 78 | |
|
| 117 | 79 | | if (devPath != null) |
| | 80 | | { |
| 117 | 81 | | Log.Information("🌿 Development module found."); |
| 117 | 82 | | return devPath; |
| | 83 | | } |
| 0 | 84 | | if (semver == null) |
| | 85 | | { |
| 0 | 86 | | Log.Error("🚫 Unable to determine assembly version for Kestrun module lookup."); |
| 0 | 87 | | return null; |
| | 88 | | } |
| 0 | 89 | | Log.Information("🔍 Searching for Kestrun PowerShell module version: {Semver}", semver); |
| | 90 | | // 2. Production mode - ask pwsh |
| 0 | 91 | | Log.Information("🛰 Switching to production lookup via pwsh..."); |
| 0 | 92 | | foreach (var path in GetPSModulePathsViaPwsh()) |
| | 93 | | { |
| 0 | 94 | | var full = Path.Combine(path, "Kestrun", semver, "Kestrun.psm1"); |
| 0 | 95 | | if (File.Exists(full)) |
| | 96 | | { |
| 0 | 97 | | Console.WriteLine($"✅ Found production module: {full}"); |
| 0 | 98 | | return full; |
| | 99 | | } |
| | 100 | | } |
| | 101 | |
|
| 0 | 102 | | Log.Error("🚫 Kestrun.psm1 not found in any known location."); |
| 0 | 103 | | return null; |
| | 104 | | } |
| | 105 | |
|
| | 106 | | /// <summary> |
| | 107 | | /// Finds a file upwards from the current directory. |
| | 108 | | /// </summary> |
| | 109 | | /// <param name="startDir">The starting directory to search from.</param> |
| | 110 | | /// <param name="relativeTarget">The relative path of the target file.</param> |
| | 111 | | /// <returns>The full path to the file if found, otherwise null.</returns> |
| | 112 | | private static string? FindFileUpwards(string startDir, string relativeTarget) |
| | 113 | | { |
| 119 | 114 | | var current = startDir; |
| | 115 | |
|
| 824 | 116 | | while (current != null) |
| | 117 | | { |
| 823 | 118 | | var candidate = Path.Combine(current, relativeTarget); |
| 823 | 119 | | if (File.Exists(candidate)) |
| | 120 | | { |
| 118 | 121 | | return candidate; |
| | 122 | | } |
| | 123 | |
|
| 705 | 124 | | current = Path.GetDirectoryName(current); |
| | 125 | | } |
| | 126 | |
|
| 1 | 127 | | return null; |
| | 128 | | } |
| | 129 | | } |