diff --git a/Desktop.Core/Interfaces/IKeyboardMouseInput.cs b/Desktop.Core/Interfaces/IKeyboardMouseInput.cs index b2fe19ea..28a68bb6 100644 --- a/Desktop.Core/Interfaces/IKeyboardMouseInput.cs +++ b/Desktop.Core/Interfaces/IKeyboardMouseInput.cs @@ -5,6 +5,7 @@ namespace Remotely.Desktop.Core.Interfaces { public interface IKeyboardMouseInput { + void Init(); void SendKeyDown(string key); void SendKeyUp(string key); void SendMouseMove(double percentX, double percentY, Services.Viewer viewer); diff --git a/Desktop.Linux/Program.cs b/Desktop.Linux/Program.cs index 6872d833..69d1ab8d 100644 --- a/Desktop.Linux/Program.cs +++ b/Desktop.Linux/Program.cs @@ -66,6 +66,7 @@ namespace Remotely.Desktop.Linux await casterSocket.NotifyRequesterUnattendedReady(Conductor.RequesterID); Services.GetRequiredService().Start(); Services.GetRequiredService().BeginWatching(); + Services.GetRequiredService().Init(); }); } else diff --git a/Desktop.Linux/Services/KeyboardMouseInputLinux.cs b/Desktop.Linux/Services/KeyboardMouseInputLinux.cs index cf83211a..52be72fa 100644 --- a/Desktop.Linux/Services/KeyboardMouseInputLinux.cs +++ b/Desktop.Linux/Services/KeyboardMouseInputLinux.cs @@ -11,14 +11,21 @@ namespace Remotely.Desktop.Linux.Services { private IntPtr Display { get; set; } + public void Init() + { + // Nothing to do here. The Windows implementation needs to start + // a processing queue to keep all input simulation on the same + // thread. Linux doesn't. + } + public void SendKeyDown(string key) { try { - Init(); + InitDisplay(); key = ConvertJavaScriptKeyToX11Key(key); var keySim = LibX11.XStringToKeysym(key); - if (keySim == null) + if (keySim == IntPtr.Zero) { Logger.Write($"Key not mapped: {key}"); return; @@ -38,10 +45,10 @@ namespace Remotely.Desktop.Linux.Services { try { - Init(); + InitDisplay(); key = ConvertJavaScriptKeyToX11Key(key); var keySim = LibX11.XStringToKeysym(key); - if (keySim == null) + if (keySim == IntPtr.Zero) { Logger.Write($"Key not mapped: {key}"); return; @@ -59,11 +66,30 @@ namespace Remotely.Desktop.Linux.Services } + public void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, Viewer viewer) + { + try + { + var isPressed = buttonAction == ButtonAction.Down; + // Browser buttons start at 0. XTest starts at 1. + var mouseButton = (uint)(button + 1); + + InitDisplay(); + SendMouseMove(percentX, percentY, viewer); + LibXtst.XTestFakeButtonEvent(Display, mouseButton, isPressed, 0); + LibX11.XSync(Display, false); + } + catch (Exception ex) + { + Logger.Write(ex); + } + } + public void SendMouseMove(double percentX, double percentY, Viewer viewer) { try { - Init(); + InitDisplay(); LibXtst.XTestFakeMotionEvent(Display, viewer.Capturer.GetSelectedScreenIndex(), (int)(viewer.Capturer.CurrentScreenBounds.Width * percentX), @@ -81,7 +107,7 @@ namespace Remotely.Desktop.Linux.Services { try { - Init(); + InitDisplay(); if (deltaY > 0) { LibXtst.XTestFakeButtonEvent(Display, 4, true, 0); @@ -104,7 +130,7 @@ namespace Remotely.Desktop.Linux.Services { try { - Init(); + InitDisplay(); SendMouseMove(percentX, percentY, viewer); LibXtst.XTestFakeButtonEvent(Display, 3, true, 0); LibX11.XSync(Display, false); @@ -119,7 +145,7 @@ namespace Remotely.Desktop.Linux.Services { try { - Init(); + InitDisplay(); SendMouseMove(percentX, percentY, viewer); LibXtst.XTestFakeButtonEvent(Display, 3, false, 0); LibX11.XSync(Display, false); @@ -129,26 +155,6 @@ namespace Remotely.Desktop.Linux.Services Logger.Write(ex); } } - - public void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, Viewer viewer) - { - try - { - var isPressed = buttonAction == ButtonAction.Down; - // Browser buttons start at 0. XTest starts at 1. - var mouseButton = (uint)(button + 1); - - Init(); - SendMouseMove(percentX, percentY, viewer); - LibXtst.XTestFakeButtonEvent(Display, mouseButton, isPressed, 0); - LibX11.XSync(Display, false); - } - catch (Exception ex) - { - Logger.Write(ex); - } - } - public void SendText(string transferText) { foreach (var key in transferText) @@ -228,8 +234,7 @@ namespace Remotely.Desktop.Linux.Services }; return keySym; } - - private void Init() + private void InitDisplay() { try { @@ -243,5 +248,6 @@ namespace Remotely.Desktop.Linux.Services Logger.Write(ex); } } + } } diff --git a/Desktop.Linux/ViewModels/MainWindowViewModel.cs b/Desktop.Linux/ViewModels/MainWindowViewModel.cs index 21fc0a92..f12d6195 100644 --- a/Desktop.Linux/ViewModels/MainWindowViewModel.cs +++ b/Desktop.Linux/ViewModels/MainWindowViewModel.cs @@ -35,6 +35,9 @@ namespace Remotely.Desktop.Linux.ViewModels return; } + Services.GetRequiredService().BeginWatching(); + Services.GetRequiredService().Init(); + Conductor = Services.GetRequiredService(); CasterSocket = Services.GetRequiredService(); diff --git a/Desktop.Win/Program.cs b/Desktop.Win/Program.cs index 77fd4949..db6e8d40 100644 --- a/Desktop.Win/Program.cs +++ b/Desktop.Win/Program.cs @@ -78,6 +78,8 @@ namespace Remotely.Desktop.Win { StartUiThreads(true); } + + WaitForAppExit(); } catch (Exception ex) { @@ -86,6 +88,21 @@ namespace Remotely.Desktop.Win } } + private static void WaitForAppExit() + { + var appExitEvent = new ManualResetEventSlim(); + + App.Current.Dispatcher.Invoke(() => + { + App.Current.Exit += (s, a) => + { + appExitEvent.Set(); + }; + }); + + appExitEvent.Wait(); + } + private static void BuildServices() { var serviceCollection = new ServiceCollection(); @@ -173,6 +190,7 @@ namespace Remotely.Desktop.Win Services.GetRequiredService().Start(); CursorIconWatcher.OnChange += CursorIconWatcher_OnChange; Services.GetRequiredService().BeginWatching(); + Services.GetRequiredService().Init(); } private static void StartUiThreads(bool createMainWindow) @@ -192,12 +210,14 @@ namespace Remotely.Desktop.Win } }); wpfUiThread.TrySetApartmentState(ApartmentState.STA); + wpfUiThread.IsBackground = true; wpfUiThread.Start(); var winformsThread = new Thread(() => { System.Windows.Forms.Application.Run(BackgroundForm); }); + winformsThread.IsBackground = true; winformsThread.TrySetApartmentState(ApartmentState.STA); winformsThread.Start(); diff --git a/Desktop.Win/Services/ClipboardServiceWin.cs b/Desktop.Win/Services/ClipboardServiceWin.cs index 9f21f7ee..de31b1b1 100644 --- a/Desktop.Win/Services/ClipboardServiceWin.cs +++ b/Desktop.Win/Services/ClipboardServiceWin.cs @@ -11,7 +11,7 @@ namespace Remotely.Desktop.Win.Services public class ClipboardServiceWin : IClipboardService { - private CancellationTokenSource cancelTokenSource; + private CancellationTokenSource _cancelTokenSource; public event EventHandler ClipboardTextChanged; @@ -19,15 +19,16 @@ namespace Remotely.Desktop.Win.Services public void BeginWatching() { - try + StopWatching(); + _cancelTokenSource = new CancellationTokenSource(); + + App.Current.Dispatcher.Invoke(() => { - StopWatching(); - } - finally - { - cancelTokenSource = new CancellationTokenSource(); - WatchClipboard(cancelTokenSource.Token); - } + App.Current.Exit -= App_Exit; + App.Current.Exit += App_Exit; + }); + + WatchClipboard(_cancelTokenSource.Token); } public Task SetText(string clipboardText) @@ -61,19 +62,22 @@ namespace Remotely.Desktop.Win.Services { try { - cancelTokenSource?.Cancel(); - cancelTokenSource?.Dispose(); + _cancelTokenSource?.Cancel(); + _cancelTokenSource?.Dispose(); } catch { } } + private void App_Exit(object sender, System.Windows.ExitEventArgs e) + { + _cancelTokenSource?.Cancel(); + } private void WatchClipboard(CancellationToken cancelToken) { var thread = new Thread(() => { - while (!cancelToken.IsCancellationRequested && - !Environment.HasShutdownStarted) + while (!cancelToken.IsCancellationRequested) { try diff --git a/Desktop.Win/Services/KeyboardMouseInputWin.cs b/Desktop.Win/Services/KeyboardMouseInputWin.cs index f6dfe2ad..25e2c358 100644 --- a/Desktop.Win/Services/KeyboardMouseInputWin.cs +++ b/Desktop.Win/Services/KeyboardMouseInputWin.cs @@ -15,12 +15,8 @@ namespace Remotely.Desktop.Win.Services { private volatile bool inputBlocked; - public KeyboardMouseInputWin() - { - StartInputProcessingThread(); - } - private CancellationTokenSource CancelTokenSource { get; set; } + private ConcurrentQueue InputActions { get; } = new ConcurrentQueue(); public Tuple GetAbsolutePercentFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer) @@ -37,6 +33,11 @@ namespace Remotely.Desktop.Win.Services return new Tuple(absoluteX, absoluteY); } + public void Init() + { + StartInputProcessingThread(); + } + public void SendKeyDown(string key) { TryOnInputDesktop(() => @@ -223,10 +224,13 @@ namespace Remotely.Desktop.Win.Services }); } + private void App_Exit(object sender, System.Windows.ExitEventArgs e) + { + CancelTokenSource?.Cancel(); + } private void CheckQueue(CancellationToken cancelToken) { - while (!Environment.HasShutdownStarted && - !cancelToken.IsCancellationRequested) + while (!cancelToken.IsCancellationRequested) { try { @@ -293,10 +297,20 @@ namespace Remotely.Desktop.Win.Services CancelTokenSource?.Cancel(); CancelTokenSource?.Dispose(); + // After BlockInput is enabled, only simulated input coming from the same thread + // will work. So we have to start a new thread that runs continuously and + // processes a queue of input events. var newThread = new Thread(() => { Logger.Write($"New input processing thread started on thread {Thread.CurrentThread.ManagedThreadId}."); CancelTokenSource = new CancellationTokenSource(); + + App.Current.Dispatcher.Invoke(() => + { + App.Current.Exit -= App_Exit; + App.Current.Exit += App_Exit; + }); + if (inputBlocked) { ToggleBlockInput(true); diff --git a/Desktop.Win/ViewModels/MainWindowViewModel.cs b/Desktop.Win/ViewModels/MainWindowViewModel.cs index b33251fc..aeadf721 100644 --- a/Desktop.Win/ViewModels/MainWindowViewModel.cs +++ b/Desktop.Win/ViewModels/MainWindowViewModel.cs @@ -40,9 +40,10 @@ namespace Remotely.Desktop.Win.ViewModels CursorIconWatcher = Services?.GetRequiredService(); CursorIconWatcher.OnChange += CursorIconWatcher_OnChange; - Services?.GetRequiredService().BeginWatching(); - Conductor = Services?.GetRequiredService(); - CasterSocket = Services?.GetRequiredService(); + Services.GetRequiredService().BeginWatching(); + Services.GetRequiredService().Init(); + Conductor = Services.GetRequiredService(); + CasterSocket = Services.GetRequiredService(); Conductor.SessionIDChanged += SessionIDChanged; Conductor.ViewerRemoved += ViewerRemoved; Conductor.ViewerAdded += ViewerAdded; @@ -191,7 +192,7 @@ namespace Remotely.Desktop.Win.ViewModels public async Task Init() { SessionID = "Retrieving..."; - + Host = Config.GetConfig().Host; while (string.IsNullOrWhiteSpace(Host)) @@ -205,7 +206,6 @@ namespace Remotely.Desktop.Win.ViewModels { await CasterSocket.Connect(Conductor.Host); - CasterSocket.Connection.Closed += async (ex) => { await App.Current.Dispatcher.InvokeAsync(() =>