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..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 @@ -102,7 +102,7 @@ protected void CleanPublishedOutput() protected string GetDotNetExeForArchitecture() { - var executableName = DotnetCommandName; + var executableName = GetHostDotNetExecutable(); // 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,17 +116,51 @@ 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 != 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); + Logger.LogWarning("Unable to terminate the host process with process Id '{processId}'", hostProcess.Id); } else { @@ -139,6 +173,22 @@ protected void ShutDownIfAnyHostProcess(Process hostProcess) } } + // Process.HasExited throws InvalidOperationException ("No process is associated with this object") + // 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 + { + 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..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,7 +121,8 @@ protected async Task StartSelfHostAsync() AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); - var started = new TaskCompletionSource(); + var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostExitTokenSource = new CancellationTokenSource(); HostProcess = new Process() { StartInfo = startInfo }; HostProcess.EnableRaisingEvents = true; @@ -134,7 +135,6 @@ protected async Task StartSelfHostAsync() OutputReceived?.Invoke(sender, dataArgs); }; - var hostExitTokenSource = new CancellationTokenSource(); HostProcess.Exited += (sender, e) => { Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id); @@ -151,7 +151,9 @@ protected async Task StartSelfHostAsync() } catch (Exception ex) { + // 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)