Skip to content

Commit 4b16dce

Browse files
authored
Support overriding MsQuic.dll in the application directory on Windows. (#103351)
* Support overriding MsQuic.dll in the application directory on Windows. * Hide functionality behind appctx switch * Prioritize appctx switch, deduplicate usage * Change method name * No envvar, no fallback * Run tests on older windows. * Fix tests, log msquic load path * Make sure the msquic.dll is included in the helix package * Don't run on Win 8 * Simplify support condition * Fix log * Code review feedback
1 parent 20d6554 commit 4b16dce

File tree

8 files changed

+93
-26
lines changed

8 files changed

+93
-26
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
6+
namespace System
7+
{
8+
internal static class AppContextSwitchHelper
9+
{
10+
internal static bool GetBooleanConfig(string switchName, bool defaultValue = false) =>
11+
AppContext.TryGetSwitch(switchName, out bool value) ? value : defaultValue;
12+
13+
internal static bool GetBooleanConfig(string switchName, string envVariable, bool defaultValue = false)
14+
{
15+
if (AppContext.TryGetSwitch(switchName, out bool value))
16+
{
17+
return value;
18+
}
19+
20+
if (Environment.GetEnvironmentVariable(envVariable) is string str)
21+
{
22+
if (str == "1" || string.Equals(str, bool.TrueString, StringComparison.OrdinalIgnoreCase))
23+
{
24+
return true;
25+
}
26+
if (str == "0" || string.Equals(str, bool.FalseString, StringComparison.OrdinalIgnoreCase))
27+
{
28+
return false;
29+
}
30+
}
31+
32+
return defaultValue;
33+
}
34+
}
35+
}

src/libraries/System.Net.Quic/src/System.Net.Quic.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<Compile Include="System\Net\Quic\**\*.cs" Exclude="System\Net\Quic\*.Unsupported.cs"/>
2626
<!-- System.Net common -->
2727
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs" Link="Common\DisableRuntimeMarshalling.cs" />
28+
<Compile Include="$(CommonPath)System\AppContextSwitchHelper.cs" Link="Common\System\AppContextSwitchHelper.cs" />
2829
<Compile Include="$(CommonPath)System\Net\SafeHandleCache.cs" Link="Common\System\Net\SafeHandleCache.cs" />
2930
<Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="Common\System\Net\ArrayBuffer.cs" />
3031
<Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="Common\System\Net\MultiArrayBuffer.cs" />

src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Net.Sockets;
7+
using System.IO;
78
using System.Runtime.InteropServices;
89
using Microsoft.Quic;
910
using static Microsoft.Quic.MsQuic;
@@ -75,7 +76,7 @@ private MsQuicApi(QUIC_API_TABLE* apiTable)
7576
static MsQuicApi()
7677
{
7778
bool loaded = false;
78-
IntPtr msQuicHandle;
79+
IntPtr msQuicHandle = IntPtr.Zero;
7980
Version = default;
8081

8182
// MsQuic is using DualMode sockets and that will fail even for IPv4 if AF_INET6 is not available.
@@ -94,7 +95,7 @@ static MsQuicApi()
9495
// Windows ships msquic in the assembly directory next to System.Net.Quic, so load that.
9596
// For single-file deployments, the assembly location is an empty string so we fall back
9697
// to AppContext.BaseDirectory which is the directory containing the single-file executable.
97-
string path = typeof(MsQuicApi).Assembly.Location is string assemblyLocation && !string.IsNullOrEmpty(assemblyLocation)
98+
string path = !ShouldUseAppLocalMsQuic() && typeof(MsQuicApi).Assembly.Location is string assemblyLocation && !string.IsNullOrEmpty(assemblyLocation)
9899
? System.IO.Path.GetDirectoryName(assemblyLocation)!
99100
: AppContext.BaseDirectory;
100101

@@ -278,4 +279,7 @@ private static bool IsTls13Disabled(bool isServer)
278279
#endif
279280
return false;
280281
}
282+
283+
private static bool ShouldUseAppLocalMsQuic() => AppContextSwitchHelper.GetBooleanConfig(
284+
"System.Net.Quic.AppLocalMsQuic");
281285
}

src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.Cache.cs

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,9 @@ namespace System.Net.Quic;
1616

1717
internal static partial class MsQuicConfiguration
1818
{
19-
private const string DisableCacheEnvironmentVariable = "DOTNET_SYSTEM_NET_QUIC_DISABLE_CONFIGURATION_CACHE";
20-
private const string DisableCacheCtxSwitch = "System.Net.Quic.DisableConfigurationCache";
21-
22-
internal static bool ConfigurationCacheEnabled { get; } = GetConfigurationCacheEnabled();
23-
24-
private static bool GetConfigurationCacheEnabled()
25-
{
26-
// AppContext switch takes precedence
27-
if (AppContext.TryGetSwitch(DisableCacheCtxSwitch, out bool value))
28-
{
29-
return !value;
30-
}
31-
// check environment variable second
32-
else if (Environment.GetEnvironmentVariable(DisableCacheEnvironmentVariable) is string envVar)
33-
{
34-
return !(envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
35-
}
36-
37-
// enabled by default
38-
return true;
39-
}
19+
internal static bool ConfigurationCacheEnabled { get; } = !AppContextSwitchHelper.GetBooleanConfig(
20+
"System.Net.Quic.DisableConfigurationCache",
21+
"DOTNET_SYSTEM_NET_QUIC_DISABLE_CONFIGURATION_CACHE");
4022

4123
private static readonly MsQuicConfigurationCache s_configurationCache = new MsQuicConfigurationCache();
4224

src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ public async Task ConnectWithClientCertificate(bool sendCertificate, ClientCertS
720720
await serverConnection.DisposeAsync();
721721
}
722722

723-
[Fact]
723+
[ConditionalFact(typeof(QuicTestCollection), nameof(QuicTestCollection.IsUsingSchannelBackend))]
724724
[PlatformSpecific(TestPlatforms.Windows)]
725725
public async Task Server_CertificateWithEphemeralKey_Throws()
726726
{
@@ -764,7 +764,7 @@ public async Task Server_CertificateWithEphemeralKey_Throws()
764764
}
765765
}
766766

767-
[Fact]
767+
[ConditionalFact(typeof(QuicTestCollection), nameof(QuicTestCollection.IsUsingSchannelBackend))]
768768
[PlatformSpecific(TestPlatforms.Windows)]
769769
public async Task Client_CertificateWithEphemeralKey_Throws()
770770
{

src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ public abstract class QuicTestBase : IDisposable
4444
public const int PassingTestTimeoutMilliseconds = 4 * 60 * 1000;
4545
public static TimeSpan PassingTestTimeout => TimeSpan.FromMilliseconds(PassingTestTimeoutMilliseconds);
4646

47+
static QuicTestBase()
48+
{
49+
// Opt in to run with OpenSSL version on MsQuic on older windows where Schannel
50+
// version is not supported.
51+
//
52+
// This has to happen here in order to be called before QuicTestBase.IsSupported
53+
if (PlatformDetection.IsWindows10OrLater && !QuicTestCollection.IsWindowsVersionWithSchannelSupport())
54+
{
55+
AppContext.SetSwitch("System.Net.Quic.AppLocalMsQuic", true);
56+
}
57+
}
58+
4759
public QuicTestBase(ITestOutputHelper output)
4860
{
4961
_output = output;

src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ public unsafe class QuicTestCollection : ICollectionFixture<QuicTestCollection>,
2626
public QuicTestCollection()
2727
{
2828
string msQuicLibraryVersion = GetMsQuicLibraryVersion();
29+
string tlsBackend = IsUsingSchannelBackend() ? "Schannel" : "OpenSSL";
2930
// If any of the reflection bellow breaks due to changes in "System.Net.Quic.MsQuicApi", also check and fix HttpStress project as it uses the same hack.
30-
Console.WriteLine($"MsQuic {(IsSupported ? "supported" : "not supported")} and using '{msQuicLibraryVersion}'.");
31+
Console.WriteLine($"MsQuic {(IsSupported ? "supported" : "not supported")} and using '{msQuicLibraryVersion}' {(IsSupported ? $"({tlsBackend})" : "")}.");
3132

3233
if (IsSupported)
3334
{
@@ -97,6 +98,13 @@ void DumpCounter(QUIC_PERFORMANCE_COUNTERS counter)
9798
Console.WriteLine($"Unobserved exceptions of {s_unobservedExceptions.Count} different types: {Environment.NewLine}{string.Join(Environment.NewLine + new string('=', 120) + Environment.NewLine, s_unobservedExceptions.Select(pair => $"Count {pair.Value}: {pair.Key}"))}");
9899
}
99100

101+
internal static bool IsWindowsVersionWithSchannelSupport()
102+
{
103+
// copied from MsQuicApi implementation to avoid triggering the static constructor
104+
Version minWindowsVersion = new Version(10, 0, 20145, 1000);
105+
return OperatingSystem.IsWindowsVersionAtLeast(minWindowsVersion.Major, minWindowsVersion.Minor, minWindowsVersion.Build, minWindowsVersion.Revision);
106+
}
107+
100108
private static Version GetMsQuicVersion()
101109
{
102110
Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic");
@@ -111,6 +119,13 @@ private static Version GetMsQuicVersion()
111119
return (string)msQuicApiType.GetProperty("MsQuicLibraryVersion", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty<object?>());
112120
}
113121

122+
internal static bool IsUsingSchannelBackend()
123+
{
124+
Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic");
125+
126+
return (bool)msQuicApiType.GetProperty("UsesSChannelBackend", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty<object?>());
127+
}
128+
114129
private static QUIC_API_TABLE* GetApiTable()
115130
{
116131
Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic");

src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,22 @@
4242
<ItemGroup>
4343
<PackageReference Include="System.Net.TestData" Version="$(SystemNetTestDataVersion)" />
4444
</ItemGroup>
45+
46+
<!-- Support running tests on older windows by explicitly including OpenSSL version of MsQuic,
47+
The binary will be used only if "System.Net.Quic.AppLocalMsQuic" appctx switch is 'true',
48+
Which we manually flip in the test setup on appropriate platform. -->
49+
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
50+
<PackageReference Include="Microsoft.Native.Quic.MsQuic.OpenSSL"
51+
Version="$(MicrosoftNativeQuicMsQuicSchannelVersion)"
52+
PrivateAssets="all"
53+
GeneratePathProperty="true" />
54+
55+
</ItemGroup>
56+
<Target Name="CopyCustomContent" AfterTargets="Build" BeforeTargets="PrepareForRun" Condition="'$(TargetPlatformIdentifier)' == 'windows'">
57+
<Copy SourceFiles="$(PkgMicrosoft_Native_Quic_MsQuic_OpenSSL)\build\native\bin\x64\msquic.dll" DestinationFolder="$(OutDir)" />
58+
</Target>
59+
<Target Name="CopyCustomContentOnPublish" AfterTargets="Publish" BeforeTargets="PrepareForRun" Condition="'$(TargetPlatformIdentifier)' == 'windows'">
60+
<Copy SourceFiles="$(PkgMicrosoft_Native_Quic_MsQuic_OpenSSL)\build\native\bin\x64\msquic.dll" DestinationFolder="$(PublishDir)" />
61+
</Target>
62+
4563
</Project>

0 commit comments

Comments
 (0)