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 + ); }