mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-09-01 19:51:34 +02:00
[CmdPal] Add "Update and restart" / "Update and shut down" system com… (#49437)
## Summary of the Pull Request Adds **Update and restart** and **Update and shut down** to the Windows System Commands extension, matching what Windows shows in the Start menu power flyout when updates are waiting for a reboot. Both only show up while Windows Update is actually waiting on a restart. When nothing is pending, the command list is exactly what it is today. ## PR Checklist - [x] Closes: #48849 - [x] **Communication:** commented on the issue before starting; zadjii-msft had greenlit the idea as long as the commands actually do something rather than just report status - [x] **Tests:** added and passing (27/27) - [ ] **Localization:** 6 new resx strings, each with a translator comment - [ ] **Dev docs:** n/a - [ ] **New binaries:** n/a ## Detailed Description of the Pull Request / Additional comments **Detecting the pending update.** `WindowsUpdateHelper.IsUpdatePending()` reads `ISystemInformation::RebootRequired` from WUAPI, which is the same signal the Start menu uses, so the commands appear exactly when Windows would offer them itself. A few notes on that file, since the interop is a bit unusual: - I used `[GeneratedComInterface]` rather than `ComImport` to keep it AOT-compatible. - `ISystemInformation` is a dual interface, so its first four vtable slots belong to `IDispatch`. They're declared as placeholder methods that are never called, and the two real members follow in vtable order. - The result is cached for 5 seconds. `GetItems()` runs on every keystroke and would otherwise create a COM object each time — same reasoning as the existing network info cache in this extension. - If anything goes wrong (COM creation fails, an exception is thrown) it falls back to "no update pending", so the commands stay hidden and the extension behaves exactly as it does now. The failure is logged through `ExtensionHost.LogMessage`. **Running the command.** `InitiateShutdown` with `SHUTDOWN_INSTALL_UPDATES` plus either `SHUTDOWN_RESTART` (0x44) or `SHUTDOWN_POWEROFF` (0x48). That first flag is what makes this "update and restart" instead of a plain restart. `SeShutdownPrivilege` is disabled by default on the process token, so it gets enabled first. **Wiring.** Both items use the existing `ExecuteCommandConfirmation` flow, so they respect the "confirm system commands" setting like the other commands here. They're registered on the System Commands page and the top-level search fallback, with stable ids (`...system.update_restart`, `...system.update_shutdown`). ## One question for reviewers `ShowDialogToConfirmCommand` defaults to `false`, so out of the box these run immediately when you press Enter, the same as the existing Shutdown and Restart commands. I kept them consistent rather than special-casing them, but I hit this myself while testing — I pressed Enter and my machine started updating and rebooting straight away, which was a bit of a surprise. Happy to force a confirmation for these two regardless of the setting if you'd prefer that. ## Validation Steps Performed 27/27 unit tests pass. The 5 new test methods cover the commands being present/absent in both states, query matching, stable ids, the 0x44 / 0x48 flag values, and that the real WUAPI call doesn't throw. I also tested it on a machine with a genuine pending update, confirmed via WUAPI `RebootRequired` and the Windows Update and CBS registry keys: 1. **Before** — the installed 0.11 build, same machine, same pending update: no update commands. 2. **After** — this build: both commands show up, in the same situation the Start menu offers them. 3. **Search** — typing `update` matches both, which is the discoverability gap the issue is about. 4. **Actually ran it** — pressing Enter on "Update and restart" installed the pending update (KB5121767) and restarted the machine. After it came back up, `RebootRequired` was false and the two commands were correctly gone from the list. ### Screenshots **1. Before** <img width="785" height="473" alt="Screenshot 2026-07-21 214038" src="https://github.com/user-attachments/assets/27b12a4c-f636-4815-b8e2-fc918282959b" /> **After** <img width="762" height="445" alt="Screenshot 2026-07-21 215452" src="https://github.com/user-attachments/assets/80e1936d-af67-48ab-91b4-59b94fb56827" /> <img width="762" height="149" alt="pr48849-search-update" src="https://github.com/user-attachments/assets/53b7f065-4b7c-4317-9e66-e621eed44e61" /> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b3fb20d-6e9d-4fef-a5cd-f8921d28c220
This commit is contained in:
committed by
Boliang Zhang (from Dev Box)
parent
d46c64ddf4
commit
deaf6dce03
@@ -4,6 +4,8 @@
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.System.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.System.UnitTests;
|
||||
@@ -38,6 +40,72 @@ public class BasicTests
|
||||
Assert.IsTrue(Enum.IsDefined(typeof(FirmwareType), firmwareType));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateShutdownFlagsTest()
|
||||
{
|
||||
// SHUTDOWN_INSTALL_UPDATES | SHUTDOWN_RESTART
|
||||
Assert.AreEqual(0x44u, WindowsUpdateHelper.GetUpdateShutdownFlags(restart: true));
|
||||
|
||||
// SHUTDOWN_INSTALL_UPDATES | SHUTDOWN_POWEROFF
|
||||
Assert.AreEqual(0x48u, WindowsUpdateHelper.GetUpdateShutdownFlags(restart: false));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdatePendingDetectionDoesNotThrowTest()
|
||||
{
|
||||
// The WUAPI query must never throw; on any failure it reports false. The actual
|
||||
// value depends on the machine's update state, but back-to-back calls within the
|
||||
// cache interval must agree (the second call takes the cached path).
|
||||
var first = WindowsUpdateHelper.IsUpdatePending();
|
||||
var second = WindowsUpdateHelper.IsUpdatePending();
|
||||
|
||||
Assert.AreEqual(first, second, "Cached query should return the same value within the cache interval.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExecuteCommandSurfacesFuncResult()
|
||||
{
|
||||
// A command built from a Func<CommandResult> must return that result rather than
|
||||
// always dismissing, so a failed action can keep the palette open.
|
||||
var command = new ExecuteCommand(() => CommandResult.ShowToast("failure"));
|
||||
|
||||
var result = command.Invoke();
|
||||
|
||||
Assert.AreEqual(CommandResultKind.ShowToast, result.Kind);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConfirmationCommandDirectPathSurfacesFailureResult()
|
||||
{
|
||||
// Confirmation off: invoking runs the action directly and must return its result
|
||||
// (here a failure toast) instead of silently dismissing.
|
||||
var command = new ExecuteCommandConfirmation("name", confirm: false, "message", () => CommandResult.ShowToast("failure"));
|
||||
|
||||
var result = command.Invoke();
|
||||
|
||||
Assert.AreEqual(CommandResultKind.ShowToast, result.Kind);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConfirmationCommandConfirmedPathSurfacesFailureResult()
|
||||
{
|
||||
// Confirmation on: Invoke returns a Confirm result, and the primary command it
|
||||
// carries must surface the same failure result when the user confirms.
|
||||
var command = new ExecuteCommandConfirmation("name", confirm: true, "message", () => CommandResult.ShowToast("failure"));
|
||||
|
||||
var confirmResult = command.Invoke();
|
||||
Assert.AreEqual(CommandResultKind.Confirm, confirmResult.Kind);
|
||||
|
||||
var args = confirmResult.Args as ConfirmationArgs;
|
||||
Assert.IsNotNull(args, "Confirm result should carry ConfirmationArgs.");
|
||||
|
||||
var primary = args.PrimaryCommand as InvokableCommand;
|
||||
Assert.IsNotNull(primary, "Confirmation should carry an invokable primary command.");
|
||||
|
||||
var innerResult = primary.Invoke();
|
||||
Assert.AreEqual(CommandResultKind.ShowToast, innerResult.Kind);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NetworkConnectionPropertiesTest()
|
||||
{
|
||||
|
||||
@@ -145,4 +145,54 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
var firstItemIsUefiCommand = firstItem?.Title.Contains("UEFI", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
Assert.AreEqual(hasCommand, firstItemIsUefiCommand, $"Expected to match (or not match) 'UEFI firmware settings' but got '{firstItem?.Title}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(true)]
|
||||
[DataRow(false)]
|
||||
public void UpdateCommandsAvailabilityTest(bool updatePending)
|
||||
{
|
||||
var settings = new Settings(updatePending: updatePending);
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
|
||||
var hasUpdateRestart = allCommands.Any(i => i.Title.Equals("Update and restart", StringComparison.Ordinal));
|
||||
var hasUpdateShutdown = allCommands.Any(i => i.Title.Equals("Update and shut down", StringComparison.Ordinal));
|
||||
|
||||
Assert.AreEqual(updatePending, hasUpdateRestart, "'Update and restart' should only be listed while updates are pending.");
|
||||
Assert.AreEqual(updatePending, hasUpdateShutdown, "'Update and shut down' should only be listed while updates are pending.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateCommandsMatchQueryTest()
|
||||
{
|
||||
var settings = new Settings(updatePending: true);
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
|
||||
var result = Query("update", allCommands);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsTrue(
|
||||
result.Any(i => i.Title.Equals("Update and restart", StringComparison.Ordinal)),
|
||||
"'update' query should match the 'Update and restart' command.");
|
||||
Assert.IsTrue(
|
||||
result.Any(i => i.Title.Equals("Update and shut down", StringComparison.Ordinal)),
|
||||
"'update' query should match the 'Update and shut down' command.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateCommandsHaveStableIdsTest()
|
||||
{
|
||||
var settings = new Settings(updatePending: true);
|
||||
var pages = new SystemCommandPage(settings);
|
||||
var allCommands = pages.GetItems();
|
||||
|
||||
var updateRestart = allCommands.FirstOrDefault(i => i.Title.Equals("Update and restart", StringComparison.Ordinal));
|
||||
var updateShutdown = allCommands.FirstOrDefault(i => i.Title.Equals("Update and shut down", StringComparison.Ordinal));
|
||||
|
||||
Assert.IsNotNull(updateRestart);
|
||||
Assert.IsNotNull(updateShutdown);
|
||||
Assert.AreEqual("com.microsoft.cmdpal.builtin.system.update_restart", updateRestart.Command?.Id);
|
||||
Assert.AreEqual("com.microsoft.cmdpal.builtin.system.update_shutdown", updateShutdown.Command?.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,16 @@ public class Settings : ISettingsInterface
|
||||
private bool showDialogToConfirmCommand;
|
||||
private bool showSuccessMessageAfterEmptyingRecycleBin;
|
||||
private FirmwareType firmwareType;
|
||||
private bool updatePending;
|
||||
|
||||
public Settings(bool hideDisconnectedNetworkInfo = false, bool hideEmptyRecycleBin = false, bool showDialogToConfirmCommand = false, bool showSuccessMessageAfterEmptyingRecycleBin = false, FirmwareType firmwareType = FirmwareType.Uefi)
|
||||
public Settings(bool hideDisconnectedNetworkInfo = false, bool hideEmptyRecycleBin = false, bool showDialogToConfirmCommand = false, bool showSuccessMessageAfterEmptyingRecycleBin = false, FirmwareType firmwareType = FirmwareType.Uefi, bool updatePending = false)
|
||||
{
|
||||
this.hideDisconnectedNetworkInfo = hideDisconnectedNetworkInfo;
|
||||
this.hideEmptyRecycleBin = hideEmptyRecycleBin;
|
||||
this.showDialogToConfirmCommand = showDialogToConfirmCommand;
|
||||
this.showSuccessMessageAfterEmptyingRecycleBin = showSuccessMessageAfterEmptyingRecycleBin;
|
||||
this.firmwareType = firmwareType;
|
||||
this.updatePending = updatePending;
|
||||
}
|
||||
|
||||
public bool HideDisconnectedNetworkInfo() => hideDisconnectedNetworkInfo;
|
||||
@@ -37,4 +39,6 @@ public class Settings : ISettingsInterface
|
||||
public bool ShowSuccessMessageAfterEmptyingRecycleBin() => showSuccessMessageAfterEmptyingRecycleBin;
|
||||
|
||||
public FirmwareType GetSystemFirmwareType() => firmwareType;
|
||||
|
||||
public bool IsUpdatePending() => updatePending;
|
||||
}
|
||||
|
||||
@@ -10,15 +10,20 @@ namespace Microsoft.CmdPal.Ext.System;
|
||||
public sealed partial class ExecuteCommand : InvokableCommand
|
||||
{
|
||||
public ExecuteCommand(Action command)
|
||||
: this(() =>
|
||||
{
|
||||
command();
|
||||
return CommandResult.Dismiss();
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
public ExecuteCommand(Func<CommandResult> command)
|
||||
{
|
||||
_command = command;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
_command();
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
public override CommandResult Invoke() => _command();
|
||||
|
||||
private Action _command;
|
||||
private Func<CommandResult> _command;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,15 @@ namespace Microsoft.CmdPal.Ext.System;
|
||||
public sealed partial class ExecuteCommandConfirmation : InvokableCommand
|
||||
{
|
||||
public ExecuteCommandConfirmation(string name, bool confirm, string confirmationMessage, Action command)
|
||||
: this(name, confirm, confirmationMessage, () =>
|
||||
{
|
||||
command();
|
||||
return CommandResult.Dismiss();
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
public ExecuteCommandConfirmation(string name, bool confirm, string confirmationMessage, Func<CommandResult> command)
|
||||
{
|
||||
Name = name;
|
||||
_command = command;
|
||||
@@ -32,11 +41,10 @@ public sealed partial class ExecuteCommandConfirmation : InvokableCommand
|
||||
return CommandResult.Confirm(confirmationArgs);
|
||||
}
|
||||
|
||||
_command();
|
||||
return CommandResult.Dismiss();
|
||||
return _command();
|
||||
}
|
||||
|
||||
private bool _confirm;
|
||||
private string _confirmationMessage;
|
||||
private Action _command;
|
||||
private Func<CommandResult> _command;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,15 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
|
||||
{
|
||||
private const string _id = "com.microsoft.cmdpal.builtin.system.fallback";
|
||||
|
||||
private readonly ISettingsInterface _settings;
|
||||
|
||||
// Whether Windows Update was waiting for a restart the last time the command list
|
||||
// was built. Unlike the other inputs this can change during a session, so we track
|
||||
// it and rebuild the list when it flips instead of capturing it once.
|
||||
private bool _isUpdatePending;
|
||||
|
||||
private List<IListItem> systemCommands;
|
||||
|
||||
public FallbackSystemCommandItem(ISettingsInterface settings)
|
||||
: base(new NoOpCommand(), Resources.Microsoft_plugin_ext_fallback_display_title, _id)
|
||||
{
|
||||
@@ -21,15 +30,20 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
|
||||
Subtitle = string.Empty;
|
||||
Icon = Icons.LockIcon;
|
||||
|
||||
var isBootedInUefiMode = settings.GetSystemFirmwareType() == FirmwareType.Uefi;
|
||||
var hideEmptyRB = settings.HideEmptyRecycleBin();
|
||||
var confirmSystemCommands = settings.ShowDialogToConfirmCommand();
|
||||
var showSuccessOnEmptyRB = settings.ShowSuccessMessageAfterEmptyingRecycleBin();
|
||||
|
||||
systemCommands = Commands.GetSystemCommands(isBootedInUefiMode, hideEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB);
|
||||
_settings = settings;
|
||||
_isUpdatePending = settings.IsUpdatePending();
|
||||
systemCommands = BuildSystemCommands(_isUpdatePending);
|
||||
}
|
||||
|
||||
private readonly List<IListItem> systemCommands;
|
||||
private List<IListItem> BuildSystemCommands(bool isUpdatePending)
|
||||
{
|
||||
var isBootedInUefiMode = _settings.GetSystemFirmwareType() == FirmwareType.Uefi;
|
||||
var hideEmptyRB = _settings.HideEmptyRecycleBin();
|
||||
var confirmSystemCommands = _settings.ShowDialogToConfirmCommand();
|
||||
var showSuccessOnEmptyRB = _settings.ShowSuccessMessageAfterEmptyingRecycleBin();
|
||||
|
||||
return Commands.GetSystemCommands(isBootedInUefiMode, isUpdatePending, hideEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB);
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
@@ -41,6 +55,17 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
|
||||
return;
|
||||
}
|
||||
|
||||
// The update-pending state can change while CmdPal is running (an update gets
|
||||
// staged, or a restart clears it). Re-check it and rebuild the list only when it
|
||||
// changes so the "Update and restart"/"shut down" items don't go stale. The
|
||||
// underlying WUAPI query is cached, so this stays cheap per keystroke.
|
||||
var isUpdatePending = _settings.IsUpdatePending();
|
||||
if (isUpdatePending != _isUpdatePending)
|
||||
{
|
||||
_isUpdatePending = isUpdatePending;
|
||||
systemCommands = BuildSystemCommands(isUpdatePending);
|
||||
}
|
||||
|
||||
IListItem? result = null;
|
||||
var resultScore = 0;
|
||||
|
||||
|
||||
@@ -29,15 +29,30 @@ internal static class Commands
|
||||
private static List<NetworkConnectionProperties> networkPropertiesCache = new List<NetworkConnectionProperties>();
|
||||
private static DateTime timeOfLastNetworkQuery;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the "update and restart"/"update and shut down" action and turns the outcome
|
||||
/// into a <see cref="CommandResult"/>. When the shutdown can't be started we keep the
|
||||
/// palette open and show a localized error instead of silently dismissing.
|
||||
/// </summary>
|
||||
private static CommandResult UpdateShutdownResult(bool restart)
|
||||
=> WindowsUpdateHelper.InitiateUpdateShutdown(restart)
|
||||
? CommandResult.Dismiss()
|
||||
: CommandResult.ShowToast(new ToastArgs
|
||||
{
|
||||
Message = Resources.Microsoft_plugin_sys_update_failed,
|
||||
Result = CommandResult.KeepOpen(),
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list with all system command results
|
||||
/// </summary>
|
||||
/// <param name="isUefi">Value indicating if the system is booted in uefi mode</param>
|
||||
/// <param name="isUpdatePending">Value indicating if Windows Update is waiting for a restart to install updates.</param>
|
||||
/// <param name="hideEmptyRecycleBin">Value indicating if we should hide the Empty Recycle Bin command.</param>
|
||||
/// <param name="confirmCommands">A value indicating if the user should confirm the system commands</param>
|
||||
/// <param name="emptyRBSuccessMessage">Show a success message after empty Recycle Bin.</param>
|
||||
/// <returns>A list of all results</returns>
|
||||
public static List<IListItem> GetSystemCommands(bool isUefi, bool hideEmptyRecycleBin, bool confirmCommands, bool emptyRBSuccessMessage)
|
||||
public static List<IListItem> GetSystemCommands(bool isUefi, bool isUpdatePending, bool hideEmptyRecycleBin, bool confirmCommands, bool emptyRBSuccessMessage)
|
||||
{
|
||||
var results = new List<IListItem>();
|
||||
results.AddRange(new[]
|
||||
@@ -60,6 +75,39 @@ internal static class Commands
|
||||
Title = Resources.Microsoft_plugin_sys_restart_computer,
|
||||
Icon = Icons.RestartIcon,
|
||||
},
|
||||
});
|
||||
|
||||
// Update and restart/shut down commands. Only available while Windows Update
|
||||
// is waiting for a restart to finish installing updates (mirrors the Start menu).
|
||||
if (isUpdatePending)
|
||||
{
|
||||
results.AddRange(new[]
|
||||
{
|
||||
new ListItem(
|
||||
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_update_and_shutdown_confirmation, () => UpdateShutdownResult(restart: false))
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.builtin.system.update_shutdown",
|
||||
})
|
||||
{
|
||||
Title = Resources.Microsoft_plugin_sys_update_and_shutdown,
|
||||
Subtitle = Resources.Microsoft_plugin_sys_update_and_shutdown_description,
|
||||
Icon = Icons.ShutdownIcon,
|
||||
},
|
||||
new ListItem(
|
||||
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_update_and_restart_confirmation, () => UpdateShutdownResult(restart: true))
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.builtin.system.update_restart",
|
||||
})
|
||||
{
|
||||
Title = Resources.Microsoft_plugin_sys_update_and_restart,
|
||||
Subtitle = Resources.Microsoft_plugin_sys_update_and_restart_description,
|
||||
Icon = Icons.RestartIcon,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
results.AddRange(new[]
|
||||
{
|
||||
new ListItem(
|
||||
new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0))
|
||||
{
|
||||
@@ -242,13 +290,14 @@ internal static class Commands
|
||||
var networkConnectionResults = Commands.GetNetworkConnectionResults(manager);
|
||||
|
||||
var isBootedInUefiMode = manager.GetSystemFirmwareType() == FirmwareType.Uefi;
|
||||
var isUpdatePending = manager.IsUpdatePending();
|
||||
|
||||
var hideEmptyRB = manager.HideEmptyRecycleBin();
|
||||
var confirmSystemCommands = manager.ShowDialogToConfirmCommand();
|
||||
var showSuccessOnEmptyRB = manager.ShowSuccessMessageAfterEmptyingRecycleBin();
|
||||
|
||||
// normal system commands are fast and can be returned immediately
|
||||
var systemCommands = Commands.GetSystemCommands(isBootedInUefiMode, hideEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB);
|
||||
var systemCommands = Commands.GetSystemCommands(isBootedInUefiMode, isUpdatePending, hideEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB);
|
||||
list.AddRange(systemCommands);
|
||||
list.AddRange(networkConnectionResults);
|
||||
|
||||
|
||||
@@ -21,4 +21,6 @@ public interface ISettingsInterface
|
||||
public bool HideDisconnectedNetworkInfo();
|
||||
|
||||
public FirmwareType GetSystemFirmwareType();
|
||||
|
||||
public bool IsUpdatePending();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
public FirmwareType GetSystemFirmwareType() => Win32Helpers.GetSystemFirmwareType();
|
||||
|
||||
public bool IsUpdatePending() => WindowsUpdateHelper.IsUpdatePending();
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Security;
|
||||
using Windows.Win32.System.Shutdown;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.System.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Detects whether Windows Update is waiting for a restart to finish installing updates
|
||||
/// and initiates the "update and restart" / "update and shut down" actions.
|
||||
/// </summary>
|
||||
internal static partial class WindowsUpdateHelper
|
||||
{
|
||||
// SHTDN_REASON_FLAG_PLANNED | SHTDN_REASON_MAJOR_OPERATINGSYSTEM | SHTDN_REASON_MINOR_UPGRADE
|
||||
private const SHUTDOWN_REASON ShutdownReasonPlannedOsUpgrade =
|
||||
SHUTDOWN_REASON.SHTDN_REASON_FLAG_PLANNED | SHUTDOWN_REASON.SHTDN_REASON_MAJOR_OPERATINGSYSTEM | SHUTDOWN_REASON.SHTDN_REASON_MINOR_UPGRADE;
|
||||
|
||||
private const uint ClsCtxInprocServer = 0x1;
|
||||
|
||||
// WUAPI SystemInformation coclass (wuapi.idl)
|
||||
private static readonly Guid SystemInformationClsid = new("C01B9BA0-BEA7-41BA-B604-D0A36F469133");
|
||||
private static readonly Guid SystemInformationIid = new("ADE87BF7-7B56-4275-8FAB-B9B0E591844B");
|
||||
|
||||
private static readonly StrategyBasedComWrappers ComWrappers = new();
|
||||
|
||||
// Cache the WUAPI answer for a few seconds so that per-keystroke queries don't
|
||||
// repeatedly instantiate the COM object (same approach as the network info cache).
|
||||
private const long UpdateCacheIntervalMs = 5000;
|
||||
private static readonly object CacheLock = new();
|
||||
private static bool cachedRebootRequired;
|
||||
private static bool hasCachedValue;
|
||||
|
||||
// Monotonic timestamp (Environment.TickCount64) of the last successful query, so a
|
||||
// wall-clock change can't make the cache look newer or older than it really is.
|
||||
private static long lastQueryTicks;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Windows Update requires a restart to finish
|
||||
/// installing updates, i.e. whether the Start menu would show "Update and restart".
|
||||
/// Returns false if the state cannot be determined.
|
||||
/// </summary>
|
||||
public static bool IsUpdatePending()
|
||||
{
|
||||
// The whole check-and-refresh runs under the lock so the timestamp and the value
|
||||
// are always published together; otherwise a concurrent caller could read a stale
|
||||
// value while the timestamp already claims it is fresh.
|
||||
lock (CacheLock)
|
||||
{
|
||||
var now = Environment.TickCount64;
|
||||
if (hasCachedValue && (now - lastQueryTicks) < UpdateCacheIntervalMs)
|
||||
{
|
||||
return cachedRebootRequired;
|
||||
}
|
||||
|
||||
cachedRebootRequired = QueryRebootRequired();
|
||||
lastQueryTicks = Environment.TickCount64;
|
||||
hasCachedValue = true;
|
||||
return cachedRebootRequired;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool QueryRebootRequired()
|
||||
{
|
||||
try
|
||||
{
|
||||
var hr = CoCreateInstance(in SystemInformationClsid, IntPtr.Zero, ClsCtxInprocServer, in SystemInformationIid, out var instance);
|
||||
if (hr < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var systemInformation = (ISystemInformation)ComWrappers.GetOrCreateObjectForComInstance(instance, CreateObjectFlags.None);
|
||||
return systemInformation.GetRebootRequired();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.Release(instance);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to query Windows Update reboot state: {ex.Message}" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the InitiateShutdown flags for an "update and restart" (true) or
|
||||
/// "update and shut down" (false) request.
|
||||
/// </summary>
|
||||
public static uint GetUpdateShutdownFlags(bool restart)
|
||||
=> (uint)(SHUTDOWN_FLAGS.SHUTDOWN_INSTALL_UPDATES | (restart ? SHUTDOWN_FLAGS.SHUTDOWN_RESTART : SHUTDOWN_FLAGS.SHUTDOWN_POWEROFF));
|
||||
|
||||
/// <summary>
|
||||
/// Installs pending updates and restarts (true) or shuts down (false) the computer.
|
||||
/// </summary>
|
||||
/// <returns>True if the system accepted the shutdown request.</returns>
|
||||
public static unsafe bool InitiateUpdateShutdown(bool restart)
|
||||
{
|
||||
HANDLE token;
|
||||
if (!PInvoke.OpenProcessToken(PInvoke.GetCurrentProcess(), TOKEN_ACCESS_MASK.TOKEN_ADJUST_PRIVILEGES | TOKEN_ACCESS_MASK.TOKEN_QUERY, &token))
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"OpenProcessToken failed with Win32 error {Marshal.GetLastPInvokeError()}" });
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// InitiateShutdown requires the (normally disabled) shutdown privilege. Enable it,
|
||||
// remembering the previous state so we can put it back afterwards instead of
|
||||
// leaving it enabled for the rest of the process lifetime.
|
||||
if (!TryEnableShutdownPrivilege(token, out var previousState, out var hasPreviousState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = PInvoke.InitiateShutdown(null, null, 0, (SHUTDOWN_FLAGS)GetUpdateShutdownFlags(restart), ShutdownReasonPlannedOsUpgrade);
|
||||
if (result != 0)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"InitiateShutdown failed with Win32 error {result}" });
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (hasPreviousState)
|
||||
{
|
||||
_ = PInvoke.AdjustTokenPrivileges(token, false, &previousState, (uint)sizeof(TOKEN_PRIVILEGES), null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = NativeMethods.CloseHandle(token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables <c>SeShutdownPrivilege</c> on the given token, returning the prior state so
|
||||
/// the caller can restore it. Returns false (and logs) if the privilege could not be
|
||||
/// assigned — <c>AdjustTokenPrivileges</c> reports success even then, so the last error
|
||||
/// must be checked for <c>ERROR_NOT_ALL_ASSIGNED</c>.
|
||||
/// </summary>
|
||||
private static unsafe bool TryEnableShutdownPrivilege(HANDLE token, out TOKEN_PRIVILEGES previousState, out bool hasPreviousState)
|
||||
{
|
||||
previousState = default;
|
||||
hasPreviousState = false;
|
||||
|
||||
if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_SHUTDOWN_NAME, out var luid))
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"LookupPrivilegeValue failed with Win32 error {Marshal.GetLastPInvokeError()}" });
|
||||
return false;
|
||||
}
|
||||
|
||||
var privileges = new TOKEN_PRIVILEGES
|
||||
{
|
||||
PrivilegeCount = 1,
|
||||
};
|
||||
privileges.Privileges[0] = new LUID_AND_ATTRIBUTES
|
||||
{
|
||||
Luid = luid,
|
||||
Attributes = TOKEN_PRIVILEGES_ATTRIBUTES.SE_PRIVILEGE_ENABLED,
|
||||
};
|
||||
|
||||
TOKEN_PRIVILEGES prior;
|
||||
uint returnLength;
|
||||
var adjusted = PInvoke.AdjustTokenPrivileges(token, false, &privileges, (uint)sizeof(TOKEN_PRIVILEGES), &prior, &returnLength);
|
||||
var lastError = Marshal.GetLastPInvokeError();
|
||||
|
||||
if (!adjusted || lastError == (int)WIN32_ERROR.ERROR_NOT_ALL_ASSIGNED)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to enable SeShutdownPrivilege (Win32 error {lastError})" });
|
||||
return false;
|
||||
}
|
||||
|
||||
previousState = prior;
|
||||
hasPreviousState = returnLength > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
private static partial int CoCreateInstance(in Guid rclsid, IntPtr pUnkOuter, uint dwClsContext, in Guid riid, out IntPtr ppv);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WUAPI ISystemInformation (wuapi.idl). This is a dual interface; the first four
|
||||
/// methods are placeholders occupying the IDispatch vtable slots and must never be
|
||||
/// called. The remaining members are called through the vtable, which dual interfaces
|
||||
/// populate alongside IDispatch.
|
||||
/// </summary>
|
||||
[GeneratedComInterface]
|
||||
[Guid("ADE87BF7-7B56-4275-8FAB-B9B0E591844B")]
|
||||
internal partial interface ISystemInformation
|
||||
{
|
||||
// IDispatch slots — placeholders, do not call.
|
||||
void GetTypeInfoCountPlaceholder(nint pctinfo);
|
||||
|
||||
void GetTypeInfoPlaceholder(uint iTInfo, uint lcid, nint ppTInfo);
|
||||
|
||||
void GetIDsOfNamesPlaceholder(nint riid, nint rgszNames, uint cNames, uint lcid, nint rgDispId);
|
||||
|
||||
void InvokePlaceholder(int dispIdMember, nint riid, uint lcid, ushort wFlags, nint pDispParams, nint pVarResult, nint pExcepInfo, nint puArgErr);
|
||||
|
||||
// ISystemInformation members, in vtable order.
|
||||
[return: MarshalAs(UnmanagedType.BStr)]
|
||||
string GetOemHardwareSupportLink();
|
||||
|
||||
[return: MarshalAs(UnmanagedType.VariantBool)]
|
||||
bool GetRebootRequired();
|
||||
}
|
||||
@@ -11,8 +11,19 @@
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CsWin32RunAsBuildTask>true</CsWin32RunAsBuildTask>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- WASDK, WebView2, CmdPal Toolkit references now included via Common.ExtDependencies.props -->
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"useSafeHandles": false
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
InitiateShutdown
|
||||
OpenProcessToken
|
||||
GetCurrentProcess
|
||||
LookupPrivilegeValue
|
||||
AdjustTokenPrivileges
|
||||
|
||||
SHUTDOWN_RESTART
|
||||
SHUTDOWN_POWEROFF
|
||||
SHUTDOWN_INSTALL_UPDATES
|
||||
SHTDN_REASON_FLAG_PLANNED
|
||||
SHTDN_REASON_MAJOR_OPERATINGSYSTEM
|
||||
SHTDN_REASON_MINOR_UPGRADE
|
||||
SE_PRIVILEGE_ENABLED
|
||||
TOKEN_ADJUST_PRIVILEGES
|
||||
TOKEN_QUERY
|
||||
SE_SHUTDOWN_NAME
|
||||
ERROR_NOT_ALL_ASSIGNED
|
||||
@@ -779,7 +779,70 @@ namespace Microsoft.CmdPal.Ext.System {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_Unknown", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Update and restart.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_sys_update_and_restart {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_update_and_restart", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You are about to install pending Windows updates and restart this computer, are you sure?.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_sys_update_and_restart_confirmation {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_update_and_restart_confirmation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Install pending Windows updates and restart the computer.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_sys_update_and_restart_description {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_update_and_restart_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Update and shut down.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_sys_update_and_shutdown {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_update_and_shutdown", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You are about to install pending Windows updates and shut down this computer, are you sure?.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_sys_update_and_shutdown_confirmation {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_update_and_shutdown_confirmation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Install pending Windows updates and shut down the computer.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_sys_update_and_shutdown_description {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_update_and_shutdown_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Couldn't start the Windows update. Please try again..
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_sys_update_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_sys_update_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to WINS servers.
|
||||
/// </summary>
|
||||
|
||||
@@ -374,6 +374,34 @@
|
||||
<data name="Microsoft_plugin_sys_Unknown" xml:space="preserve">
|
||||
<value>Unknown</value>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_update_and_restart" xml:space="preserve">
|
||||
<value>Update and restart</value>
|
||||
<comment>This should align to the "Update and restart" entry in the Windows Start menu power flyout.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_update_and_restart_confirmation" xml:space="preserve">
|
||||
<value>You are about to install pending Windows updates and restart this computer, are you sure?</value>
|
||||
<comment>Confirmation message for the "Update and restart" command.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_update_and_restart_description" xml:space="preserve">
|
||||
<value>Install pending Windows updates and restart the computer</value>
|
||||
<comment>This should align to the "Update and restart" entry in the Windows Start menu power flyout.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_update_and_shutdown" xml:space="preserve">
|
||||
<value>Update and shut down</value>
|
||||
<comment>This should align to the "Update and shut down" entry in the Windows Start menu power flyout.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_update_and_shutdown_confirmation" xml:space="preserve">
|
||||
<value>You are about to install pending Windows updates and shut down this computer, are you sure?</value>
|
||||
<comment>Confirmation message for the "Update and shut down" command.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_update_and_shutdown_description" xml:space="preserve">
|
||||
<value>Install pending Windows updates and shut down the computer</value>
|
||||
<comment>This should align to the "Update and shut down" entry in the Windows Start menu power flyout.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_update_failed" xml:space="preserve">
|
||||
<value>Couldn't start the Windows update. Please try again.</value>
|
||||
<comment>Error shown when the "Update and restart"/"Update and shut down" command fails to start.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_Wins" xml:space="preserve">
|
||||
<value>WINS servers</value>
|
||||
</data>
|
||||
|
||||
Reference in New Issue
Block a user