From d8c0690d90306fd066c7be3167900c29428399f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 14 Mar 2026 11:06:54 -0400 Subject: [PATCH] Prefer bundled WinGet COM activation and log activation source Prefer the bundled WinGet COM server over packaged COM registration when initializing the native WinGet helper. Also add structured activation failure handling and surface the selected activation mode and source in logs and manager status so fallback behavior is easier to diagnose. --- src/UniGetUI.Core.Logger/Logger.cs | 34 +++++ .../ClientHelpers/NativeWinGetHelper.cs | 115 ++++++++++++-- .../WinGet.cs | 31 +++- .../WinGetComActivationException.cs | 55 +++++++ .../WindowsPackageManagerBundledFactory.cs | 141 ++++++++++++++++++ .../WindowsPackageManagerStandardFactory.cs | 87 +++++++---- 6 files changed, 418 insertions(+), 45 deletions(-) create mode 100644 src/WindowsPackageManager.Interop/Exceptions/WinGetComActivationException.cs create mode 100644 src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerBundledFactory.cs diff --git a/src/UniGetUI.Core.Logger/Logger.cs b/src/UniGetUI.Core.Logger/Logger.cs index 19a817e82d..a668fdce40 100644 --- a/src/UniGetUI.Core.Logger/Logger.cs +++ b/src/UniGetUI.Core.Logger/Logger.cs @@ -5,6 +5,30 @@ namespace UniGetUI.Core.Logging public static class Logger { private static readonly List LogContents = []; + private static readonly Lock LogWriteLock = new(); + private static readonly string SessionLogPath = Path.Combine( + Path.GetTempPath(), + "UniGetUI", + "session.log" + ); + + public static string GetSessionLogPath() => SessionLogPath; + + private static void AppendToSessionLog(string text) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(SessionLogPath)!); + lock (LogWriteLock) + { + File.AppendAllText( + SessionLogPath, + $"[{DateTime.Now:yyyy-MM-dd h:mm:ss tt}] {text}{Environment.NewLine}" + ); + } + } + catch { } + } // String parameter log functions public static void ImportantInfo( @@ -13,6 +37,7 @@ public static void ImportantInfo( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + s); + AppendToSessionLog(s); LogContents.Add(new LogEntry(s, LogEntry.SeverityLevel.Success)); } @@ -22,6 +47,7 @@ public static void Debug( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + s); + AppendToSessionLog(s); LogContents.Add(new LogEntry(s, LogEntry.SeverityLevel.Debug)); } @@ -31,6 +57,7 @@ public static void Info( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + s); + AppendToSessionLog(s); LogContents.Add(new LogEntry(s, LogEntry.SeverityLevel.Info)); } @@ -40,6 +67,7 @@ public static void Warn( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + s); + AppendToSessionLog(s); LogContents.Add(new LogEntry(s, LogEntry.SeverityLevel.Warning)); } @@ -49,6 +77,7 @@ public static void Error( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + s); + AppendToSessionLog(s); LogContents.Add(new LogEntry(s, LogEntry.SeverityLevel.Error)); } @@ -59,6 +88,7 @@ public static void ImportantInfo( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + e.ToString()); + AppendToSessionLog(e.ToString()); LogContents.Add(new LogEntry(e.ToString(), LogEntry.SeverityLevel.Success)); } @@ -68,6 +98,7 @@ public static void Debug( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + e.ToString()); + AppendToSessionLog(e.ToString()); LogContents.Add(new LogEntry(e.ToString(), LogEntry.SeverityLevel.Debug)); } @@ -77,6 +108,7 @@ public static void Info( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + e.ToString()); + AppendToSessionLog(e.ToString()); LogContents.Add(new LogEntry(e.ToString(), LogEntry.SeverityLevel.Info)); } @@ -86,6 +118,7 @@ public static void Warn( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + e.ToString()); + AppendToSessionLog(e.ToString()); LogContents.Add(new LogEntry(e.ToString(), LogEntry.SeverityLevel.Warning)); } @@ -95,6 +128,7 @@ public static void Error( ) { Diagnostics.Debug.WriteLine($"[{caller}] " + e.ToString()); + AppendToSessionLog(e.ToString()); LogContents.Add(new LogEntry(e.ToString(), LogEntry.SeverityLevel.Error)); } diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs index dceb44ffbe..3b963124ce 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs @@ -16,12 +16,15 @@ namespace UniGetUI.PackageEngine.Managers.WingetManager; internal sealed class NativeWinGetHelper : IWinGetManagerHelper { - public WindowsPackageManagerFactory Factory; + public WindowsPackageManagerFactory Factory = null!; public static WindowsPackageManagerFactory? ExternalFactory; - public PackageManager WinGetManager; + public PackageManager WinGetManager = null!; public static PackageManager? ExternalWinGetManager; private readonly WinGet Manager; + public string ActivationMode { get; private set; } = string.Empty; + public string ActivationSource { get; private set; } = string.Empty; + public NativeWinGetHelper(WinGet manager) { Manager = manager; @@ -32,25 +35,113 @@ public NativeWinGetHelper(WinGet manager) ); } + if (TryInitializeBundledFactory()) + { + return; + } + + if (TryInitializeStandardFactory()) + { + return; + } + + InitializeLowerTrustFactory(); + } + + private bool TryInitializeBundledFactory() + { try { - Factory = new WindowsPackageManagerStandardFactory(); - WinGetManager = Factory.CreatePackageManager(); - ExternalFactory = Factory; - ExternalWinGetManager = WinGetManager; + var factory = new WindowsPackageManagerBundledFactory(); + var winGetManager = factory.CreatePackageManager(); + ApplyFactory( + factory, + winGetManager, + "bundled in-proc COM", + factory.LibraryPath, + "Connected to WinGet API using bundled in-proc activation." + ); + return true; + } + catch (WinGetComActivationException ex) + { + Logger.Warn( + $"Bundled WinGet in-proc activation failed ({ex.HResultHex}: {ex.Reason}), attempting packaged COM activation..." + ); + return false; } - catch + catch (Exception ex) { Logger.Warn( - "Couldn't connect to WinGet API, attempting to connect with lower trust... (Are you running as administrator?)" + $"Bundled WinGet in-proc activation failed ({ex.Message}), attempting packaged COM activation..." ); - Factory = new WindowsPackageManagerStandardFactory(allowLowerTrustRegistration: true); - WinGetManager = Factory.CreatePackageManager(); - ExternalFactory = Factory; - ExternalWinGetManager = WinGetManager; + return false; } } + private bool TryInitializeStandardFactory() + { + try + { + var factory = new WindowsPackageManagerStandardFactory(); + var winGetManager = factory.CreatePackageManager(); + ApplyFactory( + factory, + winGetManager, + "packaged COM registration", + "system COM registration", + "Connected to WinGet API using packaged COM activation." + ); + return true; + } + catch (WinGetComActivationException ex) + { + Logger.Warn( + $"Packaged WinGet COM activation failed ({ex.HResultHex}: {ex.Reason}), attempting lower-trust activation..." + ); + return false; + } + catch (Exception ex) + { + Logger.Warn( + $"Packaged WinGet COM activation failed ({ex.Message}), attempting lower-trust activation..." + ); + return false; + } + } + + private void InitializeLowerTrustFactory() + { + var factory = new WindowsPackageManagerStandardFactory(allowLowerTrustRegistration: true); + var winGetManager = factory.CreatePackageManager(); + ApplyFactory( + factory, + winGetManager, + "lower-trust COM registration", + "system COM registration (allow lower trust)", + "Connected to WinGet API using lower-trust COM activation." + ); + } + + private void ApplyFactory( + WindowsPackageManagerFactory factory, + PackageManager winGetManager, + string activationMode, + string activationSource, + string successMessage + ) + { + Factory = factory; + WinGetManager = winGetManager; + ActivationMode = activationMode; + ActivationSource = activationSource; + ExternalFactory = factory; + ExternalWinGetManager = winGetManager; + + Logger.Info(successMessage); + Logger.Info($"WinGet activation mode selected: {ActivationMode} | Source: {ActivationSource}"); + } + public IReadOnlyList FindPackages_UnSafe(string query) { List packages = []; diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs index 0fa062380e..131e37da5b 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs @@ -14,6 +14,7 @@ using UniGetUI.PackageEngine.ManagerClasses.Classes; using UniGetUI.PackageEngine.ManagerClasses.Manager; using UniGetUI.PackageEngine.PackageClasses; +using WindowsPackageManager.Interop; using Architecture = UniGetUI.PackageEngine.Enums.Architecture; namespace UniGetUI.PackageEngine.Managers.WingetManager @@ -319,10 +320,24 @@ out string callArguments } catch (Exception ex) { - Logger.Warn( - $"Cannot instantiate {(FORCE_BUNDLED ? "Bundled" : "Native")} WinGet Helper due to error: {ex.Message}" - ); - Logger.Warn(ex); + if ( + !FORCE_BUNDLED + && ex is WinGetComActivationException activationEx + && activationEx.IsExpectedFallbackCondition + ) + { + Logger.Warn( + $"Native WinGet helper is unavailable on this machine ({activationEx.HResultHex}: {activationEx.Reason})" + ); + } + else + { + Logger.Warn( + $"Cannot instantiate {(FORCE_BUNDLED ? "Bundled" : "Native")} WinGet Helper due to error: {ex.Message}" + ); + Logger.Warn(ex); + } + Logger.Warn("WinGet will resort to using BundledWinGetHelper()"); WinGetHelper.Instance = new BundledWinGetHelper(this); } @@ -361,8 +376,16 @@ protected override void _loadManagerVersion(out string version) if (IS_BUNDLED) version += "\nUsing bundled WinGet helper (CLI parsing)"; else + { version += "\nUsing Native WinGet helper (COM Api)"; + if (WinGetHelper.Instance is NativeWinGetHelper nativeHelper) + { + version += $"\nActivation mode: {nativeHelper.ActivationMode}"; + version += $"\nActivation source: {nativeHelper.ActivationSource}"; + } + } + string error = process.StandardError.ReadToEnd(); if (error != "") Logger.Error("WinGet STDERR not empty: " + error); diff --git a/src/WindowsPackageManager.Interop/Exceptions/WinGetComActivationException.cs b/src/WindowsPackageManager.Interop/Exceptions/WinGetComActivationException.cs new file mode 100644 index 0000000000..49cf069504 --- /dev/null +++ b/src/WindowsPackageManager.Interop/Exceptions/WinGetComActivationException.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; + +namespace WindowsPackageManager.Interop; + +public sealed class WinGetComActivationException : COMException +{ + public Guid Clsid { get; } + public Guid Iid { get; } + public bool AllowLowerTrustRegistration { get; } + public string HResultHex => $"0x{HResult:X8}"; + + public string Reason => DescribeHResult(HResult); + + public bool IsExpectedFallbackCondition => + HResult is + unchecked((int)0x80040154) or + unchecked((int)0x80070490) or + unchecked((int)0x80070002) or + unchecked((int)0x8000000F) + || Message.Contains("Element not found", StringComparison.OrdinalIgnoreCase) + || Message.Contains( + "Typename or Namespace was not found in metadata file", + StringComparison.OrdinalIgnoreCase + ); + + public WinGetComActivationException( + Guid clsid, + Guid iid, + int hresult, + bool allowLowerTrustRegistration + ) + : base(CreateMessage(clsid, iid, hresult, allowLowerTrustRegistration), hresult) + { + Clsid = clsid; + Iid = iid; + AllowLowerTrustRegistration = allowLowerTrustRegistration; + } + + private static string CreateMessage( + Guid clsid, + Guid iid, + int hresult, + bool allowLowerTrustRegistration + ) => + $"WinGet COM activation failed for CLSID {clsid} (IID {iid}, AllowLowerTrustRegistration={allowLowerTrustRegistration}): {DescribeHResult(hresult)}"; + + private static string DescribeHResult(int hresult) => hresult switch + { + unchecked((int)0x80040154) => "Class not registered", + unchecked((int)0x80070490) => "Element not found", + unchecked((int)0x80070002) => "File not found", + unchecked((int)0x8000000F) => "Typename or Namespace was not found in metadata file", + _ => Marshal.GetExceptionForHR(hresult)?.Message ?? $"HRESULT 0x{hresult:X8}", + }; +} diff --git a/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerBundledFactory.cs b/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerBundledFactory.cs new file mode 100644 index 0000000000..d6c94e6441 --- /dev/null +++ b/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerBundledFactory.cs @@ -0,0 +1,141 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using WinRT; + +namespace WindowsPackageManager.Interop; + +[SupportedOSPlatform("windows5.0")] +public sealed class WindowsPackageManagerBundledFactory : WindowsPackageManagerFactory +{ + private static readonly Lazy Native = new(LoadNativeExports); + + public string LibraryPath => Native.Value.LibraryPath; + + public WindowsPackageManagerBundledFactory(ClsidContext clsidContext = ClsidContext.Prod) + : base(clsidContext) { } + + protected override T CreateInstance(Guid clsid, Guid iid) + { + IntPtr instancePointer; + + try + { + int errorCode = Native.Value.CreateInstance(clsid, iid, out instancePointer); + if (errorCode < 0) + { + throw new WinGetComActivationException(clsid, iid, errorCode, false); + } + + return MarshalGeneric.FromAbi(instancePointer); + } + catch (COMException ex) when (ex.HResult < 0) + { + throw new WinGetComActivationException(clsid, iid, ex.HResult, false); + } + } + + private static NativeExports LoadNativeExports() + { + string libraryPath = ResolveBundledLibraryPath(); + IntPtr moduleHandle = LoadLibraryEx(libraryPath, IntPtr.Zero, LoadWithAlteredSearchPath); + if (moduleHandle == IntPtr.Zero) + { + int errorCode = Marshal.GetLastPInvokeError(); + throw new InvalidOperationException( + $"Failed to load bundled WindowsPackageManager.dll from '{libraryPath}' (Win32 error {errorCode})." + ); + } + + var initialize = GetExport( + moduleHandle, + nameof(WindowsPackageManagerServerInitialize) + ); + var createInstance = GetExport( + moduleHandle, + nameof(WindowsPackageManagerServerCreateInstance) + ); + + int errorCodeFromInitialize = initialize(); + if (errorCodeFromInitialize < 0) + { + throw new InvalidOperationException( + $"Failed to initialize bundled WindowsPackageManager in-proc module (HRESULT 0x{errorCodeFromInitialize:X8})." + ); + } + + return new NativeExports(moduleHandle, libraryPath, createInstance); + } + + private static string ResolveBundledLibraryPath() + { + string baseDirectory = AppContext.BaseDirectory; + string architectureFolder = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => "winget-cli_arm64", + Architecture.X64 => "winget-cli_x64", + Architecture.X86 => "winget-cli_x86", + _ => "winget-cli_x64", + }; + + string[] candidates = + [ + Path.Combine(baseDirectory, architectureFolder, "WindowsPackageManager.dll"), + Path.Combine(baseDirectory, "winget-cli_x64", "WindowsPackageManager.dll"), + Path.Combine(baseDirectory, "WindowsPackageManager.dll"), + ]; + + foreach (string candidate in candidates) + { + if (File.Exists(candidate)) + { + return candidate; + } + } + + throw new FileNotFoundException("Bundled WindowsPackageManager.dll was not found."); + } + + private static TDelegate GetExport(IntPtr moduleHandle, string exportName) + where TDelegate : Delegate + { + IntPtr exportPointer = GetProcAddress(moduleHandle, exportName); + if (exportPointer == IntPtr.Zero) + { + throw new EntryPointNotFoundException( + $"Export '{exportName}' was not found in bundled WindowsPackageManager.dll." + ); + } + + return Marshal.GetDelegateForFunctionPointer(exportPointer); + } + + private sealed record NativeExports( + IntPtr ModuleHandle, + string LibraryPath, + CreateInstanceDelegate CreateInstance + ); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int InitializeServerDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int CreateInstanceDelegate(in Guid clsid, in Guid iid, out IntPtr instance); + + private const uint LoadWithAlteredSearchPath = 0x00000008; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr LoadLibraryEx( + string lpFileName, + IntPtr hFile, + uint dwFlags + ); + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); + + private static string WindowsPackageManagerServerInitialize => + "WindowsPackageManagerServerInitialize"; + + private static string WindowsPackageManagerServerCreateInstance => + "WindowsPackageManagerServerCreateInstance"; +} diff --git a/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs b/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs index 95a223723e..18a1b557fa 100644 --- a/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs +++ b/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs @@ -3,7 +3,6 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; -using Windows.Win32; using Windows.Win32.System.Com; using WinRT; @@ -20,42 +19,72 @@ public WindowsPackageManagerStandardFactory( protected override T CreateInstance(Guid clsid, Guid iid) { - nint pUnknown = IntPtr.Zero; - try + if (!_allowLowerTrustRegistration) { - CLSCTX clsctx = CLSCTX.CLSCTX_LOCAL_SERVER; - if (_allowLowerTrustRegistration) + Type? projectedType = Type.GetTypeFromCLSID(clsid); + if (projectedType is null) { - clsctx |= CLSCTX.CLSCTX_ALLOW_LOWER_TRUST_REGISTRATION; + throw new WinGetComActivationException( + clsid, + iid, + unchecked((int)0x80040154), + _allowLowerTrustRegistration + ); } - Windows.Win32.Foundation.HRESULT hr = PInvoke.CoCreateInstance( - clsid, - null, - clsctx, - iid, - out object result - ); + object? activatedInstance = Activator.CreateInstance(projectedType); + if (activatedInstance is null) + { + throw new WinGetComActivationException( + clsid, + iid, + unchecked((int)0x80004003), + _allowLowerTrustRegistration + ); + } - // !! WARNING !! - // An exception may be thrown on the line below if UniGetUI - // runs as administrator and AllowLowerTrustRegistration settings is not checked - // or when WinGet is not installed on the system. - // It can be safely ignored if any of the conditions - // above are met. - Marshal.ThrowExceptionForHR(hr); + IntPtr pointer = Marshal.GetIUnknownForObject(activatedInstance); + return MarshalGeneric.FromAbi(pointer); + } - pUnknown = Marshal.GetIUnknownForObject(result); - return MarshalGeneric.FromAbi(pUnknown); + CLSCTX clsctx = CLSCTX.CLSCTX_LOCAL_SERVER; + if (_allowLowerTrustRegistration) + { + clsctx |= CLSCTX.CLSCTX_ALLOW_LOWER_TRUST_REGISTRATION; } - finally + + int errorCode = CoCreateInstanceRaw( + in clsid, + IntPtr.Zero, + (uint)clsctx, + in iid, + out IntPtr instance + ); + + if (errorCode < 0) { - // CoCreateInstance and FromAbi both AddRef on the native object. - // Release once to prevent memory leak. - if (pUnknown != IntPtr.Zero) - { - Marshal.Release(pUnknown); - } + throw new WinGetComActivationException( + clsid, + iid, + errorCode, + _allowLowerTrustRegistration + ); } + + return MarshalGeneric.FromAbi(instance); } + + [DllImport( + "api-ms-win-core-com-l1-1-0.dll", + EntryPoint = "CoCreateInstance", + ExactSpelling = true, + PreserveSig = true + )] + private static extern int CoCreateInstanceRaw( + in Guid clsid, + IntPtr pUnkOuter, + uint dwClsContext, + in Guid iid, + out IntPtr instance + ); }