// 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.Globalization; using System.IO; using System.IO.Pipes; using System.Linq; using System.Net; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using global::PowerToys.GPOWrapper; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; 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.Library.ViewModels.Commands; using Microsoft.UI; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media; using StreamJsonRpc; using Windows.ApplicationModel.DataTransfer; namespace Microsoft.PowerToys.Settings.UI.ViewModels { public class MouseWithoutBordersViewModel : Observable, IDisposable { // These should be in the same order as the ComboBoxItems in MouseWithoutBordersPage.xaml switch machine shortcut options private readonly int[] _switchBetweenMachineShortcutOptions = { 112, 49, 0, }; private readonly object _machineMatrixStringLock = new(); private static readonly Dictionary StatusColors = new Dictionary() { { SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0x71, 0x71, 0x71, 0x71)) }, { SocketStatus.Resolving, new SolidColorBrush(Colors.Yellow) }, { SocketStatus.Connecting, new SolidColorBrush(Colors.Orange) }, { SocketStatus.Handshaking, new SolidColorBrush(Colors.Blue) }, { SocketStatus.Error, new SolidColorBrush(Colors.Red) }, { SocketStatus.ForceClosed, new SolidColorBrush(Colors.Purple) }, { SocketStatus.InvalidKey, new SolidColorBrush(Colors.Brown) }, { SocketStatus.Timeout, new SolidColorBrush(Colors.Pink) }, { SocketStatus.SendError, new SolidColorBrush(Colors.Maroon) }, { SocketStatus.Connected, new SolidColorBrush(Colors.Green) }, }; private bool _connectFieldsVisible; public bool IsElevated { get => GeneralSettingsConfig.IsElevated; } public bool CanUninstallService { get => GeneralSettingsConfig.IsElevated && !UseService; } public ButtonClickCommand AddFirewallRuleEventHandler => new ButtonClickCommand(AddFirewallRule); public ButtonClickCommand UninstallServiceEventHandler => new ButtonClickCommand(UninstallService); public bool ShowOriginalUI { get => Settings.Properties.ShowOriginalUI; set { if (Settings.Properties.ShowOriginalUI != value) { Settings.Properties.ShowOriginalUI = value; NotifyPropertyChanged(nameof(ShowOriginalUI)); } } } public bool UseService { get => Settings.Properties.UseService; set { var valueChanged = Settings.Properties.UseService != value; // Set the UI property itself instantly if (valueChanged) { Settings.Properties.UseService = value; OnPropertyChanged(nameof(UseService)); // Must block here until the process exits Task.Run(async () => { await SubmitShutdownRequestAsync(); _uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => { Settings.Properties.UseService = value; NotifyPropertyChanged(nameof(UseService)); }); }); } } } public bool ConnectFieldsVisible { get => _connectFieldsVisible; set { if (_connectFieldsVisible != value) { _connectFieldsVisible = value; OnPropertyChanged(nameof(ConnectFieldsVisible)); } } } private string _connectSecurityKey; public string ConnectSecurityKey { get => _connectSecurityKey; set { if (_connectSecurityKey != value) { _connectSecurityKey = value; OnPropertyChanged(nameof(ConnectSecurityKey)); } } } private string _connectPCName; public string ConnectPCName { get => _connectPCName; set { if (_connectPCName != value) { _connectPCName = value; OnPropertyChanged(nameof(ConnectPCName)); } } } private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } private GpoRuleConfigured _enabledGpoRuleConfiguration; private bool _enabledStateIsGPOConfigured; private bool _isEnabled; public string MachineHostName { get { try { return Dns.GetHostName(); } catch { return string.Empty; } } } public bool IsEnabledGpoConfigured { get => _enabledStateIsGPOConfigured; } private enum SocketStatus : int { NA = 0, Resolving = 1, Connecting = 2, Handshaking = 3, Error = 4, ForceClosed = 5, InvalidKey = 6, Timeout = 7, SendError = 8, Connected = 9, } private interface ISettingsSyncHelper { [Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)] public struct MachineSocketState { // Disable false-positive warning due to IPC #pragma warning disable CS0649 [Newtonsoft.Json.JsonProperty] public string Name; [Newtonsoft.Json.JsonProperty] public SocketStatus Status; #pragma warning restore CS0649 } void Shutdown(); void Reconnect(); void GenerateNewKey(); void ConnectToMachine(string machineName, string securityKey); Task RequestMachineSocketStateAsync(); } private static CancellationTokenSource _cancellationTokenSource; private static Task _machinePollingThreadTask; private static VisualStudio.Threading.AsyncSemaphore _ipcSemaphore = new VisualStudio.Threading.AsyncSemaphore(1); private sealed class SyncHelper : IDisposable { public SyncHelper(NamedPipeClientStream stream) { Stream = stream; Endpoint = JsonRpc.Attach(Stream); } public NamedPipeClientStream Stream { get; } public ISettingsSyncHelper Endpoint { get; private set; } public void Dispose() { ((IDisposable)Endpoint).Dispose(); } } private static NamedPipeClientStream syncHelperStream; private async Task GetSettingsSyncHelperAsync() { try { var recreateStream = false; if (syncHelperStream == null) { recreateStream = true; } else { if (!syncHelperStream.IsConnected || !syncHelperStream.CanWrite) { await syncHelperStream.DisposeAsync(); recreateStream = true; } } if (recreateStream) { syncHelperStream = new NamedPipeClientStream(".", "MouseWithoutBorders/SettingsSync", PipeDirection.InOut, PipeOptions.Asynchronous); await syncHelperStream.ConnectAsync(10000); } return new SyncHelper(syncHelperStream); } catch (Exception ex) { Logger.LogError($"Couldn't create SettingsSync: {ex}"); return null; } } public async Task SubmitShutdownRequestAsync() { using (await _ipcSemaphore.EnterAsync()) { using (var syncHelper = await GetSettingsSyncHelperAsync()) { syncHelper?.Endpoint?.Shutdown(); var task = syncHelper?.Stream.FlushAsync(); if (task != null) { await task; } } } } public async Task SubmitReconnectRequestAsync() { using (await _ipcSemaphore.EnterAsync()) { using (var syncHelper = await GetSettingsSyncHelperAsync()) { syncHelper?.Endpoint?.Reconnect(); var task = syncHelper?.Stream.FlushAsync(); if (task != null) { await task; } } } } public async Task SubmitNewKeyRequestAsync() { using (await _ipcSemaphore.EnterAsync()) { using (var syncHelper = await GetSettingsSyncHelperAsync()) { syncHelper?.Endpoint?.GenerateNewKey(); var task = syncHelper?.Stream.FlushAsync(); if (task != null) { await task; } } } } public async Task SubmitConnectionRequestAsync(string pcName, string securityKey) { using (await _ipcSemaphore.EnterAsync()) { using (var syncHelper = await GetSettingsSyncHelperAsync()) { syncHelper?.Endpoint?.ConnectToMachine(pcName, securityKey); var task = syncHelper?.Stream.FlushAsync(); if (task != null) { await task; } } } } private async Task PollMachineSocketStateAsync() { using (await _ipcSemaphore.EnterAsync()) { using (var syncHelper = await GetSettingsSyncHelperAsync()) { var task = syncHelper?.Endpoint?.RequestMachineSocketStateAsync(); if (task != null) { return await task; } else { return null; } } } } private MouseWithoutBordersSettings Settings { get; set; } private DispatcherQueue _uiDispatcherQueue; public MouseWithoutBordersViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, Func ipcMSGCallBackFunc, DispatcherQueue uiDispatcherQueue) { SettingsUtils = settingsUtils; _uiDispatcherQueue = uiDispatcherQueue; // To obtain the general settings configurations of PowerToys Settings. if (settingsRepository == null) { throw new ArgumentNullException(nameof(settingsRepository)); } GeneralSettingsConfig = settingsRepository.SettingsConfig; InitializeEnabledValue(); // MouseWithoutBorders settings may be changed by the logic in the utility as machines connect. We need to get a fresh version everytime instead of using a repository. MouseWithoutBordersSettings moduleSettings; moduleSettings = SettingsUtils.GetSettingsOrDefault("MouseWithoutBorders"); LoadViewModelFromSettings(moduleSettings); // set the callback functions value to handle outgoing IPC message. SendConfigMSG = ipcMSGCallBackFunc; _cancellationTokenSource?.Cancel(); _cancellationTokenSource = new CancellationTokenSource(); _machinePollingThreadTask = StartMachineStatusPollingThread(_machinePollingThreadTask, _cancellationTokenSource.Token); } private Task StartMachineStatusPollingThread(Task previousThreadTask, CancellationToken token) { return Task.Run( async () => { previousThreadTask?.Wait(); while (!token.IsCancellationRequested) { Dictionary states = null; try { states = (await PollMachineSocketStateAsync())?.ToDictionary(s => s.Name, StringComparer.OrdinalIgnoreCase); } catch (Exception ex) { Logger.LogInfo($"Poll ISettingsSyncHelper.MachineSocketState error: {ex}"); continue; } if (states != null) { lock (_machineMatrixStringLock) { foreach (var machine in machineMatrixString) { if (states.TryGetValue(machine.Item.Name, out var state)) { _uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => { try { machine.Item.StatusBrush = StatusColors[state.Status]; } catch (Exception) { } }); } } } } Thread.Sleep(500); } }, _cancellationTokenSource.Token); } private void InitializeEnabledValue() { _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredMouseWithoutBordersEnabledValue(); if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) { // Get the enabled state from GPO. _enabledStateIsGPOConfigured = true; _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; } else { _isEnabled = GeneralSettingsConfig.Enabled.MouseWithoutBorders; } } private void LoadViewModelFromSettings(MouseWithoutBordersSettings moduleSettings) { if (moduleSettings == null) { throw new ArgumentNullException(nameof(moduleSettings)); } Settings = moduleSettings; /* TODO: Error handling */ _selectedSwitchBetweenMachineShortcutOptionsIndex = Array.IndexOf(_switchBetweenMachineShortcutOptions, moduleSettings.Properties.HotKeySwitchMachine.Value); _easyMouseOptionIndex = (EasyMouseOption)moduleSettings.Properties.EasyMouse.Value; _toggleEasyMouseShortcutIndex = ConvertMouseWithoutBordersHotKeyValueToIndex(moduleSettings.Properties.HotKeyToggleEasyMouse.Value); _lockMachinesShortcutIndex = ConvertMouseWithoutBordersHotKeyValueToIndex(moduleSettings.Properties.HotKeyLockMachine.Value); _reconnectShortcutIndex = ConvertMouseWithoutBordersHotKeyValueToIndex(moduleSettings.Properties.HotKeyReconnect.Value); _switch2AllPcShortcutIndex = ConvertMouseWithoutBordersHotKeyValueToIndex(moduleSettings.Properties.HotKeySwitch2AllPC.Value, 1); LoadMachineMatrixString(); } // Loads the machine matrix, taking into account changes to the machine pool. private void LoadMachineMatrixString() { List loadMachineMatrixString = Settings.Properties.MachineMatrixString ?? new List() { string.Empty, string.Empty, string.Empty, string.Empty }; if (loadMachineMatrixString.Count < 4) { // Current logic of MWB assumes there are always 4 slots. Any other configuration means data corruption here. loadMachineMatrixString = new List() { string.Empty, string.Empty, string.Empty, string.Empty }; } bool editedTheMatrix = false; // keep track of changes to the matrix because of changes to the available machine pool. if (!string.IsNullOrEmpty(Settings.Properties.MachinePool?.Value)) { List availableMachines = new List(); // Format of this field is "NAME1:ID1,NAME2:ID2,..." // Load the available machines foreach (string availableMachineIdPair in Settings.Properties.MachinePool.Value.Split(",")) { string availableMachineName = availableMachineIdPair.Split(':')[0]; availableMachines.Add(availableMachineName); } // Start by removing the machines from the matrix that are no longer available to pick. for (int i = 0; i < loadMachineMatrixString.Count; i++) { if (!availableMachines.Contains(loadMachineMatrixString[i])) { editedTheMatrix = true; loadMachineMatrixString[i] = string.Empty; } } // If an available machine is not in the matrix already, fill it in the first available spot. foreach (string availableMachineName in availableMachines) { if (!loadMachineMatrixString.Contains(availableMachineName)) { int availableIndex = loadMachineMatrixString.FindIndex(name => string.IsNullOrEmpty(name)); if (availableIndex >= 0) { loadMachineMatrixString[availableIndex] = availableMachineName; editedTheMatrix = true; } } } } // Dragging while elevated crashes on WinUI3: https://github.com/microsoft/microsoft-ui-xaml/issues/7690 machineMatrixString = new IndexedObservableCollection(loadMachineMatrixString.Select(name => new DeviceViewModel { Name = name, CanDragDrop = !IsElevated })); if (editedTheMatrix) { // Set the property directly to save the new matrix right away with the new available machines. MachineMatrixString = machineMatrixString; } } public bool CanBeEnabled { get => !_enabledStateIsGPOConfigured; } public bool CanToggleUseService { get { return IsEnabled && !(!IsElevated && !UseService); } } public bool IsEnabled { get => _isEnabled; set { if (_enabledStateIsGPOConfigured) { // If it's GPO configured, shouldn't be able to change this state. return; } if (_isEnabled != value) { _isEnabled = value; GeneralSettingsConfig.Enabled.MouseWithoutBorders = value; OnPropertyChanged(nameof(IsEnabled)); Task.Run(async () => { if (!value) { try { await SubmitShutdownRequestAsync(); } catch (Exception ex) { Logger.LogError($"Failed to shutdown MWB via SettingsSync: {ex}"); } } _uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => { OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); SendConfigMSG(outgoing.ToString()); NotifyPropertyChanged(); // Disable service mode if we're not elevated, because we cannot register service in that case if (value == true && !IsElevated && UseService) { UseService = false; } }); }); } } } public string SecurityKey { get => Settings.Properties.SecurityKey.Value; set { if (value != Settings.Properties.SecurityKey.Value) { Settings.Properties.SecurityKey.Value = value; NotifyPropertyChanged(); } } } public bool WrapMouse { get { return Settings.Properties.WrapMouse; } set { if (Settings.Properties.WrapMouse != value) { Settings.Properties.WrapMouse = value; NotifyPropertyChanged(); } } } public bool MatrixOneRow { get { return Settings.Properties.MatrixOneRow; } set { if (Settings.Properties.MatrixOneRow != value) { Settings.Properties.MatrixOneRow = value; NotifyPropertyChanged(); } } } public bool ShareClipboard { get { return Settings.Properties.ShareClipboard; } set { if (Settings.Properties.ShareClipboard != value) { Settings.Properties.ShareClipboard = value; NotifyPropertyChanged(); } } } public bool TransferFile { get { return Settings.Properties.TransferFile; } set { if (Settings.Properties.TransferFile != value) { Settings.Properties.TransferFile = value; NotifyPropertyChanged(); } } } public bool HideMouseAtScreenEdge { get { return Settings.Properties.HideMouseAtScreenEdge; } set { if (Settings.Properties.HideMouseAtScreenEdge != value) { Settings.Properties.HideMouseAtScreenEdge = value; NotifyPropertyChanged(); } } } public bool DrawMouseCursor { get { return Settings.Properties.DrawMouseCursor; } set { if (Settings.Properties.DrawMouseCursor != value) { Settings.Properties.DrawMouseCursor = value; NotifyPropertyChanged(); } } } public bool ValidateRemoteMachineIP { get { return Settings.Properties.ValidateRemoteMachineIP; } set { if (Settings.Properties.ValidateRemoteMachineIP != value) { Settings.Properties.ValidateRemoteMachineIP = value; NotifyPropertyChanged(); } } } public bool SameSubnetOnly { get { return Settings.Properties.SameSubnetOnly; } set { if (Settings.Properties.SameSubnetOnly != value) { Settings.Properties.SameSubnetOnly = value; NotifyPropertyChanged(); } } } public bool BlockScreenSaverOnOtherMachines { get { return Settings.Properties.BlockScreenSaverOnOtherMachines; } set { if (Settings.Properties.BlockScreenSaverOnOtherMachines != value) { Settings.Properties.BlockScreenSaverOnOtherMachines = value; NotifyPropertyChanged(); } } } // Should match EasyMouseOption enum from MouseWithoutBorders and the ComboBox in the MouseWithoutBordersView.cs private enum EasyMouseOption { Disable = 0, Enable = 1, Ctrl = 2, Shift = 3, } private EasyMouseOption _easyMouseOptionIndex; public int EasyMouseOptionIndex { get { return (int)_easyMouseOptionIndex; } set { if (value != (int)_easyMouseOptionIndex) { _easyMouseOptionIndex = (EasyMouseOption)value; Settings.Properties.EasyMouse.Value = value; NotifyPropertyChanged(nameof(EasyMouseOptionIndex)); } } } public int ConvertMouseWithoutBordersHotKeyValueToIndex(int value, int additionalOptions = 0) { if (value >= 0x41 && value <= 0x5A) { return value - 0x40 + additionalOptions; /* VK_A <= value <= VK_Z */ } if (value <= additionalOptions) { return value; } return 0; /* Disabled */ } public int ConvertMouseWithoutBordersHotKeyIndexToValue(int index, int additionalOptions = 0) { if (index >= additionalOptions + 1 && index <= additionalOptions + 26) { return index + 0x40 - additionalOptions; /* VK_A to VK_Z */ } if (index <= additionalOptions) { return index; } return 0; /* Disabled */ } private int _toggleEasyMouseShortcutIndex; public int ToggleEasyMouseShortcutIndex { get { return _toggleEasyMouseShortcutIndex; } set { if (_toggleEasyMouseShortcutIndex != value) { _toggleEasyMouseShortcutIndex = value; Settings.Properties.HotKeyToggleEasyMouse.Value = ConvertMouseWithoutBordersHotKeyIndexToValue(value); NotifyPropertyChanged(); } } } private int _lockMachinesShortcutIndex; public int LockMachinesShortcutIndex { get { return _lockMachinesShortcutIndex; } set { if (_lockMachinesShortcutIndex != value) { _lockMachinesShortcutIndex = value; Settings.Properties.HotKeyLockMachine.Value = ConvertMouseWithoutBordersHotKeyIndexToValue(value); NotifyPropertyChanged(); } } } private int _reconnectShortcutIndex; public int ReconnectShortcutIndex { get { return _reconnectShortcutIndex; } set { if (_reconnectShortcutIndex != value) { _reconnectShortcutIndex = value; Settings.Properties.HotKeyReconnect.Value = ConvertMouseWithoutBordersHotKeyIndexToValue(value); NotifyPropertyChanged(); } } } private int _switch2AllPcShortcutIndex; public int Switch2AllPcShortcutIndex { get { return _switch2AllPcShortcutIndex; } set { if (_switch2AllPcShortcutIndex != value) { _switch2AllPcShortcutIndex = value; Settings.Properties.HotKeySwitch2AllPC.Value = ConvertMouseWithoutBordersHotKeyIndexToValue(value, 1); NotifyPropertyChanged(); } } } private int _selectedSwitchBetweenMachineShortcutOptionsIndex; public int SelectedSwitchBetweenMachineShortcutOptionsIndex { get { return _selectedSwitchBetweenMachineShortcutOptionsIndex; } set { if (_selectedSwitchBetweenMachineShortcutOptionsIndex != value) { _selectedSwitchBetweenMachineShortcutOptionsIndex = value; Settings.Properties.HotKeySwitchMachine.Value = _switchBetweenMachineShortcutOptions[value]; NotifyPropertyChanged(); } } } public bool MoveMouseRelatively { get { return Settings.Properties.MoveMouseRelatively; } set { if (Settings.Properties.MoveMouseRelatively != value) { Settings.Properties.MoveMouseRelatively = value; NotifyPropertyChanged(); } } } public bool BlockMouseAtScreenCorners { get { return Settings.Properties.BlockMouseAtScreenCorners; } set { if (Settings.Properties.BlockMouseAtScreenCorners != value) { Settings.Properties.BlockMouseAtScreenCorners = value; NotifyPropertyChanged(); } } } private IndexedObservableCollection machineMatrixString; public class DeviceViewModel : Observable { public string Name { get; set; } public bool CanDragDrop { get; set; } private Brush _statusBrush = StatusColors[SocketStatus.NA]; public Brush StatusBrush { get { return _statusBrush; } set { if (_statusBrush != value) { _statusBrush = value; OnPropertyChanged(nameof(StatusBrush)); } } } } public IndexedObservableCollection MachineMatrixString { get { lock (_machineMatrixStringLock) { return machineMatrixString; } } set { lock (_machineMatrixStringLock) { machineMatrixString = value; } Settings.Properties.MachineMatrixString = new List(value.ToEnumerable().Select(d => d.Name)); NotifyPropertyChanged(); } } public bool ShowClipboardAndNetworkStatusMessages { get { return Settings.Properties.ShowClipboardAndNetworkStatusMessages; } set { if (Settings.Properties.ShowClipboardAndNetworkStatusMessages != value) { Settings.Properties.ShowClipboardAndNetworkStatusMessages = value; NotifyPropertyChanged(); } } } public bool LoadUpdatedSettings() { try { LoadViewModelFromSettings(SettingsUtils.GetSettings("MouseWithoutBorders")); return true; } catch (System.Exception ex) { Logger.LogError(ex.Message); return false; } } private void SendCustomAction(string actionName) { SendConfigMSG("{\"action\":{\"MouseWithoutBorders\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}"); } public void AddFirewallRule() { SendCustomAction("add_firewall"); } public void RefreshEnabledState() { InitializeEnabledValue(); OnPropertyChanged(nameof(IsEnabled)); } private void NotifyModuleUpdatedSettings() { SendConfigMSG( string.Format( CultureInfo.InvariantCulture, "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", MouseWithoutBordersSettings.ModuleName, JsonSerializer.Serialize(Settings))); } public void NotifyUpdatedSettings() { OnPropertyChanged(null); // Notify all properties might have changed. } public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { OnPropertyChanged(propertyName); SettingsUtils.SaveSettings(Settings.ToJsonString(), MouseWithoutBordersSettings.ModuleName); if (propertyName == nameof(UseService)) { NotifyModuleUpdatedSettings(); } } private Func SendConfigMSG { get; } public void CopyMachineNameToClipboard() { var data = new DataPackage(); data.SetText(Dns.GetHostName()); Clipboard.SetContent(data); } public void Dispose() { GC.SuppressFinalize(this); } internal void UninstallService() { SendCustomAction("uninstall_service"); } } }