diff --git a/PowerToys.slnx b/PowerToys.slnx index 1f2a1fdbe9b8..7bbf7ac4179a 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -234,6 +234,7 @@ + diff --git a/publish-settings-dependencies.ps1 b/publish-settings-dependencies.ps1 new file mode 100644 index 000000000000..3aacfd53b840 --- /dev/null +++ b/publish-settings-dependencies.ps1 @@ -0,0 +1,38 @@ +# Publish dependencies for Settings.UI with RuntimeIdentifier=win-x64 + +. "$PSScriptRoot\tools\build\build-common.ps1" + +# Initialize Visual Studio dev environment +if (-not (Ensure-VsDevEnvironment)) { + Write-Error "Failed to initialize VS dev environment" + exit 1 +} + +$Platform = "x64" +$Configuration = "Release" +$RuntimeId = "win-x64" + +$projects = @( + "src\common\Common.Search\Common.Search.csproj", + "src\common\LanguageModelProvider\LanguageModelProvider.csproj", + "src\common\AllExperiments\AllExperiments.csproj", + "src\common\Common.UI\Common.UI.csproj", + "src\common\ManagedCommon\ManagedCommon.csproj", + "src\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" +) + +foreach ($project in $projects) { + $projectName = Split-Path $project -Leaf + Write-Host "Publishing $projectName..." -ForegroundColor Cyan + + $args = "-t:Publish -p:Configuration=$Configuration -p:Platform=$Platform -p:RuntimeIdentifier=$RuntimeId" + RunMSBuild $project $args $Platform $Configuration + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to publish $projectName" + exit $LASTEXITCODE + } +} + +Write-Host "`nAll dependencies published successfully!" -ForegroundColor Green +Write-Host "You can now publish Settings.UI" -ForegroundColor Green diff --git a/src/Common.Dotnet.AotCompatibility.props b/src/Common.Dotnet.AotCompatibility.props index bebb88428c43..489535ef7bf8 100644 --- a/src/Common.Dotnet.AotCompatibility.props +++ b/src/Common.Dotnet.AotCompatibility.props @@ -8,6 +8,9 @@ - IL2081;CsWinRT1028;CA1416;$(WarningsNotAsErrors) + + + + IL2026;IL2067;IL2070;IL2072;IL2075;IL2081;IL2087;IL2098;IL3000;IL3002;IL3050;CsWinRT1028;CA1416;$(WarningsNotAsErrors) diff --git a/src/modules/MouseWithoutBorders/App/Class/MouseWithoutBordersIpcServer.cs b/src/modules/MouseWithoutBorders/App/Class/MouseWithoutBordersIpcServer.cs new file mode 100644 index 000000000000..70182358d628 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Class/MouseWithoutBordersIpcServer.cs @@ -0,0 +1,170 @@ +// 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.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using MouseWithoutBorders.Class; +using Logger = MouseWithoutBorders.Core.Logger; + +#pragma warning disable SA1649 // File name should match first type name + +namespace MouseWithoutBorders.Class; + +/// +/// Command types for IPC protocol. +/// Must match client-side enum in Settings.UI\Helpers\MouseWithoutBordersIpcClient.cs +/// +internal enum IpcCommandType : byte +{ + Shutdown = 1, + Reconnect = 2, + GenerateNewKey = 3, + ConnectToMachine = 4, + RequestMachineSocketState = 5, +} + +/// +/// AOT-compatible IPC server for MouseWithoutBorders Settings communication. +/// Replaces StreamJsonRpc with manual NamedPipe protocol. +/// +internal sealed class MouseWithoutBordersIpcServer +{ + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { WriteIndented = false }; + + private readonly ISettingsSyncHandler _handler; + + public MouseWithoutBordersIpcServer(ISettingsSyncHandler handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } + + /// + /// Handles a single client connection + /// + public async Task HandleClientAsync(Stream stream, CancellationToken cancellationToken) + { + using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); + using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); + + try + { + while (!cancellationToken.IsCancellationRequested && stream.CanRead) + { + // Read command type (1 byte) + var commandByte = reader.ReadByte(); + var command = (IpcCommandType)commandByte; + + switch (command) + { + case IpcCommandType.Shutdown: + _handler.Shutdown(); + break; + + case IpcCommandType.Reconnect: + _handler.Reconnect(); + break; + + case IpcCommandType.GenerateNewKey: + _handler.GenerateNewKey(); + break; + + case IpcCommandType.ConnectToMachine: + { + var machineName = ReadString(reader); + var securityKey = ReadString(reader); + _handler.ConnectToMachine(machineName, securityKey); + } + + break; + + case IpcCommandType.RequestMachineSocketState: + { + var states = await _handler.RequestMachineSocketStateAsync(); + var json = JsonSerializer.Serialize(states, JsonOptions); + WriteString(writer, json); + await stream.FlushAsync(cancellationToken); + } + + break; + + default: + Logger.Log($"Unknown IPC command: {commandByte}"); + return; // Invalid command, close connection + } + } + } + catch (EndOfStreamException) + { + // Client disconnected, normal termination + } + catch (IOException) + { + // Pipe broken, normal termination + } + catch (Exception ex) + { + Logger.Log($"IPC error: {ex}"); + } + } + + /// + /// Reads a length-prefixed UTF-8 string + /// + private static string ReadString(BinaryReader reader) + { + var length = reader.ReadInt32(); + if (length <= 0 || length > 1024 * 1024) + { + return string.Empty; + } + + var bytes = reader.ReadBytes(length); + return Encoding.UTF8.GetString(bytes); + } + + /// + /// Writes a length-prefixed UTF-8 string + /// + private static void WriteString(BinaryWriter writer, string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + writer.Write(bytes.Length); + writer.Write(bytes); + } +} + +/// +/// Interface for handling IPC commands. +/// Implemented by SettingsSyncHelper in Program.cs +/// +internal interface ISettingsSyncHandler +{ + void Shutdown(); + + void Reconnect(); + + void GenerateNewKey(); + + void ConnectToMachine(string machineName, string securityKey); + + Task RequestMachineSocketStateAsync(); +} + +/// +/// Machine socket state for serialization. +/// Uses SocketStatus from SocketStuff.cs in MouseWithoutBorders.Class namespace. +/// +public struct MachineSocketState +{ + public string Name { get; set; } + + public MouseWithoutBorders.Class.SocketStatus Status { get; set; } +} diff --git a/src/modules/MouseWithoutBorders/App/Class/Program.cs b/src/modules/MouseWithoutBorders/App/Class/Program.cs index 23513e1515e2..a5a4cb7b00e4 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Program.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Program.cs @@ -19,6 +19,7 @@ using System.IO; using System.IO.Pipes; using System.Linq; +using System.Security.AccessControl; using System.Security.Authentication.ExtendedProtection; using System.Security.Principal; using System.ServiceModel.Channels; @@ -276,7 +277,7 @@ public struct MachineSocketState Task RequestMachineSocketStateAsync(); } - private sealed class SettingsSyncHelper : ISettingsSyncHelper + private sealed class SettingsSyncHelper : ISettingsSyncHelper, ISettingsSyncHandler { public Task RequestMachineSocketStateAsync() { @@ -299,6 +300,28 @@ private sealed class SettingsSyncHelper : ISettingsSyncHelper return Task.FromResult(machineStates.Select((state) => new ISettingsSyncHelper.MachineSocketState { Name = state.Key, Status = state.Value }).ToArray()); } + // ISettingsSyncHandler implementation (AOT-compatible) + Task ISettingsSyncHandler.RequestMachineSocketStateAsync() + { + var machineStates = new Dictionary(); + if (Common.Sk == null || Common.Sk.TcpSockets == null) + { + return Task.FromResult(Array.Empty()); + } + + foreach (var client in Common.Sk.TcpSockets + .Where(t => t != null && t.IsClient && !string.IsNullOrEmpty(t.MachineName))) + { + var exists = machineStates.TryGetValue(client.MachineName, out var existingStatus); + if (!exists || existingStatus == SocketStatus.NA) + { + machineStates[client.MachineName] = client.Status; + } + } + + return Task.FromResult(machineStates.Select((state) => new MachineSocketState { Name = state.Key, Status = state.Value }).ToArray()); + } + public void ConnectToMachine(string pcName, string securityKey) { Setting.Values.PauseInstantSaving = true; @@ -379,7 +402,64 @@ internal static void StartSettingSyncThread() var serverTaskCancellationSource = new CancellationTokenSource(); CancellationToken cancellationToken = serverTaskCancellationSource.Token; + // Use AOT-compatible IPC server if available, otherwise use StreamJsonRpc +#if BUILD_INFO_PUBLISH_AOT || true // Enable for all builds + StartAotCompatibleIpcServer("MouseWithoutBorders/SettingsSync", cancellationToken); +#else IpcChannel.StartIpcServer("MouseWithoutBorders/SettingsSync", cancellationToken); +#endif + } + + private static void StartAotCompatibleIpcServer(string pipeName, CancellationToken cancellationToken) + { + var handler = new SettingsSyncHelper(); + var server = new MouseWithoutBordersIpcServer(handler); + + _ = Task.Factory.StartNew( + async () => + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + using (var serverPipe = NamedPipeServerStreamAcl.Create( + pipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 0, + 0, + CreatePipeSecurity())) + { + await serverPipe.WaitForConnectionAsync(cancellationToken); + await server.HandleClientAsync(serverPipe, cancellationToken); + } + } + } + catch (OperationCanceledException) + { + // Normal shutdown + } + catch (Exception e) + { + Logger.Log(e); + } + }, + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + } + + private static PipeSecurity CreatePipeSecurity() + { + var securityIdentifier = new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null); + var pipeSecurity = new PipeSecurity(); + pipeSecurity.AddAccessRule(new PipeAccessRule( + securityIdentifier, + PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, + AccessControlType.Allow)); + return pipeSecurity; } internal static void StartInputCallbackThread() diff --git a/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs b/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs index 575c9582df65..b7095f59cdcc 100644 --- a/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs +++ b/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs @@ -51,7 +51,11 @@ namespace MouseWithoutBorders.Class { - internal enum SocketStatus : int + /// + /// Socket status enumeration - made public for IPC serialization. + /// Must match Settings.UI.Library\MouseWithoutBordersIpcModels.cs + /// + public enum SocketStatus : int { NA = 0, Resolving = 1, diff --git a/src/modules/MouseWithoutBorders/App/Core/Common.cs b/src/modules/MouseWithoutBorders/App/Core/Common.cs index 3c2206f2ab11..0bce717e42f6 100644 --- a/src/modules/MouseWithoutBorders/App/Core/Common.cs +++ b/src/modules/MouseWithoutBorders/App/Core/Common.cs @@ -24,6 +24,7 @@ using MouseWithoutBorders.Exceptions; using Clipboard = MouseWithoutBorders.Core.Clipboard; +using SocketStatus = MouseWithoutBorders.Class.SocketStatus; using Thread = MouseWithoutBorders.Core.Thread; // Log is enough diff --git a/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj b/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj index ea4586f2175d..96af8c0ee827 100644 --- a/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj +++ b/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj @@ -1,6 +1,11 @@  + + + + true + WinExe diff --git a/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs b/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs index 8a522228068e..0d13bb750cc7 100644 --- a/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs +++ b/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs @@ -2,12 +2,13 @@ // 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.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace Microsoft.PowerToys.Settings.UI.Library { - public class GenericProperty : ICmdLineRepresentable + public class GenericProperty<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T> : ICmdLineRepresentable { [JsonPropertyName("value")] public T Value { get; set; } diff --git a/src/settings-ui/Settings.UI.Library/Interfaces/ICmdLineRepresentable.cs b/src/settings-ui/Settings.UI.Library/Interfaces/ICmdLineRepresentable.cs index 67b90674cd31..5a2cb2d8e56b 100644 --- a/src/settings-ui/Settings.UI.Library/Interfaces/ICmdLineRepresentable.cs +++ b/src/settings-ui/Settings.UI.Library/Interfaces/ICmdLineRepresentable.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; @@ -17,7 +18,7 @@ public interface ICmdLineRepresentable public abstract bool TryToCmdRepresentable(out string result); - public static sealed bool TryToCmdRepresentableFor(Type type, object value, out string result) + public static sealed bool TryToCmdRepresentableFor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, object value, out string result) { result = null; if (!typeof(ICmdLineRepresentable).IsAssignableFrom(type)) @@ -36,7 +37,7 @@ public static sealed bool TryToCmdRepresentableFor(Type type, object value, out return false; } - public static sealed bool TryParseFromCmdFor(Type type, string cmd, out object result) + public static sealed bool TryParseFromCmdFor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, string cmd, out object result) { result = null; if (!typeof(ICmdLineRepresentable).IsAssignableFrom(type)) @@ -55,7 +56,7 @@ public static sealed bool TryParseFromCmdFor(Type type, string cmd, out object r return false; } - public static sealed object ParseFor(Type type, string cmdRepr) + public static sealed object ParseFor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, string cmdRepr) { if (type.IsEnum) { @@ -98,7 +99,7 @@ public static sealed object ParseFor(Type type, string cmdRepr) throw new NotImplementedException($"Parsing type {type} is not supported yet"); } - public static string ToCmdRepr(Type type, object value) + public static string ToCmdRepr([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, object value) { if (type.IsEnum || type.IsPrimitive) { diff --git a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersIpcModels.cs b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersIpcModels.cs new file mode 100644 index 000000000000..a32952fa4ed4 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersIpcModels.cs @@ -0,0 +1,40 @@ +// 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.Text.Json.Serialization; + +#pragma warning disable SA1649 // File name should match first type name + +namespace Microsoft.PowerToys.Settings.UI.Library; + +/// +/// Socket status enumeration for MouseWithoutBorders machine connections. +/// Must match the enum in MouseWithoutBorders\App\Class\Program.cs +/// +public enum SocketStatus : int +{ + NA = 0, + Resolving = 1, + Connecting = 2, + Handshaking = 3, + Error = 4, + ForceClosed = 5, + InvalidKey = 6, + Timeout = 7, + SendError = 8, + Connected = 9, +} + +/// +/// Represents the connection state of a machine in the MouseWithoutBorders network. +/// Used for IPC communication between Settings UI and MouseWithoutBorders service. +/// +public struct MachineSocketState +{ + [JsonPropertyName("Name")] + public string Name { get; set; } + + [JsonPropertyName("Status")] + public SocketStatus Status { get; set; } +} diff --git a/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj b/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj index c3832b11c508..1727f8287320 100644 --- a/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj +++ b/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj @@ -2,6 +2,7 @@ + PowerToys Settings UI Library PowerToys.Settings.UI.Lib diff --git a/src/settings-ui/Settings.UI.Library/SettingsFactory.cs b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs index 6e53204e339e..d66a677ee3df 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsFactory.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs @@ -1,11 +1,9 @@ -// Copyright (c) Microsoft Corporation +// 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.Collections.Generic; -using System.Linq; -using System.Reflection; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -13,125 +11,112 @@ namespace Microsoft.PowerToys.Settings.UI.Services { /// - /// Factory service for getting PowerToys module Settings that implement IHotkeyConfig + /// AOT-compatible factory service for PowerToys module Settings. + /// Uses static type registration instead of reflection-based discovery. /// + /// + /// When adding a new PowerToys module, add it to both InitializeFactories() and InitializeTypes() methods. + /// public class SettingsFactory { private readonly SettingsUtils _settingsUtils; + private readonly Dictionary> _settingsFactories; private readonly Dictionary _settingsTypes; public SettingsFactory(SettingsUtils settingsUtils) { _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); - _settingsTypes = DiscoverSettingsTypes(); + _settingsFactories = InitializeFactories(); + _settingsTypes = InitializeTypes(); } /// - /// Dynamically discovers all Settings types that implement IHotkeyConfig + /// Static registry of all module settings factories. + /// IMPORTANT: When adding a new module, add it here. /// - private Dictionary DiscoverSettingsTypes() + private Dictionary> InitializeFactories() { - var settingsTypes = new Dictionary(); - - // Get the Settings.UI.Library assembly - var assembly = Assembly.GetAssembly(typeof(IHotkeyConfig)); - if (assembly == null) + return new Dictionary> { - return settingsTypes; - } - - try - { - // Find all types that implement IHotkeyConfig and ISettingsConfig - var hotkeyConfigTypes = assembly.GetTypes() - .Where(type => - type.IsClass && - !type.IsAbstract && - typeof(IHotkeyConfig).IsAssignableFrom(type) && - typeof(ISettingsConfig).IsAssignableFrom(type)) - .ToList(); - - foreach (var type in hotkeyConfigTypes) - { - // Try to get the ModuleName using SettingsRepository - try - { - var repositoryType = typeof(SettingsRepository<>).MakeGenericType(type); - var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static); - var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils }); - - if (repository != null) - { - var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig"); - var settingsInstance = settingsConfigProperty?.GetValue(repository) as ISettingsConfig; - - if (settingsInstance != null) - { - var moduleName = settingsInstance.GetModuleName(); - if (string.IsNullOrEmpty(moduleName) && type == typeof(GeneralSettings)) - { - moduleName = "GeneralSettings"; - } + ["GeneralSettings"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["AdvancedPaste"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["AlwaysOnTop"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["ColorPicker"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["CropAndLock"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["CursorWrap"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["FindMyMouse"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["LightSwitch"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["MeasureTool"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["MouseHighlighter"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["MouseJump"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["MousePointerCrosshairs"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["MouseWithoutBorders"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["Peek"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["PowerLauncher"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["PowerOCR"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["ShortcutGuide"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + ["Workspaces"] = () => SettingsRepository.GetInstance(_settingsUtils).SettingsConfig, + }; + } - if (!string.IsNullOrEmpty(moduleName)) - { - settingsTypes[moduleName] = type; - System.Diagnostics.Debug.WriteLine($"Discovered settings type: {type.Name} for module: {moduleName}"); - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Error getting module name for {type.Name}: {ex.Message}"); - } - } - } - catch (Exception ex) + /// + /// Static registry of module name to settings type mapping. + /// IMPORTANT: When adding a new module, add it here. + /// + private Dictionary InitializeTypes() + { + return new Dictionary { - System.Diagnostics.Debug.WriteLine($"Error scanning assembly {assembly.FullName}: {ex.Message}"); - } - - return settingsTypes; + ["GeneralSettings"] = typeof(GeneralSettings), + ["AdvancedPaste"] = typeof(AdvancedPasteSettings), + ["AlwaysOnTop"] = typeof(AlwaysOnTopSettings), + ["ColorPicker"] = typeof(ColorPickerSettings), + ["CropAndLock"] = typeof(CropAndLockSettings), + ["CursorWrap"] = typeof(CursorWrapSettings), + ["FindMyMouse"] = typeof(FindMyMouseSettings), + ["LightSwitch"] = typeof(LightSwitchSettings), + ["MeasureTool"] = typeof(MeasureToolSettings), + ["MouseHighlighter"] = typeof(MouseHighlighterSettings), + ["MouseJump"] = typeof(MouseJumpSettings), + ["MousePointerCrosshairs"] = typeof(MousePointerCrosshairsSettings), + ["MouseWithoutBorders"] = typeof(MouseWithoutBordersSettings), + ["Peek"] = typeof(PeekSettings), + ["PowerLauncher"] = typeof(PowerLauncherSettings), + ["PowerOCR"] = typeof(PowerOcrSettings), + ["ShortcutGuide"] = typeof(ShortcutGuideSettings), + ["Workspaces"] = typeof(WorkspacesSettings), + }; } - public IHotkeyConfig GetFreshSettings(string moduleKey) + /// + /// Gets a settings instance for the specified module using SettingsRepository. + /// AOT-compatible: uses static factory lookup instead of reflection. + /// + /// The module key/name + /// The settings instance implementing IHotkeyConfig, or null if not found + public IHotkeyConfig GetSettings(string moduleKey) { - if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType)) + if (!_settingsFactories.TryGetValue(moduleKey, out var factory)) { return null; } try { - // Create a generic method call to _settingsUtils.GetSettingsOrDefault(moduleKey) - var getSettingsMethod = typeof(SettingsUtils).GetMethod("GetSettingsOrDefault", new[] { typeof(string), typeof(string) }); - var genericMethod = getSettingsMethod?.MakeGenericMethod(settingsType); - - // Call GetSettingsOrDefault(moduleKey) to get fresh settings from file - string actualModuleKey = moduleKey; - if (moduleKey == "GeneralSettings") - { - actualModuleKey = string.Empty; - } - - var freshSettings = genericMethod?.Invoke(_settingsUtils, new object[] { actualModuleKey, "settings.json" }); - - return freshSettings as IHotkeyConfig; + return factory(); } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Error getting fresh settings for {moduleKey}: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}"); return null; } } /// - /// Gets a settings instance for the specified module using SettingsRepository + /// Gets fresh settings from disk for the specified module. + /// AOT-compatible: uses static type dispatch instead of MakeGenericMethod. /// - /// The module key/name - /// The settings instance implementing IHotkeyConfig, or null if not found - public IHotkeyConfig GetSettings(string moduleKey) + public IHotkeyConfig GetFreshSettings(string moduleKey) { if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType)) { @@ -140,22 +125,44 @@ public IHotkeyConfig GetSettings(string moduleKey) try { - var repositoryType = typeof(SettingsRepository<>).MakeGenericType(settingsType); - var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static); - var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils }); - - if (repository != null) - { - var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig"); - return settingsConfigProperty?.GetValue(repository) as IHotkeyConfig; - } + string actualModuleKey = moduleKey == "GeneralSettings" ? string.Empty : moduleKey; + return GetFreshSettingsForType(settingsType, actualModuleKey); } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Error getting fresh settings for {moduleKey}: {ex.Message}"); + return null; } + } - return null; + /// + /// Static dispatch for GetSettingsOrDefault using pattern matching. + /// Replaces reflection-based MakeGenericMethod/Invoke pattern. + /// + private IHotkeyConfig GetFreshSettingsForType(Type settingsType, string moduleKey) + { + return settingsType.Name switch + { + nameof(GeneralSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(AdvancedPasteSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(AlwaysOnTopSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(ColorPickerSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(CropAndLockSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(CursorWrapSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(FindMyMouseSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(LightSwitchSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(MeasureToolSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(MouseHighlighterSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(MouseJumpSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(MousePointerCrosshairsSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(MouseWithoutBordersSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(PeekSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(PowerLauncherSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(PowerOcrSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(ShortcutGuideSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + nameof(WorkspacesSettings) => _settingsUtils.GetSettingsOrDefault(moduleKey, "settings.json"), + _ => null, + }; } /// @@ -164,7 +171,7 @@ public IHotkeyConfig GetSettings(string moduleKey) /// List of module names public List GetAvailableModuleNames() { - return _settingsTypes.Keys.ToList(); + return new List(_settingsTypes.Keys); } /// diff --git a/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs index 2fab97c5387b..cbb8182af332 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using System.Text.Json.Serialization; using SettingsUILibrary = Settings.UI.Library; using SettingsUILibraryHelpers = Settings.UI.Library.Helpers; @@ -167,6 +168,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonSerializable(typeof(SndModuleSettings))] [JsonSerializable(typeof(SndModuleSettings))] + // CLI/DSC command support types + [JsonSerializable(typeof(PowerLauncherPluginSettings))] + [JsonSerializable(typeof(PowerLauncherPluginSettings[]))] + + // MouseWithoutBorders IPC types + [JsonSerializable(typeof(MachineSocketState))] + [JsonSerializable(typeof(MachineSocketState[]))] + [JsonSerializable(typeof(SocketStatus))] + public partial class SettingsSerializationContext : JsonSerializerContext { } diff --git a/src/settings-ui/Settings.UI.Library/Utilities/CommandLineUtils.cs b/src/settings-ui/Settings.UI.Library/Utilities/CommandLineUtils.cs index 3b10d0d63f50..82f999ac809c 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/CommandLineUtils.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/CommandLineUtils.cs @@ -1,69 +1,181 @@ -// Copyright (c) Microsoft Corporation +// 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.Linq; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; - using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library; +/// +/// AOT-compatible command line utilities. +/// Uses static type mapping instead of AppDomain reflection. +/// public class CommandLineUtils { - private static Type GetSettingsConfigType(string moduleName, Assembly settingsLibraryAssembly) + private static readonly Dictionary _settingsTypes = new() { - var settingsClassName = moduleName == "GeneralSettings" ? moduleName : moduleName + "Settings"; - return settingsLibraryAssembly.GetType(typeof(CommandLineUtils).Namespace + "." + settingsClassName); - } + ["GeneralSettings"] = typeof(GeneralSettings), + ["AdvancedPaste"] = typeof(AdvancedPasteSettings), + ["AlwaysOnTop"] = typeof(AlwaysOnTopSettings), + ["Awake"] = typeof(AwakeSettings), + ["CmdNotFound"] = typeof(CmdNotFoundSettings), + ["ColorPicker"] = typeof(ColorPickerSettings), + ["CropAndLock"] = typeof(CropAndLockSettings), + ["CursorWrap"] = typeof(CursorWrapSettings), + ["EnvironmentVariables"] = typeof(EnvironmentVariablesSettings), + ["FancyZones"] = typeof(FancyZonesSettings), + ["FileLocksmith"] = typeof(FileLocksmithSettings), + ["FindMyMouse"] = typeof(FindMyMouseSettings), + ["Hosts"] = typeof(HostsSettings), + ["ImageResizer"] = typeof(ImageResizerSettings), + ["KeyboardManager"] = typeof(KeyboardManagerSettings), + ["LightSwitch"] = typeof(LightSwitchSettings), + ["MeasureTool"] = typeof(MeasureToolSettings), + ["MouseHighlighter"] = typeof(MouseHighlighterSettings), + ["MouseJump"] = typeof(MouseJumpSettings), + ["MousePointerCrosshairs"] = typeof(MousePointerCrosshairsSettings), + ["MouseWithoutBorders"] = typeof(MouseWithoutBordersSettings), + ["NewPlus"] = typeof(NewPlusSettings), + ["Peek"] = typeof(PeekSettings), + ["PowerAccent"] = typeof(PowerAccentSettings), + ["PowerLauncher"] = typeof(PowerLauncherSettings), + ["PowerOCR"] = typeof(PowerOcrSettings), + ["PowerRename"] = typeof(PowerRenameSettings), + ["PowerPreview"] = typeof(PowerPreviewSettings), + ["RegistryPreview"] = typeof(RegistryPreviewSettings), + ["ShortcutGuide"] = typeof(ShortcutGuideSettings), + ["Workspaces"] = typeof(WorkspacesSettings), + ["ZoomIt"] = typeof(ZoomItSettings), + }; - public static ISettingsConfig GetSettingsConfigFor(string moduleName, SettingsUtils settingsUtils, Assembly settingsLibraryAssembly) + public static ISettingsConfig GetSettingsConfigFor(string moduleName, SettingsUtils settingsUtils, Assembly settingsLibraryAssembly = null) { - return GetSettingsConfigFor(GetSettingsConfigType(moduleName, settingsLibraryAssembly), settingsUtils); - } - - /// Executes SettingsRepository.GetInstance(settingsUtils).SettingsConfig - public static ISettingsConfig GetSettingsConfigFor(Type moduleSettingsType, SettingsUtils settingsUtils) - { - var genericSettingsRepositoryType = typeof(SettingsRepository<>); - var moduleSettingsRepositoryType = genericSettingsRepositoryType.MakeGenericType(moduleSettingsType); - - // Note: GeneralSettings is only used here only to satisfy nameof constrains, i.e. the choice of this particular type doesn't have any special significance. - var getInstanceInfo = moduleSettingsRepositoryType.GetMethod(nameof(SettingsRepository.GetInstance)); - var settingsRepository = getInstanceInfo.Invoke(null, new object[] { settingsUtils }); - var settingsConfigProperty = getInstanceInfo.ReturnType.GetProperty(nameof(SettingsRepository.SettingsConfig)); - return settingsConfigProperty.GetValue(settingsRepository) as ISettingsConfig; - } + if (!_settingsTypes.TryGetValue(moduleName, out var settingsType)) + { + return null; + } - public static Assembly GetSettingsAssembly() - { - return AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == "PowerToys.Settings.UI.Lib"); + return GetSettingsConfigFor(settingsType, settingsUtils); } - public static object GetPropertyValue(string propertyName, ISettingsConfig settingsConfig) + /// + /// Gets settings config for a given type using static dispatch. + /// AOT-compatible: replaces MakeGenericType/GetMethod/Invoke pattern. + /// + public static ISettingsConfig GetSettingsConfigFor(Type moduleSettingsType, SettingsUtils settingsUtils) { - var (settingInfo, properties) = LocateSetting(propertyName, settingsConfig); - return settingInfo.GetValue(properties); + return moduleSettingsType.Name switch + { + nameof(GeneralSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(AdvancedPasteSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(AlwaysOnTopSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(AwakeSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(CmdNotFoundSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(ColorPickerSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(CropAndLockSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(CursorWrapSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(EnvironmentVariablesSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(FancyZonesSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(FileLocksmithSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(FindMyMouseSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(HostsSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(ImageResizerSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(KeyboardManagerSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(LightSwitchSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(MeasureToolSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(MouseHighlighterSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(MouseJumpSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(MousePointerCrosshairsSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(MouseWithoutBordersSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(NewPlusSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(PeekSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(PowerAccentSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(PowerLauncherSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(PowerOcrSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(PowerRenameSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(PowerPreviewSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(RegistryPreviewSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(ShortcutGuideSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(WorkspacesSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + nameof(ZoomItSettings) => SettingsRepository.GetInstance(settingsUtils).SettingsConfig, + _ => null, + }; } + /// + /// Gets the Properties object from a settings config. + /// For GeneralSettings, returns the settings itself. For others, returns the Properties property. + /// public static object GetProperties(ISettingsConfig settingsConfig) { + // Use reflection fallback for all settings types to preserve compatibility + // This is needed because not all settings have static patterns var settingsType = settingsConfig.GetType(); if (settingsType == typeof(GeneralSettings)) { return settingsConfig; } - var settingsConfigInfo = settingsType.GetProperty("Properties"); - return settingsConfigInfo.GetValue(settingsConfig); + var propertiesProperty = settingsType.GetProperty("Properties"); + return propertiesProperty?.GetValue(settingsConfig); + } + + /// + /// Gets enabled state for a specific module. + /// AOT-compatible: static dispatch instead of reflection. + /// + public static bool GetEnabledModuleValue(string moduleName, EnabledModules enabled) + { + return moduleName switch + { + "AdvancedPaste" => enabled.AdvancedPaste, + "AlwaysOnTop" => enabled.AlwaysOnTop, + "Awake" => enabled.Awake, + "CmdNotFound" => enabled.CmdNotFound, + "ColorPicker" => enabled.ColorPicker, + "CropAndLock" => enabled.CropAndLock, + "CursorWrap" => enabled.CursorWrap, + "EnvironmentVariables" => enabled.EnvironmentVariables, + "FancyZones" => enabled.FancyZones, + "FileLocksmith" => enabled.FileLocksmith, + "FindMyMouse" => enabled.FindMyMouse, + "Hosts" => enabled.Hosts, + "ImageResizer" => enabled.ImageResizer, + "KeyboardManager" => enabled.KeyboardManager, + "LightSwitch" => enabled.LightSwitch, + "MeasureTool" => enabled.MeasureTool, + "MouseHighlighter" => enabled.MouseHighlighter, + "MouseJump" => enabled.MouseJump, + "MousePointerCrosshairs" => enabled.MousePointerCrosshairs, + "MouseWithoutBorders" => enabled.MouseWithoutBorders, + "NewPlus" => enabled.NewPlus, + "Peek" => enabled.Peek, + "PowerAccent" => enabled.PowerAccent, + "PowerLauncher" => enabled.PowerLauncher, + "PowerOcr" => enabled.PowerOcr, + "PowerRename" => enabled.PowerRename, + "RegistryPreview" => enabled.RegistryPreview, + "ShortcutGuide" => enabled.ShortcutGuide, + "Workspaces" => enabled.Workspaces, + "ZoomIt" => enabled.ZoomIt, + _ => false, + }; } + /// + /// Locates a setting property and returns both the PropertyInfo and the properties object. + /// Uses reflection on properties which is preserved via DynamicallyAccessedMembers on property types. + /// public static (PropertyInfo SettingInfo, object Properties) LocateSetting(string propertyName, ISettingsConfig settingsConfig) { var properties = GetProperties(settingsConfig); var propertiesType = properties.GetType(); + + // Special handling for GeneralSettings.Enabled.* if (propertiesType == typeof(GeneralSettings) && propertyName.StartsWith("Enabled.", StringComparison.InvariantCulture)) { var moduleNameToToggle = propertyName.Replace("Enabled.", string.Empty); @@ -75,6 +187,18 @@ public static (PropertyInfo SettingInfo, object Properties) LocateSetting(string return (propertiesType.GetProperty(propertyName), properties); } + /// + /// Gets the value of a property from a settings config. + /// + public static object GetPropertyValue(string propertyName, ISettingsConfig settingsConfig) + { + var (settingInfo, properties) = LocateSetting(propertyName, settingsConfig); + return settingInfo?.GetValue(properties); + } + + /// + /// Gets the PropertyInfo for a setting property. + /// public static PropertyInfo GetSettingPropertyInfo(string propertyName, ISettingsConfig settingsConfig) { return LocateSetting(propertyName, settingsConfig).SettingInfo; diff --git a/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs index 5780b95392c7..ecbbc91d0154 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs @@ -47,9 +47,7 @@ public static string Execute(Dictionary> settingNamesForMod { var modulesSettings = new Dictionary>(); - var settingsAssembly = CommandLineUtils.GetSettingsAssembly(); var settingsUtils = SettingsUtils.Default; - var enabledModules = SettingsRepository.GetInstance(settingsUtils).SettingsConfig.Enabled; foreach (var (moduleName, settings) in settingNamesForModules) @@ -57,10 +55,10 @@ public static string Execute(Dictionary> settingNamesForMod var moduleSettings = new Dictionary(); if (moduleName != nameof(GeneralSettings)) { - moduleSettings.Add("Enabled", typeof(EnabledModules).GetProperty(moduleName).GetValue(enabledModules)); + moduleSettings.Add("Enabled", CommandLineUtils.GetEnabledModuleValue(moduleName, enabledModules)); } - var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsAssembly); + var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils); foreach (var settingName in settings) { var value = CommandLineUtils.GetPropertyValue(settingName, settingsConfig); diff --git a/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs b/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs index 3fa4479b79e7..aa6d28a1708c 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs @@ -115,7 +115,8 @@ public static string LocalApplicationDataFolder() public static string GetPowerToysInstallationFolder() { // PowerToys.exe is in the parent folder relative to Settings. - var settingsPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + // Use AppContext.BaseDirectory for AOT/single-file compatibility + var settingsPath = AppContext.BaseDirectory; return Directory.GetParent(settingsPath).FullName; } diff --git a/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs index b6ba04dec8e7..c37164faafe4 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs @@ -246,10 +246,8 @@ private static Type GetUnderlyingTypeOfCollection(IEnumerable currentPro public static void Execute(string moduleName, JsonDocument settings, SettingsUtils settingsUtils) { - Assembly settingsLibraryAssembly = CommandLineUtils.GetSettingsAssembly(); - - var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsLibraryAssembly); - var settingsConfigType = settingsConfig.GetType(); + var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils); + var settingsConfigType = settingsConfig?.GetType(); if (!SupportedAdditionalPropertiesInfoForModules.TryGetValue(moduleName, out var additionalPropertiesInfo)) { diff --git a/src/settings-ui/Settings.UI.Library/Utilities/SetSettingCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/SetSettingCommandLineCommand.cs index 3cd70856a83a..b36c27e34856 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/SetSettingCommandLineCommand.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/SetSettingCommandLineCommand.cs @@ -27,11 +27,9 @@ private static (string ModuleName, string PropertyName) ParseSettingName(string public static void Execute(string settingName, string settingValue, SettingsUtils settingsUtils) { - Assembly settingsLibraryAssembly = CommandLineUtils.GetSettingsAssembly(); - var (moduleName, propertyName) = ParseSettingName(settingName); - var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsLibraryAssembly); + var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils); var propertyInfo = CommandLineUtils.GetSettingPropertyInfo(propertyName, settingsConfig); if (propertyInfo == null) diff --git a/src/settings-ui/Settings.UI/Helpers/MouseWithoutBordersIpcClient.cs b/src/settings-ui/Settings.UI/Helpers/MouseWithoutBordersIpcClient.cs new file mode 100644 index 000000000000..b03e4bc2b249 --- /dev/null +++ b/src/settings-ui/Settings.UI/Helpers/MouseWithoutBordersIpcClient.cs @@ -0,0 +1,199 @@ +// 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.IO; +using System.IO.Pipes; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace Microsoft.PowerToys.Settings.UI.Helpers; + +/// +/// AOT-compatible IPC client for MouseWithoutBorders service communication. +/// Replaces StreamJsonRpc with manual NamedPipe protocol using length-prefixed messages. +/// +public sealed class MouseWithoutBordersIpcClient : IDisposable +{ + private readonly Stream _stream; + private readonly BinaryWriter _writer; + private readonly BinaryReader _reader; + private readonly object _lock = new(); + private bool _disposed; + + /// + /// Command types for IPC protocol. + /// Must match server-side enum in MouseWithoutBorders Program.cs + /// + private enum CommandType : byte + { + Shutdown = 1, + Reconnect = 2, + GenerateNewKey = 3, + ConnectToMachine = 4, + RequestMachineSocketState = 5, + } + + public MouseWithoutBordersIpcClient(Stream stream) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _writer = new BinaryWriter(_stream, Encoding.UTF8, leaveOpen: true); + _reader = new BinaryReader(_stream, Encoding.UTF8, leaveOpen: true); + } + + /// + /// Sends shutdown command to MouseWithoutBorders service + /// + public async Task ShutdownAsync() + { + await SendCommandAsync(CommandType.Shutdown); + await FlushAsync(); + } + + /// + /// Sends reconnect command to MouseWithoutBorders service + /// + public async Task ReconnectAsync() + { + await SendCommandAsync(CommandType.Reconnect); + await FlushAsync(); + } + + /// + /// Requests generation of a new security key + /// + public async Task GenerateNewKeyAsync() + { + await SendCommandAsync(CommandType.GenerateNewKey); + await FlushAsync(); + } + + /// + /// Requests connection to a specific machine + /// + public async Task ConnectToMachineAsync(string machineName, string securityKey) + { + lock (_lock) + { + _writer.Write((byte)CommandType.ConnectToMachine); + + // Write machine name (length-prefixed string) + WriteString(machineName ?? string.Empty); + + // Write security key (length-prefixed string) + WriteString(securityKey ?? string.Empty); + } + + await FlushAsync(); + } + + /// + /// Requests current state of all connected machines + /// + public async Task RequestMachineSocketStateAsync() + { + // Send command + await SendCommandAsync(CommandType.RequestMachineSocketState); + await FlushAsync(); + + // Read response + var jsonResponse = await ReadStringAsync(); + + if (string.IsNullOrEmpty(jsonResponse)) + { + return Array.Empty(); + } + + try + { + // Use source-generated JSON serialization + return JsonSerializer.Deserialize(jsonResponse, SettingsSerializationContext.Default.MachineSocketStateArray) + ?? Array.Empty(); + } + catch (JsonException ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to deserialize MachineSocketState: {ex.Message}"); + return Array.Empty(); + } + } + + /// + /// Flushes the underlying stream asynchronously + /// + public async Task FlushAsync() + { + await _stream.FlushAsync(); + } + + /// + /// Sends a simple command without parameters + /// + private Task SendCommandAsync(CommandType command) + { + lock (_lock) + { + _writer.Write((byte)command); + } + + return Task.CompletedTask; + } + + /// + /// Writes a length-prefixed UTF-8 string + /// + private void WriteString(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + _writer.Write(bytes.Length); // 4-byte length prefix + _writer.Write(bytes); + } + + /// + /// Reads a length-prefixed UTF-8 string asynchronously + /// + private async Task ReadStringAsync() + { + var lengthBytes = new byte[4]; + var bytesRead = await _stream.ReadAsync(lengthBytes.AsMemory(0, 4)); + + if (bytesRead != 4) + { + return string.Empty; + } + + var length = BitConverter.ToInt32(lengthBytes, 0); + + // Max 1MB to prevent memory exhaustion + if (length <= 0 || length > 1024 * 1024) + { + return string.Empty; + } + + var stringBytes = new byte[length]; + bytesRead = await _stream.ReadAsync(stringBytes.AsMemory(0, length)); + + if (bytesRead != length) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(stringBytes); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _writer?.Dispose(); + _reader?.Dispose(); + + // Note: Do not dispose _stream as it's owned by the caller (NamedPipeClientStream) + } +} diff --git a/src/settings-ui/Settings.UI/Helpers/TypePreservation.cs b/src/settings-ui/Settings.UI/Helpers/TypePreservation.cs new file mode 100644 index 000000000000..d516996f667c --- /dev/null +++ b/src/settings-ui/Settings.UI/Helpers/TypePreservation.cs @@ -0,0 +1,45 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Helpers; + +/// +/// Preserves types required by XAML for Native AOT compilation. +/// Called from App constructor when BUILD_INFO_PUBLISH_AOT is defined. +/// +internal static class TypePreservation +{ + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIconSource))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.PathIcon))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.SymbolIcon))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.SymbolIconSource))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ImageIcon))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.BitmapIcon))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DataTemplate))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.DataTemplateSelector))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.NavigationView))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.NavigationViewItem))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Frame))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Page))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ContentControl))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ListView))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ListViewItem))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Grid))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.StackPanel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Border))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.TextBlock))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.TextBox))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Button))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ComboBox))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.CheckBox))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ToggleSwitch))] + public static void PreserveTypes() + { + // This method exists only to hold DynamicDependency attributes. + // Called from App constructor to ensure types aren't trimmed during AOT compilation. + } +} diff --git a/src/settings-ui/Settings.UI/ILLink.Descriptors.xml b/src/settings-ui/Settings.UI/ILLink.Descriptors.xml new file mode 100644 index 000000000000..1b1963b032a5 --- /dev/null +++ b/src/settings-ui/Settings.UI/ILLink.Descriptors.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index 261ca48155f7..c544a560c8ec 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -2,6 +2,7 @@ + WinExe @@ -18,6 +19,32 @@ ..\..\..\$(Platform)\$(Configuration)\WinUI3Apps PowerToys.Settings.pri + + + true + true + true + true + + + + + Speed + none + false + + + + + true + + + + true + false + false + true + $(DefineConstants);BUILD_INFO_PUBLISH_AOT;STANDALONE @@ -85,12 +112,13 @@ - - + + + + - @@ -111,6 +139,26 @@ + + + partial + false + + + + + + + + + + + + + + + + @@ -134,10 +182,9 @@ - - + - + VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101 @@ -218,4 +265,9 @@ + + + + $(DefineConstants);BUILD_INFO_PUBLISH_AOT + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/Properties/PublishProfiles/InstallationPublishProfile.pubxml b/src/settings-ui/Settings.UI/Properties/PublishProfiles/InstallationPublishProfile.pubxml index cff222baf4b3..170f2086a1df 100644 --- a/src/settings-ui/Settings.UI/Properties/PublishProfiles/InstallationPublishProfile.pubxml +++ b/src/settings-ui/Settings.UI/Properties/PublishProfiles/InstallationPublishProfile.pubxml @@ -1,18 +1,23 @@ - + FileSystem - net9.0-windows10.0.22621.0 + net9.0-windows10.0.26100.0 10.0.19041.0 10.0.19041.0 - $(PowerToysRoot)\$(Platform)\$(Configuration)\WinUI3Apps - win-$(Platform) + C:\Users\shuaiyuan\source\repos\PowerToys\x64\Release\WinUI3Apps\Publish + win-x64 true - False - False + true + true + false + false false + Release + x64 + true - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index 7f14f1809ed3..21551f7a1b64 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -49,8 +49,12 @@ private enum Arguments private const int RequiredArgumentsLaunchedFromRunnerQty = 10; + // IPC message queue for messages sent before IPC manager is initialized + private static readonly System.Collections.Concurrent.ConcurrentQueue PendingIPCMessages = new System.Collections.Concurrent.ConcurrentQueue(); + // Create an instance of the IPC wrapper. private static TwoWayPipeMessageIPCManaged ipcmanager; + private static bool isIPCInitialized; public static bool IsElevated { get; set; } @@ -75,6 +79,9 @@ private enum Arguments /// public App() { +#if BUILD_INFO_PUBLISH_AOT + Helpers.TypePreservation.PreserveTypes(); +#endif Logger.InitializeLogger(@"\Settings\Logs"); string appLanguage = LanguageHelper.LoadLanguage(); @@ -207,19 +214,37 @@ private void OnLaunchedFromRunner(string[] cmdArgs) Environment.Exit(0); }); - ipcmanager = new TwoWayPipeMessageIPCManaged(cmdArgs[(int)Arguments.SettingsPipeName], cmdArgs[(int)Arguments.PTPipeName], (string message) => + // Initialize IPC manager asynchronously to avoid blocking window creation + string settingsPipeName = cmdArgs[(int)Arguments.SettingsPipeName]; + string ptPipeName = cmdArgs[(int)Arguments.PTPipeName]; + _ = Task.Run(() => { - if (IPCMessageReceivedCallback != null && message.Length > 0) + try { - IPCMessageReceivedCallback(message); + ipcmanager = new TwoWayPipeMessageIPCManaged(settingsPipeName, ptPipeName, (string message) => + { + if (IPCMessageReceivedCallback != null && message.Length > 0) + { + IPCMessageReceivedCallback(message); + } + }); + ipcmanager.Start(); + + // Mark as initialized and process any pending messages + isIPCInitialized = true; + ProcessPendingIPCMessages(); + + // Initialize GlobalHotkeyConflictManager after IPC is ready + GlobalHotkeyConflictManager.Initialize(message => + { + SendIPCMessage(message); + return 0; + }); + } + catch (Exception ex) + { + Logger.LogError($"Error initializing IPC manager: {ex.Message}"); } - }); - ipcmanager.Start(); - - GlobalHotkeyConflictManager.Initialize(message => - { - ipcmanager.Send(message); - return 0; }); if (!ShowOobe && !ShowScoobe) @@ -235,6 +260,9 @@ private void OnLaunchedFromRunner(string[] cmdArgs) // https://github.com/microsoft/microsoft-ui-xaml/issues/8948 - A window's top border incorrectly // renders as black on Windows 10. WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow)); + + // Warm up search index in the background to avoid delay on first search + _ = Task.Run(() => SearchIndexService.BuildIndex()); } else { @@ -243,6 +271,9 @@ private void OnLaunchedFromRunner(string[] cmdArgs) // the Settings from the tray icon. settingsWindow = new MainWindow(true); + // Warm up search index in the background + _ = Task.Run(() => SearchIndexService.BuildIndex()); + if (ShowOobe) { PowerToysTelemetry.Log.WriteEvent(new OobeStartedEvent()); @@ -290,8 +321,8 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar } else { -#if DEBUG - // For debugging purposes +#if DEBUG || STANDALONE + // For debugging purposes or standalone mode // Window is also needed to show MessageDialog settingsWindow = new MainWindow(); settingsWindow.ExtendsContentIntoTitleBar = true; @@ -299,10 +330,10 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar settingsWindow.Activate(); settingsWindow.NavigateToSection(StartupPage); - // In DEBUG mode, we might not have IPC set up, so provide a dummy implementation + // In DEBUG/STANDALONE mode, we might not have IPC set up, so provide a dummy implementation GlobalHotkeyConflictManager.Initialize(message => { - // In debug mode, just log or do nothing + // In standalone mode, just log or do nothing System.Diagnostics.Debug.WriteLine($"IPC Message: {message}"); return 0; }); @@ -319,6 +350,33 @@ public static TwoWayPipeMessageIPCManaged GetTwoWayIPCManager() return ipcmanager; } + /// + /// Sends an IPC message, queuing it if the IPC manager is not yet initialized. + /// + public static void SendIPCMessage(string message) + { + if (isIPCInitialized && ipcmanager != null) + { + ipcmanager.Send(message); + } + else + { + // Queue the message to be sent after IPC manager is initialized + PendingIPCMessages.Enqueue(message); + } + } + + /// + /// Process all pending IPC messages after the IPC manager is initialized. + /// + private static void ProcessPendingIPCMessages() + { + while (PendingIPCMessages.TryDequeue(out string message)) + { + ipcmanager?.Send(message); + } + } + public static bool IsDarkTheme() { return ThemeService.Theme == ElementTheme.Dark || (ThemeService.Theme == ElementTheme.Default && ThemeHelpers.GetAppTheme() == AppTheme.Dark); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs index e85633f9e8f0..a66f0bb4d1d4 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs @@ -27,133 +27,177 @@ public MainWindow(bool createHidden = false) var bootTime = new System.Diagnostics.Stopwatch(); bootTime.Start(); - this.Activated += Window_Activated_SetIcon; + // Initialize UI components immediately for faster visual feedback + this.InitializeComponent(); + this.ExtendsContentIntoTitleBar = true; + SetAppTitleBar(); + // Set up critical event handlers + this.Activated += Window_Activated_SetIcon; App.ThemeService.ThemeChanged += OnThemeChanged; - App.ThemeService.ApplyTheme(); - - this.ExtendsContentIntoTitleBar = true; + // Set elevation status immediately (required for UI) ShellPage.SetElevationStatus(App.IsElevated); ShellPage.SetIsUserAnAdmin(App.IsUserAnAdmin); + // Apply theme immediately + App.ThemeService.ApplyTheme(); + + // Set window title immediately + var loader = ResourceLoaderInstance.ResourceLoader; + Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title"); + + // Handle window visibility var hWnd = WindowNative.GetWindowHandle(this); - var placement = WindowHelper.DeserializePlacementOrDefault(hWnd); if (createHidden) { - placement.ShowCmd = NativeMethods.SW_HIDE; + var placement = new WINDOWPLACEMENT + { + ShowCmd = NativeMethods.SW_HIDE, + }; + NativeMethods.SetWindowPlacement(hWnd, ref placement); // Restore the last known placement on the first activation this.Activated += Window_Activated; } - NativeMethods.SetWindowPlacement(hWnd, ref placement); - - var loader = ResourceLoaderInstance.ResourceLoader; - Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title"); - - // send IPC Message - ShellPage.SetDefaultSndMessageCallback(msg => - { - // IPC Manager is null when launching runner directly - App.GetTwoWayIPCManager()?.Send(msg); - }); - - // send IPC Message - ShellPage.SetRestartAdminSndMessageCallback(msg => - { - App.GetTwoWayIPCManager()?.Send(msg); - Environment.Exit(0); // close application - }); + // Initialize remaining components asynchronously + _ = InitializeAsync(hWnd, createHidden, bootTime); + } - // send IPC Message - ShellPage.SetCheckForUpdatesMessageCallback(msg => + private async Task InitializeAsync(IntPtr hWnd, bool createHidden, System.Diagnostics.Stopwatch bootTime) + { + try { - App.GetTwoWayIPCManager()?.Send(msg); - }); + // Load window placement asynchronously (non-blocking file I/O) + if (!createHidden) + { + await Task.Run(() => + { + var placement = WindowHelper.DeserializePlacementOrDefault(hWnd); + NativeMethods.SetWindowPlacement(hWnd, ref placement); + }); + } - // open main window - ShellPage.SetOpenMainWindowCallback(type => - { + // Set up IPC callbacks on UI thread DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => - App.OpenSettingsWindow(type)); - }); - - // open main window - ShellPage.SetUpdatingGeneralSettingsCallback((ModuleType moduleType, bool isEnabled) => - { - SettingsRepository repository = SettingsRepository.GetInstance(SettingsUtils.Default); - GeneralSettings generalSettingsConfig = repository.SettingsConfig; - bool needToUpdate = ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType) != isEnabled; - - if (needToUpdate) { - ModuleHelper.SetIsModuleEnabled(generalSettingsConfig, moduleType, isEnabled); - var outgoing = new OutGoingGeneralSettings(generalSettingsConfig); + // send IPC Message + ShellPage.SetDefaultSndMessageCallback(msg => + { + // Use SendIPCMessage which handles queuing if IPC is not yet initialized + App.SendIPCMessage(msg); + }); - // Save settings to file - SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString()); + // send IPC Message + ShellPage.SetRestartAdminSndMessageCallback(msg => + { + App.SendIPCMessage(msg); + Environment.Exit(0); // close application + }); - // Send IPC message asynchronously to avoid blocking UI and potential recursive calls - Task.Run(() => + // send IPC Message + ShellPage.SetCheckForUpdatesMessageCallback(msg => { - ShellPage.SendDefaultIPCMessage(outgoing.ToString()); + App.SendIPCMessage(msg); }); - ShellPage.ShellHandler?.SignalGeneralDataUpdate(); - } + // open main window + ShellPage.SetOpenMainWindowCallback(type => + { + DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => + App.OpenSettingsWindow(type)); + }); - return needToUpdate; - }); + // open main window + ShellPage.SetUpdatingGeneralSettingsCallback((ModuleType moduleType, bool isEnabled) => + { + SettingsRepository repository = SettingsRepository.GetInstance(SettingsUtils.Default); + GeneralSettings generalSettingsConfig = repository.SettingsConfig; + bool needToUpdate = ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType) != isEnabled; - // open oobe - ShellPage.SetOpenOobeCallback(() => - { - if (App.GetOobeWindow() == null) - { - App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.Overview)); - } + if (needToUpdate) + { + ModuleHelper.SetIsModuleEnabled(generalSettingsConfig, moduleType, isEnabled); + var outgoing = new OutGoingGeneralSettings(generalSettingsConfig); - App.GetOobeWindow().Activate(); - }); + // Save settings to file + SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString()); - // open whats new window - ShellPage.SetOpenWhatIsNewCallback(() => - { - if (App.GetScoobeWindow() == null) - { - App.SetScoobeWindow(new ScoobeWindow()); - } + // Send IPC message asynchronously to avoid blocking UI and potential recursive calls + Task.Run(() => + { + ShellPage.SendDefaultIPCMessage(outgoing.ToString()); + }); - App.GetScoobeWindow().Activate(); - }); + ShellPage.ShellHandler?.SignalGeneralDataUpdate(); + } - this.InitializeComponent(); - SetAppTitleBar(); + return needToUpdate; + }); - // receive IPC Message - App.IPCMessageReceivedCallback = (string msg) => - { - if (ShellPage.ShellHandler.IPCResponseHandleList != null) - { - var success = JsonObject.TryParse(msg, out JsonObject json); - if (success) + // open oobe + ShellPage.SetOpenOobeCallback(() => { - foreach (Action handle in ShellPage.ShellHandler.IPCResponseHandleList) + if (App.GetOobeWindow() == null) { - handle(json); + App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.Overview)); } - } - else + + App.GetOobeWindow().Activate(); + }); + + // open whats new window + ShellPage.SetOpenWhatIsNewCallback(() => { - Logger.LogError("Failed to parse JSON from IPC message."); - } - } - }; + if (App.GetScoobeWindow() == null) + { + App.SetScoobeWindow(new ScoobeWindow()); + } - bootTime.Stop(); + App.GetScoobeWindow().Activate(); + }); - PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds }); + // receive IPC Message + App.IPCMessageReceivedCallback = (string msg) => + { + // Ignore empty or whitespace-only messages + if (string.IsNullOrWhiteSpace(msg)) + { + return; + } + + if (ShellPage.ShellHandler.IPCResponseHandleList != null) + { + var success = JsonObject.TryParse(msg, out JsonObject json); + if (success) + { + foreach (Action handle in ShellPage.ShellHandler.IPCResponseHandleList) + { + handle(json); + } + } + else + { + // Log with message preview for debugging (limit to 100 chars to avoid log spam) + var msgPreview = msg.Length > 100 ? string.Concat(msg.AsSpan(0, 100), "...") : msg; + Logger.LogError($"Failed to parse JSON from IPC message. Message preview: {msgPreview}"); + } + } + }; + }); + + // Record telemetry asynchronously + bootTime.Stop(); + await Task.Run(() => + { + PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds }); + }); + } + catch (Exception ex) + { + Logger.LogError($"Error during async initialization: {ex.Message}"); + } } private void SetAppTitleBar() diff --git a/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs index 05aec49c9a8d..6e8530c2f747 100644 --- a/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs @@ -16,7 +16,6 @@ using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.SerializationContext; -using Newtonsoft.Json.Linq; using PowerToys.GPOWrapper; using Settings.UI.Library; using Settings.UI.Library.Helpers; diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs index 447c62a6dd3e..36290b75368c 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs @@ -24,7 +24,10 @@ using Microsoft.UI; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media; +#if !BUILD_INFO_PUBLISH_AOT using StreamJsonRpc; +using Newtonsoft.Json; +#endif using Windows.ApplicationModel.DataTransfer; namespace Microsoft.PowerToys.Settings.UI.ViewModels @@ -235,20 +238,8 @@ public bool IsEnabledGpoConfigured get => _enabledStateIsGPOConfigured; } - private enum SocketStatus : int - { - NA = 0, - Resolving = 1, - Connecting = 2, - Handshaking = 3, - Error = 4, - ForceClosed = 5, - InvalidKey = 6, - Timeout = 7, - SendError = 8, - Connected = 9, - } - + // SocketStatus enum is now defined in Settings.UI.Library\MouseWithoutBordersIpcModels.cs +#if !BUILD_INFO_PUBLISH_AOT private interface ISettingsSyncHelper { [Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)] @@ -274,13 +265,73 @@ public struct MachineSocketState Task RequestMachineSocketStateAsync(); } +#endif private static CancellationTokenSource _cancellationTokenSource; private static Task _machinePollingThreadTask; - private static VisualStudio.Threading.AsyncSemaphore _ipcSemaphore = new VisualStudio.Threading.AsyncSemaphore(1); + private static SemaphoreSlim _ipcSemaphore = new SemaphoreSlim(1, 1); + private static NamedPipeClientStream syncHelperStream; + +#if BUILD_INFO_PUBLISH_AOT + // AOT-compatible IPC client wrapper + private sealed partial class SyncHelper : IDisposable + { + public SyncHelper(NamedPipeClientStream stream) + { + Stream = stream; + Client = new MouseWithoutBordersIpcClient(stream); + } + + public NamedPipeClientStream Stream { get; } + + public MouseWithoutBordersIpcClient Client { get; private set; } + + public void Dispose() + { + Client?.Dispose(); + } + } + + private async Task GetSettingsSyncHelperAsync() + { + try + { + var recreateStream = false; + if (syncHelperStream == null) + { + recreateStream = true; + } + else + { + if (!syncHelperStream.IsConnected || !syncHelperStream.CanWrite) + { + await syncHelperStream.DisposeAsync(); + recreateStream = true; + } + } + + if (recreateStream) + { + syncHelperStream = new NamedPipeClientStream(".", "MouseWithoutBorders/SettingsSync", PipeDirection.InOut, PipeOptions.Asynchronous); + await syncHelperStream.ConnectAsync(10000); + } + + return new SyncHelper(syncHelperStream); + } + catch (Exception ex) + { + if (IsEnabled) + { + Logger.LogError($"Couldn't create SettingsSync (AOT): {ex}"); + } + return null; + } + } +#else + // StreamJsonRpc-based IPC client wrapper (non-AOT builds) private sealed partial class SyncHelper : IDisposable { public SyncHelper(NamedPipeClientStream stream) @@ -299,8 +350,6 @@ public void Dispose() } } - private static NamedPipeClientStream syncHelperStream; - private async Task GetSettingsSyncHelperAsync() { try @@ -337,88 +386,154 @@ private async Task GetSettingsSyncHelperAsync() return null; } } +#endif public async Task SubmitShutdownRequestAsync() { - using (await _ipcSemaphore.EnterAsync()) + await _ipcSemaphore.WaitAsync(); + try { using (var syncHelper = await GetSettingsSyncHelperAsync()) { - syncHelper?.Endpoint?.Shutdown(); - var task = syncHelper?.Stream.FlushAsync(); - if (task != null) + if (syncHelper != null) { - await task; +#if BUILD_INFO_PUBLISH_AOT + await syncHelper.Client.ShutdownAsync(); +#else + syncHelper.Endpoint?.Shutdown(); + var task = syncHelper.Stream.FlushAsync(); + if (task != null) + { + await task; + } +#endif } } } + finally + { + _ipcSemaphore.Release(); + } } public async Task SubmitReconnectRequestAsync() { - using (await _ipcSemaphore.EnterAsync()) + await _ipcSemaphore.WaitAsync(); + try { using (var syncHelper = await GetSettingsSyncHelperAsync()) { - syncHelper?.Endpoint?.Reconnect(); - var task = syncHelper?.Stream.FlushAsync(); - if (task != null) + if (syncHelper != null) { - await task; +#if BUILD_INFO_PUBLISH_AOT + await syncHelper.Client.ReconnectAsync(); +#else + syncHelper.Endpoint?.Reconnect(); + var task = syncHelper.Stream.FlushAsync(); + if (task != null) + { + await task; + } +#endif } } } + finally + { + _ipcSemaphore.Release(); + } } public async Task SubmitNewKeyRequestAsync() { - using (await _ipcSemaphore.EnterAsync()) + await _ipcSemaphore.WaitAsync(); + try { using (var syncHelper = await GetSettingsSyncHelperAsync()) { - syncHelper?.Endpoint?.GenerateNewKey(); - var task = syncHelper?.Stream.FlushAsync(); - if (task != null) + if (syncHelper != null) { - await task; +#if BUILD_INFO_PUBLISH_AOT + await syncHelper.Client.GenerateNewKeyAsync(); +#else + syncHelper.Endpoint?.GenerateNewKey(); + var task = syncHelper.Stream.FlushAsync(); + if (task != null) + { + await task; + } +#endif } } } + finally + { + _ipcSemaphore.Release(); + } } public async Task SubmitConnectionRequestAsync(string pcName, string securityKey) { - using (await _ipcSemaphore.EnterAsync()) + await _ipcSemaphore.WaitAsync(); + try { using (var syncHelper = await GetSettingsSyncHelperAsync()) { - syncHelper?.Endpoint?.ConnectToMachine(pcName, securityKey); - var task = syncHelper?.Stream.FlushAsync(); - if (task != null) + if (syncHelper != null) { - await task; +#if BUILD_INFO_PUBLISH_AOT + await syncHelper.Client.ConnectToMachineAsync(pcName, securityKey); +#else + syncHelper.Endpoint?.ConnectToMachine(pcName, securityKey); + var task = syncHelper.Stream.FlushAsync(); + if (task != null) + { + await task; + } +#endif } } } + finally + { + _ipcSemaphore.Release(); + } } - private async Task PollMachineSocketStateAsync() + private async Task PollMachineSocketStateAsync() { - using (await _ipcSemaphore.EnterAsync()) + await _ipcSemaphore.WaitAsync(); + try { using (var syncHelper = await GetSettingsSyncHelperAsync()) { - var task = syncHelper?.Endpoint?.RequestMachineSocketStateAsync(); - if (task != null) - { - return await task; - } - else + if (syncHelper != null) { - return null; +#if BUILD_INFO_PUBLISH_AOT + return await syncHelper.Client.RequestMachineSocketStateAsync(); +#else + var task = syncHelper.Endpoint?.RequestMachineSocketStateAsync(); + if (task != null) + { + var oldStates = await task; + + // Convert from ISettingsSyncHelper.MachineSocketState to MachineSocketState + return oldStates.Select(s => new MachineSocketState + { + Name = s.Name, + Status = (SocketStatus)s.Status, + }).ToArray(); + } +#endif } + + return Array.Empty(); } } + finally + { + _ipcSemaphore.Release(); + } } private MouseWithoutBordersSettings Settings { get; set; } @@ -464,14 +579,14 @@ private Task StartMachineStatusPollingThread(Task previousThreadTask, Cancellati while (!token.IsCancellationRequested) { - Dictionary states = null; + Dictionary states = null; try { states = (await PollMachineSocketStateAsync())?.ToDictionary(s => s.Name, StringComparer.OrdinalIgnoreCase); } catch (Exception ex) { - Logger.LogInfo($"Poll ISettingsSyncHelper.MachineSocketState error: {ex}"); + Logger.LogInfo($"Poll MachineSocketState error: {ex}"); continue; }