From 5b9a03fb6cdf659c5bade3444b8cd22c4b566028 Mon Sep 17 00:00:00 2001 From: rosebyte Date: Thu, 25 Jun 2026 16:07:09 +0200 Subject: [PATCH 1/8] fix shutdown tests --- .../src/Deployers/ApplicationDeployer.cs | 44 ++++++++++- .../src/Deployers/SelfHostDeployer.cs | 79 ++++++++++++------- 2 files changed, 92 insertions(+), 31 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs index 6a96659f406835..c416821d0042d2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -102,8 +102,8 @@ protected void CleanPublishedOutput() protected string GetDotNetExeForArchitecture() { - var executableName = DotnetCommandName; - // We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version. + var executableName = GetHostDotNetExecutable(); + // We expect the x64 dotnet host to be resolvable, but we have to go searching for the x86 version. if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) { executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); @@ -116,15 +116,36 @@ protected string GetDotNetExeForArchitecture() return executableName; } + // The runtime libraries Helix harness runs tests against the testhost via $RUNTIME_PATH/dotnet by + // absolute path and doesn't add dotnet to PATH (that's only done for workload tests), so the bare + // command name can fail to launch on machines without a global dotnet. Resolve the muxer next to the + // running shared framework instead, falling back to PATH for local runs. + private static string GetHostDotNetExecutable() + { + var runtimeDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); + if (!string.IsNullOrEmpty(runtimeDirectory)) + { + // runtimeDirectory is /shared/Microsoft.NETCore.App/; the muxer lives at /dotnet. + string dotnetRoot = Path.GetFullPath(Path.Combine(runtimeDirectory, "..", "..", "..")); + string muxer = Path.Combine(dotnetRoot, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"); + if (File.Exists(muxer)) + { + return muxer; + } + } + + return DotnetCommandName; + } + protected void ShutDownIfAnyHostProcess(Process hostProcess) { - if (hostProcess != null && !hostProcess.HasExited) + if (hostProcess is not null && IsRunning(hostProcess)) { Logger.LogInformation("Attempting to cancel process {0}", hostProcess.Id); // Shutdown the host process. hostProcess.KillTree(); - if (!hostProcess.HasExited) + if (IsRunning(hostProcess)) { Logger.LogWarning("Unable to terminate the host process with process Id '{processId}", hostProcess.Id); } @@ -139,6 +160,21 @@ protected void ShutDownIfAnyHostProcess(Process hostProcess) } } + // Process.HasExited throws InvalidOperationException ("No process is associated with this object") + // when the process was never started. Treat that as "not running" so disposal after a failed + // deployment doesn't mask the original start failure with a misleading exception. + private static bool IsRunning(Process hostProcess) + { + try + { + return !hostProcess.HasExited; + } + catch (InvalidOperationException) + { + return false; + } + } + protected void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, IDictionary environmentVariables) { var environment = startInfo.Environment; diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs index b5ef69dee0664d..c4043d3a8bd487 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs @@ -122,10 +122,44 @@ protected async Task StartSelfHostAsync() AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); var started = new TaskCompletionSource(); + var hostExitTokenSource = new CancellationTokenSource(); + + await StartHostWithRetryAsync(startInfo, executableName, started, hostExitTokenSource); + + if (HostProcess.HasExited) + { + Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode); + throw new Exception("Failed to start host"); + } + + Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); - HostProcess = new Process() { StartInfo = startInfo }; - HostProcess.EnableRaisingEvents = true; - HostProcess.OutputDataReceived += (sender, dataArgs) => + // Host may not write startup messages, in which case assume it started + if (DeploymentParameters.StatusMessagesEnabled) + { + // The timeout here is large, because we don't know how long the test could need + // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build + // just in case we missed one -anurse + await started.Task.WaitAsync(TimeSpan.FromMinutes(10)); + } + + return hostExitTokenSource.Token; + } + } + + // Launching the host process can fail transiently on constrained CI/Helix machines (for example a + // failed fork or a momentarily unavailable executable). Retry a few times before giving up so a + // one-off launch failure doesn't fail the test; the final failure is rethrown with its real cause. + private async Task StartHostWithRetryAsync(ProcessStartInfo startInfo, string executableName, TaskCompletionSource started, CancellationTokenSource hostExitTokenSource) + { + const int MaxAttempts = 3; + TimeSpan retryDelay = TimeSpan.FromSeconds(2); + + for (int attempt = 1; ; attempt++) + { + var process = new Process() { StartInfo = startInfo }; + process.EnableRaisingEvents = true; + process.OutputDataReceived += (sender, dataArgs) => { if (string.Equals(dataArgs.Data, ApplicationStartedMessage)) { @@ -134,44 +168,35 @@ protected async Task StartSelfHostAsync() OutputReceived?.Invoke(sender, dataArgs); }; - var hostExitTokenSource = new CancellationTokenSource(); - HostProcess.Exited += (sender, e) => + process.Exited += (sender, e) => { - Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id); + Logger.LogInformation("host process ID {pid} shut down", process.Id); // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want - started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {HostProcess.ExitCode}")); + started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {process.ExitCode}")); TriggerHostShutdown(hostExitTokenSource); }; + HostProcess = process; + try { - HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger); - } - catch (Exception ex) - { - Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString()); + process.StartAndCaptureOutAndErrToLogger(executableName, Logger); + return; } - - if (HostProcess.HasExited) + catch (Exception ex) when (attempt < MaxAttempts) { - Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode); - throw new Exception("Failed to start host"); + Logger.LogWarning("Attempt {attempt} of {maxAttempts} to start the host process failed; retrying in {delaySeconds}s. Exception: {exception}", + attempt, MaxAttempts, retryDelay.TotalSeconds, ex.ToString()); + process.Dispose(); + await Task.Delay(retryDelay); } - - Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); - - // Host may not write startup messages, in which case assume it started - if (DeploymentParameters.StatusMessagesEnabled) + catch (Exception ex) { - // The timeout here is large, because we don't know how long the test could need - // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build - // just in case we missed one -anurse - await started.Task.WaitAsync(TimeSpan.FromMinutes(10)); + Logger.LogError("Failed to start the host process after {maxAttempts} attempts. Exception: {exception}", MaxAttempts, ex.ToString()); + throw; } - - return hostExitTokenSource.Token; } } From 25d32f0c830cecd53dcdacbf88f21022a2e5f57b Mon Sep 17 00:00:00 2001 From: rosebyte Date: Tue, 30 Jun 2026 11:12:28 +0200 Subject: [PATCH 2/8] implement PR comments --- .../src/Deployers/ApplicationDeployer.cs | 34 +++++--- .../src/Deployers/SelfHostDeployer.cs | 81 +++++++------------ 2 files changed, 53 insertions(+), 62 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs index c416821d0042d2..7acddd5c5527bc 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -103,7 +103,7 @@ protected void CleanPublishedOutput() protected string GetDotNetExeForArchitecture() { var executableName = GetHostDotNetExecutable(); - // We expect the x64 dotnet host to be resolvable, but we have to go searching for the x86 version. + // We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version. if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) { executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); @@ -116,18 +116,31 @@ protected string GetDotNetExeForArchitecture() return executableName; } - // The runtime libraries Helix harness runs tests against the testhost via $RUNTIME_PATH/dotnet by - // absolute path and doesn't add dotnet to PATH (that's only done for workload tests), so the bare - // command name can fail to launch on machines without a global dotnet. Resolve the muxer next to the - // running shared framework instead, falling back to PATH for local runs. + // Mirrors dotnet/arcade's RemoteExecutor host resolution. The runtime libraries Helix harness runs + // tests against the testhost via $RUNTIME_PATH/dotnet by absolute path and doesn't add dotnet to PATH + // (that is only done for workload tests), so launching the host by the bare command name can fail on + // machines without a global dotnet. Use the host running this test, and when that isn't dotnet (for + // example an apphost-based testhost) resolve the muxer next to the running shared framework. private static string GetHostDotNetExecutable() { + string hostName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + + string hostRunner = Process.GetCurrentProcess().MainModule?.FileName; + if (!string.IsNullOrEmpty(hostRunner) && string.Equals(Path.GetFileName(hostRunner), hostName, StringComparison.OrdinalIgnoreCase)) + { + return hostRunner; + } + + // The running host isn't dotnet (e.g. an apphost). dotnet is located three directories above the + // runtime directory, for example: + // runtime -> /shared/Microsoft.NETCore.App/ + // dotnet -> /dotnet + // This also works for a locally built runtime/testhost. var runtimeDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); if (!string.IsNullOrEmpty(runtimeDirectory)) { - // runtimeDirectory is /shared/Microsoft.NETCore.App/; the muxer lives at /dotnet. string dotnetRoot = Path.GetFullPath(Path.Combine(runtimeDirectory, "..", "..", "..")); - string muxer = Path.Combine(dotnetRoot, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"); + string muxer = Path.Combine(dotnetRoot, hostName); if (File.Exists(muxer)) { return muxer; @@ -147,7 +160,7 @@ protected void ShutDownIfAnyHostProcess(Process hostProcess) hostProcess.KillTree(); if (IsRunning(hostProcess)) { - Logger.LogWarning("Unable to terminate the host process with process Id '{processId}", hostProcess.Id); + Logger.LogWarning("Unable to terminate the host process with process Id '{processId}'", hostProcess.Id); } else { @@ -161,8 +174,9 @@ protected void ShutDownIfAnyHostProcess(Process hostProcess) } // Process.HasExited throws InvalidOperationException ("No process is associated with this object") - // when the process was never started. Treat that as "not running" so disposal after a failed - // deployment doesn't mask the original start failure with a misleading exception. + // when the process was never started (and also after the Process has been disposed, which disassociates + // it). Treat that as "not running" so shutdown cleanup stays non-throwing rather than masking the + // original start failure with a misleading exception. private static bool IsRunning(Process hostProcess) { try diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs index c4043d3a8bd487..20770b5df7c453 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs @@ -121,45 +121,12 @@ protected async Task StartSelfHostAsync() AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); - var started = new TaskCompletionSource(); + var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var hostExitTokenSource = new CancellationTokenSource(); - await StartHostWithRetryAsync(startInfo, executableName, started, hostExitTokenSource); - - if (HostProcess.HasExited) - { - Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode); - throw new Exception("Failed to start host"); - } - - Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); - - // Host may not write startup messages, in which case assume it started - if (DeploymentParameters.StatusMessagesEnabled) - { - // The timeout here is large, because we don't know how long the test could need - // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build - // just in case we missed one -anurse - await started.Task.WaitAsync(TimeSpan.FromMinutes(10)); - } - - return hostExitTokenSource.Token; - } - } - - // Launching the host process can fail transiently on constrained CI/Helix machines (for example a - // failed fork or a momentarily unavailable executable). Retry a few times before giving up so a - // one-off launch failure doesn't fail the test; the final failure is rethrown with its real cause. - private async Task StartHostWithRetryAsync(ProcessStartInfo startInfo, string executableName, TaskCompletionSource started, CancellationTokenSource hostExitTokenSource) - { - const int MaxAttempts = 3; - TimeSpan retryDelay = TimeSpan.FromSeconds(2); - - for (int attempt = 1; ; attempt++) - { - var process = new Process() { StartInfo = startInfo }; - process.EnableRaisingEvents = true; - process.OutputDataReceived += (sender, dataArgs) => + HostProcess = new Process() { StartInfo = startInfo }; + HostProcess.EnableRaisingEvents = true; + HostProcess.OutputDataReceived += (sender, dataArgs) => { if (string.Equals(dataArgs.Data, ApplicationStartedMessage)) { @@ -168,35 +135,45 @@ private async Task StartHostWithRetryAsync(ProcessStartInfo startInfo, string ex OutputReceived?.Invoke(sender, dataArgs); }; - process.Exited += (sender, e) => + HostProcess.Exited += (sender, e) => { - Logger.LogInformation("host process ID {pid} shut down", process.Id); + Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id); // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want - started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {process.ExitCode}")); + started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {HostProcess.ExitCode}")); TriggerHostShutdown(hostExitTokenSource); }; - HostProcess = process; - try { - process.StartAndCaptureOutAndErrToLogger(executableName, Logger); - return; - } - catch (Exception ex) when (attempt < MaxAttempts) - { - Logger.LogWarning("Attempt {attempt} of {maxAttempts} to start the host process failed; retrying in {delaySeconds}s. Exception: {exception}", - attempt, MaxAttempts, retryDelay.TotalSeconds, ex.ToString()); - process.Dispose(); - await Task.Delay(retryDelay); + HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger); } catch (Exception ex) { - Logger.LogError("Failed to start the host process after {maxAttempts} attempts. Exception: {exception}", MaxAttempts, ex.ToString()); + // Surface the real launch failure instead of letting it be masked later during disposal. + Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString()); throw; } + + if (HostProcess.HasExited) + { + Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode); + throw new Exception("Failed to start host"); + } + + Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); + + // Host may not write startup messages, in which case assume it started + if (DeploymentParameters.StatusMessagesEnabled) + { + // The timeout here is large, because we don't know how long the test could need + // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build + // just in case we missed one -anurse + await started.Task.WaitAsync(TimeSpan.FromMinutes(10)); + } + + return hostExitTokenSource.Token; } } From 3274dea6657dbc3dc22bb1964a6e934d99f4644c Mon Sep 17 00:00:00 2001 From: rosebyte Date: Wed, 29 Jul 2026 18:42:43 +0200 Subject: [PATCH 3/8] Drop remote executor --- .../src/Deployers/ApplicationDeployer.cs | 36 +------------------ .../tests/FunctionalTests/ShutdownTests.cs | 11 ++++-- src/libraries/tests.proj | 6 ---- 3 files changed, 10 insertions(+), 43 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs index 7acddd5c5527bc..e7c95c965a1bac 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -102,7 +102,7 @@ protected void CleanPublishedOutput() protected string GetDotNetExeForArchitecture() { - var executableName = GetHostDotNetExecutable(); + var executableName = DotnetCommandName; // We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version. if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) { @@ -116,40 +116,6 @@ protected string GetDotNetExeForArchitecture() return executableName; } - // Mirrors dotnet/arcade's RemoteExecutor host resolution. The runtime libraries Helix harness runs - // tests against the testhost via $RUNTIME_PATH/dotnet by absolute path and doesn't add dotnet to PATH - // (that is only done for workload tests), so launching the host by the bare command name can fail on - // machines without a global dotnet. Use the host running this test, and when that isn't dotnet (for - // example an apphost-based testhost) resolve the muxer next to the running shared framework. - private static string GetHostDotNetExecutable() - { - string hostName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; - - string hostRunner = Process.GetCurrentProcess().MainModule?.FileName; - if (!string.IsNullOrEmpty(hostRunner) && string.Equals(Path.GetFileName(hostRunner), hostName, StringComparison.OrdinalIgnoreCase)) - { - return hostRunner; - } - - // The running host isn't dotnet (e.g. an apphost). dotnet is located three directories above the - // runtime directory, for example: - // runtime -> /shared/Microsoft.NETCore.App/ - // dotnet -> /dotnet - // This also works for a locally built runtime/testhost. - var runtimeDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); - if (!string.IsNullOrEmpty(runtimeDirectory)) - { - string dotnetRoot = Path.GetFullPath(Path.Combine(runtimeDirectory, "..", "..", "..")); - string muxer = Path.Combine(dotnetRoot, hostName); - if (File.Exists(muxer)) - { - return muxer; - } - } - - return DotnetCommandName; - } - protected void ShutDownIfAnyHostProcess(Process hostProcess) { if (hostProcess is not null && IsRunning(hostProcess)) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs index 5ab3a61adfdc6a..1fc0b2d1db07b3 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs @@ -24,19 +24,26 @@ public class ShutdownTests private static readonly TimeSpan s_shutdownExitTimeout = TimeSpan.FromSeconds(30); private readonly ITestOutputHelper _output; + // The deployer launches the test app through the dotnet muxer. The NativeAOT and ReadyToRun + // test legs publish the test itself as a self-contained app, so there is no muxer sitting next + // to the running host for Process.Start to resolve. The child app would run on the ordinary + // shared framework anyway, so those legs gain nothing from these tests; the single file test + // runner turns RemoteExecutor off for the same reason. + public static bool IsPortableAppLaunchSupported => PlatformDetection.IsNotNativeAot && !PlatformDetection.IsReadyToRunCompiled; + public ShutdownTests(ITestOutputHelper output) { _output = output; } - [Fact] + [ConditionalFact(typeof(ShutdownTests), nameof(IsPortableAppLaunchSupported))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestRun() { await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run"); } - [Fact] + [ConditionalFact(typeof(ShutdownTests), nameof(IsPortableAppLaunchSupported))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestWaitForShutdown() { diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 0dd94483146f90..9f48d7b47b225b 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -394,9 +394,6 @@ - - - @@ -663,9 +660,6 @@ - - - From 407742360169f7531fbab763ff52f8275cc1fd89 Mon Sep 17 00:00:00 2001 From: rosebyte Date: Tue, 4 Aug 2026 11:12:12 +0200 Subject: [PATCH 4/8] lookup dotnet executable --- .../src/Common/DotNetCommands.cs | 96 +++++++++++++++++-- .../src/Deployers/ApplicationDeployer.cs | 15 +-- .../tests/FunctionalTests/ShutdownTests.cs | 16 ++-- 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs index 7282154c33498c..ab6f74d5d27ff3 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; @@ -13,6 +14,15 @@ public static class DotNetCommands internal static string DotNetHome { get; } = GetDotNetHome(); + /// + /// Gets the full path of the muxer that portable applications are launched with, or + /// when the current test environment has not got one. + /// + public static string DotNetMuxerPath { get; } = FindDotNetMuxer(); + + public static string DotNetExecutableName + => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + // Compare to https://github.com/aspnet/BuildTools/blob/314c98e4533217a841ff9767bb38e144eb6c93e4/tools/KoreBuild.Console/Commands/CommandContext.cs#L76 public static string GetDotNetHome() { @@ -55,23 +65,93 @@ public static string GetDotNetInstallDir(RuntimeArchitecture arch) } public static string GetDotNetExecutable(RuntimeArchitecture arch) + => Path.Combine(GetDotNetInstallDir(arch), DotNetExecutableName); + + public static bool IsRunningX86OnX64(RuntimeArchitecture arch) { - var dotnetDir = GetDotNetInstallDir(arch); + return (RuntimeInformation.OSArchitecture == Architecture.X64 || RuntimeInformation.OSArchitecture == Architecture.Arm64) + && arch == RuntimeArchitecture.x86; + } - var dotnetFile = "dotnet"; + private static string FindDotNetMuxer() + { + var fileName = DotNetExecutableName; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + foreach (var directory in GetMuxerProbingDirectories()) { - dotnetFile += ".exe"; + if (string.IsNullOrEmpty(directory)) + { + continue; + } + + string candidate; + try + { + candidate = Path.Combine(directory, fileName); + } + catch (ArgumentException) + { + // Ignore malformed PATH entries. + continue; + } + + if (IsExecutable(candidate)) + { + return candidate; + } } - return Path.Combine(dotnetDir, dotnetFile); + return null; } - public static bool IsRunningX86OnX64(RuntimeArchitecture arch) + // Process.Start only accepts a candidate that is executable, so a stray non-executable file named + // "dotnet" must not shadow a real muxer further along the search path. + private static bool IsExecutable(string path) { - return (RuntimeInformation.OSArchitecture == Architecture.X64 || RuntimeInformation.OSArchitecture == Architecture.Arm64) - && arch == RuntimeArchitecture.x86; + if (!File.Exists(path)) + { + return false; + } + +#if NETFRAMEWORK + return true; +#else + if (OperatingSystem.IsWindows()) + { + return true; + } + + const UnixFileMode ExecuteBits = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; + return (File.GetUnixFileMode(path) & ExecuteBits) != 0; +#endif } + + // Probe the same places Process.Start searches when it is handed a bare file name: the directory + // holding the executable that is running the tests, the current directory, and then every entry on + // PATH. See ResolvePath in System.Diagnostics.Process. + private static IEnumerable GetMuxerProbingDirectories() + { + yield return HostExecutableDirectory; + yield return Directory.GetCurrentDirectory(); + + const StringSplitOptions splitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; + var path = Environment.GetEnvironmentVariable("PATH"); + if (!string.IsNullOrEmpty(path)) + { + foreach (var directory in path.Split(Path.PathSeparator, splitOptions)) + { + yield return directory; + } + } + } + + private static string HostExecutableDirectory => +#if NETFRAMEWORK + // On .NET Framework the entry executable is the application itself, so the app base + // directory is the directory holding it. + AppContext.BaseDirectory; +#else + Path.GetDirectoryName(Environment.ProcessPath); +#endif } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs index e7c95c965a1bac..aa84832a8d6650 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -18,8 +18,6 @@ namespace Microsoft.Extensions.Hosting.IntegrationTesting /// public abstract class ApplicationDeployer : IDisposable { - public static readonly string DotnetCommandName = "dotnet"; - private readonly Stopwatch _stopwatch = new Stopwatch(); private PublishedApplication _publishedApplication; @@ -102,18 +100,21 @@ protected void CleanPublishedOutput() protected string GetDotNetExeForArchitecture() { - var executableName = DotnetCommandName; - // We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version. + // We have to go searching for the x86 version, everything else runs on the muxer the tests + // themselves were launched with. if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) { - executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); + var executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); if (!File.Exists(executableName)) { - throw new Exception($"Unable to find '{executableName}'.'"); + throw new Exception($"Unable to find '{executableName}'."); } + + return executableName; } - return executableName; + return DotNetCommands.DotNetMuxerPath + ?? throw new Exception($"Unable to find '{DotNetCommands.DotNetExecutableName}'."); } protected void ShutDownIfAnyHostProcess(Process hostProcess) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs index 1fc0b2d1db07b3..eba3566a43707e 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs @@ -24,26 +24,26 @@ public class ShutdownTests private static readonly TimeSpan s_shutdownExitTimeout = TimeSpan.FromSeconds(30); private readonly ITestOutputHelper _output; - // The deployer launches the test app through the dotnet muxer. The NativeAOT and ReadyToRun - // test legs publish the test itself as a self-contained app, so there is no muxer sitting next - // to the running host for Process.Start to resolve. The child app would run on the ordinary - // shared framework anyway, so those legs gain nothing from these tests; the single file test - // runner turns RemoteExecutor off for the same reason. - public static bool IsPortableAppLaunchSupported => PlatformDetection.IsNotNativeAot && !PlatformDetection.IsReadyToRunCompiled; + // The deployer launches the test application through the dotnet muxer, so these tests can only run + // where the environment actually has one. That is a property of the environment rather than of how + // the tests themselves were published: legs that publish the tests as a self-contained application + // run under their own apphost, so nothing resolves a bare "dotnet" for them unless the machine + // happens to have one on PATH. + public static bool IsDotNetHostAvailable => DotNetCommands.DotNetMuxerPath is not null; public ShutdownTests(ITestOutputHelper output) { _output = output; } - [ConditionalFact(typeof(ShutdownTests), nameof(IsPortableAppLaunchSupported))] + [ConditionalFact(typeof(ShutdownTests), nameof(IsDotNetHostAvailable))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestRun() { await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run"); } - [ConditionalFact(typeof(ShutdownTests), nameof(IsPortableAppLaunchSupported))] + [ConditionalFact(typeof(ShutdownTests), nameof(IsDotNetHostAvailable))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestWaitForShutdown() { From e05dbef167764d27c7abf0f5ff698309f41f29b5 Mon Sep 17 00:00:00 2001 From: rosebyte Date: Tue, 4 Aug 2026 13:22:12 +0200 Subject: [PATCH 5/8] net framework fix --- .../IntegrationTesting/src/Common/DotNetCommands.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs index ab6f74d5d27ff3..e8e4db122eff7e 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs @@ -133,12 +133,11 @@ private static IEnumerable GetMuxerProbingDirectories() { yield return HostExecutableDirectory; yield return Directory.GetCurrentDirectory(); - - const StringSplitOptions splitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; + var path = Environment.GetEnvironmentVariable("PATH"); if (!string.IsNullOrEmpty(path)) { - foreach (var directory in path.Split(Path.PathSeparator, splitOptions)) + foreach (var directory in path.Split(Path.PathSeparator)) { yield return directory; } From b91e10f8b62c534b1b8cc5bfb9a091f203aea787 Mon Sep 17 00:00:00 2001 From: rosebyte Date: Thu, 6 Aug 2026 07:49:47 +0200 Subject: [PATCH 6/8] use local dotnet --- .../src/Common/DotNetCommands.cs | 86 ++++--------------- ...Extensions.Hosting.Functional.Tests.csproj | 15 ++++ .../tests/FunctionalTests/ShutdownTests.cs | 31 +++++-- 3 files changed, 52 insertions(+), 80 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs index e8e4db122eff7e..cce29bb797a97a 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; @@ -12,6 +11,11 @@ public static class DotNetCommands { private const string _dotnetFolderName = ".dotnet"; + // Set by the test run script to the host it was handed via --runtime-path: the locally built + // testhost for a local run, the Helix correlation payload in CI. See the SetTestDotNetHostPath + // target in Microsoft.Extensions.Hosting.Functional.Tests.csproj. + private const string HostPathVariableName = "__TestDotNetHostPath"; + internal static string DotNetHome { get; } = GetDotNetHome(); /// @@ -75,82 +79,22 @@ public static bool IsRunningX86OnX64(RuntimeArchitecture arch) private static string FindDotNetMuxer() { - var fileName = DotNetExecutableName; - - foreach (var directory in GetMuxerProbingDirectories()) + var fromRunScript = Environment.GetEnvironmentVariable(HostPathVariableName); + if (!string.IsNullOrEmpty(fromRunScript) && File.Exists(fromRunScript)) { - if (string.IsNullOrEmpty(directory)) - { - continue; - } - - string candidate; - try - { - candidate = Path.Combine(directory, fileName); - } - catch (ArgumentException) - { - // Ignore malformed PATH entries. - continue; - } - - if (IsExecutable(candidate)) - { - return candidate; - } - } - - return null; - } - - // Process.Start only accepts a candidate that is executable, so a stray non-executable file named - // "dotnet" must not shadow a real muxer further along the search path. - private static bool IsExecutable(string path) - { - if (!File.Exists(path)) - { - return false; + return fromRunScript; } #if NETFRAMEWORK - return true; + return null; #else - if (OperatingSystem.IsWindows()) - { - return true; - } - - const UnixFileMode ExecuteBits = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; - return (File.GetUnixFileMode(path) & ExecuteBits) != 0; + // Outside the run script the only host we can vouch for is the one running the tests, which is + // the muxer itself on every leg that does not publish the tests as a self-contained application. + var processPath = Environment.ProcessPath; + return string.Equals(Path.GetFileName(processPath), DotNetExecutableName, StringComparison.OrdinalIgnoreCase) + ? processPath + : null; #endif } - - // Probe the same places Process.Start searches when it is handed a bare file name: the directory - // holding the executable that is running the tests, the current directory, and then every entry on - // PATH. See ResolvePath in System.Diagnostics.Process. - private static IEnumerable GetMuxerProbingDirectories() - { - yield return HostExecutableDirectory; - yield return Directory.GetCurrentDirectory(); - - var path = Environment.GetEnvironmentVariable("PATH"); - if (!string.IsNullOrEmpty(path)) - { - foreach (var directory in path.Split(Path.PathSeparator)) - { - yield return directory; - } - } - } - - private static string HostExecutableDirectory => -#if NETFRAMEWORK - // On .NET Framework the entry executable is the application itself, so the app base - // directory is the directory holding it. - AppContext.BaseDirectory; -#else - Path.GetDirectoryName(Environment.ProcessPath); -#endif } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj index 124e4c6ec57bdf..8dbf430fa870d2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj @@ -31,4 +31,19 @@ + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs index eba3566a43707e..ed84d22abc6701 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Hosting.IntegrationTesting; @@ -22,28 +23,40 @@ public class ShutdownTests "Stopped firing\n" + "Stopped end"; private static readonly TimeSpan s_shutdownExitTimeout = TimeSpan.FromSeconds(30); + private const string TestApplicationName = "Microsoft.Extensions.Hosting.TestApp"; private readonly ITestOutputHelper _output; - // The deployer launches the test application through the dotnet muxer, so these tests can only run - // where the environment actually has one. That is a property of the environment rather than of how - // the tests themselves were published: legs that publish the tests as a self-contained application - // run under their own apphost, so nothing resolves a bare "dotnet" for them unless the machine - // happens to have one on PATH. - public static bool IsDotNetHostAvailable => DotNetCommands.DotNetMuxerPath is not null; + // The deployer launches the test application as a portable application: that needs a muxer, plus the + // application's assembly and its dependency manifest sitting next to the tests. Legs that publish the + // tests (single file, NativeAOT, ReadyToRun) run from a self-contained publish layout which carries + // the assembly but no deps.json for it, so there is nothing there to launch portably. + public static bool CanDeployTestApplication + { + get + { + if (DotNetCommands.DotNetMuxerPath is null) + { + return false; + } + + var applicationPath = Path.Combine(AppContext.BaseDirectory, TestApplicationName); + return File.Exists(applicationPath + ".dll") && File.Exists(applicationPath + ".deps.json"); + } + } public ShutdownTests(ITestOutputHelper output) { _output = output; } - [ConditionalFact(typeof(ShutdownTests), nameof(IsDotNetHostAvailable))] + [ConditionalFact(typeof(ShutdownTests), nameof(CanDeployTestApplication))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestRun() { await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run"); } - [ConditionalFact(typeof(ShutdownTests), nameof(IsDotNetHostAvailable))] + [ConditionalFact(typeof(ShutdownTests), nameof(CanDeployTestApplication))] [PlatformSpecific(TestPlatforms.Linux)] public async Task ShutdownTestWaitForShutdown() { @@ -66,7 +79,7 @@ private async Task ExecuteShutdownTest(string testName, string shutdownMechanic) RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) { - ApplicationName = "Microsoft.Extensions.Hosting.TestApp", + ApplicationName = TestApplicationName, TargetFramework = $"net{version.Major}.{version.Minor}", ApplicationType = ApplicationType.Portable, PublishApplicationBeforeDeployment = true, From a2e7c57c5ef1d4b014bafba7ae107aca3ef1aadb Mon Sep 17 00:00:00 2001 From: rosebyte Date: Fri, 7 Aug 2026 13:50:08 +0200 Subject: [PATCH 7/8] use RUNTIME_PATH --- eng/testing/RunnerTemplate.sh | 4 +++- .../src/Common/DotNetCommands.cs | 18 +++++++++++------- ....Extensions.Hosting.Functional.Tests.csproj | 15 --------------- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/eng/testing/RunnerTemplate.sh b/eng/testing/RunnerTemplate.sh index 0e6e0b778bd4e5..c990b67d06496f 100644 --- a/eng/testing/RunnerTemplate.sh +++ b/eng/testing/RunnerTemplate.sh @@ -22,7 +22,9 @@ while [[ $# -gt 0 ]]; do exit 0 ;; --runtime-path|-r) - RUNTIME_PATH=$2 + # Exported so that tests which need to launch a portable application of their own can find the + # same host the test run itself was handed. RunnerTemplate.cmd's "set" already does this. + export RUNTIME_PATH=$2 shift ;; --rsp-file) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs index cce29bb797a97a..1952d8869ab108 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs @@ -11,10 +11,10 @@ public static class DotNetCommands { private const string _dotnetFolderName = ".dotnet"; - // Set by the test run script to the host it was handed via --runtime-path: the locally built - // testhost for a local run, the Helix correlation payload in CI. See the SetTestDotNetHostPath - // target in Microsoft.Extensions.Hosting.Functional.Tests.csproj. - private const string HostPathVariableName = "__TestDotNetHostPath"; + // Set by the test run script to the testhost it was handed via --runtime-path: the locally built + // testhost for a local run, the Helix correlation payload in CI. See eng/testing/RunnerTemplate.sh + // and eng/testing/RunnerTemplate.cmd. + private const string RuntimePathVariableName = "RUNTIME_PATH"; internal static string DotNetHome { get; } = GetDotNetHome(); @@ -79,10 +79,14 @@ public static bool IsRunningX86OnX64(RuntimeArchitecture arch) private static string FindDotNetMuxer() { - var fromRunScript = Environment.GetEnvironmentVariable(HostPathVariableName); - if (!string.IsNullOrEmpty(fromRunScript) && File.Exists(fromRunScript)) + var runtimePath = Environment.GetEnvironmentVariable(RuntimePathVariableName); + if (!string.IsNullOrEmpty(runtimePath)) { - return fromRunScript; + var fromRunScript = Path.Combine(runtimePath, DotNetExecutableName); + if (File.Exists(fromRunScript)) + { + return fromRunScript; + } } #if NETFRAMEWORK diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj index 8dbf430fa870d2..124e4c6ec57bdf 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/Microsoft.Extensions.Hosting.Functional.Tests.csproj @@ -31,19 +31,4 @@ - - - - - - - From b29cee6cfe986925628caf2f3ef9941081888343 Mon Sep 17 00:00:00 2001 From: rosebyte Date: Tue, 11 Aug 2026 23:01:40 +0200 Subject: [PATCH 8/8] use RUNTIME_PATH --- .../src/Common/DotNetCommands.cs | 54 ------------------- .../src/Deployers/ApplicationDeployer.cs | 19 +------ .../src/Deployers/SelfHostDeployer.cs | 2 +- 3 files changed, 3 insertions(+), 72 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs index 1952d8869ab108..3e6c5eabde7ac4 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs @@ -9,15 +9,11 @@ namespace Microsoft.Extensions.Hosting.IntegrationTesting { public static class DotNetCommands { - private const string _dotnetFolderName = ".dotnet"; - // Set by the test run script to the testhost it was handed via --runtime-path: the locally built // testhost for a local run, the Helix correlation payload in CI. See eng/testing/RunnerTemplate.sh // and eng/testing/RunnerTemplate.cmd. private const string RuntimePathVariableName = "RUNTIME_PATH"; - internal static string DotNetHome { get; } = GetDotNetHome(); - /// /// Gets the full path of the muxer that portable applications are launched with, or /// when the current test environment has not got one. @@ -27,56 +23,6 @@ public static class DotNetCommands public static string DotNetExecutableName => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; - // Compare to https://github.com/aspnet/BuildTools/blob/314c98e4533217a841ff9767bb38e144eb6c93e4/tools/KoreBuild.Console/Commands/CommandContext.cs#L76 - public static string GetDotNetHome() - { - var dotnetHome = Environment.GetEnvironmentVariable("DOTNET_HOME"); - var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); - var userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); - var home = Environment.GetEnvironmentVariable("HOME"); - - var result = Path.Combine(Directory.GetCurrentDirectory(), _dotnetFolderName); - if (!string.IsNullOrEmpty(dotnetHome)) - { - result = dotnetHome; - } - else if (!string.IsNullOrEmpty(dotnetRoot)) - { - // DOTNET_ROOT has x64 appended to the path, which we append again in GetDotNetInstallDir - result = dotnetRoot.Substring(0, dotnetRoot.Length - 3); - } - else if (!string.IsNullOrEmpty(userProfile)) - { - result = Path.Combine(userProfile, _dotnetFolderName); - } - else if (!string.IsNullOrEmpty(home)) - { - result = home; - } - - return result; - } - - public static string GetDotNetInstallDir(RuntimeArchitecture arch) - { - var dotnetDir = DotNetHome; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - dotnetDir = Path.Combine(dotnetDir, arch.ToString()); - } - - return dotnetDir; - } - - public static string GetDotNetExecutable(RuntimeArchitecture arch) - => Path.Combine(GetDotNetInstallDir(arch), DotNetExecutableName); - - public static bool IsRunningX86OnX64(RuntimeArchitecture arch) - { - return (RuntimeInformation.OSArchitecture == Architecture.X64 || RuntimeInformation.OSArchitecture == Architecture.Arm64) - && arch == RuntimeArchitecture.x86; - } - private static string FindDotNetMuxer() { var runtimePath = Environment.GetEnvironmentVariable(RuntimePathVariableName); diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs index aa84832a8d6650..31b689d3f1114a 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -98,24 +98,9 @@ protected void CleanPublishedOutput() } } - protected string GetDotNetExeForArchitecture() - { - // We have to go searching for the x86 version, everything else runs on the muxer the tests - // themselves were launched with. - if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) - { - var executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); - if (!File.Exists(executableName)) - { - throw new Exception($"Unable to find '{executableName}'."); - } - - return executableName; - } - - return DotNetCommands.DotNetMuxerPath + protected static string GetDotNetMuxerPath() + => DotNetCommands.DotNetMuxerPath ?? throw new Exception($"Unable to find '{DotNetCommands.DotNetExecutableName}'."); - } protected void ShutDownIfAnyHostProcess(Process hostProcess) { diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs index 20770b5df7c453..9cd579b1886962 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs @@ -96,7 +96,7 @@ protected async Task StartSelfHostAsync() if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) { - executableName = GetDotNetExeForArchitecture(); + executableName = GetDotNetMuxerPath(); executableArgs = executable; } else