mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-09-02 04:01:25 +02:00
## Summary of the Pull Request **Regression:** PR #48683 changed Shortcut Guide from a process launched per invocation to a persistent background process. Before that change, every activation started a process and evaluated the current foreground app against the latest excluded-app settings. The excluded-app check remained in `Program.Main`, so after #48683 it ran only when the persistent process started. Excluded apps therefore stopped blocking later regular-hotkey and Windows-key-hold activations, and newly saved exclusions had no effect until the process was recycled. This PR restores the pre-#48683 excluded-app behavior by: - starting the persistent Shortcut Guide listener regardless of the startup foreground app; - evaluating the current foreground app against the latest excluded-app settings before each hidden-overlay activation; - applying the same gate to the regular hotkey and Windows-key-hold paths; and - preserving close and hold-surface promotion behavior when the overlay is already visible. ## PR Checklist - [x] Closes: #50030 - [ ] **Communication:** This regression was diagnosed from the issue and recent Shortcut Guide lifecycle changes; it has not yet been discussed with core contributors - [x] **Tests:** Added/updated and all pass - [x] **Localization:** N/A - no end-user-facing strings were added or changed - [x] **Dev docs:** N/A - no public behavior or developer contract changed - [x] **New binaries:** N/A - no binaries or projects were added - [x] **Documentation updated:** N/A - this is a regression fix restoring existing behavior ## Detailed Description of the Pull Request / Additional comments `src/modules/ShortcutGuide/ShortcutGuide.Ui/Program.cs` no longer exits at process startup when the then-foreground application is excluded. The background listener must remain available for later activations. `src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs` calls the existing native excluded-app helper for each activation while the overlay is hidden. That helper already reloads `settings.json` and rebuilds its excluded-app list on every call, so no watcher or cache is needed. The result is passed into `src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/ShortcutGuideActivationPolicy.cs`, keeping the decision shared by both activation sources and directly testable. The check is intentionally skipped while the overlay is visible. This preserves the regular hotkey's ability to close its guide or take ownership of a guide opened by Windows-key hold. Suppression logging is generic and does not include application names, paths, window titles, or settings content. This is a regression fix rather than a new excluded-app feature or settings-schema change. ## Validation Steps Performed - Built `src/modules/ShortcutGuide/ShortcutGuide.UnitTests/ShortcutGuide.UnitTests.csproj` for x64 Release from the final rebased commit. - Ran the Shortcut Guide test executable: **48 passed, 0 failed, 0 skipped**. - Added cases covering: - regular-hotkey suppression over an excluded app; - Windows-key-hold suppression for taskbar indicators and the full guide; - closing a visible regular guide; and - promoting visible hold indicators with the regular hotkey. - Built a complete local x64 Release payload and confirmed its Runner launched the local Shortcut Guide process and handled open/close activation. Copilot-Session: 8271bded-18e8-474e-8e3b-addd71f67f50
208 lines
8.0 KiB
C#
208 lines
8.0 KiB
C#
// 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.Diagnostics;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using ManagedCommon;
|
|
using Microsoft.PowerToys.Settings.UI.Library;
|
|
using Microsoft.PowerToys.Telemetry;
|
|
using Microsoft.UI.Dispatching;
|
|
using Microsoft.Windows.AppLifecycle;
|
|
using ShortcutGuide.Helpers;
|
|
using ShortcutGuide.Telemetry;
|
|
using Application = Microsoft.UI.Xaml.Application;
|
|
|
|
namespace ShortcutGuide
|
|
{
|
|
public sealed class Program
|
|
{
|
|
public static Thread CopyAndIndexGenerationThread { get; private set; } = null!;
|
|
|
|
public static nint ForegroundWindowHandle { get; set; } = nint.Zero;
|
|
|
|
[STAThread]
|
|
public static void Main(string[] args)
|
|
{
|
|
ForegroundWindowHandle = NativeMethods.GetForegroundWindow();
|
|
Logger.InitializeLogger("\\ShortcutGuide\\Logs");
|
|
LogForegroundCapture(ForegroundWindowHandle);
|
|
|
|
// The module interface passes: <powertoys_pid> [telemetry]
|
|
if (args.Length >= 2 && args[1] == "telemetry")
|
|
{
|
|
Logger.LogInfo("Telemetry mode requested. Sending settings telemetry.");
|
|
SendSettingsTelemetry();
|
|
return;
|
|
}
|
|
|
|
if (args.Length >= 1 && int.TryParse(args[0], out int runnerPID))
|
|
{
|
|
RunnerHelper.WaitForPowerToysRunner(runnerPID, () =>
|
|
{
|
|
Logger.LogInfo($"PowerToys runner process (PID={runnerPID}) exited. Exiting ShortcutGuide.");
|
|
Environment.Exit(0);
|
|
});
|
|
}
|
|
|
|
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredShortcutGuideEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
|
{
|
|
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
|
return;
|
|
}
|
|
|
|
Directory.CreateDirectory(ManifestInterpreter.PathOfManifestFiles);
|
|
|
|
// Copy every shipped manifest from the install directory to the per-user manifest folder.
|
|
// Enumerating the source folder avoids drift between the deployed assets and a hard-coded list.
|
|
// Todo: Only copy files after an update.
|
|
string sourceManifestFolder = Path.Combine(
|
|
Path.GetDirectoryName(Environment.ProcessPath)!,
|
|
"Assets",
|
|
"ShortcutGuide",
|
|
"Manifests");
|
|
|
|
CopyAndIndexGenerationThread = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
foreach (string sourceFile in Directory.EnumerateFiles(sourceManifestFolder, "*.yml"))
|
|
{
|
|
string destinationFile = Path.Combine(ManifestInterpreter.PathOfManifestFiles, Path.GetFileName(sourceFile));
|
|
File.Copy(sourceFile, destinationFile, true);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"Failed to copy bundled shortcut manifests from '{sourceManifestFolder}'.", ex);
|
|
}
|
|
|
|
string indexGeneratorPath = Path.Combine(
|
|
Path.GetDirectoryName(Environment.ProcessPath)!,
|
|
"PowerToys.ShortcutGuide.IndexYmlGenerator.exe");
|
|
|
|
try
|
|
{
|
|
using Process? indexGeneration = Process.Start(indexGeneratorPath);
|
|
|
|
if (indexGeneration is null)
|
|
{
|
|
Logger.LogError($"Failed to start index generation process '{indexGeneratorPath}'.");
|
|
return;
|
|
}
|
|
|
|
indexGeneration.WaitForExit();
|
|
|
|
if (indexGeneration.ExitCode != 0)
|
|
{
|
|
Logger.LogError($"Index generation failed with exit code {indexGeneration.ExitCode}. There may be a corrupt shortcuts file in \"{ManifestInterpreter.PathOfManifestFiles}\".");
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError($"Failed to start or wait for index generation process '{indexGeneratorPath}'.", ex);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
PowerToysShortcutsPopulator.Populate();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Failed to populate PowerToys shortcuts in manifest.", ex);
|
|
}
|
|
});
|
|
CopyAndIndexGenerationThread.IsBackground = true;
|
|
CopyAndIndexGenerationThread.Start();
|
|
|
|
WinRT.ComWrappersSupport.InitializeComWrappers();
|
|
|
|
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_ShortcutGuide_Instance");
|
|
|
|
if (instanceKey.IsCurrent)
|
|
{
|
|
Application.Start((p) =>
|
|
{
|
|
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
|
SynchronizationContext.SetSynchronizationContext(context);
|
|
_ = new App();
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Another instance of ShortcutGuide is running. Exiting ShortcutGuide");
|
|
}
|
|
|
|
// The WinRT/WinUI dispatcher thread doesn't terminate cleanly; force exit.
|
|
Environment.Exit(0);
|
|
}
|
|
|
|
private static void SendSettingsTelemetry()
|
|
{
|
|
try
|
|
{
|
|
var settingsUtils = SettingsUtils.Default;
|
|
var settings = settingsUtils.GetSettingsOrDefault<ShortcutGuideSettings>(ShortcutGuideSettings.ModuleName);
|
|
if (settings?.Properties != null)
|
|
{
|
|
var props = settings.Properties;
|
|
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSettingsEvent(
|
|
props.OpenShortcutGuide?.ToString() ?? string.Empty,
|
|
props.Theme?.Value ?? "system",
|
|
props.DisabledApps?.Value ?? string.Empty));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Failed to send settings telemetry.", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs the foreground window captured at startup so we can see in
|
|
/// the SG log which app was foreground at the moment SG.exe began
|
|
/// running. The dictionary populated from this HWND drives the order
|
|
/// of nav items, so a wrong/empty capture surfaces as the wrong app
|
|
/// being auto-selected.
|
|
/// </summary>
|
|
private static void LogForegroundCapture(nint hwnd)
|
|
{
|
|
try
|
|
{
|
|
if (hwnd == nint.Zero)
|
|
{
|
|
Logger.LogInfo("Foreground capture: HWND=0 (no foreground window).");
|
|
return;
|
|
}
|
|
|
|
if (NativeMethods.GetWindowThreadProcessId(hwnd, out uint processId) == 0)
|
|
{
|
|
Logger.LogInfo($"Foreground capture: HWND=0x{hwnd:X}; GetWindowThreadProcessId failed.");
|
|
return;
|
|
}
|
|
|
|
string moduleName;
|
|
try
|
|
{
|
|
using var proc = Process.GetProcessById((int)processId);
|
|
moduleName = proc.MainModule?.ModuleName ?? "(null)";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
moduleName = $"(failed: {ex.GetType().Name})";
|
|
}
|
|
|
|
Logger.LogInfo($"Foreground capture: HWND=0x{hwnd:X}, PID={processId}, Module={moduleName}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Failed to log foreground capture.", ex);
|
|
}
|
|
}
|
|
}
|
|
}
|