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
26 changes: 25 additions & 1 deletion docs/IPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,17 @@ These keys are used by package-related endpoints such as install, update, uninst
| Method | Path | Auth | Parameters/body | CLI equivalent | Notes |
| --- | --- | --- | --- | --- | --- |
| `GET` | `/uniget/v1/status` | No | None | `status`, `version` | Returns `running`, `transport`, `tcpPort`, `namedPipeName`, `namedPipePath`, `baseAddress`, `version`, and `buildNumber`. |
| `GET` | `/uniget/v1/app` | Yes | None | `app status` | Returns app/headless/window state. |
| `GET` | `/uniget/v1/app` | Yes | None | `app status` | Returns app/headless/window state, plus `systemRestartPending` and `systemRestartPendingPackages` (see [Pending machine restarts](#pending-machine-restarts)). |
| `POST` | `/uniget/v1/app/show` | Yes | None | `app show` | UI-only in practice. |
| `POST` | `/uniget/v1/app/navigate` | Yes | Query: `page`, optional `manager`, optional `helpAttachment` | `app navigate` | UI-only in practice. |
| `POST` | `/uniget/v1/app/quit` | Yes | None | `app quit` | Shuts down the selected session. |

### Operations

Every operation payload carries `systemRestartRequired`: `true` when the operation succeeded but
the machine must be restarted before the change takes effect. See
[Pending machine restarts](#pending-machine-restarts).

| Method | Path | Auth | Parameters/body | CLI equivalent |
| --- | --- | --- | --- | --- |
| `GET` | `/uniget/v1/operations` | Yes | None | `operation list` |
Expand Down Expand Up @@ -380,6 +384,10 @@ them first.

### Packages

Every package payload carries `systemRestartPending`: `true` when that package is one of the
packages this machine is waiting on a restart for. See
[Pending machine restarts](#pending-machine-restarts).

| Method | Path | Auth | Parameters/body | CLI equivalent | Notes |
| --- | --- | --- | --- | --- | --- |
| `GET` | `/uniget/v1/packages/search` | Yes | Query `query`, optional `manager`, optional `maxResults` | `package search` | Search endpoint. |
Expand All @@ -400,6 +408,22 @@ them first.
| `POST` | `/uniget/v1/packages/update-all` | Yes | None | `package update-all` | Requires `OnUpgradeAll` handler to be wired. |
| `POST` | `/uniget/v1/packages/update-manager` | Yes | Query `manager` | `package update-manager` | Requires `OnUpgradeAllForManager` handler to be wired. |

## Pending machine restarts

Some installers finish successfully but only take effect after the machine reboots (WinGet
`0x8A150109`/`0x8A15010B`, Chocolatey `3010`/`1641`). UniGetUI records those packages and keeps the
record until the machine has actually rebooted — closing and reopening UniGetUI does not clear it.

These fields expose it, so a fleet tool can tell which machines are waiting on a restart and why:

| Payload | Field | Meaning |
| --- | --- | --- |
| `GET /uniget/v1/app` | `systemRestartPending` | The machine is waiting on a restart. |
| `GET /uniget/v1/app` | `systemRestartPendingPackages` | How many packages are waiting on it. |
| Operation payloads | `systemRestartRequired` | This operation succeeded but needs a restart. |
| Operation-history entries | `systemRestartRequired` | Same, for a finished operation. |
| Package payloads | `systemRestartPending` | This package is waiting on a restart. |

## Headless-specific limitations

In headless sessions:
Expand Down
17 changes: 16 additions & 1 deletion src/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1728,5 +1728,20 @@
"Runs elevated": "Runs elevated",
"Processes terminated before the operation": "Processes terminated before the operation",
"Unknown package source": "Unknown package source",
"Non-default package source": "Non-default package source"
"Non-default package source": "Non-default package source",
"This operation finished, but the computer must be restarted before the changes take effect": "This operation finished, but the computer must be restarted before the changes take effect",
Comment thread
GabrielDuf marked this conversation as resolved.
"Restart computer": "Restart computer",
"Restart computer (1 package pending)": "Restart computer (1 package pending)",
"Restart computer ({0} packages pending)": "Restart computer ({0} packages pending)",
"1 package is waiting for your computer to restart": "1 package is waiting for your computer to restart",
"{0} packages are waiting for your computer to restart": "{0} packages are waiting for your computer to restart",
"UniGetUI and your computer both need to be restarted": "UniGetUI and your computer both need to be restarted",
"Restart your computer?": "Restart your computer?",
"Restart now": "Restart now",
"Not now": "Not now",
"1 package cannot finish until your computer restarts.": "1 package cannot finish until your computer restarts.",
"{0} packages cannot finish until your computer restarts.": "{0} packages cannot finish until your computer restarts.",
"Your computer will restart now. Save your work and close any open applications first.": "Your computer will restart now. Save your work and close any open applications first.",
"The computer could not be restarted": "The computer could not be restarted",
"UniGetUI could not start the restart. Please restart your computer manually.": "UniGetUI could not start the restart. Please restart your computer manually."
}
3 changes: 3 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using UniGetUI.PackageEngine.Enums;
using UniGetUI.PackageEngine.Interfaces;
using UniGetUI.PackageEngine.Operations;
using UniGetUI.PackageEngine.Operations.Reboot;
using UniGetUI.PackageOperations;

namespace UniGetUI.Avalonia.Infrastructure;
Expand Down Expand Up @@ -255,6 +256,8 @@ private static IpcAppInfo GetAppInfo()
CanQuit = true,
CurrentPage = window is null ? "" : IpcAppPages.ToPageName(window.CurrentPage.ToString()),
SupportedPages = IpcAppPages.SupportedPages,
SystemRestartPending = PendingRebootStore.HasPending,
SystemRestartPendingPackages = PendingRebootStore.PendingCount,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static class AvaloniaOperationRegistry
private static readonly ConcurrentDictionary<AbstractOperation, int> _errorCounts = new();
private static int _errorsOccurred;
public static int ErrorsOccurred => _errorsOccurred;
public static bool RestartRequired { get; set; }
public static bool AppRestartRequired { get; set; }

private static bool _shortcutDialogOpen;

Expand Down Expand Up @@ -229,13 +229,8 @@ private static void ShowOperationSuccessNotification(AbstractOperation op)
if (Settings.AreSuccessNotificationsDisabled())
return;

string title = op.Metadata.SuccessTitle.Length > 0
? op.Metadata.SuccessTitle
: CoreTools.Translate("Success!");

string message = op.Metadata.SuccessMessage.Length > 0
? op.Metadata.SuccessMessage
: CoreTools.Translate("Success!");
string title = OperationNotificationText.SuccessTitle(op);
string message = OperationNotificationText.SuccessMessage(op);

AccessibilityAnnouncementService.Announce(
$"{title}. {message}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,8 @@ public static bool ShowSuccess(AbstractOperation operation)
if (Settings.AreSuccessNotificationsDisabled()) return false;
try
{
string title = operation.Metadata.SuccessTitle.Length > 0
? operation.Metadata.SuccessTitle
: CoreTools.Translate("Success!");
string message = operation.Metadata.SuccessMessage.Length > 0
? operation.Metadata.SuccessMessage
: CoreTools.Translate("Success!");
string title = OperationNotificationText.SuccessTitle(operation);
string message = OperationNotificationText.SuccessMessage(operation);
DeliverNotification(title, message, MainWindow.RuntimeNotificationLevel.Success, allowInAppFallback: false);
return true;
}
Expand Down
30 changes: 30 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/OperationNotificationText.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using UniGetUI.Core.Tools;
using UniGetUI.PackageOperations;

namespace UniGetUI.Avalonia.Infrastructure;

internal static class OperationNotificationText
{
public static string SuccessTitle(AbstractOperation operation)
=> operation.Metadata.SuccessTitle.Length > 0
? operation.Metadata.SuccessTitle
: CoreTools.Translate("Success!");

public static string SuccessMessage(AbstractOperation operation)
{
string message = operation.Metadata.SuccessMessage.Length > 0
? operation.Metadata.SuccessMessage
: CoreTools.Translate("Success!");

if (!operation.SystemRestartRequired) return message;

message = message.TrimEnd();
if (message.Length > 0 && !".!?:;。!?".Contains(message[^1]))
message += ".";

return message
+ " "
+ CoreTools.Translate(
"This operation finished, but the computer must be restarted before the changes take effect");
}
}
146 changes: 146 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/SystemRestartService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using UniGetUI.Avalonia.Views.DialogPages;
using UniGetUI.Core.Logging;
using UniGetUI.Core.Tools;
using UniGetUI.PackageEngine.Operations.Reboot;

namespace UniGetUI.Avalonia.Infrastructure;

internal static class SystemRestartService
{
private const int ShutdownRequestTimeoutMs = 10000;

public static async Task ConfirmAndRestartAsync(Window owner)
{
try
{
if (!await ConfirmAsync(owner)) return;

if (!await RestartAsync()) ReportRestartFailure();
}
catch (Exception ex)
{
Logger.Error("Failed to prompt for a system restart:");
Logger.Error(ex);
}
}

private static async Task<bool> ConfirmAsync(Window owner)
{
var pending = PendingRebootStore.GetPending();
if (pending.Count == 0) return false;

var body = new StackPanel { Spacing = 12 };
body.Children.Add(new TextBlock
{
Text = pending.Count == 1
? CoreTools.Translate("1 package cannot finish until your computer restarts.")
: CoreTools.Translate(
"{0} packages cannot finish until your computer restarts.",
pending.Count),
Opacity = 0.82,
TextWrapping = TextWrapping.Wrap,
});

body.Children.Add(new ScrollViewer
{
MaxHeight = 180,
Content = new TextBlock
{
Text = string.Join(
Environment.NewLine,
pending
.Select(entry => entry.PackageName.Length > 0 ? entry.PackageName : entry.PackageId)
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.Select(name => "* " + name)),
FontFamily = new FontFamily("Consolas"),
TextWrapping = TextWrapping.Wrap,
},
});

body.Children.Add(new TextBlock
{
Text = CoreTools.Translate(
"Your computer will restart now. Save your work and close any open applications first."),
TextWrapping = TextWrapping.Wrap,
});

var dialog = new ImmersiveConfirmationDialog(
CoreTools.Translate("Restart your computer?"),
body,
CoreTools.Translate("Restart now"),
CoreTools.Translate("Not now"))
{
MaxWidth = 560,
MaxHeight = 420,
FocusPrimaryButton = false,
};

await dialog.ShowDialog(owner);
return dialog.Result is true;
}

private static async Task<bool> RestartAsync()
{
if (!OperatingSystem.IsWindows())
{
Logger.Warn("A system restart was requested on a platform where UniGetUI cannot trigger one");
return false;
}

try
{
using var process = Process.Start(new ProcessStartInfo
{
FileName = Path.Combine(Environment.SystemDirectory, "shutdown.exe"),
Arguments = "/r /t 0",
UseShellExecute = false,
CreateNoWindow = true,
});

if (process is null)
{
Logger.Error("Could not start shutdown.exe to request a system restart");
return false;
}

using var timeout = new CancellationTokenSource(ShutdownRequestTimeoutMs);
try
{
await process.WaitForExitAsync(timeout.Token);
}
catch (OperationCanceledException)
{
Logger.Warn("shutdown.exe did not return in time; assuming the restart was requested");
return true;
}

if (process.ExitCode != 0)
{
Logger.Error($"shutdown.exe refused the restart request with exit code {process.ExitCode}");
return false;
}

return true;
}
catch (Exception ex)
{
Logger.Error("Failed to request a system restart:");
Logger.Error(ex);
return false;
}
}

private static void ReportRestartFailure()
{
Dispatcher.UIThread.Post(() =>
Views.MainWindow.Instance?.ShowRuntimeNotification(
CoreTools.Translate("The computer could not be restarted"),
CoreTools.Translate(
"UniGetUI could not start the restart. Please restart your computer manually."),
Views.MainWindow.RuntimeNotificationLevel.Error));
}
}
Loading
Loading