1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
5 using System.Diagnostics;
7 using System.Runtime.InteropServices;
8 using System.Text.RegularExpressions;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Microsoft.Extensions.Logging;
13 namespace Microsoft.Extensions.Hosting.IntegrationTesting
16 /// Deployer for WebListener and Kestrel.
18 public class SelfHostDeployer : ApplicationDeployer
20 private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down.";
22 public Process HostProcess { get; private set; }
24 public SelfHostDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
25 : base(deploymentParameters, loggerFactory)
29 public override async Task<DeploymentResult> DeployAsync()
31 using (Logger.BeginScope("SelfHost.Deploy"))
36 if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr
37 && DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x86)
39 // Publish is required to rebuild for the right bitness
40 DeploymentParameters.PublishApplicationBeforeDeployment = true;
43 if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr
44 && DeploymentParameters.ApplicationType == ApplicationType.Standalone)
46 // Publish is required to get the correct files in the output directory
47 DeploymentParameters.PublishApplicationBeforeDeployment = true;
50 if (DeploymentParameters.PublishApplicationBeforeDeployment)
55 // Launch the host process.
56 var hostExitToken = await StartSelfHostAsync();
58 Logger.LogInformation("Application ready");
60 return new DeploymentResult(
63 contentRoot: DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath,
64 hostShutdownToken: hostExitToken);
68 protected async Task<CancellationToken> StartSelfHostAsync()
70 using (Logger.BeginScope("StartSelfHost"))
72 var executableName = string.Empty;
73 var executableArgs = string.Empty;
74 var workingDirectory = string.Empty;
75 var executableExtension = DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll"
76 : (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "");
78 if (DeploymentParameters.PublishApplicationBeforeDeployment)
80 workingDirectory = DeploymentParameters.PublishedApplicationRootPath;
84 // Core+Standalone always publishes. This must be Clr+Standalone or Core+Portable.
85 // Run from the pre-built bin/{config}/{tfm} directory.
86 var targetFramework = DeploymentParameters.TargetFramework
87 ?? (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? Tfm.Net462 : Tfm.NetCoreApp22);
88 workingDirectory = Path.Combine(DeploymentParameters.ApplicationPath, "bin", DeploymentParameters.Configuration, targetFramework);
89 // CurrentDirectory will point to bin/{config}/{tfm}, but the config and static files aren't copied, point to the app base instead.
90 DeploymentParameters.EnvironmentVariables["DOTNET_CONTENTROOT"] = DeploymentParameters.ApplicationPath;
93 var executable = Path.Combine(workingDirectory, DeploymentParameters.ApplicationName + executableExtension);
95 if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable)
97 executableName = GetDotNetExeForArchitecture();
98 executableArgs = executable;
102 executableName = executable;
105 Logger.LogInformation($"Executing {executableName} {executableArgs}");
107 var startInfo = new ProcessStartInfo
109 FileName = executableName,
110 Arguments = executableArgs,
111 UseShellExecute = false,
112 CreateNoWindow = true,
113 RedirectStandardError = true,
114 RedirectStandardOutput = true,
115 // Trying a work around for https://github.com/aspnet/Hosting/issues/140.
116 RedirectStandardInput = true,
117 WorkingDirectory = workingDirectory
120 AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables);
122 var started = new TaskCompletionSource<object>();
124 HostProcess = new Process() { StartInfo = startInfo };
125 HostProcess.EnableRaisingEvents = true;
126 HostProcess.OutputDataReceived += (sender, dataArgs) =>
128 if (string.Equals(dataArgs.Data, ApplicationStartedMessage))
130 started.TrySetResult(null);
133 var hostExitTokenSource = new CancellationTokenSource();
134 HostProcess.Exited += (sender, e) =>
136 Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id);
138 // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want
139 started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {HostProcess.ExitCode}"));
141 TriggerHostShutdown(hostExitTokenSource);
146 HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger);
150 Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString());
153 if (HostProcess.HasExited)
155 Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode);
156 throw new Exception("Failed to start host");
159 Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id);
161 // Host may not write startup messages, in which case assume it started
162 if (DeploymentParameters.StatusMessagesEnabled)
164 // The timeout here is large, because we don't know how long the test could need
165 // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build
166 // just in case we missed one -anurse
167 await started.Task.WaitAsync(TimeSpan.FromMinutes(10));
170 return hostExitTokenSource.Token;
174 public override void Dispose()
176 using (Logger.BeginScope("SelfHost.Dispose"))
178 ShutDownIfAnyHostProcess(HostProcess);
180 if (DeploymentParameters.PublishApplicationBeforeDeployment)
182 CleanPublishedOutput();
185 InvokeUserApplicationCleanup();