Compare commits

...

2 Commits

Author SHA1 Message Date
Shawn Yuan (from Dev Box)
b0754a3e25 update 2026-01-29 13:43:51 +08:00
Shawn Yuan (from Dev Box)
b69b991d4b init 2026-01-29 09:44:43 +08:00
24 changed files with 1253 additions and 322 deletions

View File

@@ -8,6 +8,9 @@
<!-- 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+ -->
<WarningsNotAsErrors>IL2081;CsWinRT1028;CA1416;$(WarningsNotAsErrors)</WarningsNotAsErrors>
<!-- 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>
</PropertyGroup>
</Project>

View File

@@ -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;
/// <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,6 +19,7 @@ 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;
@@ -276,7 +277,7 @@ namespace MouseWithoutBorders.Class
Task<MachineSocketState[]> RequestMachineSocketStateAsync();
}
private sealed class SettingsSyncHelper : ISettingsSyncHelper
private sealed class SettingsSyncHelper : ISettingsSyncHelper, ISettingsSyncHandler
{
public Task<ISettingsSyncHelper.MachineSocketState[]> RequestMachineSocketStateAsync()
{
@@ -299,6 +300,28 @@ 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;
@@ -379,7 +402,64 @@ 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,7 +51,11 @@ using Thread = MouseWithoutBorders.Core.Thread;
namespace MouseWithoutBorders.Class
{
internal enum SocketStatus : int
/// <summary>
/// Socket status enumeration - made public for IPC serialization.
/// Must match Settings.UI.Library\MouseWithoutBordersIpcModels.cs
/// </summary>
public enum SocketStatus : int
{
NA = 0,
Resolving = 1,

View File

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

View File

@@ -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 interface ICmdLineRepresentable
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 interface ICmdLineRepresentable
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 interface ICmdLineRepresentable
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)
{

View File

@@ -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;
/// <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,6 +2,7 @@
<!-- 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,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,88 +11,111 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Services
{
/// <summary>
/// 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.
/// </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));
_settingsTypes = DiscoverSettingsTypes();
_settingsFactories = InitializeFactories();
_settingsTypes = InitializeTypes();
}
/// <summary>
/// Dynamically discovers all Settings types that implement IHotkeyConfig
/// Static registry of all module settings factories.
/// IMPORTANT: When adding a new module, add it here.
/// </summary>
private Dictionary<string, Type> DiscoverSettingsTypes()
private Dictionary<string, Func<IHotkeyConfig>> InitializeFactories()
{
var settingsTypes = new Dictionary<string, Type>();
// Get the Settings.UI.Library assembly
var assembly = Assembly.GetAssembly(typeof(IHotkeyConfig));
if (assembly == null)
return new Dictionary<string, Func<IHotkeyConfig>>
{
return settingsTypes;
["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,
};
}
/// <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>
{
["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;
}
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";
}
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}");
}
}
return factory();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error scanning assembly {assembly.FullName}: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}");
return null;
}
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))
@@ -104,20 +125,8 @@ namespace Microsoft.PowerToys.Settings.UI.Services
try
{
// 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;
string actualModuleKey = moduleKey == "GeneralSettings" ? string.Empty : moduleKey;
return GetFreshSettingsForType(settingsType, actualModuleKey);
}
catch (Exception ex)
{
@@ -127,35 +136,33 @@ namespace Microsoft.PowerToys.Settings.UI.Services
}
/// <summary>
/// Gets a settings instance for the specified module using SettingsRepository
/// Static dispatch for GetSettingsOrDefault using pattern matching.
/// Replaces reflection-based MakeGenericMethod/Invoke pattern.
/// </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)
private IHotkeyConfig GetFreshSettingsForType(Type settingsType, string moduleKey)
{
if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType))
return settingsType.Name switch
{
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;
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,
};
}
/// <summary>
@@ -164,7 +171,7 @@ namespace Microsoft.PowerToys.Settings.UI.Services
/// <returns>List of module names</returns>
public List<string> GetAvailableModuleNames()
{
return _settingsTypes.Keys.ToList();
return new List<string>(_settingsTypes.Keys);
}
/// <summary>

View File

@@ -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<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,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;
/// <summary>
/// AOT-compatible command line utilities.
/// Uses static type mapping instead of AppDomain reflection.
/// </summary>
public class CommandLineUtils
{
private static Type GetSettingsConfigType(string moduleName, Assembly settingsLibraryAssembly)
private static readonly Dictionary<string, Type> _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 = null)
{
if (!_settingsTypes.TryGetValue(moduleName, out var settingsType))
{
return null;
}
return GetSettingsConfigFor(settingsType, settingsUtils);
}
public static ISettingsConfig GetSettingsConfigFor(string moduleName, SettingsUtils settingsUtils, Assembly settingsLibraryAssembly)
{
return GetSettingsConfigFor(GetSettingsConfigType(moduleName, settingsLibraryAssembly), settingsUtils);
}
/// Executes SettingsRepository<moduleSettingsType>.GetInstance(settingsUtils).SettingsConfig
/// <summary>
/// Gets settings config for a given type using static dispatch.
/// AOT-compatible: replaces MakeGenericType/GetMethod/Invoke pattern.
/// </summary>
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<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);
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,
};
}
/// <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 settingsConfigInfo = settingsType.GetProperty("Properties");
return settingsConfigInfo.GetValue(settingsConfig);
var propertiesProperty = settingsType.GetProperty("Properties");
return propertiesProperty?.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);
@@ -75,6 +187,18 @@ 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,9 +47,7 @@ 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)
@@ -57,10 +55,10 @@ public sealed class GetSettingCommandLineCommand
var moduleSettings = new Dictionary<string, object>();
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);

View File

@@ -115,7 +115,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Utilities
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;
}

View File

@@ -246,10 +246,8 @@ public sealed class SetAdditionalSettingsCommandLineCommand
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))
{

View File

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

View File

@@ -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;
/// <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

@@ -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;
/// <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,6 +2,7 @@
<!-- 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>
@@ -18,6 +19,31 @@
<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" />
@@ -85,12 +111,12 @@
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="System.Text.RegularExpressions" />
<PackageReference Include="WinUIEx" />
<!-- 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" />
<!-- StreamJsonRpc and MessagePack are excluded in AOT builds due to reflection dependencies -->
<PackageReference Include="MessagePack" Condition="'$(EnableSettingsAOT)' != 'true'" />
<PackageReference Include="StreamJsonRpc" Condition="'$(EnableSettingsAOT)' != 'true'" />
<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. -->
@@ -134,10 +160,9 @@
<!-- No RID/Platform plumbing needed here. XamlIndexBuilder handles generation after its own Build. -->
<PropertyGroup>
<!-- TODO: fix issues and reenable -->
<PropertyGroup Condition="'$(EnableSettingsAOT)' != 'true'">
<!-- These are caused by streamjsonrpc dependency on Microsoft.VisualStudio.Threading.Analyzers -->
<!-- We might want to add that to the project and fix the issues as well -->
<!-- Only relevant when StreamJsonRpc is included (non-AOT builds) -->
<NoWarn>VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101</NoWarn>
</PropertyGroup>
@@ -218,4 +243,9 @@
<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,8 +49,12 @@ 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; }
@@ -75,6 +79,9 @@ 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();
@@ -207,19 +214,37 @@ namespace Microsoft.PowerToys.Settings.UI
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.Start();
ipcmanager = new TwoWayPipeMessageIPCManaged(settingsPipeName, ptPipeName, (string message) =>
{
if (IPCMessageReceivedCallback != null && message.Length > 0)
{
IPCMessageReceivedCallback(message);
}
});
ipcmanager.Start();
GlobalHotkeyConflictManager.Initialize(message =>
{
ipcmanager.Send(message);
return 0;
// 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}");
}
});
if (!ShowOobe && !ShowScoobe)
@@ -235,6 +260,9 @@ 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
{
@@ -243,6 +271,9 @@ 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());
@@ -319,6 +350,33 @@ 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,133 +27,177 @@ namespace Microsoft.PowerToys.Settings.UI
var bootTime = new System.Diagnostics.Stopwatch();
bootTime.Start();
this.Activated += Window_Activated_SetIcon;
App.ThemeService.ThemeChanged += OnThemeChanged;
App.ThemeService.ApplyTheme();
// 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)
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);
// Initialize remaining components asynchronously
_ = InitializeAsync(hWnd, createHidden, bootTime);
}
var loader = ResourceLoaderInstance.ResourceLoader;
Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title");
// send IPC Message
ShellPage.SetDefaultSndMessageCallback(msg =>
private async Task InitializeAsync(IntPtr hWnd, bool createHidden, System.Diagnostics.Stopwatch bootTime)
{
try
{
// 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)
// Load window placement asynchronously (non-blocking file I/O)
if (!createHidden)
{
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(() =>
await Task.Run(() =>
{
ShellPage.SendDefaultIPCMessage(outgoing.ToString());
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
NativeMethods.SetWindowPlacement(hWnd, ref placement);
});
}
// 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);
});
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();
});
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)
// send IPC Message
ShellPage.SetRestartAdminSndMessageCallback(msg =>
{
foreach (Action<JsonObject> handle in ShellPage.ShellHandler.IPCResponseHandleList)
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)
{
handle(json);
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();
}
}
else
return needToUpdate;
});
// open oobe
ShellPage.SetOpenOobeCallback(() =>
{
Logger.LogError("Failed to parse JSON from IPC message.");
}
}
};
if (App.GetOobeWindow() == null)
{
App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.Overview));
}
bootTime.Stop();
App.GetOobeWindow().Activate();
});
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
// 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)
{
Logger.LogError($"Error during async initialization: {ex.Message}");
}
}
private void SetAppTitleBar()

View File

@@ -16,7 +16,6 @@ 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,7 +24,9 @@ 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
@@ -235,20 +237,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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 +264,73 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Task<MachineSocketState[]> 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<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)
@@ -299,8 +349,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
private static NamedPipeClientStream syncHelperStream;
private async Task<SyncHelper> GetSettingsSyncHelperAsync()
{
try
@@ -337,88 +385,154 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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<ISettingsSyncHelper.MachineSocketState[]> PollMachineSocketStateAsync()
private async Task<MachineSocketState[]> PollMachineSocketStateAsync()
{
using (await _ipcSemaphore.EnterAsync())
await _ipcSemaphore.WaitAsync();
try
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
var task = syncHelper?.Endpoint?.RequestMachineSocketStateAsync();
if (task != null)
if (syncHelper != null)
{
return await task;
}
else
{
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<MachineSocketState>();
}
}
finally
{
_ipcSemaphore.Release();
}
}
private MouseWithoutBordersSettings Settings { get; set; }
@@ -464,14 +578,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
while (!token.IsCancellationRequested)
{
Dictionary<string, ISettingsSyncHelper.MachineSocketState> states = null;
Dictionary<string, MachineSocketState> 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;
}