diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index aa0b4787e..62198fd36 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -190,17 +190,11 @@ jobs: dotnet publish src/UniGetUI/UniGetUI.csproj /noLogo /p:Configuration=Release /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform -v m if ($LASTEXITCODE -ne 0) { throw "dotnet publish WinUI failed" } - dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj /noLogo /p:Configuration=Release /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform --self-contained true -v m - if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" } - # Stage binaries $PublishDir = "src/UniGetUI/bin/$Platform/Release/$TargetFramework/win-$Platform/publish" - $AvaloniaPublishDir = "src/UniGetUI.Avalonia/bin/$Platform/Release/$TargetFramework/win-$Platform/publish" if (Test-Path "unigetui_bin") { Remove-Item "unigetui_bin" -Recurse -Force } New-Item "unigetui_bin" -ItemType Directory | Out-Null Get-ChildItem $PublishDir | Move-Item -Destination "unigetui_bin" -Force - New-Item "unigetui_bin/Avalonia" -ItemType Directory | Out-Null - Get-ChildItem $AvaloniaPublishDir | Move-Item -Destination "unigetui_bin/Avalonia" -Force # Backward-compat alias Copy-Item "unigetui_bin/UniGetUI.exe" "unigetui_bin/WingetUI.exe" -Force diff --git a/scripts/build.ps1 b/scripts/build.ps1 index d0ee0d69e..afb68d981 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -39,7 +39,6 @@ $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $SrcDir = Join-Path $RepoRoot "src" $WindowsSolution = Join-Path $SrcDir "UniGetUI.Windows.slnx" $PublishProject = Join-Path $SrcDir "UniGetUI" "UniGetUI.csproj" -$AvaloniaPublishProject = Join-Path $SrcDir "UniGetUI.Avalonia" "UniGetUI.Avalonia.csproj" $BinDir = Join-Path $RepoRoot "unigetui_bin" $BuildPropsPath = Join-Path $SrcDir "Directory.Build.props" [xml] $BuildProps = Get-Content $BuildPropsPath @@ -52,7 +51,6 @@ if ([string]::IsNullOrWhiteSpace($PortableTargetFramework) -or [string]::IsNullO $TargetFramework = "$PortableTargetFramework-windows$WindowsTargetPlatformVersion" $PublishDir = Join-Path $SrcDir "UniGetUI" "bin" $Platform $Configuration $TargetFramework "win-$Platform" "publish" -$AvaloniaPublishDir = Join-Path $SrcDir "UniGetUI.Avalonia" "bin" $Platform $Configuration $TargetFramework "win-$Platform" "publish" # --- Version stamping --- if ($Version) { @@ -84,19 +82,11 @@ if ($LASTEXITCODE -ne 0) { throw "dotnet publish WinUI failed with exit code $LASTEXITCODE" } -dotnet publish $AvaloniaPublishProject /noLogo /p:Configuration=$Configuration /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform --self-contained true --ignore-failed-sources -v m -if ($LASTEXITCODE -ne 0) { - throw "dotnet publish Avalonia failed with exit code $LASTEXITCODE" -} - # --- Stage binaries --- if (Test-Path $BinDir) { Remove-Item $BinDir -Recurse -Force } New-Item $BinDir -ItemType Directory | Out-Null # Move published output into unigetui_bin Get-ChildItem $PublishDir | Move-Item -Destination $BinDir -Force -$AvaloniaBinDir = Join-Path $BinDir "Avalonia" -New-Item $AvaloniaBinDir -ItemType Directory | Out-Null -Get-ChildItem $AvaloniaPublishDir | Move-Item -Destination $AvaloniaBinDir -Force # WingetUI.exe alias for backward compat Copy-Item (Join-Path $BinDir "UniGetUI.exe") (Join-Path $BinDir "WingetUI.exe") -Force diff --git a/scripts/merge-publish-output.ps1 b/scripts/merge-publish-output.ps1 new file mode 100644 index 000000000..9b1889090 --- /dev/null +++ b/scripts/merge-publish-output.ps1 @@ -0,0 +1,85 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Merges one publish directory into another, allowing only identical file collisions. + +.PARAMETER Source + The publish directory to copy from. + +.PARAMETER Destination + The publish directory to copy into. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $Source, + + [Parameter(Mandatory)] + [string] $Destination +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path $Source -PathType Container)) { + throw "Source directory '$Source' does not exist." +} + +if (-not (Test-Path $Destination -PathType Container)) { + throw "Destination directory '$Destination' does not exist." +} + +$Source = (Resolve-Path $Source).Path +$Destination = (Resolve-Path $Destination).Path + +$sourceWinsConflicts = @{ + 'Microsoft.Extensions.DependencyInjection.Abstractions.dll' = $true + 'Microsoft.VisualBasic.dll' = $true + 'Microsoft.Win32.SystemEvents.dll' = $true + 'System.Diagnostics.EventLog.dll' = $true + 'System.Drawing.Common.dll' = $true + 'System.Drawing.dll' = $true + 'System.Private.Windows.Core.dll' = $true + 'System.Security.Cryptography.Pkcs.dll' = $true + 'System.Security.Cryptography.Xml.dll' = $true + 'WindowsBase.dll' = $true +} + +$destinationWinsConflicts = @{ + 'Microsoft.Windows.SDK.NET.dll' = $true + 'WinRT.Runtime.dll' = $true +} + +Get-ChildItem $Source -Recurse -File | ForEach-Object { + $relativePath = $_.FullName.Substring($Source.Length).TrimStart('\', '/') + $destinationPath = Join-Path $Destination $relativePath + $destinationDirectory = Split-Path $destinationPath -Parent + + if (Test-Path $destinationPath -PathType Leaf) { + $sourceHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + $destinationHash = (Get-FileHash $destinationPath -Algorithm SHA256).Hash + + if ($sourceHash -ne $destinationHash) { + $fileName = [System.IO.Path]::GetFileName($relativePath) + + if ($sourceWinsConflicts.ContainsKey($fileName)) { + Copy-Item $_.FullName -Destination $destinationPath -Force + return + } + + if ($destinationWinsConflicts.ContainsKey($fileName)) { + return + } + + throw "Publish merge conflict for '$relativePath': source and destination files differ." + } + + return + } + + if (-not (Test-Path $destinationDirectory -PathType Container)) { + New-Item $destinationDirectory -ItemType Directory -Force | Out-Null + } + + Copy-Item $_.FullName -Destination $destinationPath -Force +} \ No newline at end of file diff --git a/src/Shared/SharedPreUiCommandDispatcher.cs b/src/Shared/SharedPreUiCommandDispatcher.cs new file mode 100644 index 000000000..99178b1f4 --- /dev/null +++ b/src/Shared/SharedPreUiCommandDispatcher.cs @@ -0,0 +1,347 @@ +using UniGetUI.Core.SettingsEngine; +using UniGetUI.Core.SettingsEngine.SecureSettings; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Shared; + +internal readonly record struct SharedPreUiCommandExitCodes( + int Success, + int Failed, + int InvalidParameter, + int NoSuchFile, + int UnknownSettingsKey +); + +internal static class SharedPreUiCommandDispatcher +{ + internal static readonly SharedPreUiCommandExitCodes WinUiExitCodes = new( + Success: 0, + Failed: -1, + InvalidParameter: -1073741811, + NoSuchFile: -1073741809, + UnknownSettingsKey: -2 + ); + + internal static readonly SharedPreUiCommandExitCodes AvaloniaExitCodes = new( + Success: 0, + Failed: 1, + InvalidParameter: 2, + NoSuchFile: 3, + UnknownSettingsKey: 4 + ); + + private const string HelpArgument = "--help"; + private const string ImportSettingsArgument = "--import-settings"; + private const string ExportSettingsArgument = "--export-settings"; + private const string EnableSettingArgument = "--enable-setting"; + private const string DisableSettingArgument = "--disable-setting"; + private const string SetSettingValueArgument = "--set-setting-value"; + private const string EnableSecureSettingArgument = "--enable-secure-setting"; + private const string DisableSecureSettingArgument = "--disable-secure-setting"; + + private const string EnableSecureSettingForUserArgument = SecureSettings.Args.ENABLE_FOR_USER; + private const string DisableSecureSettingForUserArgument = SecureSettings.Args.DISABLE_FOR_USER; + + public static int? TryHandle(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (args.Contains(HelpArgument)) + { + return Help(); + } + + if (args.Contains(ImportSettingsArgument)) + { + return ImportSettings(args, exitCodes); + } + + if (args.Contains(ExportSettingsArgument)) + { + return ExportSettings(args, exitCodes); + } + + if (args.Contains(EnableSettingArgument)) + { + return EnableSetting(args, exitCodes); + } + + if (args.Contains(DisableSettingArgument)) + { + return DisableSetting(args, exitCodes); + } + + if (args.Contains(SetSettingValueArgument)) + { + return SetSettingValue(args, exitCodes); + } + + if (args.Contains(EnableSecureSettingArgument)) + { + return EnableSecureSetting(args, exitCodes); + } + + if (args.Contains(DisableSecureSettingArgument)) + { + return DisableSecureSetting(args, exitCodes); + } + + if (args.Contains(EnableSecureSettingForUserArgument)) + { + return EnableSecureSettingForUser(args, exitCodes); + } + + if (args.Contains(DisableSecureSettingForUserArgument)) + { + return DisableSecureSettingForUser(args, exitCodes); + } + + return null; + } + + public static int Help() + { + CoreTools.Launch( + "https://github.com/Devolutions/UniGetUI/blob/main/docs/CLI.md#unigetui-command-line-interface" + ); + return 0; + } + + public static int ImportSettings(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetValueAfterArgument(args, ImportSettingsArgument, 1, out string file)) + { + return exitCodes.InvalidParameter; + } + + if (!File.Exists(file)) + { + return exitCodes.NoSuchFile; + } + + try + { + Settings.ImportFromFile_JSON(file); + return exitCodes.Success; + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int ExportSettings(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetValueAfterArgument(args, ExportSettingsArgument, 1, out string file)) + { + return exitCodes.InvalidParameter; + } + + try + { + Settings.ExportToFile_JSON(file); + return exitCodes.Success; + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int EnableSetting(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetEnumArgument(args, EnableSettingArgument, out Settings.K validKey)) + { + return GetEnumArgumentErrorCode(args, EnableSettingArgument, exitCodes); + } + + try + { + Settings.Set(validKey, true); + return exitCodes.Success; + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int DisableSetting(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetEnumArgument(args, DisableSettingArgument, out Settings.K validKey)) + { + return GetEnumArgumentErrorCode(args, DisableSettingArgument, exitCodes); + } + + try + { + Settings.Set(validKey, false); + return exitCodes.Success; + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int SetSettingValue(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetValueAfterArgument(args, SetSettingValueArgument, 1, out string setting)) + { + return exitCodes.InvalidParameter; + } + + if (!TryGetValueAfterArgument(args, SetSettingValueArgument, 2, out string value)) + { + return exitCodes.InvalidParameter; + } + + if (!Enum.TryParse(setting, out Settings.K validKey)) + { + return exitCodes.UnknownSettingsKey; + } + + try + { + Settings.SetValue(validKey, value); + return exitCodes.Success; + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int EnableSecureSetting(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetEnumArgument(args, EnableSecureSettingArgument, out SecureSettings.K validKey)) + { + return GetEnumArgumentErrorCode(args, EnableSecureSettingArgument, exitCodes); + } + + try + { + bool success = SecureSettings.TrySet(validKey, true).GetAwaiter().GetResult(); + return success ? exitCodes.Success : exitCodes.Failed; + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int DisableSecureSetting(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetEnumArgument(args, DisableSecureSettingArgument, out SecureSettings.K validKey)) + { + return GetEnumArgumentErrorCode(args, DisableSecureSettingArgument, exitCodes); + } + + try + { + bool success = SecureSettings.TrySet(validKey, false).GetAwaiter().GetResult(); + return success ? exitCodes.Success : exitCodes.Failed; + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int EnableSecureSettingForUser(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetValueAfterArgument(args, EnableSecureSettingForUserArgument, 1, out string user)) + { + return exitCodes.InvalidParameter; + } + + if (!TryGetValueAfterArgument(args, EnableSecureSettingForUserArgument, 2, out string setting)) + { + return exitCodes.InvalidParameter; + } + + try + { + return SecureSettings.ApplyForUser(user, setting, true); + } + catch (Exception ex) + { + return ex.HResult; + } + } + + public static int DisableSecureSettingForUser(IReadOnlyList args, SharedPreUiCommandExitCodes exitCodes) + { + if (!TryGetValueAfterArgument(args, DisableSecureSettingForUserArgument, 1, out string user)) + { + return exitCodes.InvalidParameter; + } + + if (!TryGetValueAfterArgument(args, DisableSecureSettingForUserArgument, 2, out string setting)) + { + return exitCodes.InvalidParameter; + } + + try + { + return SecureSettings.ApplyForUser(user, setting, false); + } + catch (Exception ex) + { + return ex.HResult; + } + } + + private static bool TryGetValueAfterArgument( + IReadOnlyList args, + string argument, + int offset, + out string value + ) + { + int basePos = FindArgumentIndex(args, argument); + if (basePos < 0 || basePos + offset >= args.Count) + { + value = string.Empty; + return false; + } + + value = args[basePos + offset].Trim('"').Trim('\''); + return true; + } + + private static bool TryGetEnumArgument( + IReadOnlyList args, + string argument, + out TEnum value + ) where TEnum : struct + { + if (!TryGetValueAfterArgument(args, argument, 1, out string rawValue)) + { + value = default; + return false; + } + + return Enum.TryParse(rawValue, out value); + } + + private static int GetEnumArgumentErrorCode( + IReadOnlyList args, + string argument, + SharedPreUiCommandExitCodes exitCodes + ) + { + return TryGetValueAfterArgument(args, argument, 1, out _) ? exitCodes.UnknownSettingsKey : exitCodes.InvalidParameter; + } + + private static int FindArgumentIndex(IReadOnlyList args, string argument) + { + for (int i = 0; i < args.Count; i++) + { + if (string.Equals(args[i], argument, StringComparison.Ordinal)) + { + return i; + } + } + + return -1; + } +} diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index 5b748d6f6..92a8beaea 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -80,7 +80,7 @@ public override void OnFrameworkInitializationCompleted() ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme)); var mainWindow = new MainWindow(); desktop.MainWindow = mainWindow; - Program.SecondaryInstanceArgsReceived += args => + AvaloniaAppHost.SecondaryInstanceArgsReceived += args => HandleSecondaryInstanceArgs(mainWindow, args); if (CoreData.WasDaemon) diff --git a/src/UniGetUI.Avalonia/AvaloniaCliHandler.cs b/src/UniGetUI.Avalonia/AvaloniaCliHandler.cs index ad0d76d0d..fb5a2546c 100644 --- a/src/UniGetUI.Avalonia/AvaloniaCliHandler.cs +++ b/src/UniGetUI.Avalonia/AvaloniaCliHandler.cs @@ -1,221 +1,17 @@ -using UniGetUI.Core.Data; -using UniGetUI.Core.Logging; -using UniGetUI.Core.SettingsEngine; -using UniGetUI.Core.SettingsEngine.SecureSettings; -using UniGetUI.Core.Tools; +using UniGetUI.Shared; namespace UniGetUI.Avalonia; -/// -/// Pre-UI CLI argument handler. Mirrors WinUI's CLIHandler. -/// Methods that return a non-null exit code should cause the process to exit -/// without launching the Avalonia app. -/// internal static class AvaloniaCliHandler { - public const string HELP = "--help"; public const string DAEMON = "--daemon"; public const string NO_CORRUPT_DIALOG = "--no-corrupt-dialog"; - public const string IMPORT_SETTINGS = "--import-settings"; - public const string EXPORT_SETTINGS = "--export-settings"; - - public const string ENABLE_SETTING = "--enable-setting"; - public const string DISABLE_SETTING = "--disable-setting"; - public const string SET_SETTING_VAL = "--set-setting-value"; - - public const string ENABLE_SECURE_SETTING = "--enable-secure-setting"; - public const string DISABLE_SECURE_SETTING = "--disable-secure-setting"; - public const string ENABLE_SECURE_SETTING_FOR_USER = SecureSettings.Args.ENABLE_FOR_USER; - public const string DISABLE_SECURE_SETTING_FOR_USER = SecureSettings.Args.DISABLE_FOR_USER; - - private enum ExitCode - { - Success = 0, - Failed = 1, - InvalidParameter = 2, - NoSuchFile = 3, - UnknownSettingsKey = 4, - } - - /// - /// Inspect and, for recognised pre-UI arguments, - /// execute the requested action and return the desired process exit code. - /// Returns null when no pre-UI argument was found and the app should start normally. - /// public static int? HandlePreUiArgs(string[] args) { - if (args.Contains(HELP)) - { - CoreTools.Launch("https://github.com/Devolutions/UniGetUI/blob/main/docs/CLI.md#unigetui-command-line-interface"); - return (int)ExitCode.Success; - } - - if (args.Contains(IMPORT_SETTINGS)) - return ImportSettings(args); - - if (args.Contains(EXPORT_SETTINGS)) - return ExportSettings(args); - - if (args.Contains(ENABLE_SETTING)) - return EnableSetting(args); - - if (args.Contains(DISABLE_SETTING)) - return DisableSetting(args); - - if (args.Contains(SET_SETTING_VAL)) - return SetSettingsValue(args); - - if (args.Contains(ENABLE_SECURE_SETTING)) - return EnableSecureSetting(args); - - if (args.Contains(DISABLE_SECURE_SETTING)) - return DisableSecureSetting(args); - - if (args.Contains(ENABLE_SECURE_SETTING_FOR_USER)) - return EnableSecureSettingForUser(args); - - if (args.Contains(DISABLE_SECURE_SETTING_FOR_USER)) - return DisableSecureSettingForUser(args); - - return null; - } - - private static int ImportSettings(string[] args) - { - int idx = Array.IndexOf(args, IMPORT_SETTINGS); - if (idx < 0 || idx + 1 >= args.Length) - return (int)ExitCode.InvalidParameter; - - var file = args[idx + 1].Trim('"').Trim('\''); - if (!File.Exists(file)) - return (int)ExitCode.NoSuchFile; - - try - { - Settings.ImportFromFile_JSON(file); - return (int)ExitCode.Success; - } - catch (Exception ex) - { - Logger.Error(ex); - return ex.HResult; - } - } - - private static int ExportSettings(string[] args) - { - int idx = Array.IndexOf(args, EXPORT_SETTINGS); - if (idx < 0 || idx + 1 >= args.Length) - return (int)ExitCode.InvalidParameter; - - var file = args[idx + 1].Trim('"').Trim('\''); - try - { - Settings.ExportToFile_JSON(file); - return (int)ExitCode.Success; - } - catch (Exception ex) - { - Logger.Error(ex); - return ex.HResult; - } - } - - private static int EnableSetting(string[] args) - { - int idx = Array.IndexOf(args, ENABLE_SETTING); - if (idx < 0 || idx + 1 >= args.Length) - return (int)ExitCode.InvalidParameter; - - if (!Enum.TryParse(args[idx + 1].Trim('"').Trim('\''), out Settings.K key)) - return (int)ExitCode.UnknownSettingsKey; - - try { Settings.Set(key, true); return (int)ExitCode.Success; } - catch (Exception ex) { return ex.HResult; } - } - - private static int DisableSetting(string[] args) - { - int idx = Array.IndexOf(args, DISABLE_SETTING); - if (idx < 0 || idx + 1 >= args.Length) - return (int)ExitCode.InvalidParameter; - - if (!Enum.TryParse(args[idx + 1].Trim('"').Trim('\''), out Settings.K key)) - return (int)ExitCode.UnknownSettingsKey; - - try { Settings.Set(key, false); return (int)ExitCode.Success; } - catch (Exception ex) { return ex.HResult; } - } - - private static int SetSettingsValue(string[] args) - { - int idx = Array.IndexOf(args, SET_SETTING_VAL); - if (idx < 0 || idx + 2 >= args.Length) - return (int)ExitCode.InvalidParameter; - - if (!Enum.TryParse(args[idx + 1].Trim('"').Trim('\''), out Settings.K key)) - return (int)ExitCode.UnknownSettingsKey; - - try { Settings.SetValue(key, args[idx + 2]); return (int)ExitCode.Success; } - catch (Exception ex) { return ex.HResult; } - } - - private static int EnableSecureSetting(string[] args) - { - int idx = Array.IndexOf(args, ENABLE_SECURE_SETTING); - if (idx < 0 || idx + 1 >= args.Length) - return (int)ExitCode.InvalidParameter; - - if (!Enum.TryParse(args[idx + 1].Trim('"').Trim('\''), out SecureSettings.K key)) - return (int)ExitCode.UnknownSettingsKey; - - try - { - bool ok = SecureSettings.TrySet(key, true).GetAwaiter().GetResult(); - return ok ? (int)ExitCode.Success : (int)ExitCode.Failed; - } - catch (Exception ex) { return ex.HResult; } - } - - private static int DisableSecureSetting(string[] args) - { - int idx = Array.IndexOf(args, DISABLE_SECURE_SETTING); - if (idx < 0 || idx + 1 >= args.Length) - return (int)ExitCode.InvalidParameter; - - if (!Enum.TryParse(args[idx + 1].Trim('"').Trim('\''), out SecureSettings.K key)) - return (int)ExitCode.UnknownSettingsKey; - - try - { - bool ok = SecureSettings.TrySet(key, false).GetAwaiter().GetResult(); - return ok ? (int)ExitCode.Success : (int)ExitCode.Failed; - } - catch (Exception ex) { return ex.HResult; } - } - - private static int EnableSecureSettingForUser(string[] args) - { - int idx = Array.IndexOf(args, ENABLE_SECURE_SETTING_FOR_USER); - if (idx < 0 || idx + 2 >= args.Length) - return (int)ExitCode.InvalidParameter; - - var user = args[idx + 1].Trim('"').Trim('\''); - var setting = args[idx + 2].Trim('"').Trim('\''); - try { return SecureSettings.ApplyForUser(user, setting, true); } - catch (Exception ex) { return ex.HResult; } - } - - private static int DisableSecureSettingForUser(string[] args) - { - int idx = Array.IndexOf(args, DISABLE_SECURE_SETTING_FOR_USER); - if (idx < 0 || idx + 2 >= args.Length) - return (int)ExitCode.InvalidParameter; - - var user = args[idx + 1].Trim('"').Trim('\''); - var setting = args[idx + 2].Trim('"').Trim('\''); - try { return SecureSettings.ApplyForUser(user, setting, false); } - catch (Exception ex) { return ex.HResult; } + return SharedPreUiCommandDispatcher.TryHandle( + args, + SharedPreUiCommandDispatcher.AvaloniaExitCodes + ); } } diff --git a/src/UniGetUI.Avalonia/Infrastructure/AppRestartHelper.cs b/src/UniGetUI.Avalonia/Infrastructure/AppRestartHelper.cs index 3657e8706..def6de74c 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AppRestartHelper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AppRestartHelper.cs @@ -1,6 +1,6 @@ -using System.Diagnostics; using Avalonia.Controls.ApplicationLifetimes; using UniGetUI.Avalonia.Views; +using UniGetUI.Core.Tools; namespace UniGetUI.Avalonia.Infrastructure; @@ -11,10 +11,7 @@ internal static class AppRestartHelper public static void Restart() { string executablePath = ResolveRestartExecutablePath(AppContext.BaseDirectory); - Process.Start(new ProcessStartInfo(executablePath) - { - UseShellExecute = false, - }); + CoreTools.ScheduleRelaunchAfterExit(executablePath); if (MainWindow.Instance is { } mainWindow) { diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs new file mode 100644 index 000000000..d047aa694 --- /dev/null +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs @@ -0,0 +1,93 @@ +using Avalonia; +using UniGetUI.Core.Data; +using UniGetUI.Core.Logging; +using UniGetUI.Interface; + +namespace UniGetUI.Avalonia.Infrastructure; + +public static class AvaloniaAppHost +{ + private static Mutex? _singleInstanceMutex; + + public static event Action? SecondaryInstanceArgsReceived; + + public static void Run(string[] args) + { + AppDomain.CurrentDomain.UnhandledException += (_, e) => + CrashHandler.ReportFatalException((Exception)e.ExceptionObject); + + if (ShouldPrepareCliConsole(args)) + { + WindowsConsoleHost.PrepareCliIO(); + } + + if (AvaloniaCliHandler.HandlePreUiArgs(args) is { } exitCode) + { + Environment.Exit(exitCode); + return; + } + + if (IpcCliSyntax.IsIpcCommand(args)) + { + Environment.ExitCode = IpcCliCommandRunner.RunAsync(args, Console.Out, Console.Error) + .GetAwaiter() + .GetResult(); + return; + } + + if (HeadlessModeOptions.IsHeadless(args)) + { + Environment.ExitCode = HeadlessDaemonHost.RunAsync().GetAwaiter().GetResult(); + return; + } + + CoreData.WasDaemon = CoreData.IsDaemon = args.Contains(AvaloniaCliHandler.DAEMON); + + if (!TryRegisterSingleInstance(args)) + { + return; + } + + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + + private static bool ShouldPrepareCliConsole(IReadOnlyList args) + { + return IpcCliSyntax.HasVerbCommand(args); + } + + private static bool TryRegisterSingleInstance(string[] args) + { + if (!OperatingSystem.IsWindows()) + return true; + + _singleInstanceMutex = new Mutex( + initiallyOwned: true, + name: CoreData.MainWindowIdentifier, + createdNew: out bool createdNew + ); + + if (createdNew) + { + SingleInstanceRedirector.StartListener(args => + SecondaryInstanceArgsReceived?.Invoke(args) + ); + return true; + } + + if (SingleInstanceRedirector.TryForwardToFirstInstance(args)) + { + _singleInstanceMutex.Dispose(); + _singleInstanceMutex = null; + return false; + } + + Logger.Warn("Could not redirect to the existing Avalonia instance; starting a new one"); + return true; + } +} diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs index 299dae90b..c7ccf41fb 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs @@ -345,14 +345,7 @@ private static async Task LoadElevatorAsync() Logger.Warn($"Using system GSudo since UniGetUI Elevator is not available in DEBUG builds"); CoreData.ElevatorPath = (await CoreTools.WhichAsync("gsudo.exe")).Item2; #else - string installRoot = Path.GetDirectoryName(CoreData.UniGetUIExecutableDirectory) - ?? CoreData.UniGetUIExecutableDirectory; - CoreData.ElevatorPath = Path.Join( - installRoot, - "Assets", - "Utilities", - "UniGetUI Elevator.exe" - ); + CoreData.ElevatorPath = ResolveBundledElevatorPath(); Logger.Debug($"Using built-in UniGetUI Elevator at {CoreData.ElevatorPath}"); #endif } @@ -363,6 +356,42 @@ private static async Task LoadElevatorAsync() } } +#if !DEBUG + private static string ResolveBundledElevatorPath() + { + string executableDirectory = CoreData.UniGetUIExecutableDirectory; + string localPath = Path.Join( + executableDirectory, + "Assets", + "Utilities", + "UniGetUI Elevator.exe" + ); + + if (File.Exists(localPath)) + { + return localPath; + } + + string? parentDirectory = Path.GetDirectoryName(executableDirectory); + if (!string.IsNullOrEmpty(parentDirectory)) + { + string parentPath = Path.Join( + parentDirectory, + "Assets", + "Utilities", + "UniGetUI Elevator.exe" + ); + + if (File.Exists(parentPath)) + { + return parentPath; + } + } + + return localPath; + } +#endif + [System.Runtime.Versioning.SupportedOSPlatform("linux")] private static async Task LoadLinuxElevatorAsync() { diff --git a/src/UniGetUI.Avalonia/Program.cs b/src/UniGetUI.Avalonia/Program.cs index 8f5d3e856..71c65b2bf 100644 --- a/src/UniGetUI.Avalonia/Program.cs +++ b/src/UniGetUI.Avalonia/Program.cs @@ -1,100 +1,21 @@ using System; using Avalonia; using UniGetUI.Avalonia.Infrastructure; -using UniGetUI.Core.Data; -using UniGetUI.Core.Logging; -using UniGetUI.Interface; namespace UniGetUI.Avalonia; sealed class Program { - private static Mutex? _singleInstanceMutex; - - internal static event Action? SecondaryInstanceArgsReceived; - // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] public static void Main(string[] args) { - AppDomain.CurrentDomain.UnhandledException += (_, e) => - CrashHandler.ReportFatalException((Exception)e.ExceptionObject); - - if (ShouldPrepareCliConsole(args)) - { - WindowsConsoleHost.PrepareCliIO(); - } - - // Handle pre-UI CLI arguments (settings manipulation, help, etc.) without - // launching the Avalonia UI. Mirrors WinUI's EntryPoint.cs dispatch logic. - if (AvaloniaCliHandler.HandlePreUiArgs(args) is { } exitCode) - { - Environment.Exit(exitCode); - return; - } - - if (IpcCliSyntax.IsIpcCommand(args)) - { - Environment.ExitCode = IpcCliCommandRunner.RunAsync(args, Console.Out, Console.Error) - .GetAwaiter() - .GetResult(); - return; - } - - if (HeadlessModeOptions.IsHeadless(args)) - { - Environment.ExitCode = HeadlessDaemonHost.RunAsync().GetAwaiter().GetResult(); - return; - } - - CoreData.WasDaemon = CoreData.IsDaemon = args.Contains(AvaloniaCliHandler.DAEMON); - - if (!TryRegisterSingleInstance(args)) - return; - - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } - - private static bool ShouldPrepareCliConsole(IReadOnlyList args) - { - return IpcCliSyntax.HasVerbCommand(args); - } - - private static bool TryRegisterSingleInstance(string[] args) - { - if (!OperatingSystem.IsWindows()) - return true; - - _singleInstanceMutex = new Mutex( - initiallyOwned: true, - name: CoreData.MainWindowIdentifier, - createdNew: out bool createdNew - ); - - if (createdNew) - { - SingleInstanceRedirector.StartListener(args => - SecondaryInstanceArgsReceived?.Invoke(args) - ); - return true; - } - - if (SingleInstanceRedirector.TryForwardToFirstInstance(args)) - { - _singleInstanceMutex.Dispose(); - _singleInstanceMutex = null; - return false; - } - - Logger.Warn("Could not redirect to the existing Avalonia instance; starting a new one"); - return true; + AvaloniaAppHost.Run(args); } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + => AvaloniaAppHost.BuildAvaloniaApp(); } diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 000ac0fc9..2d8609ab5 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -40,7 +40,7 @@ + + + + diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 82842dc11..67d513d5d 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -550,9 +550,28 @@ public void ShowFromTray() public void QuitApplication() { _allowClose = true; - AvaloniaBootstrapper.StopIpcApiAsync().GetAwaiter().GetResult(); - (global::Avalonia.Application.Current?.ApplicationLifetime - as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); + _ = QuitApplicationAsync(); + } + + private async Task QuitApplicationAsync() + { + try + { + await AvaloniaBootstrapper.StopIpcApiAsync().WaitAsync(TimeSpan.FromSeconds(5)); + } + catch (TimeoutException ex) + { + Logger.Warn("Timed out while stopping Avalonia IPC API during shutdown"); + Logger.Warn(ex); + } + catch (Exception ex) + { + Logger.Error(ex); + } + + Dispatcher.UIThread.Post(() => + (global::Avalonia.Application.Current?.ApplicationLifetime + as IClassicDesktopStyleApplicationLifetime)?.Shutdown()); } public static void ApplyProxyVariableToProcess() diff --git a/src/UniGetUI.Core.Tools/Tools.cs b/src/UniGetUI.Core.Tools/Tools.cs index 553fd609b..df054d383 100644 --- a/src/UniGetUI.Core.Tools/Tools.cs +++ b/src/UniGetUI.Core.Tools/Tools.cs @@ -110,11 +110,30 @@ public static string AutoTranslated(string text) public static void RelaunchProcess() { Logger.Debug("Launching process: " + CoreData.UniGetUIExecutableFile); - Process.Start(CoreData.UniGetUIExecutableFile); + ScheduleRelaunchAfterExit(); Logger.Warn("About to kill process"); Environment.Exit(0); } + public static void ScheduleRelaunchAfterExit(string? executablePath = null) + { + executablePath ??= CoreData.UniGetUIExecutableFile; + int currentProcessId = Environment.ProcessId; + string escapedExecutablePath = executablePath.Replace("'", "''"); + string command = + $"Wait-Process -Id {currentProcessId}; Start-Process -FilePath '{escapedExecutablePath}'"; + + Process.Start( + new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = $"-NoProfile -WindowStyle Hidden -Command \"{command}\"", + UseShellExecute = false, + CreateNoWindow = true, + } + ); + } + /// /// Finds an executable in path and returns its location /// diff --git a/src/UniGetUI.Tests/ModernAppLauncherTests.cs b/src/UniGetUI.Tests/ModernAppLauncherTests.cs index b34b01dbe..2ac76d353 100644 --- a/src/UniGetUI.Tests/ModernAppLauncherTests.cs +++ b/src/UniGetUI.Tests/ModernAppLauncherTests.cs @@ -38,7 +38,23 @@ public void ClassicModeDefaultsToEnabled() } [Fact] - public void ResolveModernExecutablePath_PrefersAvaloniaSubdirectory() + public void ResolveModernExecutablePath_PrefersRootExecutable() + { + string baseDirectory = Path.Combine(_testRoot, "Launcher"); + Directory.CreateDirectory(baseDirectory); + + string expected = Path.Combine(baseDirectory, ModernAppLauncher.ModernAppExecutableName); + File.WriteAllText(expected, ""); + + string avaloniaDirectory = Path.Combine(baseDirectory, ModernAppLauncher.ModernAppDirectoryName); + Directory.CreateDirectory(avaloniaDirectory); + File.WriteAllText(Path.Combine(avaloniaDirectory, ModernAppLauncher.ModernAppExecutableName), ""); + + Assert.Equal(expected, ModernAppLauncher.ResolveModernExecutablePath(baseDirectory)); + } + + [Fact] + public void ResolveModernExecutablePath_FallsBackToAvaloniaSubdirectory() { string baseDirectory = Path.Combine(_testRoot, "Launcher"); string avaloniaDirectory = Path.Combine(baseDirectory, ModernAppLauncher.ModernAppDirectoryName); diff --git a/src/UniGetUI.Tests/UniGetUI.Tests.csproj b/src/UniGetUI.Tests/UniGetUI.Tests.csproj index 1b8678ef7..7a3b4a57e 100644 --- a/src/UniGetUI.Tests/UniGetUI.Tests.csproj +++ b/src/UniGetUI.Tests/UniGetUI.Tests.csproj @@ -44,6 +44,7 @@ + diff --git a/src/UniGetUI/App.xaml.cs b/src/UniGetUI/App.xaml.cs index 78720ed53..179146076 100644 --- a/src/UniGetUI/App.xaml.cs +++ b/src/UniGetUI/App.xaml.cs @@ -715,17 +715,36 @@ public void DisposeAndQuit(int outputCode = 0) DWMThreadHelper.ChangeState_DWM(false); DWMThreadHelper.ChangeState_XAML(false); MainWindow?.Close(); - IpcApi.Stop().GetAwaiter().GetResult(); - Exit(); + _ = StopIpcAndExitAsync(outputCode); // await Task.Delay(100); // Environment.Exit(outputCode); } + public bool IsQuitting => Interlocked.CompareExchange(ref _isQuitting, 0, 0) == 1; + + private async Task StopIpcAndExitAsync(int outputCode) + { + try + { + await IpcApi.Stop().WaitAsync(TimeSpan.FromSeconds(5)); + } + catch (TimeoutException ex) + { + Logger.Warn("Timed out while stopping IPC API during shutdown"); + Logger.Warn(ex); + } + catch (Exception ex) + { + Logger.Error(ex); + } + + Environment.Exit(outputCode); + } + public void KillAndRestart() { - Process.Start(CoreData.UniGetUIExecutableFile); - Instance.MainWindow?.Close(); - Environment.Exit(0); + CoreTools.ScheduleRelaunchAfterExit(); + DisposeAndQuit(); } } } diff --git a/src/UniGetUI/CLIHandler.cs b/src/UniGetUI/CLIHandler.cs index 48597c72b..59702e26f 100644 --- a/src/UniGetUI/CLIHandler.cs +++ b/src/UniGetUI/CLIHandler.cs @@ -5,6 +5,7 @@ using UniGetUI.Core.SettingsEngine.SecureSettings; using UniGetUI.Core.Tools; using UniGetUI.Interface; +using UniGetUI.Shared; namespace UniGetUI; @@ -43,10 +44,7 @@ private enum HRESULT public static int Help() { - var url = - "https://github.com/Devolutions/UniGetUI/blob/main/docs/CLI.md#unigetui-command-line-interface"; - CoreTools.Launch(url); - return 0; + return SharedPreUiCommandDispatcher.Help(); } public static int ImportSettings() @@ -56,28 +54,10 @@ public static int ImportSettings() internal static int ImportSettings(IReadOnlyList args) { - var arguments = args.ToList(); - var filePos = arguments.IndexOf(IMPORT_SETTINGS); - if (filePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater --import-settings was not found - - if (filePos + 1 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The file parameter does not exist (import settings requires "--import-settings file") - - var file = arguments[filePos + 1].Trim('"').Trim('\''); - if (!File.Exists(file)) - return (int)HRESULT.STATUS_NO_SUCH_FILE; // The given file does not exist - - try - { - Settings.ImportFromFile_JSON(file); - } - catch (Exception ex) - { - return ex.HResult; - } - - return (int)HRESULT.SUCCESS; + return SharedPreUiCommandDispatcher.ImportSettings( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int ExportSettings() @@ -87,26 +67,10 @@ public static int ExportSettings() internal static int ExportSettings(IReadOnlyList args) { - var arguments = args.ToList(); - var filePos = arguments.IndexOf(EXPORT_SETTINGS); - if (filePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater --export-settings was not found - - if (filePos + 1 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The file parameter does not exist (export settings requires "--export-settings file") - - var file = arguments[filePos + 1].Trim('"').Trim('\''); - - try - { - Settings.ExportToFile_JSON(file); - } - catch (Exception ex) - { - return ex.HResult; - } - - return (int)HRESULT.SUCCESS; + return SharedPreUiCommandDispatcher.ExportSettings( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int EnableSetting() @@ -116,28 +80,10 @@ public static int EnableSetting() internal static int EnableSetting(IReadOnlyList args) { - var arguments = args.ToList(); - var basePos = arguments.IndexOf(ENABLE_SETTING); - if (basePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater --export-settings was not found - - if (basePos + 1 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The file parameter does not exist (export settings requires "--export-settings file") - - var setting = arguments[basePos + 1].Trim('"').Trim('\''); - if (!Enum.TryParse(setting, out Settings.K validKey)) - return (int)HRESULT.STATUS_UNKNOWN__SETTINGS_KEY; - - try - { - Settings.Set(validKey, true); - } - catch (Exception ex) - { - return ex.HResult; - } - - return (int)HRESULT.SUCCESS; + return SharedPreUiCommandDispatcher.EnableSetting( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int DisableSetting() @@ -147,27 +93,10 @@ public static int DisableSetting() internal static int DisableSetting(IReadOnlyList args) { - var arguments = args.ToList(); - var basePos = arguments.IndexOf(DISABLE_SETTING); - if (basePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater --export-settings was not found - - if (basePos + 1 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The file parameter does not exist (export settings requires "--export-settings file") - - var setting = arguments[basePos + 1].Trim('"').Trim('\''); - if (!Enum.TryParse(setting, out Settings.K validKey)) - return (int)HRESULT.STATUS_UNKNOWN__SETTINGS_KEY; - try - { - Settings.Set(validKey, false); - } - catch (Exception ex) - { - return ex.HResult; - } - - return (int)HRESULT.SUCCESS; + return SharedPreUiCommandDispatcher.DisableSetting( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int SetSettingsValue() @@ -177,29 +106,10 @@ public static int SetSettingsValue() internal static int SetSettingsValue(IReadOnlyList args) { - var arguments = args.ToList(); - var basePos = arguments.IndexOf(SET_SETTING_VAL); - if (basePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater --export-settings was not found - - if (basePos + 2 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The file parameter does not exist (export settings requires "--export-settings file") - - var setting = arguments[basePos + 1].Trim('"').Trim('\''); - var value = arguments[basePos + 2]; - if (!Enum.TryParse(setting, out Settings.K validKey)) - return (int)HRESULT.STATUS_UNKNOWN__SETTINGS_KEY; - - try - { - Settings.SetValue(validKey, value); - } - catch (Exception ex) - { - return ex.HResult; - } - - return (int)HRESULT.SUCCESS; + return SharedPreUiCommandDispatcher.SetSettingValue( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int WingetUIToUniGetUIMigrator() @@ -286,30 +196,10 @@ public static int EnableSecureSetting() internal static int EnableSecureSetting(IReadOnlyList args) { - var arguments = args.ToList(); - var basePos = arguments.IndexOf(ENABLE_SECURE_SETTING); - if (basePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater was not found - - if (basePos + 1 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The file parameter does not exist (export settings requires "--export-settings file") - - var setting = arguments[basePos + 1].Trim('"').Trim('\''); - if (!Enum.TryParse(setting, out SecureSettings.K validKey)) - return (int)HRESULT.STATUS_UNKNOWN__SETTINGS_KEY; - - try - { - bool success = SecureSettings.TrySet(validKey, true).GetAwaiter().GetResult(); - if (!success) - return (int)HRESULT.STATUS_FAILED; - else - return (int)HRESULT.SUCCESS; - } - catch (Exception ex) - { - return ex.HResult; - } + return SharedPreUiCommandDispatcher.EnableSecureSetting( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int DisableSecureSetting() @@ -319,30 +209,10 @@ public static int DisableSecureSetting() internal static int DisableSecureSetting(IReadOnlyList args) { - var arguments = args.ToList(); - var basePos = arguments.IndexOf(DISABLE_SECURE_SETTING); - if (basePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater was not found - - if (basePos + 1 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The first positional argument does not exist - - var setting = arguments[basePos + 1].Trim('"').Trim('\''); - if (!Enum.TryParse(setting, out SecureSettings.K validKey)) - return (int)HRESULT.STATUS_UNKNOWN__SETTINGS_KEY; - - try - { - bool success = SecureSettings.TrySet(validKey, false).GetAwaiter().GetResult(); - if (!success) - return (int)HRESULT.STATUS_FAILED; - else - return (int)HRESULT.SUCCESS; - } - catch (Exception ex) - { - return ex.HResult; - } + return SharedPreUiCommandDispatcher.DisableSecureSetting( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int EnableSecureSettingForUser() @@ -352,25 +222,10 @@ public static int EnableSecureSettingForUser() internal static int EnableSecureSettingForUser(IReadOnlyList args) { - var arguments = args.ToList(); - var basePos = arguments.IndexOf(ENABLE_SECURE_SETTING_FOR_USER); - if (basePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater was not found - - if (basePos + 2 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The required parameters do not exist - - var user = arguments[basePos + 1].Trim('"').Trim('\''); - var setting = arguments[basePos + 2].Trim('"').Trim('\''); - - try - { - return SecureSettings.ApplyForUser(user, setting, true); - } - catch (Exception ex) - { - return ex.HResult; - } + return SharedPreUiCommandDispatcher.EnableSecureSettingForUser( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int DisableSecureSettingForUser() @@ -380,25 +235,10 @@ public static int DisableSecureSettingForUser() internal static int DisableSecureSettingForUser(IReadOnlyList args) { - var arguments = args.ToList(); - var basePos = arguments.IndexOf(DISABLE_SECURE_SETTING_FOR_USER); - if (basePos < 0) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The base paramater was not found - - if (basePos + 2 >= arguments.Count) - return (int)HRESULT.STATUS_INVALID_PARAMETER; // The required parameters do not exist - - var user = arguments[basePos + 1].Trim('"').Trim('\''); - var setting = arguments[basePos + 2].Trim('"').Trim('\''); - - try - { - return SecureSettings.ApplyForUser(user, setting, false); - } - catch (Exception ex) - { - return ex.HResult; - } + return SharedPreUiCommandDispatcher.DisableSecureSettingForUser( + args, + SharedPreUiCommandDispatcher.WinUiExitCodes + ); } public static int Automation() diff --git a/src/UniGetUI/EntryPoint.cs b/src/UniGetUI/EntryPoint.cs index 3df9a068c..fd8e85187 100644 --- a/src/UniGetUI/EntryPoint.cs +++ b/src/UniGetUI/EntryPoint.cs @@ -4,6 +4,7 @@ using UniGetUI.Core.Data; using UniGetUI.Core.Logging; using UniGetUI.Interface; +using UniGetUI.Shared; namespace UniGetUI { @@ -20,9 +21,9 @@ private static void Main(string[] args) WindowsConsoleHost.PrepareCliIO(); } - if (args.Contains(CLIHandler.HELP)) + if (SharedPreUiCommandDispatcher.TryHandle(args, SharedPreUiCommandDispatcher.WinUiExitCodes) is { } preUiExitCode) { - Environment.ExitCode = CLIHandler.Help(); + Environment.ExitCode = preUiExitCode; return; } else if (args.Contains(CLIHandler.MIGRATE_WINGETUI_TO_UNIGETUI)) @@ -38,51 +39,6 @@ private static void Main(string[] args) Environment.ExitCode = CLIHandler.UninstallUniGetUI(); return; } - else if (args.Contains(CLIHandler.IMPORT_SETTINGS)) - { - Environment.ExitCode = CLIHandler.ImportSettings(); - return; - } - else if (args.Contains(CLIHandler.EXPORT_SETTINGS)) - { - Environment.ExitCode = CLIHandler.ExportSettings(); - return; - } - else if (args.Contains(CLIHandler.ENABLE_SETTING)) - { - Environment.ExitCode = CLIHandler.EnableSetting(); - return; - } - else if (args.Contains(CLIHandler.DISABLE_SETTING)) - { - Environment.ExitCode = CLIHandler.DisableSetting(); - return; - } - else if (args.Contains(CLIHandler.SET_SETTING_VAL)) - { - Environment.ExitCode = CLIHandler.SetSettingsValue(); - return; - } - else if (args.Contains(CLIHandler.ENABLE_SECURE_SETTING)) - { - Environment.ExitCode = CLIHandler.EnableSecureSetting(); - return; - } - else if (args.Contains(CLIHandler.DISABLE_SECURE_SETTING)) - { - Environment.ExitCode = CLIHandler.DisableSecureSetting(); - return; - } - else if (args.Contains(CLIHandler.ENABLE_SECURE_SETTING_FOR_USER)) - { - Environment.ExitCode = CLIHandler.EnableSecureSettingForUser(); - return; - } - else if (args.Contains(CLIHandler.DISABLE_SECURE_SETTING_FOR_USER)) - { - Environment.ExitCode = CLIHandler.DisableSecureSettingForUser(); - return; - } else if (IpcCliSyntax.IsIpcCommand(args)) { Environment.ExitCode = CLIHandler.Automation(args); diff --git a/src/UniGetUI/MainWindow.xaml.cs b/src/UniGetUI/MainWindow.xaml.cs index 0f3434714..d6f83843a 100644 --- a/src/UniGetUI/MainWindow.xaml.cs +++ b/src/UniGetUI/MainWindow.xaml.cs @@ -253,6 +253,11 @@ public void HandleClosingEvent(AppWindow sender, AppWindowClosingEventArgs args) { AutoUpdater.ReleaseLockForAutoupdate_Window = true; _ = SaveGeometry(Force: true); + if (MainApp.Instance.IsQuitting) + { + return; + } + if ( !Settings.Get(Settings.K.DisableSystemTray) || AutoUpdater.UpdateReadyToBeInstalled diff --git a/src/UniGetUI/ModernAppLauncher.cs b/src/UniGetUI/ModernAppLauncher.cs index 48dbb73c1..68cd98274 100644 --- a/src/UniGetUI/ModernAppLauncher.cs +++ b/src/UniGetUI/ModernAppLauncher.cs @@ -58,12 +58,12 @@ internal static ProcessStartInfo CreateStartInfo(string executablePath, IEnumera private static IEnumerable GetModernExecutableCandidates(string baseDirectory) { + yield return Path.Combine(baseDirectory, ModernAppExecutableName); yield return Path.Combine( baseDirectory, ModernAppDirectoryName, ModernAppExecutableName ); - yield return Path.Combine(baseDirectory, ModernAppExecutableName); foreach (string candidate in GetDevelopmentBuildCandidates(baseDirectory)) yield return candidate; diff --git a/src/UniGetUI/UniGetUI.csproj b/src/UniGetUI/UniGetUI.csproj index 9397f4a2c..e558c3e58 100644 --- a/src/UniGetUI/UniGetUI.csproj +++ b/src/UniGetUI/UniGetUI.csproj @@ -48,6 +48,18 @@ $(PingetCliPublishDir)pinget.exe + + true + false + $(MSBuildThisFileDirectory)..\UniGetUI.Avalonia\UniGetUI.Avalonia.csproj + $(RuntimeIdentifier) + win-arm64 + win-x64 + $(MSBuildProjectDirectory)\obj\$(Platform)\$(Configuration)\BundledAvalonia\$(AvaloniaRuntimeIdentifier)\ + $(AvaloniaPublishDir)UniGetUI.Avalonia.exe + $(MSBuildProjectDirectory)\..\..\scripts\merge-publish-output.ps1 + + $(IntermediateOutputPath)\Generated Files\Secrets.Generated.cs @@ -93,7 +105,39 @@ SourceFiles="$(PingetCliExecutablePath)" DestinationFolder="$(PublishDir)" SkipUnchangedFiles="true" - Condition="Exists('$(PingetCliExecutablePath)') and '$(PublishDir)' != '' and Exists('$(PublishDir)')" + Condition="Exists('$(PingetCliExecutablePath)') and '$(PublishDir)' != ''" + /> + + + + + + + + + @@ -180,6 +224,7 @@ + @@ -424,6 +469,7 @@ +