diff --git a/.ci/Scripts/PrepareFreeRdpDev.bat b/.ci/Scripts/PrepareFreeRdpDev.bat index e9b3cce11168..f6fab96dd8ad 100644 --- a/.ci/Scripts/PrepareFreeRdpDev.bat +++ b/.ci/Scripts/PrepareFreeRdpDev.bat @@ -1,3 +1,3 @@ -call .\getOpenSsl -call .\buildOpenSsl -call .\buildFreeRDP Debug +call %~dp0\getOpenSsl +call %~dp0\buildOpenSsl +call %~dp0\buildFreeRDP Debug diff --git a/.ci/Scripts/buildOpenSsl.bat b/.ci/Scripts/buildOpenSsl.bat index 538ce9c7bfa7..7d5e96ca304c 100644 --- a/.ci/Scripts/buildOpenSsl.bat +++ b/.ci/Scripts/buildOpenSsl.bat @@ -3,10 +3,6 @@ pushd . cd %freeRdpDir%\..\OpenSSL git clean -xdff -pushd . -call "%vsDir%\VC\Auxiliary\Build\vcvars64.bat" -popd - perl Configure VC-WIN64A no-asm no-shared no-module --prefix=%~dp0\..\..\..\OpenSSL-VC-64 nmake nmake install_dev diff --git a/.ci/Scripts/getOpenSsl.bat b/.ci/Scripts/getOpenSsl.bat index 873691b1bb96..553bb4681cd0 100644 --- a/.ci/Scripts/getOpenSsl.bat +++ b/.ci/Scripts/getOpenSsl.bat @@ -6,7 +6,7 @@ cd %freeRdpDir%\.. rem checkout openssl mkdir OpenSSL cd OpenSSL -git clone https://github.com/openssl/openssl . +git clone --no-checkout --filter=tree:0 --depth=1 --single-branch --branch=%openSSLTag% https://github.com/openssl/openssl . git branch buildOpenSSl %openSSLTag% git checkout buildOpenSSl popd \ No newline at end of file diff --git a/.ci/Scripts/getvars.bat b/.ci/Scripts/getvars.bat index 0d98a97aa25a..d8925b2178a2 100644 --- a/.ci/Scripts/getvars.bat +++ b/.ci/Scripts/getvars.bat @@ -1,4 +1,6 @@ @echo off +if defined freeRdpDir (exit /b) + set openSSLTag=openssl-3.0.12 set freeRdpDir=%~dp0\..\.. @@ -14,4 +16,5 @@ for /f "usebackq delims=" %%i in (`%vswhere% -prerelease -latest -property insta set vsDir=%%i ) -call "%vsdir%\Common7\Tools\vsdevcmd.bat" +call "%vsdir%\Common7\Tools\vsdevcmd.bat" -arch=amd64 + diff --git a/.ci/main.yml b/.ci/main.yml index 60ddbb119589..1378a1e8b476 100644 --- a/.ci/main.yml +++ b/.ci/main.yml @@ -19,7 +19,7 @@ steps: - task: NuGetToolInstaller@1 displayName: 'Use NuGet' -- task: NuGetAuthenticate@0 +- task: NuGetAuthenticate@1 - script: | call .ci\Scripts\getVars @@ -110,5 +110,5 @@ steps: name: publishOpenSSL inputs: targetPath: '..\OpenSSL-VC-64' - artifactName: 'openSSL' + artifactName: 'OpenSSL-VC-64' parallel: true diff --git a/UiPath.FreeRdpClient/Directory.Build.props b/UiPath.FreeRdpClient/Directory.Build.props index af5983f3eb95..d6c05ddb1853 100644 --- a/UiPath.FreeRdpClient/Directory.Build.props +++ b/UiPath.FreeRdpClient/Directory.Build.props @@ -1,6 +1,6 @@ - 23.12.0 + 24.2.0 UiPath UiPath UiPath diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/LoggingTests.cs b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/LoggingTests.cs index 00c50982b80c..aae06104e7bc 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/LoggingTests.cs +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/LoggingTests.cs @@ -13,15 +13,10 @@ public class LoggingTests : TestsBase private readonly ConcurrentDictionary> _logsByCategory = new(); private readonly ConcurrentBag _scopes = new(); private readonly Mock _loggerMock = new(); - private Wts WtsApi => Host.GetWts(); - - private IFreeRdpClient FreeRdpClient => Host.GetRequiredService(); - private ILogger Log => Host.GetRequiredService>(); private async Task Connect(RdpConnectionSettings connectionSettings) { - using var logScope = Log.BeginScope($"{NativeLoggingForwarder.ScopeName}", connectionSettings.ClientName + "_fromTest"); - return await FreeRdpClient.Connect(connectionSettings); + return await Host.Connect(connectionSettings); } public LoggingTests(ITestOutputHelper output) : base(output) @@ -63,14 +58,13 @@ public async Task ShouldProduceLogsFromFreerdp() var connectionSettings = user.ToRdpConnectionSettings(); await using var sut = await Connect(connectionSettings); - var sessionId = WtsApi.FindFirstSessionByClientName(connectionSettings.ClientName) - .ShouldNotBeNull(); - await WaitFor.Predicate(() => WtsApi.QuerySessionInformation(sessionId).ConnectState() + var sessionId = await Host.FindSession(connectionSettings); + await WaitFor.Predicate(() => Host.GetWts().QuerySessionInformation(sessionId).ConnectState() is Windows.Win32.System.RemoteDesktop.WTS_CONNECTSTATE_CLASS.WTSActive or Windows.Win32.System.RemoteDesktop.WTS_CONNECTSTATE_CLASS.WTSConnected); await sut.DisposeAsync(); - await WaitFor.Predicate(() => WtsApi.FindFirstSessionByClientName(connectionSettings.ClientName) == null); + await Host.WaitNoSession(connectionSettings); const string acceptedDebugCategory = "com.freerdp.core.nego"; var negoLogs = _logsByCategory.Where(kv => kv.Key.StartsWith(acceptedDebugCategory)) @@ -94,7 +88,7 @@ is Windows.Win32.System.RemoteDesktop.WTS_CONNECTSTATE_CLASS.WTSActive nonDebugFreeRdpLogs.ShouldContain(l => l.message.Contains("forwardFreeRdpLogs:true")); var scopes = _scopes.OfType>>() - .Where(kvl => kvl.Any(kv => kv.Key == NativeLoggingForwarder.ScopeName && connectionSettings.ClientName.Equals(kv.Value))) + .Where(kvl => kvl.Any(kv => kv.Key == NativeLoggingForwarder.ScopeName && connectionSettings.ScopeName.Equals(kv.Value))) .ToArray(); scopes.ShouldNotBeEmpty(); } @@ -139,6 +133,4 @@ public async Task ErrorLogsShouldBeFilteredAndTranslatedToWarn() testLogs.Where(l => l.logLevel is LogLevel.Error) .ShouldBeEmpty(); } - - } \ No newline at end of file diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/RdpClientTests.cs b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/RdpClientTests.cs index 34ffbaea9f55..246d368b0090 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/RdpClientTests.cs +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/RdpClientTests.cs @@ -1,200 +1,192 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Nito.Disposables; -using System.Diagnostics; -using System.Runtime.InteropServices; -using UiPath.Rdp; -using UiPath.SessionTools; - -namespace UiPath.FreeRdp.Tests; - -public class RdpClientTests : TestsBase -{ - private const string StateEstablished = "ESTABLISHED"; - private const string StateListening = "LISTENING"; - private readonly ITestOutputHelper _output; - - private readonly Wts _wts; - private IFreeRdpClient FreeRdpClient => Host.GetRequiredService(); - private ILogger Log => Host.GetRequiredService>(); - - private async Task Connect(RdpConnectionSettings connectionSettings) - { - using var logScope = Log.BeginScope($"{NativeLoggingForwarder.ScopeName}", connectionSettings.ClientName); - return await FreeRdpClient.Connect(connectionSettings); - } - - public RdpClientTests(ITestOutputHelper output) : base(output) - { - _output = output; - _wts = Host.GetWts(); - } - - [Fact] - public async Task ClientNameIsUniqueForAWhile() - { - var user = await Host.GivenUser(); - var count = 10_000; - var clientNamesHistory = new HashSet(); - - while (count-- > 0) - { - var connectionSettings = user.ToRdpConnectionSettings(); - - clientNamesHistory.Add(connectionSettings.ClientName).ShouldBe(true); - } - } - - [InlineData(32, 32)] - [InlineData(24, 8)] - [InlineData(16, 4)] - //[InlineData(15, 16, Skip = "This retuns same as 16 bit on my machine")] - //[InlineData(8, 2, Skip = "This retuns same as 16 bit on my machine")] - //[InlineData(4, 1, Skip = "This retuns same as 16 bit on my machine")] - [Theory] - public async Task ShouldConnect(int colorDepthInput, int expectedWtsApiValue) - { - var user = await Host.GivenUser(); - - var connectionSettings = user.ToRdpConnectionSettings(); - - connectionSettings.DesktopWidth = 3 * 4 * 101; - connectionSettings.DesktopHeight = 3 * 4 * 71; - connectionSettings.ColorDepth = colorDepthInput; - - await using (var sut = await Connect(connectionSettings)) - { - var sessionId = _wts.FindFirstSessionByClientName(connectionSettings.ClientName); - sessionId.ShouldNotBeNull(); - var displayInfo = _wts.QuerySessionInformation(sessionId.Value).ClientDisplay(); - - ((int)displayInfo.HorizontalResolution).ShouldBe(connectionSettings.DesktopWidth); - ((int)displayInfo.VerticalResolution).ShouldBe(connectionSettings.DesktopHeight); - //((int)displayInfo.ColorDepth).ShouldBe(expectedWtsApiValue); - } - - await WaitFor.Predicate(() => _wts.FindFirstSessionByClientName(connectionSettings.ClientName) == null); - } - - [Fact] - public async Task HostDispose_ShouldNotTriggerCrash_WhenSessionIsStillActive() - { - var user = await Host.GivenUser(); - var connectionSettings = user.ToRdpConnectionSettings(); - - await using var sut = await Connect(connectionSettings); - - await Host.DisposeAsync(); - await Task.Delay(1000); - - // The assertion is that this process hasn't crashed. - // To make it crash, comment out the following guard: - // https://github.com/UiPath/FreeRDP/blob/8da62c46223929d548fbd6984453d9ccf50a80af/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/Logging.cpp#L14-L15 - } - - [Fact] - public async Task ParallelConnectWorksOnFirstUse() - { - var user = await Host.GivenUser(); - var connectionSettings1 = user.ToRdpConnectionSettings(); - - user = await Host.GivenUserOther(); - var connectionSettings2 = user.ToRdpConnectionSettings(); - - var iterations = 10; - while (iterations-- > 0) - { - var host = new TestHost(_output).AddFreeRdp(); - await host.StartAsync(); - await using var hostStop = AsyncDisposable.Create(async () => - { - await host.StopAsync(); - await host.DisposeAsync(); - }); - - var freerdpAppDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "freerdp"); - if (Directory.Exists(freerdpAppDataFolder)) - Directory.Delete(freerdpAppDataFolder, recursive: true); - - var connect1Task = host.GetFreeRdpClient().Connect(connectionSettings1); - var connect2Task = host.GetFreeRdpClient().Connect(connectionSettings2); - - await using var d = new CollectionAsyncDisposable(await Task.WhenAll(connect1Task, connect2Task)); - - await d.DisposeAsync(); - await WaitFor.Predicate(() => _wts.FindFirstSessionByClientName(connectionSettings1.ClientName) == null); - await WaitFor.Predicate(() => _wts.FindFirstSessionByClientName(connectionSettings2.ClientName) == null); - } - } - - [Fact] - public async Task ShouldConnectWithDifferentPort() - { - var port = 44444 + DateTime.Now.Millisecond % 10; - if (port == Environment.ProcessId) - port++; - await WithPortRedirectToDefaultRdp(port); - var user = await Host.GivenUser(); - - var connectionSettings = user.ToRdpConnectionSettings(); - - connectionSettings.Port = port; - - - - - await ShouldNotHavePortWithState(port, StateEstablished); - - await using var sut = await Connect(connectionSettings); - var sessionId = _wts.FindFirstSessionByClientName(connectionSettings.ClientName); - sessionId.HasValue.ShouldBeTrue(); - - await ShouldHavePortWithState(port, StateEstablished, Environment.ProcessId); - - await sut.DisposeAsync(); - await ShouldNotHavePortWithState(port, StateEstablished); - await WaitFor.Predicate(() => _wts.FindFirstSessionByClientName(connectionSettings.ClientName) == null); - } - - private async Task WithPortRedirectToDefaultRdp(int port) - { - var log = Host.GetRequiredService>(); - - using var ctsTimeout = new CancellationTokenSource(GlobalSettings.DefaultTimeout); - - await new ProcessRunner(log).CreateRDPRedirect(port, ctsTimeout.Token); - - await ShouldHavePortWithState(port, StateListening); - } - - private async Task ShouldHavePortWithState(int port, string state, int? processId = null) - { - var log = Host.GetRequiredService>(); - - using var ctsTimeout = new CancellationTokenSource(GlobalSettings.DefaultTimeout); - - (await new ProcessRunner(log).PortWithStateExists(port, state, processId, ctsTimeout.Token)).ShouldBeTrue(); - } - - - private async Task ShouldNotHavePortWithState(int port, string state) - { - var log = Host.GetRequiredService>(); - - using var ctsTimeout = new CancellationTokenSource(GlobalSettings.DefaultTimeout); - - (await new ProcessRunner(log).PortWithStateExists(port, state, ctsTimeout.Token)).ShouldBeFalse(); - } - - [Fact] - public async Task WrongPassword_ShouldFail() - { - var user = await Host.GivenUser(); - user.Password += "_"; - - var connectionSettings = user.ToRdpConnectionSettings(); - - var exception = await Connect(connectionSettings).ShouldThrowAsync(); - exception.Message.Contains("Logon Failed", StringComparison.InvariantCultureIgnoreCase); - } +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Nito.Disposables; +using System.Diagnostics; +using System.Runtime.InteropServices; +using UiPath.Rdp; +using UiPath.SessionTools; + +namespace UiPath.FreeRdp.Tests; + +public class RdpClientTests : TestsBase +{ + private const string StateEstablished = "ESTABLISHED"; + private const string StateListening = "LISTENING"; + private readonly ITestOutputHelper _output; + + private readonly Wts _wts; + + private async Task Connect(RdpConnectionSettings connectionSettings) + { + return await Host.Connect(connectionSettings); + } + + public RdpClientTests(ITestOutputHelper output) : base(output) + { + _output = output; + _wts = Host.GetWts(); + } + + [Fact] + public async Task ScopeNameIsUniqueForAWhile() + { + var user = await Host.GivenUser(); + var count = 10_000; + var clientNamesHistory = new HashSet(); + + while (count-- > 0) + { + var connectionSettings = user.ToRdpConnectionSettings(); + + clientNamesHistory.Add(connectionSettings.ScopeName).ShouldBe(true); + } + } + + [InlineData(32, 32)] + [InlineData(24, 8)] + [InlineData(16, 4)] + //[InlineData(15, 16, Skip = "This retuns same as 16 bit on my machine")] + //[InlineData(8, 2, Skip = "This retuns same as 16 bit on my machine")] + //[InlineData(4, 1, Skip = "This retuns same as 16 bit on my machine")] + [Theory] + public async Task ShouldConnect(int colorDepthInput, int expectedWtsApiValue) + { + var user = await Host.GivenUser(); + + var connectionSettings = user.ToRdpConnectionSettings(); + + connectionSettings.DesktopWidth = 3 * 4 * 101; + connectionSettings.DesktopHeight = 3 * 4 * 71; + connectionSettings.ColorDepth = colorDepthInput; + + await using (var sut = await Connect(connectionSettings)) + { + var sessionId = await Host.FindSession(connectionSettings); + var displayInfo = _wts.QuerySessionInformation(sessionId).ClientDisplay(); + + ((int)displayInfo.HorizontalResolution).ShouldBe(connectionSettings.DesktopWidth); + ((int)displayInfo.VerticalResolution).ShouldBe(connectionSettings.DesktopHeight); + //((int)displayInfo.ColorDepth).ShouldBe(expectedWtsApiValue); + } + + await Host.WaitNoSession(connectionSettings); + } + + + [Fact] + public async Task HostDispose_ShouldNotTriggerCrash_WhenSessionIsStillActive() + { + var user = await Host.GivenUser(); + var connectionSettings = user.ToRdpConnectionSettings(); + + await using var sut = await Connect(connectionSettings); + + await Host.DisposeAsync(); + await Task.Delay(1000); + + // The assertion is that this process hasn't crashed. + // To make it crash, comment out the following guard: + // https://github.com/UiPath/FreeRDP/blob/8da62c46223929d548fbd6984453d9ccf50a80af/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/Logging.cpp#L14-L15 + } + + [Fact] + public async Task ParallelConnectWorksOnFirstUse() + { + var user = await Host.GivenUser(); + var connectionSettings1 = user.ToRdpConnectionSettings(); + + user = await Host.GivenUserOther(); + var connectionSettings2 = user.ToRdpConnectionSettings(); + + var iterations = 10; + while (iterations-- > 0) + { + var host = new TestHost(_output).AddFreeRdp(); + await host.StartAsync(); + await using var hostStop = AsyncDisposable.Create(async () => + { + await host.StopAsync(); + await host.DisposeAsync(); + }); + + var freerdpAppDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "freerdp"); + if (Directory.Exists(freerdpAppDataFolder)) + Directory.Delete(freerdpAppDataFolder, recursive: true); + + var connect1Task = host.Connect(connectionSettings1); + var connect2Task = host.Connect(connectionSettings2); + + await using var d = new CollectionAsyncDisposable(await Task.WhenAll(connect1Task, connect2Task)); + + await d.DisposeAsync(); + await host.WaitNoSession(connectionSettings1); + await host.WaitNoSession(connectionSettings2); + } + } + + [Fact] + public async Task ShouldConnectWithDifferentPort() + { + var port = 44444 + DateTime.Now.Millisecond % 10; + if (port == Environment.ProcessId) + port++; + await WithPortRedirectToDefaultRdp(port); + var user = await Host.GivenUser(); + + var connectionSettings = user.ToRdpConnectionSettings(); + + connectionSettings.Port = port; + + await ShouldNotHavePortWithState(port, StateEstablished); + + await using var sut = await Connect(connectionSettings); + var sessionId = await Host.FindSession(connectionSettings); + + await ShouldHavePortWithState(port, StateEstablished, Environment.ProcessId); + + await sut.DisposeAsync(); + await ShouldNotHavePortWithState(port, StateEstablished); + await Host.WaitNoSession(connectionSettings); + } + + private async Task WithPortRedirectToDefaultRdp(int port) + { + var log = Host.GetRequiredService>(); + + using var ctsTimeout = new CancellationTokenSource(GlobalSettings.DefaultTimeout); + + await new ProcessRunner(log).CreateRDPRedirect(port, ctsTimeout.Token); + + await ShouldHavePortWithState(port, StateListening); + } + + private async Task ShouldHavePortWithState(int port, string state, int? processId = null) + { + var log = Host.GetRequiredService>(); + + using var ctsTimeout = new CancellationTokenSource(GlobalSettings.DefaultTimeout); + + (await new ProcessRunner(log).PortWithStateExists(port, state, processId, ctsTimeout.Token)).ShouldBeTrue(); + } + + private async Task ShouldNotHavePortWithState(int port, string state) + { + var log = Host.GetRequiredService>(); + + using var ctsTimeout = new CancellationTokenSource(GlobalSettings.DefaultTimeout); + + (await new ProcessRunner(log).PortWithStateExists(port, state, ctsTimeout.Token)).ShouldBeFalse(); + } + + [Fact] + public async Task WrongPassword_ShouldFail() + { + var user = await Host.GivenUser(); + user.Password += "_"; + + var connectionSettings = user.ToRdpConnectionSettings(); + + var exception = await Connect(connectionSettings).ShouldThrowAsync(); + exception.Message.Contains("Logon Failed", StringComparison.InvariantCultureIgnoreCase); + } } \ No newline at end of file diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/TestHostExtensions.cs b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/TestHostExtensions.cs index cb5c3fb44c0f..a4e70997768b 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/TestHostExtensions.cs +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/TestHostExtensions.cs @@ -1,15 +1,12 @@ using Microsoft.Extensions.DependencyInjection; using UiPath.Rdp; -using UiPath.SessionTools; +using UiPath.SessionTools; namespace UiPath.FreeRdp.Tests.TestInfra; internal static class TestHostExtensions { public static TestHost AddFreeRdp(this TestHost host) - => host.AddRegistry(s => s.AddFreeRdp()); - - public static IFreeRdpClient GetFreeRdpClient(this TestHost host) => host.GetRequiredService(); - - public static Wts GetWts(this TestHost _) => new(); + => host.AddRegistry(s => s.AddFreeRdp()) + .AddBeforeConnectTimestamps(); } diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/WtsExtensions.cs b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/WtsExtensions.cs index 7309c064dee1..cb26a875eb28 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/WtsExtensions.cs +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpClient.Tests/TestInfra/WtsExtensions.cs @@ -1,21 +1,64 @@ -using UiPath.SessionTools; - -namespace UiPath.FreeRdp.Tests; - -internal static class WtsExtensions -{ - public static int? FindFirstSessionByClientName(this Wts wts, string clientName) - { - var sessionIds = wts.GetSessionIdList(); - - foreach (int sessionId in sessionIds) - { - if (wts.QuerySessionInformation(sessionId).ClientName() == clientName) - { - return sessionId; - } - } - - return null; - } -} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using UiPath.Rdp; +using UiPath.SessionTools; + +namespace UiPath.FreeRdp.Tests; +using ScopeToBeforeConnectTimestamp = Dictionary; + +internal static class HostExtensions +{ + public static Wts GetWts(this TestHost _) => new(); + + public static TestHost AddBeforeConnectTimestamps(this TestHost host) + => host.AddRegistry(s => s.AddSingleton()); + public static ScopeToBeforeConnectTimestamp GetScopeTimestamps(this TestHost host) + => host.GetRequiredService(); + + + private static void SaveBeforeConnectTimestamp(this TestHost host, RdpConnectionSettings connectionSettings) + => host.GetScopeTimestamps()[connectionSettings.ScopeName] = DateTimeOffset.UtcNow; + private static DateTimeOffset GetBeforeConnectTimestamp(this TestHost host, RdpConnectionSettings connectionSettings) + => host.GetScopeTimestamps()[connectionSettings.ScopeName]; + + public static async Task Connect(this TestHost host, RdpConnectionSettings connectionSettings) + { + var freeRdpClient = host.GetRequiredService(); + var log = host.GetRequiredService>(); + using var logScope = log.BeginScope($"{NativeLoggingForwarder.ScopeName}", connectionSettings.ScopeName + "_fromTest"); + host.SaveBeforeConnectTimestamp(connectionSettings); + return await freeRdpClient.Connect(connectionSettings); + } + + public static async Task FindSession(this TestHost host, RdpConnectionSettings connectionSettings) + { + int? sessionId = null; + await WaitFor.Predicate(() => (sessionId = host.FindFirstSession(connectionSettings)) is not null); + return sessionId!.Value; + } + + public static async Task WaitNoSession(this TestHost host, RdpConnectionSettings connectionSettings) + { + await WaitFor.Predicate(() => host.FindFirstSession(connectionSettings) is null); + } + + private static int? FindFirstSession(this TestHost host, RdpConnectionSettings connectionSettings) + { + var wts = host.GetWts(); + var sessionIds = wts.GetSessionIdList(); + + foreach (int sessionId in sessionIds) + { + var sessionInfo = wts.QuerySessionInformation(sessionId).SessionInfo(); + if (DateTimeOffset + .FromFileTime(sessionInfo.Data.ConnectTime) >= host.GetBeforeConnectTimestamp(connectionSettings) + && sessionInfo.Data.ConnectTime > sessionInfo.Data.DisconnectTime + ) + { + return sessionId; + } + } + + return null; + } +} diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/FreeRdpClient.cs b/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/FreeRdpClient.cs index 358da85218d4..7818abcde779 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/FreeRdpClient.cs +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/FreeRdpClient.cs @@ -38,6 +38,7 @@ public async Task Connect(RdpConnectionSettings connectionSett User = connectionSettings.Username, Domain = connectionSettings.Domain, Password = connectionSettings.Password, + ScopeName = connectionSettings.ScopeName, ClientName = connectionSettings.ClientName, HostName = connectionSettings.HostName, Port = connectionSettings.Port ?? default diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/NativeInterface.cs b/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/NativeInterface.cs index d053cc5b6ee6..77e9088ddfba 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/NativeInterface.cs +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpClient/Internals/NativeInterface.cs @@ -21,7 +21,9 @@ public struct ConnectOptions [MarshalAs(UnmanagedType.BStr)] public string Password; [MarshalAs(UnmanagedType.BStr)] - public string ClientName; + public string ScopeName; + [MarshalAs(UnmanagedType.BStr)] + public string? ClientName; [MarshalAs(UnmanagedType.BStr)] public string HostName; public int Port; diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpClient/RdpConnectionSettings.cs b/UiPath.FreeRdpClient/UiPath.FreeRdpClient/RdpConnectionSettings.cs index 807adde9ea9e..19ae0b6bfb3a 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpClient/RdpConnectionSettings.cs +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpClient/RdpConnectionSettings.cs @@ -17,22 +17,20 @@ public class RdpConnectionSettings public string HostName { get; set; } = "localhost"; public int? Port { get; set; } + public string ScopeName { get; set; } [MaxLength(15, ErrorMessage = "Sometimes :) Windows returns only first 15 chars for a session ClientName")] - public string ClientName { get; } + public string? ClientName { get; set; } public RdpConnectionSettings(string username, string domain, string password) { Username = username; Domain = domain; Password = password; - ClientName = GetUniqueClientName(); + ScopeName = GetUniqueScopeName(); } - private const int Keep4DecimalsDivider = 1_0000; - private const int Keep6DecimalsDivider = 1_000_000; - private static volatile int ConnectionId = 0; - private static readonly string ClientNameBase = "RDP_" + (Environment.ProcessId % Keep6DecimalsDivider).ToString(CultureInfo.InvariantCulture) + "_"; - private static string GetUniqueClientName() => ClientNameBase - + (Interlocked.Increment(ref ConnectionId) % Keep4DecimalsDivider).ToString(CultureInfo.InvariantCulture); + private static readonly string ScopeNameBase = "RDP_" + Environment.ProcessId.ToString(CultureInfo.InvariantCulture) + "_"; + private static string GetUniqueScopeName() => ScopeNameBase + + Interlocked.Increment(ref ConnectionId).ToString(CultureInfo.InvariantCulture); } diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.cpp b/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.cpp index bf4336a315c7..f5564ae8a087 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.cpp +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.cpp @@ -12,10 +12,42 @@ using namespace FreeRdpClient; namespace FreeRdpClient { - struct instance_data + char* ConvToUtf8(BSTR source) { + std::wstring_convert, wchar_t> convToUTF8; + return _strdup(convToUTF8.to_bytes(source).c_str()); + } + + class instance_data + { + public: rdpContext* context; HANDLE transportStopEvent; + char* scopeName; + + instance_data(rdpContext* context, ConnectOptions* rdpOptions) + { + transportStopEvent = NULL; + this->context = context; + this->scopeName = ConvToUtf8(rdpOptions->ScopeName); + } + ~instance_data() + { + if (this->transportStopEvent) + { + CloseHandle(this->transportStopEvent); + this->transportStopEvent = NULL; + } + if (this->scopeName) + { + free(this->scopeName); + this->scopeName = nullptr; + } + } + _bstr_t getEventName() + { + return "Global\\" + (_bstr_t)(this->scopeName); + } }; inline HRESULT SetErrorInfo(LPCWSTR szError) @@ -66,18 +98,17 @@ namespace FreeRdpClient void PrepareRdpContext(rdpContext* context, const ConnectOptions* rdpOptions) { - std::wstring_convert, wchar_t> convToUTF8; - - context->settings->ServerHostname = - _strdup(convToUTF8.to_bytes(rdpOptions->HostName).c_str()); + context->settings->ServerHostname = ConvToUtf8(rdpOptions->HostName); if (rdpOptions->Port) context->settings->ServerPort = rdpOptions->Port; - context->settings->Domain = _strdup(convToUTF8.to_bytes(rdpOptions->Domain).c_str()); - context->settings->Username = _strdup(convToUTF8.to_bytes(rdpOptions->User).c_str()); - context->settings->Password = _strdup(convToUTF8.to_bytes(rdpOptions->Pass).c_str()); - context->settings->ClientHostname = _strdup(convToUTF8.to_bytes(rdpOptions->ClientName).c_str()); + context->settings->Domain = ConvToUtf8(rdpOptions->Domain); + context->settings->Username = ConvToUtf8(rdpOptions->User); + context->settings->Password = ConvToUtf8(rdpOptions->Pass); + + if (rdpOptions->ClientName) + context->settings->ClientHostname = ConvToUtf8(rdpOptions->ClientName); context->settings->SoftwareGdi = TRUE; context->settings->LocalConnection = TRUE; @@ -113,12 +144,6 @@ namespace FreeRdpClient { DT_TRACE(L"RdpRelease: Start"); - if (instanceData == NULL || instanceData->context == NULL) - { - DT_ERROR(L"RdpRelease: Invalid context data"); - return ERROR_INVALID_PARAMETER; - } - freerdp* instance = instanceData->context->instance; if (instance->context->cache != NULL) { @@ -129,10 +154,7 @@ namespace FreeRdpClient freerdp_context_free(instance); freerdp_free(instance); - CloseHandle(instanceData->transportStopEvent); - instanceData->transportStopEvent = NULL; - - free(instanceData); + delete instanceData; DT_TRACE(L"RdpRelease: Finish"); return ERROR_SUCCESS; @@ -146,16 +168,10 @@ namespace FreeRdpClient DWORD WINAPI transport_thread(LPVOID pData) { instance_data* instanceData = (instance_data*)pData; - if (instanceData == NULL || instanceData->context == NULL || - instanceData->transportStopEvent == NULL) - { - DT_ERROR(L"Invalid freerdp instance data"); - return 1; - } rdpContext* context = instanceData->context; - Logging::RegisterCurrentThreadScope(context->instance->settings->ClientHostname); + Logging::RegisterCurrentThreadScope(instanceData->scopeName); context->cache = cache_new(context->instance->settings); @@ -203,28 +219,25 @@ namespace FreeRdpClient return 0; } - instance_data* transport_start(rdpContext* context, LPCWSTR eventName) + instance_data* transport_start(rdpContext* context, ConnectOptions* rdpOptions) { - instance_data* instanceData; - instanceData = (instance_data*)calloc(1, sizeof(instance_data)); - if (!instanceData) - return NULL; + instance_data* instanceData = new instance_data(context, rdpOptions); - instanceData->context = context; - auto existingEvent = OpenEvent(NULL, false, eventName); + auto eventName = instanceData->getEventName(); + auto existingEvent = OpenEvent(NULL, false, eventName.GetBSTR()); if (existingEvent) { CloseHandle(existingEvent); - DT_ERROR(L"Failed to create freerdp transport stop event, error: alreadyExists: %s", eventName); - free(instanceData); + DT_ERROR(L"Failed to create freerdp transport stop event, error: alreadyExists: %s", eventName.GetBSTR()); + delete instanceData; return NULL; } - instanceData->transportStopEvent = CreateEvent(NULL, TRUE, FALSE, eventName); + instanceData->transportStopEvent = CreateEvent(NULL, TRUE, FALSE, eventName.GetBSTR()); if (!instanceData->transportStopEvent) { DT_ERROR(L"Failed to create freerdp transport stop event, error: %u", GetLastError()); - free(instanceData); + delete instanceData; return NULL; } @@ -232,8 +245,7 @@ namespace FreeRdpClient if (!transportThreadHandle) { DT_ERROR(L"Failed to create freerdp transport client thread, error: %u", GetLastError()); - CloseHandle(instanceData->transportStopEvent); - free(instanceData); + delete instanceData; return NULL; } CloseHandle(transportThreadHandle); @@ -242,8 +254,8 @@ namespace FreeRdpClient HRESULT STDAPICALLTYPE RdpLogon(ConnectOptions* rdpOptions, BSTR& releaseEventName) { - DT_TRACE(L"Start for user: [%s], domain: [%s], clientName: [%s]", rdpOptions->User, - rdpOptions->Domain, rdpOptions->ClientName); + DT_TRACE(L"Start for user: [%s], domain: [%s], scopeName: [%s]", rdpOptions->User, + rdpOptions->Domain, rdpOptions->ScopeName); releaseEventName = NULL; auto instance = CreateFreeRdpInstance(); if (!instance) @@ -255,10 +267,10 @@ namespace FreeRdpClient auto connectResult = freerdp_connect(instance); if (connectResult) { - auto eventName = L"Global\\" + (_bstr_t)(rdpOptions->ClientName); - auto lpData = transport_start(context, eventName); + auto lpData = transport_start(context, rdpOptions); if (lpData) { + auto eventName = lpData->getEventName(); releaseEventName = eventName.Detach(); DT_TRACE(L"Connection succeeded"); return S_OK; diff --git a/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.h b/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.h index 59a306b44d0c..f3241682a9cd 100644 --- a/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.h +++ b/UiPath.FreeRdpClient/UiPath.FreeRdpWrapper/FreeRdpWrapper.h @@ -13,6 +13,7 @@ namespace FreeRdpClient BSTR User; BSTR Domain; BSTR Pass; + BSTR ScopeName; BSTR ClientName; BSTR HostName; long Port; diff --git a/UiPath.FreeRdpClient/UiPath.SessionTools.Tests/WtsTests.cs b/UiPath.FreeRdpClient/UiPath.SessionTools.Tests/WtsTests.cs index b167e56567a6..5c1c36d6e271 100644 --- a/UiPath.FreeRdpClient/UiPath.SessionTools.Tests/WtsTests.cs +++ b/UiPath.FreeRdpClient/UiPath.SessionTools.Tests/WtsTests.cs @@ -18,4 +18,11 @@ public void ClientDisplay_ShouldWork() var act = () => _ = new Wts().QuerySessionInformation(sessionId: 0).ClientDisplay(); act.ShouldNotThrow(); } + + [Fact(DisplayName = $"{nameof(Wts.QuerySessionInformation)}.{nameof(WtsInfoProviderExtensions.SessionInfo)} should work.")] + public void SessionInfo_ShouldWork() + { + var act = () => _ = new Wts().QuerySessionInformation(sessionId: 0).SessionInfo(); + act.ShouldNotThrow(); + } } diff --git a/UiPath.FreeRdpClient/UiPath.SessionTools/WTS/WTSINFOEX.cs b/UiPath.FreeRdpClient/UiPath.SessionTools/WTS/WTSINFOEX.cs new file mode 100644 index 000000000000..7c03394eafc1 --- /dev/null +++ b/UiPath.FreeRdpClient/UiPath.SessionTools/WTS/WTSINFOEX.cs @@ -0,0 +1,372 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using winmdroot = Windows.Win32; + +namespace Windows.Win32.System.RemoteDesktop; + +public struct WTSINFOEX +{ + public uint Level; + public WTSINFOEX_LEVEL1_W Data; +} + +/// Contains extended information about a Remote Desktop Services session. +/// +/// Learn more about this API from docs.microsoft.com. +/// +[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.2.63-beta+89e7e0c43f")] +public partial struct WTSINFOEX_LEVEL1_W +{ + /// The session identifier. + public uint SessionId; + /// A value of the WTS_CONNECTSTATE_CLASS enumeration type that specifies the connection state of a Remote Desktop Services session. + public winmdroot.System.RemoteDesktop.WTS_CONNECTSTATE_CLASS SessionState; + /// + public int SessionFlags; + /// A null-terminated string that contains the name of the window station for the session. + public __char_33 WinStationName; + /// A null-terminated string that contains the name of the user who owns the session. + public __char_21 UserName; + /// A null-terminated string that contains the name of the domain that the user belongs to. + public __char_18 DomainName; + /// The time that the user logged on to the session. This value is stored as a large integer that represents the number of 100-nanosecond intervals since January 1, 1601 Coordinated Universal Time (Greenwich Mean Time). + public long LogonTime; + /// The time of the most recent client connection to the session. This value is stored as a large integer that represents the number of 100-nanosecond intervals since January 1, 1601 Coordinated Universal Time. + public long ConnectTime; + /// The time of the most recent client disconnection to the session. This value is stored as a large integer that represents the number of 100-nanosecond intervals since January 1, 1601 Coordinated Universal Time. + public long DisconnectTime; + /// The time of the last user input in the session. This value is stored as a large integer that represents the number of 100-nanosecond intervals since January 1, 1601 Coordinated Universal Time. + public long LastInputTime; + /// The time that this structure was filled. This value is stored as a large integer that represents the number of 100-nanosecond intervals since January 1, 1601 Coordinated Universal Time. + public long CurrentTime; + /// The number of bytes of uncompressed Remote Desktop Protocol (RDP) data sent from the client to the server since the client connected. + public uint IncomingBytes; + /// The number of bytes of uncompressed RDP data sent from the server to the client since the client connected. + public uint OutgoingBytes; + /// The number of frames of RDP data sent from the client to the server since the client connected. + public uint IncomingFrames; + /// The number of frames of RDP data sent from the server to the client since the client connected. + public uint OutgoingFrames; + /// The number of bytes of compressed RDP data sent from the client to the server since the client connected. + public uint IncomingCompressedBytes; + /// The number of bytes of compressed RDP data sent from the server to the client since the client connected. + public uint OutgoingCompressedBytes; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public partial struct __char_33 + { + public char _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32; + + /// Always 33. + public readonly int Length => 33; + + /// + /// Gets a ref to an individual element of the inline array. + /// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it. + /// + [UnscopedRef] + public ref char this[int index] => ref AsSpan()[index]; + + /// + /// Gets this inline array as a span. + /// + /// + /// ⚠ Important ⚠: When this struct is on the stack, do not let the returned span outlive the stack frame that defines it. + /// + [UnscopedRef] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _0, 33); + + public unsafe readonly void CopyTo(Span target, int length = 33) + { + if (length > 33) throw new ArgumentOutOfRangeException("length"); + fixed (char* p0 = &_0) + { + for (int i = 0; i < length; i++) + { + target[i] = p0[i]; + } + } + } + + public readonly char[] ToArray(int length = 33) + { + if (length > 33) throw new ArgumentOutOfRangeException("length"); + char[] target = new char[length]; + CopyTo(target, length); + return target; + } + + public unsafe readonly bool Equals(ReadOnlySpan value) + { + fixed (char* p0 = &_0) + { + int commonLength = Math.Min(value.Length, 33); + for (int i = 0; i < commonLength; i++) + { + if (p0[i] != value[i]) + { + return false; + } + } + for (int i = commonLength; i < 33; i++) + { + if (p0[i] != default(char)) + { + return false; + } + } + } + return true; + } + + public readonly bool Equals(string value) => Equals(value.AsSpan()); + + /// + /// Copies the fixed array to a new string up to the specified length regardless of whether there are null terminating characters. + /// + /// + /// Thrown when is less than 0 or greater than . + /// + public unsafe readonly string ToString(int length) + { + if (length < 0 || length > Length) throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be between 0 and the fixed array length."); + fixed (char* p0 = &_0) + return new string(p0, 0, length); + } + + /// + /// Copies the fixed array to a new string, stopping before the first null terminator character or at the end of the fixed array (whichever is shorter). + /// + public override readonly unsafe string ToString() + { + int length; + fixed (char* p = &_0) + { + char* pLastExclusive = p + Length; + char* pCh = p; + for (; pCh < pLastExclusive && *pCh != '\0'; pCh++) ; + length = checked((int)(pCh - p)); + } + return ToString(length); + } + public static implicit operator __char_33(string value) => value.AsSpan(); + public static implicit operator __char_33(ReadOnlySpan value) + { + __char_33 result = default(__char_33); + value.CopyTo(result.AsSpan()); + return result; + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public partial struct __char_21 + { + public char _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20; + + /// Always 21. + public readonly int Length => 21; + + /// + /// Gets a ref to an individual element of the inline array. + /// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it. + /// + [UnscopedRef] + public ref char this[int index] => ref AsSpan()[index]; + + /// + /// Gets this inline array as a span. + /// + /// + /// ⚠ Important ⚠: When this struct is on the stack, do not let the returned span outlive the stack frame that defines it. + /// + [UnscopedRef] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _0, 21); + + public unsafe readonly void CopyTo(Span target, int length = 21) + { + if (length > 21) throw new ArgumentOutOfRangeException("length"); + fixed (char* p0 = &_0) + { + for (int i = 0; i < length; i++) + { + target[i] = p0[i]; + } + } + } + + public readonly char[] ToArray(int length = 21) + { + if (length > 21) throw new ArgumentOutOfRangeException("length"); + char[] target = new char[length]; + CopyTo(target, length); + return target; + } + + public unsafe readonly bool Equals(ReadOnlySpan value) + { + fixed (char* p0 = &_0) + { + int commonLength = Math.Min(value.Length, 21); + for (int i = 0; i < commonLength; i++) + { + if (p0[i] != value[i]) + { + return false; + } + } + for (int i = commonLength; i < 21; i++) + { + if (p0[i] != default(char)) + { + return false; + } + } + } + return true; + } + + public readonly bool Equals(string value) => Equals(value.AsSpan()); + + /// + /// Copies the fixed array to a new string up to the specified length regardless of whether there are null terminating characters. + /// + /// + /// Thrown when is less than 0 or greater than . + /// + public unsafe readonly string ToString(int length) + { + if (length < 0 || length > Length) throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be between 0 and the fixed array length."); + fixed (char* p0 = &_0) + return new string(p0, 0, length); + } + + /// + /// Copies the fixed array to a new string, stopping before the first null terminator character or at the end of the fixed array (whichever is shorter). + /// + public override readonly unsafe string ToString() + { + int length; + fixed (char* p = &_0) + { + char* pLastExclusive = p + Length; + char* pCh = p; + for (; pCh < pLastExclusive && *pCh != '\0'; pCh++) ; + length = checked((int)(pCh - p)); + } + return ToString(length); + } + public static implicit operator __char_21(string value) => value.AsSpan(); + public static implicit operator __char_21(ReadOnlySpan value) + { + __char_21 result = default(__char_21); + value.CopyTo(result.AsSpan()); + return result; + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public partial struct __char_18 + { + public char _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17; + + /// Always 18. + public readonly int Length => 18; + + /// + /// Gets a ref to an individual element of the inline array. + /// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it. + /// + [UnscopedRef] + public ref char this[int index] => ref AsSpan()[index]; + + /// + /// Gets this inline array as a span. + /// + /// + /// ⚠ Important ⚠: When this struct is on the stack, do not let the returned span outlive the stack frame that defines it. + /// + [UnscopedRef] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _0, 18); + + public unsafe readonly void CopyTo(Span target, int length = 18) + { + if (length > 18) throw new ArgumentOutOfRangeException("length"); + fixed (char* p0 = &_0) + { + for (int i = 0; i < length; i++) + { + target[i] = p0[i]; + } + } + } + + public readonly char[] ToArray(int length = 18) + { + if (length > 18) throw new ArgumentOutOfRangeException("length"); + char[] target = new char[length]; + CopyTo(target, length); + return target; + } + + public unsafe readonly bool Equals(ReadOnlySpan value) + { + fixed (char* p0 = &_0) + { + int commonLength = Math.Min(value.Length, 18); + for (int i = 0; i < commonLength; i++) + { + if (p0[i] != value[i]) + { + return false; + } + } + for (int i = commonLength; i < 18; i++) + { + if (p0[i] != default(char)) + { + return false; + } + } + } + return true; + } + + public readonly bool Equals(string value) => Equals(value.AsSpan()); + + /// + /// Copies the fixed array to a new string up to the specified length regardless of whether there are null terminating characters. + /// + /// + /// Thrown when is less than 0 or greater than . + /// + public unsafe readonly string ToString(int length) + { + if (length < 0 || length > Length) throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be between 0 and the fixed array length."); + fixed (char* p0 = &_0) + return new string(p0, 0, length); + } + + /// + /// Copies the fixed array to a new string, stopping before the first null terminator character or at the end of the fixed array (whichever is shorter). + /// + public override readonly unsafe string ToString() + { + int length; + fixed (char* p = &_0) + { + char* pLastExclusive = p + Length; + char* pCh = p; + for (; pCh < pLastExclusive && *pCh != '\0'; pCh++) ; + length = checked((int)(pCh - p)); + } + return ToString(length); + } + public static implicit operator __char_18(string value) => value.AsSpan(); + public static implicit operator __char_18(ReadOnlySpan value) + { + __char_18 result = default(__char_18); + value.CopyTo(result.AsSpan()); + return result; + } + } +} diff --git a/UiPath.FreeRdpClient/UiPath.SessionTools/WTS/Wts.InformationProvider.cs b/UiPath.FreeRdpClient/UiPath.SessionTools/WTS/Wts.InformationProvider.cs index 7d16d7a54f05..878a77fab0d3 100644 --- a/UiPath.FreeRdpClient/UiPath.SessionTools/WTS/Wts.InformationProvider.cs +++ b/UiPath.FreeRdpClient/UiPath.SessionTools/WTS/Wts.InformationProvider.cs @@ -1,67 +1,71 @@ -using Nito.Disposables; -using System.Diagnostics; -using System.Runtime.InteropServices; -using Windows.Win32; -using Windows.Win32.System.RemoteDesktop; - -namespace UiPath.SessionTools; - -partial class Wts -{ - public record InfoProvider(int SessionId) - { - public virtual string? QueryString(WTS_INFO_CLASS infoClass) - { - using var _ = Query(infoClass, out var pInfo); - - if (pInfo == default) - { - return null; - } - - return Marshal.PtrToStringAuto(pInfo); - } - - public virtual unsafe T QueryStructure(WTS_INFO_CLASS infoClass) - where T : unmanaged - { - using var _ = Query(infoClass, out var pInfo); - - return *(T*)pInfo.ToPointer(); - } - - private IDisposable? Query(WTS_INFO_CLASS infoClass, out IntPtr pInfo) - { - bool pInvokeResult = PInvoke.WTSQuerySessionInformation( - hServer: default, - SessionId: (uint)SessionId, - WTSInfoClass: infoClass, - ppBuffer: out IntPtr localPtr, - pBytesReturned: out _); - - pInfo = localPtr; - if (pInfo == default) - { - return null; - } - - return new Disposable(() => - { - PInvoke.WTSFreeMemory(localPtr); - }); - } - } -} - -public static class WtsInfoProviderExtensions -{ - public static string? DomainName(this Wts.InfoProvider reader) => reader.QueryString(WTS_INFO_CLASS.WTSDomainName); - - public static string UserName(this Wts.InfoProvider reader) - => reader.QueryString(WTS_INFO_CLASS.WTSUserName) - ?? throw new UnreachableException($"{nameof(PInvoke.WTSQuerySessionInformation)} did not fail but yielded a null buffer for {nameof(WTS_INFO_CLASS)}.{WTS_INFO_CLASS.WTSUserName}."); - - public static string? ClientName(this Wts.InfoProvider reader) => reader.QueryString(WTS_INFO_CLASS.WTSClientName); - public static WTS_CLIENT_DISPLAY ClientDisplay(this Wts.InfoProvider reader) => reader.QueryStructure(WTS_INFO_CLASS.WTSClientDisplay); - public static WTS_CONNECTSTATE_CLASS ConnectState(this Wts.InfoProvider reader) => reader.QueryStructure(WTS_INFO_CLASS.WTSConnectState); -} +using Nito.Disposables; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.System.RemoteDesktop; + +namespace UiPath.SessionTools; + +partial class Wts +{ + public record InfoProvider(int SessionId) + { + public virtual string? QueryString(WTS_INFO_CLASS infoClass) + { + using var _ = Query(infoClass, out var pInfo); + + if (pInfo == default) + { + return null; + } + + return Marshal.PtrToStringAuto(pInfo); + } + + public virtual unsafe T QueryStructure(WTS_INFO_CLASS infoClass) + where T : unmanaged + { + using var _ = Query(infoClass, out var pInfo); + if (pInfo == default) + { + return default; + } + return *(T*)pInfo.ToPointer(); + } + + private IDisposable? Query(WTS_INFO_CLASS infoClass, out IntPtr pInfo) + { + bool pInvokeResult = PInvoke.WTSQuerySessionInformation( + hServer: default, + SessionId: (uint)SessionId, + WTSInfoClass: infoClass, + ppBuffer: out IntPtr localPtr, + pBytesReturned: out _); + + pInfo = localPtr; + if (pInfo == default) + { + return null; + } + + return new Disposable(() => + { + PInvoke.WTSFreeMemory(localPtr); + }); + } + } +} + +public static class WtsInfoProviderExtensions +{ + public static string? DomainName(this Wts.InfoProvider reader) => reader.QueryString(WTS_INFO_CLASS.WTSDomainName); + + public static string UserName(this Wts.InfoProvider reader) + => reader.QueryString(WTS_INFO_CLASS.WTSUserName) + ?? throw new UnreachableException($"{nameof(PInvoke.WTSQuerySessionInformation)} did not fail but yielded a null buffer for {nameof(WTS_INFO_CLASS)}.{WTS_INFO_CLASS.WTSUserName}."); + + public static string? ClientName(this Wts.InfoProvider reader) => reader.QueryString(WTS_INFO_CLASS.WTSClientName); + public static WTS_CLIENT_DISPLAY ClientDisplay(this Wts.InfoProvider reader) => reader.QueryStructure(WTS_INFO_CLASS.WTSClientDisplay); + public static WTS_CONNECTSTATE_CLASS ConnectState(this Wts.InfoProvider reader) => reader.QueryStructure(WTS_INFO_CLASS.WTSConnectState); + public static WTSINFOEX SessionInfo(this Wts.InfoProvider reader) => reader.QueryStructure(WTS_INFO_CLASS.WTSSessionInfoEx); +}