Compare commits

..

1 Commits

Author SHA1 Message Date
Shawn Yuan (from Dev Box)
3289fb9917 fix ap crash issue 2026-01-30 09:24:03 +08:00
24 changed files with 324 additions and 1255 deletions

View File

@@ -8,9 +8,6 @@
<!-- Suppress DynamicallyAccessedMemberTypes.PublicParameterlessConstructor in fallback code path of Windows SDK projection -->
<!-- Suppress CA1416 for Windows-specific APIs that are used in PowerToys which only runs on Windows 10.0.19041.0+ -->
<!-- Suppress IL2026/IL3050 for JSON serialization in specific scenarios (backup/restore, CLI commands) -->
<!-- Suppress IL2067/IL2070/IL2072/IL2075/IL2087/IL2098 for reflection in CLI/DSC command utilities -->
<!-- Suppress IL3000/IL3002 for Assembly.Location and Marshal.GetHINSTANCE in single-file/AOT scenarios -->
<WarningsNotAsErrors>IL2026;IL2067;IL2070;IL2072;IL2075;IL2081;IL2087;IL2098;IL3000;IL3002;IL3050;CsWinRT1028;CA1416;$(WarningsNotAsErrors)</WarningsNotAsErrors>
<WarningsNotAsErrors>IL2081;CsWinRT1028;CA1416;$(WarningsNotAsErrors)</WarningsNotAsErrors>
</PropertyGroup>
</Project>

View File

@@ -1,170 +0,0 @@
// 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;
/// <summary>
/// Command types for IPC protocol.
/// Must match client-side enum in Settings.UI\Helpers\MouseWithoutBordersIpcClient.cs
/// </summary>
internal enum IpcCommandType : byte
{
Shutdown = 1,
Reconnect = 2,
GenerateNewKey = 3,
ConnectToMachine = 4,
RequestMachineSocketState = 5,
}
/// <summary>
/// AOT-compatible IPC server for MouseWithoutBorders Settings communication.
/// Replaces StreamJsonRpc with manual NamedPipe protocol.
/// </summary>
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));
}
/// <summary>
/// Handles a single client connection
/// </summary>
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}");
}
}
/// <summary>
/// Reads a length-prefixed UTF-8 string
/// </summary>
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);
}
/// <summary>
/// Writes a length-prefixed UTF-8 string
/// </summary>
private static void WriteString(BinaryWriter writer, string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
writer.Write(bytes.Length);
writer.Write(bytes);
}
}
/// <summary>
/// Interface for handling IPC commands.
/// Implemented by SettingsSyncHelper in Program.cs
/// </summary>
internal interface ISettingsSyncHandler
{
void Shutdown();
void Reconnect();
void GenerateNewKey();
void ConnectToMachine(string machineName, string securityKey);
Task<MachineSocketState[]> RequestMachineSocketStateAsync();
}
/// <summary>
/// Machine socket state for serialization.
/// Uses SocketStatus from SocketStuff.cs in MouseWithoutBorders.Class namespace.
/// </summary>
public struct MachineSocketState
{
public string Name { get; set; }
public MouseWithoutBorders.Class.SocketStatus Status { get; set; }
}

View File

@@ -19,7 +19,6 @@ using System.Globalization;
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;
@@ -277,7 +276,7 @@ namespace MouseWithoutBorders.Class
Task<MachineSocketState[]> RequestMachineSocketStateAsync();
}
private sealed class SettingsSyncHelper : ISettingsSyncHelper, ISettingsSyncHandler
private sealed class SettingsSyncHelper : ISettingsSyncHelper
{
public Task<ISettingsSyncHelper.MachineSocketState[]> RequestMachineSocketStateAsync()
{
@@ -300,28 +299,6 @@ namespace MouseWithoutBorders.Class
return Task.FromResult(machineStates.Select((state) => new ISettingsSyncHelper.MachineSocketState { Name = state.Key, Status = state.Value }).ToArray());
}
// ISettingsSyncHandler implementation (AOT-compatible)
Task<MachineSocketState[]> ISettingsSyncHandler.RequestMachineSocketStateAsync()
{
var machineStates = new Dictionary<string, SocketStatus>();
if (Common.Sk == null || Common.Sk.TcpSockets == null)
{
return Task.FromResult(Array.Empty<MachineSocketState>());
}
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;
@@ -402,64 +379,7 @@ namespace MouseWithoutBorders.Class
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<SettingsSyncHelper>.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()

View File

@@ -51,11 +51,7 @@ using Thread = MouseWithoutBorders.Core.Thread;
namespace MouseWithoutBorders.Class
{
/// <summary>
/// Socket status enumeration - made public for IPC serialization.
/// Must match Settings.UI.Library\MouseWithoutBordersIpcModels.cs
/// </summary>
public enum SocketStatus : int
internal enum SocketStatus : int
{
NA = 0,
Resolving = 1,

View File

@@ -24,7 +24,6 @@ using MouseWithoutBorders.Class;
using MouseWithoutBorders.Exceptions;
using Clipboard = MouseWithoutBorders.Core.Clipboard;
using SocketStatus = MouseWithoutBorders.Class.SocketStatus;
using Thread = MouseWithoutBorders.Core.Thread;
// Log is enough

View File

@@ -1,11 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\Common.SelfContained.props" />
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup Condition="'$(EnableSettingsAOT)' == 'true'">
<PublishAot>true</PublishAot>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>

View File

@@ -2,13 +2,12 @@
// 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<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T> : ICmdLineRepresentable
public class GenericProperty<T> : ICmdLineRepresentable
{
[JsonPropertyName("value")]
public T Value { get; set; }

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
@@ -18,7 +17,7 @@ public interface ICmdLineRepresentable
public abstract bool TryToCmdRepresentable(out string result);
public static sealed bool TryToCmdRepresentableFor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, object value, out string result)
public static sealed bool TryToCmdRepresentableFor(Type type, object value, out string result)
{
result = null;
if (!typeof(ICmdLineRepresentable).IsAssignableFrom(type))
@@ -37,7 +36,7 @@ public interface ICmdLineRepresentable
return false;
}
public static sealed bool TryParseFromCmdFor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, string cmd, out object result)
public static sealed bool TryParseFromCmdFor(Type type, string cmd, out object result)
{
result = null;
if (!typeof(ICmdLineRepresentable).IsAssignableFrom(type))
@@ -56,7 +55,7 @@ public interface ICmdLineRepresentable
return false;
}
public static sealed object ParseFor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, string cmdRepr)
public static sealed object ParseFor(Type type, string cmdRepr)
{
if (type.IsEnum)
{
@@ -99,7 +98,7 @@ public interface ICmdLineRepresentable
throw new NotImplementedException($"Parsing type {type} is not supported yet");
}
public static string ToCmdRepr([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, object value)
public static string ToCmdRepr(Type type, object value)
{
if (type.IsEnum || type.IsPrimitive)
{

View File

@@ -1,40 +0,0 @@
// 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;
/// <summary>
/// Socket status enumeration for MouseWithoutBorders machine connections.
/// Must match the enum in MouseWithoutBorders\App\Class\Program.cs
/// </summary>
public enum SocketStatus : int
{
NA = 0,
Resolving = 1,
Connecting = 2,
Handshaking = 3,
Error = 4,
ForceClosed = 5,
InvalidKey = 6,
Timeout = 7,
SendError = 8,
Connected = 9,
}
/// <summary>
/// Represents the connection state of a machine in the MouseWithoutBorders network.
/// Used for IPC communication between Settings UI and MouseWithoutBorders service.
/// </summary>
public struct MachineSocketState
{
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("Status")]
public SocketStatus Status { get; set; }
}

View File

@@ -2,7 +2,6 @@
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\Common.SelfContained.props" />
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<Description>PowerToys Settings UI Library</Description>
<AssemblyName>PowerToys.Settings.UI.Lib</AssemblyName>

View File

@@ -1,9 +1,11 @@
// 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;
@@ -11,111 +13,88 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Services
{
/// <summary>
/// AOT-compatible factory service for PowerToys module Settings.
/// Uses static type registration instead of reflection-based discovery.
/// Factory service for getting PowerToys module Settings that implement IHotkeyConfig
/// </summary>
/// <remarks>
/// When adding a new PowerToys module, add it to both InitializeFactories() and InitializeTypes() methods.
/// </remarks>
public class SettingsFactory
{
private readonly SettingsUtils _settingsUtils;
private readonly Dictionary<string, Func<IHotkeyConfig>> _settingsFactories;
private readonly Dictionary<string, Type> _settingsTypes;
public SettingsFactory(SettingsUtils settingsUtils)
{
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
_settingsFactories = InitializeFactories();
_settingsTypes = InitializeTypes();
_settingsTypes = DiscoverSettingsTypes();
}
/// <summary>
/// Static registry of all module settings factories.
/// IMPORTANT: When adding a new module, add it here.
/// Dynamically discovers all Settings types that implement IHotkeyConfig
/// </summary>
private Dictionary<string, Func<IHotkeyConfig>> InitializeFactories()
private Dictionary<string, Type> DiscoverSettingsTypes()
{
return new Dictionary<string, Func<IHotkeyConfig>>
{
["GeneralSettings"] = () => SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils).SettingsConfig,
["AdvancedPaste"] = () => SettingsRepository<AdvancedPasteSettings>.GetInstance(_settingsUtils).SettingsConfig,
["AlwaysOnTop"] = () => SettingsRepository<AlwaysOnTopSettings>.GetInstance(_settingsUtils).SettingsConfig,
["ColorPicker"] = () => SettingsRepository<ColorPickerSettings>.GetInstance(_settingsUtils).SettingsConfig,
["CropAndLock"] = () => SettingsRepository<CropAndLockSettings>.GetInstance(_settingsUtils).SettingsConfig,
["CursorWrap"] = () => SettingsRepository<CursorWrapSettings>.GetInstance(_settingsUtils).SettingsConfig,
["FindMyMouse"] = () => SettingsRepository<FindMyMouseSettings>.GetInstance(_settingsUtils).SettingsConfig,
["LightSwitch"] = () => SettingsRepository<LightSwitchSettings>.GetInstance(_settingsUtils).SettingsConfig,
["MeasureTool"] = () => SettingsRepository<MeasureToolSettings>.GetInstance(_settingsUtils).SettingsConfig,
["MouseHighlighter"] = () => SettingsRepository<MouseHighlighterSettings>.GetInstance(_settingsUtils).SettingsConfig,
["MouseJump"] = () => SettingsRepository<MouseJumpSettings>.GetInstance(_settingsUtils).SettingsConfig,
["MousePointerCrosshairs"] = () => SettingsRepository<MousePointerCrosshairsSettings>.GetInstance(_settingsUtils).SettingsConfig,
["MouseWithoutBorders"] = () => SettingsRepository<MouseWithoutBordersSettings>.GetInstance(_settingsUtils).SettingsConfig,
["Peek"] = () => SettingsRepository<PeekSettings>.GetInstance(_settingsUtils).SettingsConfig,
["PowerLauncher"] = () => SettingsRepository<PowerLauncherSettings>.GetInstance(_settingsUtils).SettingsConfig,
["PowerOCR"] = () => SettingsRepository<PowerOcrSettings>.GetInstance(_settingsUtils).SettingsConfig,
["ShortcutGuide"] = () => SettingsRepository<ShortcutGuideSettings>.GetInstance(_settingsUtils).SettingsConfig,
["Workspaces"] = () => SettingsRepository<WorkspacesSettings>.GetInstance(_settingsUtils).SettingsConfig,
};
}
var settingsTypes = new Dictionary<string, Type>();
/// <summary>
/// Static registry of module name to settings type mapping.
/// IMPORTANT: When adding a new module, add it here.
/// </summary>
private Dictionary<string, Type> InitializeTypes()
{
return new Dictionary<string, Type>
// Get the Settings.UI.Library assembly
var assembly = Assembly.GetAssembly(typeof(IHotkeyConfig));
if (assembly == null)
{
["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),
};
}
/// <summary>
/// Gets a settings instance for the specified module using SettingsRepository.
/// AOT-compatible: uses static factory lookup instead of reflection.
/// </summary>
/// <param name="moduleKey">The module key/name</param>
/// <returns>The settings instance implementing IHotkeyConfig, or null if not found</returns>
public IHotkeyConfig GetSettings(string moduleKey)
{
if (!_settingsFactories.TryGetValue(moduleKey, out var factory))
{
return null;
return settingsTypes;
}
try
{
return factory();
// 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";
}
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)
{
System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}");
return null;
System.Diagnostics.Debug.WriteLine($"Error scanning assembly {assembly.FullName}: {ex.Message}");
}
return settingsTypes;
}
/// <summary>
/// Gets fresh settings from disk for the specified module.
/// AOT-compatible: uses static type dispatch instead of MakeGenericMethod.
/// </summary>
public IHotkeyConfig GetFreshSettings(string moduleKey)
{
if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType))
@@ -125,8 +104,20 @@ namespace Microsoft.PowerToys.Settings.UI.Services
try
{
string actualModuleKey = moduleKey == "GeneralSettings" ? string.Empty : moduleKey;
return GetFreshSettingsForType(settingsType, actualModuleKey);
// Create a generic method call to _settingsUtils.GetSettingsOrDefault<T>(moduleKey)
var getSettingsMethod = typeof(SettingsUtils).GetMethod("GetSettingsOrDefault", new[] { typeof(string), typeof(string) });
var genericMethod = getSettingsMethod?.MakeGenericMethod(settingsType);
// Call GetSettingsOrDefault<T>(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;
}
catch (Exception ex)
{
@@ -136,33 +127,35 @@ namespace Microsoft.PowerToys.Settings.UI.Services
}
/// <summary>
/// Static dispatch for GetSettingsOrDefault using pattern matching.
/// Replaces reflection-based MakeGenericMethod/Invoke pattern.
/// Gets a settings instance for the specified module using SettingsRepository
/// </summary>
private IHotkeyConfig GetFreshSettingsForType(Type settingsType, string moduleKey)
/// <param name="moduleKey">The module key/name</param>
/// <returns>The settings instance implementing IHotkeyConfig, or null if not found</returns>
public IHotkeyConfig GetSettings(string moduleKey)
{
return settingsType.Name switch
if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType))
{
nameof(GeneralSettings) => _settingsUtils.GetSettingsOrDefault<GeneralSettings>(moduleKey, "settings.json"),
nameof(AdvancedPasteSettings) => _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(moduleKey, "settings.json"),
nameof(AlwaysOnTopSettings) => _settingsUtils.GetSettingsOrDefault<AlwaysOnTopSettings>(moduleKey, "settings.json"),
nameof(ColorPickerSettings) => _settingsUtils.GetSettingsOrDefault<ColorPickerSettings>(moduleKey, "settings.json"),
nameof(CropAndLockSettings) => _settingsUtils.GetSettingsOrDefault<CropAndLockSettings>(moduleKey, "settings.json"),
nameof(CursorWrapSettings) => _settingsUtils.GetSettingsOrDefault<CursorWrapSettings>(moduleKey, "settings.json"),
nameof(FindMyMouseSettings) => _settingsUtils.GetSettingsOrDefault<FindMyMouseSettings>(moduleKey, "settings.json"),
nameof(LightSwitchSettings) => _settingsUtils.GetSettingsOrDefault<LightSwitchSettings>(moduleKey, "settings.json"),
nameof(MeasureToolSettings) => _settingsUtils.GetSettingsOrDefault<MeasureToolSettings>(moduleKey, "settings.json"),
nameof(MouseHighlighterSettings) => _settingsUtils.GetSettingsOrDefault<MouseHighlighterSettings>(moduleKey, "settings.json"),
nameof(MouseJumpSettings) => _settingsUtils.GetSettingsOrDefault<MouseJumpSettings>(moduleKey, "settings.json"),
nameof(MousePointerCrosshairsSettings) => _settingsUtils.GetSettingsOrDefault<MousePointerCrosshairsSettings>(moduleKey, "settings.json"),
nameof(MouseWithoutBordersSettings) => _settingsUtils.GetSettingsOrDefault<MouseWithoutBordersSettings>(moduleKey, "settings.json"),
nameof(PeekSettings) => _settingsUtils.GetSettingsOrDefault<PeekSettings>(moduleKey, "settings.json"),
nameof(PowerLauncherSettings) => _settingsUtils.GetSettingsOrDefault<PowerLauncherSettings>(moduleKey, "settings.json"),
nameof(PowerOcrSettings) => _settingsUtils.GetSettingsOrDefault<PowerOcrSettings>(moduleKey, "settings.json"),
nameof(ShortcutGuideSettings) => _settingsUtils.GetSettingsOrDefault<ShortcutGuideSettings>(moduleKey, "settings.json"),
nameof(WorkspacesSettings) => _settingsUtils.GetSettingsOrDefault<WorkspacesSettings>(moduleKey, "settings.json"),
_ => null,
};
return null;
}
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;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}");
}
return null;
}
/// <summary>
@@ -171,7 +164,7 @@ namespace Microsoft.PowerToys.Settings.UI.Services
/// <returns>List of module names</returns>
public List<string> GetAvailableModuleNames()
{
return new List<string>(_settingsTypes.Keys);
return _settingsTypes.Keys.ToList();
}
/// <summary>

View File

@@ -2,7 +2,6 @@
// 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;
@@ -168,15 +167,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(SndModuleSettings<SndPowerRenameSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndShortcutGuideSettings>))]
// 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
{
}

View File

@@ -1,181 +1,69 @@
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library;
/// <summary>
/// AOT-compatible command line utilities.
/// Uses static type mapping instead of AppDomain reflection.
/// </summary>
public class CommandLineUtils
{
private static readonly Dictionary<string, Type> _settingsTypes = new()
private static Type GetSettingsConfigType(string moduleName, Assembly settingsLibraryAssembly)
{
["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 = null)
{
if (!_settingsTypes.TryGetValue(moduleName, out var settingsType))
{
return null;
}
return GetSettingsConfigFor(settingsType, settingsUtils);
var settingsClassName = moduleName == "GeneralSettings" ? moduleName : moduleName + "Settings";
return settingsLibraryAssembly.GetType(typeof(CommandLineUtils).Namespace + "." + settingsClassName);
}
/// <summary>
/// Gets settings config for a given type using static dispatch.
/// AOT-compatible: replaces MakeGenericType/GetMethod/Invoke pattern.
/// </summary>
public static ISettingsConfig GetSettingsConfigFor(string moduleName, SettingsUtils settingsUtils, Assembly settingsLibraryAssembly)
{
return GetSettingsConfigFor(GetSettingsConfigType(moduleName, settingsLibraryAssembly), settingsUtils);
}
/// Executes SettingsRepository<moduleSettingsType>.GetInstance(settingsUtils).SettingsConfig
public static ISettingsConfig GetSettingsConfigFor(Type moduleSettingsType, SettingsUtils settingsUtils)
{
return moduleSettingsType.Name switch
{
nameof(GeneralSettings) => SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(AdvancedPasteSettings) => SettingsRepository<AdvancedPasteSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(AlwaysOnTopSettings) => SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(AwakeSettings) => SettingsRepository<AwakeSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(CmdNotFoundSettings) => SettingsRepository<CmdNotFoundSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(ColorPickerSettings) => SettingsRepository<ColorPickerSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(CropAndLockSettings) => SettingsRepository<CropAndLockSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(CursorWrapSettings) => SettingsRepository<CursorWrapSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(EnvironmentVariablesSettings) => SettingsRepository<EnvironmentVariablesSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(FancyZonesSettings) => SettingsRepository<FancyZonesSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(FileLocksmithSettings) => SettingsRepository<FileLocksmithSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(FindMyMouseSettings) => SettingsRepository<FindMyMouseSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(HostsSettings) => SettingsRepository<HostsSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(ImageResizerSettings) => SettingsRepository<ImageResizerSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(KeyboardManagerSettings) => SettingsRepository<KeyboardManagerSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(LightSwitchSettings) => SettingsRepository<LightSwitchSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(MeasureToolSettings) => SettingsRepository<MeasureToolSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(MouseHighlighterSettings) => SettingsRepository<MouseHighlighterSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(MouseJumpSettings) => SettingsRepository<MouseJumpSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(MousePointerCrosshairsSettings) => SettingsRepository<MousePointerCrosshairsSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(MouseWithoutBordersSettings) => SettingsRepository<MouseWithoutBordersSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(NewPlusSettings) => SettingsRepository<NewPlusSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(PeekSettings) => SettingsRepository<PeekSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(PowerAccentSettings) => SettingsRepository<PowerAccentSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(PowerLauncherSettings) => SettingsRepository<PowerLauncherSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(PowerOcrSettings) => SettingsRepository<PowerOcrSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(PowerRenameSettings) => SettingsRepository<PowerRenameSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(PowerPreviewSettings) => SettingsRepository<PowerPreviewSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(RegistryPreviewSettings) => SettingsRepository<RegistryPreviewSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(ShortcutGuideSettings) => SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(WorkspacesSettings) => SettingsRepository<WorkspacesSettings>.GetInstance(settingsUtils).SettingsConfig,
nameof(ZoomItSettings) => SettingsRepository<ZoomItSettings>.GetInstance(settingsUtils).SettingsConfig,
_ => null,
};
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<GeneralSettings>.GetInstance));
var settingsRepository = getInstanceInfo.Invoke(null, new object[] { settingsUtils });
var settingsConfigProperty = getInstanceInfo.ReturnType.GetProperty(nameof(SettingsRepository<GeneralSettings>.SettingsConfig));
return settingsConfigProperty.GetValue(settingsRepository) as ISettingsConfig;
}
public static Assembly GetSettingsAssembly()
{
return AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "PowerToys.Settings.UI.Lib");
}
public static object GetPropertyValue(string propertyName, ISettingsConfig settingsConfig)
{
var (settingInfo, properties) = LocateSetting(propertyName, settingsConfig);
return settingInfo.GetValue(properties);
}
/// <summary>
/// Gets the Properties object from a settings config.
/// For GeneralSettings, returns the settings itself. For others, returns the Properties property.
/// </summary>
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 propertiesProperty = settingsType.GetProperty("Properties");
return propertiesProperty?.GetValue(settingsConfig);
var settingsConfigInfo = settingsType.GetProperty("Properties");
return settingsConfigInfo.GetValue(settingsConfig);
}
/// <summary>
/// Gets enabled state for a specific module.
/// AOT-compatible: static dispatch instead of reflection.
/// </summary>
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,
};
}
/// <summary>
/// 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.
/// </summary>
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);
@@ -187,18 +75,6 @@ public class CommandLineUtils
return (propertiesType.GetProperty(propertyName), properties);
}
/// <summary>
/// Gets the value of a property from a settings config.
/// </summary>
public static object GetPropertyValue(string propertyName, ISettingsConfig settingsConfig)
{
var (settingInfo, properties) = LocateSetting(propertyName, settingsConfig);
return settingInfo?.GetValue(properties);
}
/// <summary>
/// Gets the PropertyInfo for a setting property.
/// </summary>
public static PropertyInfo GetSettingPropertyInfo(string propertyName, ISettingsConfig settingsConfig)
{
return LocateSetting(propertyName, settingsConfig).SettingInfo;

View File

@@ -47,7 +47,9 @@ public sealed class GetSettingCommandLineCommand
{
var modulesSettings = new Dictionary<string, Dictionary<string, object>>();
var settingsAssembly = CommandLineUtils.GetSettingsAssembly();
var settingsUtils = SettingsUtils.Default;
var enabledModules = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.Enabled;
foreach (var (moduleName, settings) in settingNamesForModules)
@@ -55,10 +57,10 @@ public sealed class GetSettingCommandLineCommand
var moduleSettings = new Dictionary<string, object>();
if (moduleName != nameof(GeneralSettings))
{
moduleSettings.Add("Enabled", CommandLineUtils.GetEnabledModuleValue(moduleName, enabledModules));
moduleSettings.Add("Enabled", typeof(EnabledModules).GetProperty(moduleName).GetValue(enabledModules));
}
var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils);
var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsAssembly);
foreach (var settingName in settings)
{
var value = CommandLineUtils.GetPropertyValue(settingName, settingsConfig);

View File

@@ -115,8 +115,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Utilities
public static string GetPowerToysInstallationFolder()
{
// PowerToys.exe is in the parent folder relative to Settings.
// Use AppContext.BaseDirectory for AOT/single-file compatibility
var settingsPath = AppContext.BaseDirectory;
var settingsPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
return Directory.GetParent(settingsPath).FullName;
}

View File

@@ -246,8 +246,10 @@ public sealed class SetAdditionalSettingsCommandLineCommand
public static void Execute(string moduleName, JsonDocument settings, SettingsUtils settingsUtils)
{
var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils);
var settingsConfigType = settingsConfig?.GetType();
Assembly settingsLibraryAssembly = CommandLineUtils.GetSettingsAssembly();
var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsLibraryAssembly);
var settingsConfigType = settingsConfig.GetType();
if (!SupportedAdditionalPropertiesInfoForModules.TryGetValue(moduleName, out var additionalPropertiesInfo))
{

View File

@@ -27,9 +27,11 @@ public sealed class SetSettingCommandLineCommand
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);
var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsLibraryAssembly);
var propertyInfo = CommandLineUtils.GetSettingPropertyInfo(propertyName, settingsConfig);
if (propertyInfo == null)

View File

@@ -1,199 +0,0 @@
// 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;
/// <summary>
/// AOT-compatible IPC client for MouseWithoutBorders service communication.
/// Replaces StreamJsonRpc with manual NamedPipe protocol using length-prefixed messages.
/// </summary>
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;
/// <summary>
/// Command types for IPC protocol.
/// Must match server-side enum in MouseWithoutBorders Program.cs
/// </summary>
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);
}
/// <summary>
/// Sends shutdown command to MouseWithoutBorders service
/// </summary>
public async Task ShutdownAsync()
{
await SendCommandAsync(CommandType.Shutdown);
await FlushAsync();
}
/// <summary>
/// Sends reconnect command to MouseWithoutBorders service
/// </summary>
public async Task ReconnectAsync()
{
await SendCommandAsync(CommandType.Reconnect);
await FlushAsync();
}
/// <summary>
/// Requests generation of a new security key
/// </summary>
public async Task GenerateNewKeyAsync()
{
await SendCommandAsync(CommandType.GenerateNewKey);
await FlushAsync();
}
/// <summary>
/// Requests connection to a specific machine
/// </summary>
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();
}
/// <summary>
/// Requests current state of all connected machines
/// </summary>
public async Task<MachineSocketState[]> RequestMachineSocketStateAsync()
{
// Send command
await SendCommandAsync(CommandType.RequestMachineSocketState);
await FlushAsync();
// Read response
var jsonResponse = await ReadStringAsync();
if (string.IsNullOrEmpty(jsonResponse))
{
return Array.Empty<MachineSocketState>();
}
try
{
// Use source-generated JSON serialization
return JsonSerializer.Deserialize(jsonResponse, SettingsSerializationContext.Default.MachineSocketStateArray)
?? Array.Empty<MachineSocketState>();
}
catch (JsonException ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to deserialize MachineSocketState: {ex.Message}");
return Array.Empty<MachineSocketState>();
}
}
/// <summary>
/// Flushes the underlying stream asynchronously
/// </summary>
public async Task FlushAsync()
{
await _stream.FlushAsync();
}
/// <summary>
/// Sends a simple command without parameters
/// </summary>
private Task SendCommandAsync(CommandType command)
{
lock (_lock)
{
_writer.Write((byte)command);
}
return Task.CompletedTask;
}
/// <summary>
/// Writes a length-prefixed UTF-8 string
/// </summary>
private void WriteString(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
_writer.Write(bytes.Length); // 4-byte length prefix
_writer.Write(bytes);
}
/// <summary>
/// Reads a length-prefixed UTF-8 string asynchronously
/// </summary>
private async Task<string> 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)
}
}

View File

@@ -1,45 +0,0 @@
// 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;
/// <summary>
/// Preserves types required by XAML for Native AOT compilation.
/// Called from App constructor when BUILD_INFO_PUBLISH_AOT is defined.
/// </summary>
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.
}
}

View File

@@ -2,7 +2,6 @@
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\Common.SelfContained.props" />
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
@@ -19,31 +18,6 @@
<OutputPath>..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>PowerToys.Settings.pri</ProjectPriFileName>
<!-- .NET Performance Optimizations -->
<TieredCompilation>true</TieredCompilation>
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
<PublishReadyToRun>true</PublishReadyToRun>
<ReadyToRunUseCrossgen2>true</ReadyToRunUseCrossgen2>
</PropertyGroup>
<!-- Additional optimizations for Release builds -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
<PropertyGroup>
<EnableSettingsAOT>true</EnableSettingsAOT>
</PropertyGroup>
<PropertyGroup Condition="'$(EnableSettingsAOT)' == 'true'">
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\Settings\Icons\Models\Azure.svg" />
@@ -111,12 +85,12 @@
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="System.Text.RegularExpressions" />
<PackageReference Include="WinUIEx" />
<!-- StreamJsonRpc and MessagePack are excluded in AOT builds due to reflection dependencies -->
<PackageReference Include="MessagePack" Condition="'$(EnableSettingsAOT)' != 'true'" />
<PackageReference Include="StreamJsonRpc" Condition="'$(EnableSettingsAOT)' != 'true'" />
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
<PackageReference Include="MessagePack" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="StreamJsonRpc" />
<!-- HACK: Microsoft.Extensions.Hosting is referenced, even if it is not used, to force dll versions to be the same as in other projects. Really only needed since the Experimentation APIs that are added in CI reference some net standard 2.0 assemblies. -->
<PackageReference Include="Microsoft.Extensions.Hosting" />
<!-- HACK: To make sure the version pulled in by Microsoft.Extensions.Hosting is current. -->
@@ -160,9 +134,10 @@
<!-- No RID/Platform plumbing needed here. XamlIndexBuilder handles generation after its own Build. -->
<PropertyGroup Condition="'$(EnableSettingsAOT)' != 'true'">
<PropertyGroup>
<!-- TODO: fix issues and reenable -->
<!-- These are caused by streamjsonrpc dependency on Microsoft.VisualStudio.Threading.Analyzers -->
<!-- Only relevant when StreamJsonRpc is included (non-AOT builds) -->
<!-- We might want to add that to the project and fix the issues as well -->
<NoWarn>VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101</NoWarn>
</PropertyGroup>
@@ -243,9 +218,4 @@
<Message Importance="high" Text="[Settings] Building XamlIndexBuilder prior to compile. Views='$(MSBuildProjectDirectory)\SettingsXAML\Views' Out='$(GeneratedJsonFile)'" />
<MSBuild Projects="..\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj" Targets="Build" Properties="Configuration=$(Configuration);Platform=Any CPU;TargetFramework=net9.0;XamlViewsDir=$(MSBuildProjectDirectory)\SettingsXAML\Views;GeneratedJsonFile=$(GeneratedJsonFile)" />
</Target>
<!-- Build information defines -->
<PropertyGroup Condition="'$(PublishAot)' == 'true'">
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_AOT</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -49,12 +49,8 @@ namespace Microsoft.PowerToys.Settings.UI
private const int RequiredArgumentsLaunchedFromRunnerQty = 10;
// IPC message queue for messages sent before IPC manager is initialized
private static readonly System.Collections.Concurrent.ConcurrentQueue<string> PendingIPCMessages = new System.Collections.Concurrent.ConcurrentQueue<string>();
// Create an instance of the IPC wrapper.
private static TwoWayPipeMessageIPCManaged ipcmanager;
private static bool isIPCInitialized;
public static bool IsElevated { get; set; }
@@ -79,9 +75,6 @@ namespace Microsoft.PowerToys.Settings.UI
/// </summary>
public App()
{
#if BUILD_INFO_PUBLISH_AOT
Helpers.TypePreservation.PreserveTypes();
#endif
Logger.InitializeLogger(@"\Settings\Logs");
string appLanguage = LanguageHelper.LoadLanguage();
@@ -214,38 +207,20 @@ namespace Microsoft.PowerToys.Settings.UI
Environment.Exit(0);
});
// Initialize IPC manager asynchronously to avoid blocking window creation
string settingsPipeName = cmdArgs[(int)Arguments.SettingsPipeName];
string ptPipeName = cmdArgs[(int)Arguments.PTPipeName];
_ = Task.Run(() =>
ipcmanager = new TwoWayPipeMessageIPCManaged(cmdArgs[(int)Arguments.SettingsPipeName], cmdArgs[(int)Arguments.PTPipeName], (string message) =>
{
try
if (IPCMessageReceivedCallback != null && message.Length > 0)
{
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}");
IPCMessageReceivedCallback(message);
}
});
ipcmanager.Start();
GlobalHotkeyConflictManager.Initialize(message =>
{
ipcmanager.Send(message);
return 0;
});
if (!ShowOobe && !ShowScoobe)
{
@@ -260,9 +235,6 @@ namespace Microsoft.PowerToys.Settings.UI
// 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
{
@@ -271,9 +243,6 @@ namespace Microsoft.PowerToys.Settings.UI
// 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());
@@ -350,33 +319,6 @@ namespace Microsoft.PowerToys.Settings.UI
return ipcmanager;
}
/// <summary>
/// Sends an IPC message, queuing it if the IPC manager is not yet initialized.
/// </summary>
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);
}
}
/// <summary>
/// Process all pending IPC messages after the IPC manager is initialized.
/// </summary>
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);

View File

@@ -27,177 +27,133 @@ namespace Microsoft.PowerToys.Settings.UI
var bootTime = new System.Diagnostics.Stopwatch();
bootTime.Start();
// 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;
// Set elevation status immediately (required for UI)
App.ThemeService.ThemeChanged += OnThemeChanged;
App.ThemeService.ApplyTheme();
this.ExtendsContentIntoTitleBar = true;
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)
{
var placement = new WINDOWPLACEMENT
{
ShowCmd = NativeMethods.SW_HIDE,
};
NativeMethods.SetWindowPlacement(hWnd, ref placement);
placement.ShowCmd = NativeMethods.SW_HIDE;
// Restore the last known placement on the first activation
this.Activated += Window_Activated;
}
// Initialize remaining components asynchronously
_ = InitializeAsync(hWnd, createHidden, bootTime);
}
NativeMethods.SetWindowPlacement(hWnd, ref placement);
private async Task InitializeAsync(IntPtr hWnd, bool createHidden, System.Diagnostics.Stopwatch bootTime)
{
try
var loader = ResourceLoaderInstance.ResourceLoader;
Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title");
// send IPC Message
ShellPage.SetDefaultSndMessageCallback(msg =>
{
// Load window placement asynchronously (non-blocking file I/O)
if (!createHidden)
// 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
});
// send IPC Message
ShellPage.SetCheckForUpdatesMessageCallback(msg =>
{
App.GetTwoWayIPCManager()?.Send(msg);
});
// open main window
ShellPage.SetOpenMainWindowCallback(type =>
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
App.OpenSettingsWindow(type));
});
// open main window
ShellPage.SetUpdatingGeneralSettingsCallback((ModuleType moduleType, bool isEnabled) =>
{
SettingsRepository<GeneralSettings> repository = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default);
GeneralSettings generalSettingsConfig = repository.SettingsConfig;
bool needToUpdate = ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType) != isEnabled;
if (needToUpdate)
{
await Task.Run(() =>
ModuleHelper.SetIsModuleEnabled(generalSettingsConfig, moduleType, isEnabled);
var outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
// Save settings to file
SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString());
// Send IPC message asynchronously to avoid blocking UI and potential recursive calls
Task.Run(() =>
{
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
NativeMethods.SetWindowPlacement(hWnd, ref placement);
ShellPage.SendDefaultIPCMessage(outgoing.ToString());
});
ShellPage.ShellHandler?.SignalGeneralDataUpdate();
}
// Set up IPC callbacks on UI thread
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
// send IPC Message
ShellPage.SetDefaultSndMessageCallback(msg =>
{
// Use SendIPCMessage which handles queuing if IPC is not yet initialized
App.SendIPCMessage(msg);
});
return needToUpdate;
});
// send IPC Message
ShellPage.SetRestartAdminSndMessageCallback(msg =>
{
App.SendIPCMessage(msg);
Environment.Exit(0); // close application
});
// send IPC Message
ShellPage.SetCheckForUpdatesMessageCallback(msg =>
{
App.SendIPCMessage(msg);
});
// open main window
ShellPage.SetOpenMainWindowCallback(type =>
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
App.OpenSettingsWindow(type));
});
// open main window
ShellPage.SetUpdatingGeneralSettingsCallback((ModuleType moduleType, bool isEnabled) =>
{
SettingsRepository<GeneralSettings> repository = SettingsRepository<GeneralSettings>.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);
// Save settings to file
SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString());
// Send IPC message asynchronously to avoid blocking UI and potential recursive calls
Task.Run(() =>
{
ShellPage.SendDefaultIPCMessage(outgoing.ToString());
});
ShellPage.ShellHandler?.SignalGeneralDataUpdate();
}
return needToUpdate;
});
// open oobe
ShellPage.SetOpenOobeCallback(() =>
{
if (App.GetOobeWindow() == null)
{
App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.Overview));
}
App.GetOobeWindow().Activate();
});
// open whats new window
ShellPage.SetOpenWhatIsNewCallback(() =>
{
if (App.GetScoobeWindow() == null)
{
App.SetScoobeWindow(new ScoobeWindow());
}
App.GetScoobeWindow().Activate();
});
// 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<JsonObject> 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)
// open oobe
ShellPage.SetOpenOobeCallback(() =>
{
Logger.LogError($"Error during async initialization: {ex.Message}");
}
if (App.GetOobeWindow() == null)
{
App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.Overview));
}
App.GetOobeWindow().Activate();
});
// open whats new window
ShellPage.SetOpenWhatIsNewCallback(() =>
{
if (App.GetScoobeWindow() == null)
{
App.SetScoobeWindow(new ScoobeWindow());
}
App.GetScoobeWindow().Activate();
});
this.InitializeComponent();
SetAppTitleBar();
// receive IPC Message
App.IPCMessageReceivedCallback = (string msg) =>
{
if (ShellPage.ShellHandler.IPCResponseHandleList != null)
{
var success = JsonObject.TryParse(msg, out JsonObject json);
if (success)
{
foreach (Action<JsonObject> handle in ShellPage.ShellHandler.IPCResponseHandleList)
{
handle(json);
}
}
else
{
Logger.LogError("Failed to parse JSON from IPC message.");
}
}
};
bootTime.Stop();
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
}
private void SetAppTitleBar()

View File

@@ -16,6 +16,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
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;

View File

@@ -24,9 +24,7 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
#if !BUILD_INFO_PUBLISH_AOT
using StreamJsonRpc;
#endif
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
@@ -237,8 +235,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => _enabledStateIsGPOConfigured;
}
// SocketStatus enum is now defined in Settings.UI.Library\MouseWithoutBordersIpcModels.cs
#if !BUILD_INFO_PUBLISH_AOT
private enum SocketStatus : int
{
NA = 0,
Resolving = 1,
Connecting = 2,
Handshaking = 3,
Error = 4,
ForceClosed = 5,
InvalidKey = 6,
Timeout = 7,
SendError = 8,
Connected = 9,
}
private interface ISettingsSyncHelper
{
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
@@ -264,73 +274,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Task<MachineSocketState[]> RequestMachineSocketStateAsync();
}
#endif
private static CancellationTokenSource _cancellationTokenSource;
private static Task _machinePollingThreadTask;
private static SemaphoreSlim _ipcSemaphore = new SemaphoreSlim(1, 1);
private static NamedPipeClientStream syncHelperStream;
private static VisualStudio.Threading.AsyncSemaphore _ipcSemaphore = new VisualStudio.Threading.AsyncSemaphore(1);
#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<SyncHelper> 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)
@@ -349,6 +299,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
private static NamedPipeClientStream syncHelperStream;
private async Task<SyncHelper> GetSettingsSyncHelperAsync()
{
try
@@ -385,154 +337,88 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return null;
}
}
#endif
public async Task SubmitShutdownRequestAsync()
{
await _ipcSemaphore.WaitAsync();
try
using (await _ipcSemaphore.EnterAsync())
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
syncHelper?.Endpoint?.Shutdown();
var task = syncHelper?.Stream.FlushAsync();
if (task != null)
{
#if BUILD_INFO_PUBLISH_AOT
await syncHelper.Client.ShutdownAsync();
#else
syncHelper.Endpoint?.Shutdown();
var task = syncHelper.Stream.FlushAsync();
if (task != null)
{
await task;
}
#endif
await task;
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
public async Task SubmitReconnectRequestAsync()
{
await _ipcSemaphore.WaitAsync();
try
using (await _ipcSemaphore.EnterAsync())
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
syncHelper?.Endpoint?.Reconnect();
var task = syncHelper?.Stream.FlushAsync();
if (task != null)
{
#if BUILD_INFO_PUBLISH_AOT
await syncHelper.Client.ReconnectAsync();
#else
syncHelper.Endpoint?.Reconnect();
var task = syncHelper.Stream.FlushAsync();
if (task != null)
{
await task;
}
#endif
await task;
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
public async Task SubmitNewKeyRequestAsync()
{
await _ipcSemaphore.WaitAsync();
try
using (await _ipcSemaphore.EnterAsync())
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
syncHelper?.Endpoint?.GenerateNewKey();
var task = syncHelper?.Stream.FlushAsync();
if (task != null)
{
#if BUILD_INFO_PUBLISH_AOT
await syncHelper.Client.GenerateNewKeyAsync();
#else
syncHelper.Endpoint?.GenerateNewKey();
var task = syncHelper.Stream.FlushAsync();
if (task != null)
{
await task;
}
#endif
await task;
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
public async Task SubmitConnectionRequestAsync(string pcName, string securityKey)
{
await _ipcSemaphore.WaitAsync();
try
using (await _ipcSemaphore.EnterAsync())
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
syncHelper?.Endpoint?.ConnectToMachine(pcName, securityKey);
var task = syncHelper?.Stream.FlushAsync();
if (task != null)
{
#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
await task;
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
private async Task<MachineSocketState[]> PollMachineSocketStateAsync()
private async Task<ISettingsSyncHelper.MachineSocketState[]> PollMachineSocketStateAsync()
{
await _ipcSemaphore.WaitAsync();
try
using (await _ipcSemaphore.EnterAsync())
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
var task = syncHelper?.Endpoint?.RequestMachineSocketStateAsync();
if (task != 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 await task;
}
else
{
return null;
}
return Array.Empty<MachineSocketState>();
}
}
finally
{
_ipcSemaphore.Release();
}
}
private MouseWithoutBordersSettings Settings { get; set; }
@@ -578,14 +464,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
while (!token.IsCancellationRequested)
{
Dictionary<string, MachineSocketState> states = null;
Dictionary<string, ISettingsSyncHelper.MachineSocketState> states = null;
try
{
states = (await PollMachineSocketStateAsync())?.ToDictionary(s => s.Name, StringComparer.OrdinalIgnoreCase);
}
catch (Exception ex)
{
Logger.LogInfo($"Poll MachineSocketState error: {ex}");
Logger.LogInfo($"Poll ISettingsSyncHelper.MachineSocketState error: {ex}");
continue;
}