Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/Shared/StartupBundleArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using UniGetUI.Interface;

namespace UniGetUI.Shared;

internal static class StartupBundleArguments
{
private static readonly string[] BundleExtensions = [".ubundle", ".json", ".yaml", ".xml"];

private static readonly string[] ValueTakingArguments =
[
IpcTransportOptions.TransportArgument,
IpcTransportOptions.TcpPortArgument,
IpcTransportOptions.NamedPipeArgument,
IpcTransportOptions.CliTransportArgument,
IpcTransportOptions.CliTcpPortArgument,
IpcTransportOptions.CliNamedPipeArgument,
];

public static List<string> Resolve(IReadOnlyList<string> args, string workingDirectory)
{
List<string> bundles = [];
foreach ((_, string path) in Enumerate(args, workingDirectory))
{
bundles.Add(path);
}

return bundles;
}

public static string[] Normalize(IReadOnlyList<string> args, string workingDirectory)
{
string[] normalized = [.. args];
foreach ((int index, string path) in Enumerate(args, workingDirectory))
{
normalized[index] = path;
}

return normalized;
}

private static IEnumerable<(int Index, string Path)> Enumerate(
IReadOnlyList<string> args,
string workingDirectory
)
{
for (int i = 0; i < args.Count; i++)
{
string arg = Unquote(args[i]);

if (arg.StartsWith('-'))
{
if (ValueTakingArguments.Contains(arg, StringComparer.Ordinal))
{
i++;
}

continue;
}

if (TryResolveBundleFile(arg, workingDirectory, out string path))
{
yield return (i, path);
}
}
}

private static bool TryResolveBundleFile(string arg, string workingDirectory, out string fullPath)
{
fullPath = string.Empty;

if (arg.Length == 0)
{
return false;
}

try
{
if (!BundleExtensions.Contains(Path.GetExtension(arg), StringComparer.OrdinalIgnoreCase))
{
return false;
}

string candidate = Path.GetFullPath(arg, workingDirectory);
if (!File.Exists(candidate))
{
return false;
}

fullPath = candidate;
return true;
}
catch (Exception)
{
return false;
}
}

private static string Unquote(string arg)
{
if (arg.Length > 1 && (arg[0] == '"' || arg[0] == '\'') && arg[^1] == arg[0])
{
return arg[1..^1];
}

return arg;
}
}
19 changes: 17 additions & 2 deletions src/UniGetUI.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ void CloseSplashOnce(object? s, EventArgs e)
if (!CoreData.WasDaemon)
mainWindow.Show();

_ = StartupAsync(mainWindow);
_ = StartupAsync(mainWindow, desktop.Args ?? []);
}

private static async Task StartupAsync(MainWindow mainWindow)
private static async Task StartupAsync(MainWindow mainWindow, string[] args)
{
// Show crash report from the previous session and wait for the user
// to dismiss it before continuing with normal startup.
Expand All @@ -188,6 +188,15 @@ private static async Task StartupAsync(MainWindow mainWindow)
}

await AvaloniaBootstrapper.InitializeAsync();

if (CoreData.WasDaemon)
{
StartupArgumentProcessor.WarnIfBundlesIgnored(
args, $"the launch requested {AvaloniaCliHandler.DAEMON}");
return;
}

await StartupArgumentProcessor.ProcessAsync(args);
}

private static void HandleSecondaryInstanceArgs(MainWindow mainWindow, string[] args)
Expand All @@ -211,9 +220,15 @@ private static void HandleSecondaryInstanceArgs(MainWindow mainWindow, string[]
}

if (isDaemonLaunch)
{
StartupArgumentProcessor.WarnIfBundlesIgnored(
args, $"the launch requested {AvaloniaCliHandler.DAEMON}");
return;
}

mainWindow.ShowFromTray();

_ = StartupArgumentProcessor.ProcessAsync(args);
}

public static void ApplyTheme(string value)
Expand Down
43 changes: 40 additions & 3 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using UniGetUI.Core.Logging;
using UniGetUI.Core.Tools;
using UniGetUI.Interface;
using UniGetUI.Shared;

namespace UniGetUI.Avalonia.Infrastructure;

Expand All @@ -18,7 +19,41 @@ public static class AvaloniaAppHost
private static Mutex? _singleInstanceMutex;
private static FileStream? _singleInstanceLock;

public static event Action<string[]>? SecondaryInstanceArgsReceived;
private static Action<string[]>? _secondaryInstanceArgsReceived;
private static readonly Queue<string[]> _bufferedSecondaryInstanceArgs = new();

public static event Action<string[]>? SecondaryInstanceArgsReceived
{
add
{
_secondaryInstanceArgsReceived += value;

if (value is null || _bufferedSecondaryInstanceArgs.Count == 0)
return;

string[][] buffered = [.. _bufferedSecondaryInstanceArgs];
_bufferedSecondaryInstanceArgs.Clear();

Dispatcher.UIThread.Post(() =>
{
foreach (string[] bufferedArgs in buffered)
value(bufferedArgs);
});
}
remove => _secondaryInstanceArgsReceived -= value;
}

private static void RaiseSecondaryInstanceArgs(string[] args)
{
if (_secondaryInstanceArgsReceived is null)
{
Logger.Info("Buffering forwarded arguments until the main window is ready");
_bufferedSecondaryInstanceArgs.Enqueue(args);
return;
}

_secondaryInstanceArgsReceived(args);
}

public static void Run(string[] args)
{
Expand Down Expand Up @@ -80,6 +115,8 @@ Welcome to UniGetUI Version {CoreData.VersionName}
// would otherwise bind it to the worker thread and make Win32Platform.Initialize throw.
_ = Dispatcher.UIThread;

args = StartupBundleArguments.Normalize(args, Environment.CurrentDirectory);

if (!TryRegisterSingleInstance(args))
{
return;
Expand Down Expand Up @@ -139,7 +176,7 @@ private static bool TryRegisterWithMutex(string[] args)

if (createdNew)
{
SingleInstanceRedirector.StartListener(a => SecondaryInstanceArgsReceived?.Invoke(a));
SingleInstanceRedirector.StartListener(RaiseSecondaryInstanceArgs);
return true;
}

Expand Down Expand Up @@ -173,7 +210,7 @@ private static bool TryRegisterWithFileLock(string[] args)
return true;
}

SingleInstanceRedirector.StartListener(a => SecondaryInstanceArgsReceived?.Invoke(a));
SingleInstanceRedirector.StartListener(RaiseSecondaryInstanceArgs);
return true;
}
}
24 changes: 19 additions & 5 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ namespace UniGetUI.Avalonia.Infrastructure;
internal static class AvaloniaBootstrapper
{
private static bool _hasStarted;
private static readonly TaskCompletionSource<bool> _initialized =
new(TaskCreationOptions.RunContinuationsAsynchronously);

public static Task<bool> Initialized => _initialized.Task;

// Coalesces broker-unavailable notifications: during bulk operations every failed
// package raises the event, but only one dialog should be visible at a time.
Expand All @@ -38,12 +42,22 @@ public static async Task InitializeAsync()
_hasStarted = true;
Logger.Info("Starting Avalonia shell bootstrap");

await Task.WhenAll(
InitializeSharedServicesAsync(),
InitializePackageEngineAsync()
);
try
{
await Task.WhenAll(
InitializeSharedServicesAsync(),
InitializePackageEngineAsync()
);

await RunPostLoadChecksAsync();
}
catch (Exception)
{
_initialized.TrySetResult(false);
throw;
}

await RunPostLoadChecksAsync();
_initialized.TrySetResult(true);

Logger.Info("Avalonia shell bootstrap completed");
}
Expand Down
69 changes: 69 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/StartupArgumentProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using UniGetUI.Avalonia.ViewModels;
using UniGetUI.Avalonia.Views;
using UniGetUI.Core.Logging;
using UniGetUI.Shared;

namespace UniGetUI.Avalonia.Infrastructure;

internal static class StartupArgumentProcessor
{
public static void WarnIfBundlesIgnored(IReadOnlyList<string> args, string reason)
{
if (args.Count == 0)
return;

List<string> bundles = StartupBundleArguments.Resolve(args, Environment.CurrentDirectory);
foreach (string bundle in bundles)
{
Logger.Warn($"Ignoring the package bundle {bundle}: {reason}");
}
}

public static async Task ProcessAsync(IReadOnlyList<string> args)
{
if (args.Count == 0)
{
return;
}

List<string> bundles = StartupBundleArguments.Resolve(args, Environment.CurrentDirectory);
if (bundles.Count == 0)
{
return;
}

if (bundles.Count > 1)
{
Logger.Warn(
$"{bundles.Count} package bundles were passed on the command line; only {bundles[0]} will be loaded");
}

if (!await AvaloniaBootstrapper.Initialized)
{
Logger.Warn(
$"Could not load the package bundle {bundles[0]}: UniGetUI failed to finish initializing");
return;
}

if (MainWindow.Instance is not { } window)
{
Logger.Warn($"Could not load the package bundle {bundles[0]}: the main window is not available");
return;
}

if (window.DataContext is not MainWindowViewModel viewModel)
{
Logger.Warn($"Could not load the package bundle {bundles[0]}: the main window has no view model");
return;
}

if (!window.IsVisible)
{
window.ShowFromTray();
}

Logger.ImportantInfo($"Loading the package bundle {bundles[0]} requested on the command line");
await viewModel.LoadBundleFromFileAsync(bundles[0]);
}
}
1 change: 1 addition & 0 deletions src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@
<ItemGroup>
<Compile Include="..\SharedAssemblyInfo.cs" Link="SharedAssemblyInfo.cs" />
<Compile Include="..\Shared\SharedPreUiCommandDispatcher.cs" Link="Shared\SharedPreUiCommandDispatcher.cs" />
<Compile Include="..\Shared\StartupBundleArguments.cs" Link="Shared\StartupBundleArguments.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,16 @@
private void ToggleOperationsPanel() => OperationsPanelExpanded = !OperationsPanelExpanded;

[RelayCommand]
private void RetryFailedOperations() => AvaloniaOperationRegistry.RetryFailed();

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (Avalonia)

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (NativeAOT)

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 163 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void ClearSuccessfulOperations() => AvaloniaOperationRegistry.ClearSuccessful();

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (Avalonia)

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (NativeAOT)

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 166 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void ClearFinishedOperations() => AvaloniaOperationRegistry.ClearFinished();

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (Avalonia)

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (NativeAOT)

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 169 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void CancelAllOperations() => AvaloniaOperationRegistry.CancelAll();

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (Avalonia)

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Linux (NativeAOT)

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 172 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

// ─── Sidebar ─────────────────────────────────────────────────────────────
public SidebarViewModel Sidebar { get; } = new();
Expand Down Expand Up @@ -758,6 +758,12 @@
await BundlesPage.OpenFromString(content, BundleFormatType.UBUNDLE, "GitHub Gist");
}

public async Task LoadBundleFromFileAsync(string path)
{
NavigateTo(PageType.Bundles);
await BundlesPage.OpenFromFile(path);
}

private async Task ShowAboutDialog()
{
Sidebar.SelectNavButtonForPage(PageType.Null);
Expand Down
Loading
Loading