Fix WPF app thread handling to allow process to close on app exit event.

This commit is contained in:
Jared 2020-12-18 12:38:21 -08:00 committed by Jared Goodwin
parent c5ad58433e
commit effb27cdd9
8 changed files with 104 additions and 55 deletions

View File

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

View File

@ -66,6 +66,7 @@ namespace Remotely.Desktop.Linux
await casterSocket.NotifyRequesterUnattendedReady(Conductor.RequesterID);
Services.GetRequiredService<IdleTimer>().Start();
Services.GetRequiredService<IClipboardService>().BeginWatching();
Services.GetRequiredService<IKeyboardMouseInput>().Init();
});
}
else

View File

@ -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);
}
}
}
}

View File

@ -35,6 +35,9 @@ namespace Remotely.Desktop.Linux.ViewModels
return;
}
Services.GetRequiredService<IClipboardService>().BeginWatching();
Services.GetRequiredService<IKeyboardMouseInput>().Init();
Conductor = Services.GetRequiredService<Conductor>();
CasterSocket = Services.GetRequiredService<CasterSocket>();

View File

@ -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<IdleTimer>().Start();
CursorIconWatcher.OnChange += CursorIconWatcher_OnChange;
Services.GetRequiredService<IClipboardService>().BeginWatching();
Services.GetRequiredService<IKeyboardMouseInput>().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();

View File

@ -11,7 +11,7 @@ namespace Remotely.Desktop.Win.Services
public class ClipboardServiceWin : IClipboardService
{
private CancellationTokenSource cancelTokenSource;
private CancellationTokenSource _cancelTokenSource;
public event EventHandler<string> 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

View File

@ -15,12 +15,8 @@ namespace Remotely.Desktop.Win.Services
{
private volatile bool inputBlocked;
public KeyboardMouseInputWin()
{
StartInputProcessingThread();
}
private CancellationTokenSource CancelTokenSource { get; set; }
private ConcurrentQueue<Action> InputActions { get; } = new ConcurrentQueue<Action>();
public Tuple<double, double> GetAbsolutePercentFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer)
@ -37,6 +33,11 @@ namespace Remotely.Desktop.Win.Services
return new Tuple<double, double>(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);

View File

@ -40,9 +40,10 @@ namespace Remotely.Desktop.Win.ViewModels
CursorIconWatcher = Services?.GetRequiredService<ICursorIconWatcher>();
CursorIconWatcher.OnChange += CursorIconWatcher_OnChange;
Services?.GetRequiredService<IClipboardService>().BeginWatching();
Conductor = Services?.GetRequiredService<Conductor>();
CasterSocket = Services?.GetRequiredService<CasterSocket>();
Services.GetRequiredService<IClipboardService>().BeginWatching();
Services.GetRequiredService<IKeyboardMouseInput>().Init();
Conductor = Services.GetRequiredService<Conductor>();
CasterSocket = Services.GetRequiredService<CasterSocket>();
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(() =>