mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
Desktop switching and connection recovery improvements.
This commit is contained in:
parent
f5480effdb
commit
1ce76d3da7
@ -9,17 +9,23 @@ using System.Runtime.InteropServices;
|
||||
using System.ServiceProcess;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.Agent
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static bool IsDebug { get; set; }
|
||||
static void Main(string[] args)
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
|
||||
#if DEBUG
|
||||
IsDebug = true;
|
||||
#endif
|
||||
|
||||
SetWorkingDirectory();
|
||||
var argDict = ProcessArgs(args);
|
||||
|
||||
@ -33,34 +39,49 @@ namespace Remotely.Agent
|
||||
return settings;
|
||||
};
|
||||
|
||||
|
||||
if (argDict.ContainsKey("update"))
|
||||
{
|
||||
Updater.CoreUpdate();
|
||||
}
|
||||
|
||||
if (OSUtils.IsWindows)
|
||||
if (!IsDebug && OSUtils.IsWindows)
|
||||
{
|
||||
#if DEBUG
|
||||
IsDebug = true;
|
||||
DeviceSocket.Connect();
|
||||
#else
|
||||
ServiceBase.Run(new WindowsService());
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
DeviceSocket.Connect();
|
||||
Task.Run(() =>
|
||||
{
|
||||
ServiceBase.Run(new WindowsService());
|
||||
});
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
}
|
||||
HandleConnection();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleConnection()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!DeviceSocket.IsConnected)
|
||||
{
|
||||
var waitTime = new Random().Next(1000, 30000);
|
||||
Logger.Write($"Websocket closed. Reconnecting in {waitTime / 1000} seconds...");
|
||||
Task.Delay(waitTime).Wait();
|
||||
DeviceSocket.Connect().Wait();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
HandleConnection();
|
||||
}
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +98,6 @@ namespace Remotely.Agent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string,string> ProcessArgs(string[] args)
|
||||
{
|
||||
var argDict = new Dictionary<string, string>();
|
||||
|
||||
@ -24,7 +24,7 @@ namespace Remotely.Agent.Services
|
||||
|
||||
private static HubConnection HubConnection { get; set; }
|
||||
|
||||
public static async void Connect()
|
||||
public static async Task Connect()
|
||||
{
|
||||
ConnectionInfo = Utilities.GetConnectionInfo();
|
||||
|
||||
@ -32,8 +32,6 @@ namespace Remotely.Agent.Services
|
||||
.WithUrl(ConnectionInfo.Host + "/DeviceHub")
|
||||
.Build();
|
||||
|
||||
HubConnection.Closed += HubConn_Closed;
|
||||
|
||||
RegisterMessageHandlers(HubConnection);
|
||||
|
||||
await HubConnection.StartAsync();
|
||||
@ -63,6 +61,8 @@ namespace Remotely.Agent.Services
|
||||
HeartbeatTimer.Start();
|
||||
}
|
||||
|
||||
public static bool IsConnected => HubConnection?.State == HubConnectionState.Connected;
|
||||
|
||||
public static void SendHeartbeat()
|
||||
{
|
||||
var currentInfo = Device.Create(ConnectionInfo);
|
||||
@ -160,12 +160,6 @@ namespace Remotely.Agent.Services
|
||||
SendHeartbeat();
|
||||
}
|
||||
|
||||
private static async Task HubConn_Closed(Exception arg)
|
||||
{
|
||||
await Task.Delay(new Random().Next(5000, 30000));
|
||||
Connect();
|
||||
}
|
||||
|
||||
private static void RegisterMessageHandlers(HubConnection hubConnection)
|
||||
{
|
||||
hubConnection.On("ExecuteCommand", (async (string mode, string command, string commandID, string senderConnectionID) =>
|
||||
|
||||
@ -14,6 +14,7 @@ namespace Remotely.Agent.Services
|
||||
{
|
||||
public WindowsService()
|
||||
{
|
||||
CanHandleSessionChangeEvent = true;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
@ -28,7 +29,6 @@ namespace Remotely.Agent.Services
|
||||
var subkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System", true);
|
||||
subkey.SetValue("SoftwareSASGeneration", "3", Microsoft.Win32.RegistryValueKind.DWord);
|
||||
}
|
||||
DeviceSocket.Connect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -57,5 +57,20 @@ namespace Remotely.Agent.Services
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSessionChange(SessionChangeDescription changeDescription)
|
||||
{
|
||||
Logger.Write($"Session changed. Reason: {changeDescription.Reason}");
|
||||
if (changeDescription.Reason == SessionChangeReason.ConsoleDisconnect ||
|
||||
changeDescription.Reason == SessionChangeReason.RemoteDisconnect)
|
||||
{
|
||||
foreach (var screenCaster in Process.GetProcessesByName("Remotely_ScreenCast"))
|
||||
{
|
||||
Logger.Write($"Session changed. Kill process ID {screenCaster.Id}.");
|
||||
screenCaster.Kill();
|
||||
}
|
||||
}
|
||||
base.OnSessionChange(changeDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Remotely.Shared.Services;
|
||||
using Remotely.Shared.Win32;
|
||||
|
||||
namespace Remotely.ScreenCast.Core.Capture
|
||||
{
|
||||
@ -61,24 +63,27 @@ namespace Remotely.ScreenCast.Core.Capture
|
||||
};
|
||||
|
||||
// TODO: SetThreadDesktop causes issues with input after switching.
|
||||
//var desktopName = Win32Interop.GetCurrentDesktop();
|
||||
var desktopName = string.Empty;
|
||||
if (OSUtils.IsWindows)
|
||||
{
|
||||
desktopName = Win32Interop.GetCurrentDesktop();
|
||||
}
|
||||
|
||||
while (!viewer.DisconnectRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: SetThreadDesktop causes issues with input after switching.
|
||||
//var currentDesktopName = Win32Interop.GetCurrentDesktop();
|
||||
//if (desktopName.ToLower() != currentDesktopName.ToLower())
|
||||
//{
|
||||
// desktopName = currentDesktopName;
|
||||
// Logger.Write($"Switching to desktop {desktopName} in ScreenCaster.");
|
||||
// var inputDesktop = Win32Interop.OpenInputDesktop();
|
||||
// User32.SetThreadDesktop(inputDesktop);
|
||||
// User32.CloseDesktop(inputDesktop);
|
||||
// continue;
|
||||
//}
|
||||
var currentDesktopName = Win32Interop.GetCurrentDesktop();
|
||||
if (desktopName.ToLower() != currentDesktopName.ToLower())
|
||||
{
|
||||
desktopName = currentDesktopName;
|
||||
Logger.Write($"Switching to desktop {desktopName} in ScreenCaster.");
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
continue;
|
||||
}
|
||||
|
||||
while (viewer.PendingFrames > 10 || conductor.IsSwitchingDesktops)
|
||||
while (viewer.PendingFrames > 10)
|
||||
{
|
||||
await Task.Delay(1);
|
||||
}
|
||||
@ -135,6 +140,5 @@ namespace Remotely.ScreenCast.Core.Capture
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.ScreenCast.Linux
|
||||
{
|
||||
@ -34,7 +35,7 @@ namespace Remotely.ScreenCast.Linux
|
||||
Conductor.IdleTimer.Start();
|
||||
while (true)
|
||||
{
|
||||
System.Threading.Thread.Sleep(100);
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -11,6 +11,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
{
|
||||
public uint SendLeftMouseDown(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
||||
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
||||
var normalizedX = xyPercent.Item1 * 65535D;
|
||||
@ -21,6 +22,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
}
|
||||
public uint SendLeftMouseUp(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
||||
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
||||
var normalizedX = xyPercent.Item1 * 65535D;
|
||||
@ -31,6 +33,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
}
|
||||
public uint SendRightMouseDown(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
||||
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
||||
var normalizedX = xyPercent.Item1 * 65535D;
|
||||
@ -41,6 +44,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
}
|
||||
public uint SendRightMouseUp(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
||||
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
||||
var normalizedX = xyPercent.Item1 * 65535D;
|
||||
@ -51,6 +55,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
}
|
||||
public uint SendMouseMove(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
||||
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
||||
var normalizedX = xyPercent.Item1 * 65535D;
|
||||
@ -61,6 +66,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
}
|
||||
public uint SendMouseWheel(int deltaY, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
if (deltaY < 0)
|
||||
{
|
||||
deltaY = -120;
|
||||
@ -75,6 +81,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
}
|
||||
public void SendKeyDown(string key, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
var keyCode = ConvertJavaScriptKeyToVirtualKey(key);
|
||||
var union = new InputUnion()
|
||||
{
|
||||
@ -91,6 +98,7 @@ namespace Remotely.ScreenCast.Win.Input
|
||||
}
|
||||
public void SendKeyUp(string key, Viewer viewer)
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
var keyCode = ConvertJavaScriptKeyToVirtualKey(key);
|
||||
var union = new InputUnion()
|
||||
{
|
||||
|
||||
@ -41,33 +41,6 @@ namespace Remotely.ScreenCast.Win
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleConnection(Conductor conductor)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var desktopName = Win32Interop.GetCurrentDesktop();
|
||||
if (desktopName.ToLower() != conductor.CurrentDesktopName.ToLower() && conductor.Viewers.Count > 0)
|
||||
{
|
||||
conductor.CurrentDesktopName = desktopName;
|
||||
Logger.Write($"Switching desktops to {desktopName}.");
|
||||
conductor.IsSwitchingDesktops = true;
|
||||
// TODO: SetThreadDesktop causes issues with input after switching.
|
||||
//var inputDesktop = Win32Interop.OpenInputDesktop();
|
||||
//User32.SetThreadDesktop(inputDesktop);
|
||||
//User32.CloseDesktop(inputDesktop);
|
||||
conductor.Connection.InvokeAsync("SwitchingDesktops", conductor.Viewers.Keys.ToList()).Wait();
|
||||
var result = Win32Interop.OpenInteractiveProcess(Assembly.GetExecutingAssembly().Location + $" -mode {conductor.Mode.ToString()} -requester {conductor.RequesterID} -serviceid {conductor.ServiceID} -deviceid {conductor.DeviceID} -host {conductor.Host} -relaunch true -desktop {desktopName} -viewers {String.Join(",", conductor.Viewers.Keys.ToList())}", desktopName, true, out _);
|
||||
if (!result)
|
||||
{
|
||||
Logger.Write($"Desktop switch to {desktopName} failed.");
|
||||
conductor.CasterSocket.SendConnectionFailedToViewers(conductor.Viewers.Keys.ToList()).Wait();
|
||||
}
|
||||
conductor.Viewers.Clear();
|
||||
}
|
||||
await Task.Delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
@ -88,7 +61,7 @@ namespace Remotely.ScreenCast.Win
|
||||
CheckForRelaunch();
|
||||
Conductor.IdleTimer = new IdleTimer(Conductor.Viewers);
|
||||
Conductor.IdleTimer.Start();
|
||||
HandleConnection(Conductor).Wait();
|
||||
HandleConnection(Conductor);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -149,17 +122,43 @@ namespace Remotely.ScreenCast.Win
|
||||
|
||||
private static void Conductor_ClipboardTransferred(object sender, string transferredText)
|
||||
{
|
||||
var thread = new Thread(() => {
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
Clipboard.SetText(transferredText);
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.Write((Exception)e.ExceptionObject);
|
||||
}
|
||||
|
||||
private static void HandleConnection(Conductor conductor)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var desktopName = Win32Interop.GetCurrentDesktop();
|
||||
if (desktopName.ToLower() != conductor.CurrentDesktopName.ToLower() && conductor.Viewers.Count > 0)
|
||||
{
|
||||
conductor.CurrentDesktopName = desktopName;
|
||||
Logger.Write($"Switching desktops to {desktopName}.");
|
||||
// TODO: SetThreadDesktop causes issues with input after switching.
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
//conductor.IsSwitchingDesktops = true;
|
||||
//conductor.Connection.InvokeAsync("SwitchingDesktops", conductor.Viewers.Keys.ToList()).Wait();
|
||||
//var result = Win32Interop.OpenInteractiveProcess(Assembly.GetExecutingAssembly().Location + $" -mode {conductor.Mode.ToString()} -requester {conductor.RequesterID} -serviceid {conductor.ServiceID} -deviceid {conductor.DeviceID} -host {conductor.Host} -relaunch true -desktop {desktopName} -viewers {String.Join(",", conductor.Viewers.Keys.ToList())}", desktopName, true, out _);
|
||||
//if (!result)
|
||||
//{
|
||||
// Logger.Write($"Desktop switch to {desktopName} failed.");
|
||||
// conductor.CasterSocket.SendConnectionFailedToViewers(conductor.Viewers.Keys.ToList()).Wait();
|
||||
//}
|
||||
//conductor.Viewers.Clear();
|
||||
}
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
private static async void ScreenCastInitiated(object sender, ScreenCastRequest screenCastRequest)
|
||||
{
|
||||
ICapturer capturer;
|
||||
|
||||
@ -1 +1 @@
|
||||
2019.08.21.1359
|
||||
2019.09.13.1515
|
||||
|
||||
@ -12,6 +12,54 @@ namespace Remotely.Shared.Win32
|
||||
{
|
||||
public class Win32Interop
|
||||
{
|
||||
public static string GetCurrentDesktop()
|
||||
{
|
||||
var inputDesktop = OpenInputDesktop();
|
||||
byte[] deskBytes = new byte[256];
|
||||
uint lenNeeded;
|
||||
var success = GetUserObjectInformationW(inputDesktop, UOI_NAME, deskBytes, 256, out lenNeeded);
|
||||
if (!success)
|
||||
{
|
||||
CloseDesktop(inputDesktop);
|
||||
return "Default";
|
||||
}
|
||||
var desktopName = Encoding.Unicode.GetString(deskBytes.Take((int)lenNeeded).ToArray()).Replace("\0", "");
|
||||
CloseDesktop(inputDesktop);
|
||||
return desktopName;
|
||||
}
|
||||
|
||||
public static uint GetRDPSession()
|
||||
{
|
||||
IntPtr ppSessionInfo = IntPtr.Zero;
|
||||
Int32 count = 0;
|
||||
Int32 retval = WTSAPI32.WTSEnumerateSessions(WTSAPI32.WTS_CURRENT_SERVER_HANDLE, 0, 1, ref ppSessionInfo, ref count);
|
||||
Int32 dataSize = Marshal.SizeOf(typeof(WTSAPI32.WTS_SESSION_INFO));
|
||||
var sessList = new List<WTSAPI32.WTS_SESSION_INFO>();
|
||||
Int64 current = (Int64)ppSessionInfo;
|
||||
|
||||
if (retval != 0)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
WTSAPI32.WTS_SESSION_INFO sessInf = (WTSAPI32.WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WTSAPI32.WTS_SESSION_INFO));
|
||||
current += dataSize;
|
||||
sessList.Add(sessInf);
|
||||
}
|
||||
}
|
||||
uint retVal = 0;
|
||||
var rdpSession = sessList.Find(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0);
|
||||
if (sessList.Exists(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0))
|
||||
{
|
||||
retVal = (uint)rdpSession.SessionID;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public static IntPtr OpenInputDesktop()
|
||||
{
|
||||
return User32.OpenInputDesktop(0, false, ACCESS_MASK.GENERIC_ALL);
|
||||
}
|
||||
|
||||
public static bool OpenInteractiveProcess(string applicationName, string desktopName, bool hiddenWindow, out PROCESS_INFORMATION procInfo)
|
||||
{
|
||||
uint winlogonPid = 0;
|
||||
@ -90,54 +138,17 @@ namespace Remotely.Shared.Win32
|
||||
return result;
|
||||
}
|
||||
|
||||
public static uint GetRDPSession()
|
||||
{
|
||||
IntPtr ppSessionInfo = IntPtr.Zero;
|
||||
Int32 count = 0;
|
||||
Int32 retval = WTSAPI32.WTSEnumerateSessions(WTSAPI32.WTS_CURRENT_SERVER_HANDLE, 0, 1, ref ppSessionInfo, ref count);
|
||||
Int32 dataSize = Marshal.SizeOf(typeof(WTSAPI32.WTS_SESSION_INFO));
|
||||
var sessList = new List<WTSAPI32.WTS_SESSION_INFO>();
|
||||
Int64 current = (Int64)ppSessionInfo;
|
||||
|
||||
if (retval != 0)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
WTSAPI32.WTS_SESSION_INFO sessInf = (WTSAPI32.WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WTSAPI32.WTS_SESSION_INFO));
|
||||
current += dataSize;
|
||||
sessList.Add(sessInf);
|
||||
}
|
||||
}
|
||||
uint retVal = 0;
|
||||
var rdpSession = sessList.Find(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0);
|
||||
if (sessList.Exists(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0))
|
||||
{
|
||||
retVal = (uint)rdpSession.SessionID;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
public static IntPtr OpenInputDesktop()
|
||||
{
|
||||
return User32.OpenInputDesktop(0, false, ACCESS_MASK.GENERIC_ALL);
|
||||
}
|
||||
public static string GetCurrentDesktop()
|
||||
{
|
||||
var inputDesktop = OpenInputDesktop();
|
||||
byte[] deskBytes = new byte[256];
|
||||
uint lenNeeded;
|
||||
var success = GetUserObjectInformationW(inputDesktop, UOI_NAME, deskBytes, 256, out lenNeeded);
|
||||
if (!success)
|
||||
{
|
||||
CloseDesktop(inputDesktop);
|
||||
return "Default";
|
||||
}
|
||||
var desktopName = Encoding.Unicode.GetString(deskBytes.Take((int)lenNeeded).ToArray()).Replace("\0", "");
|
||||
CloseDesktop(inputDesktop);
|
||||
return desktopName;
|
||||
}
|
||||
public static void SetMonitorState(MonitorState state)
|
||||
{
|
||||
User32.SendMessage(0xFFFF, 0x112, 0xF170, (int)state);
|
||||
}
|
||||
|
||||
public static void SwitchToInputDesktop()
|
||||
{
|
||||
var inputDesktop = OpenInputDesktop();
|
||||
SwitchDesktop(inputDesktop);
|
||||
SetThreadDesktop(inputDesktop);
|
||||
CloseDesktop(inputDesktop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user