79e87c59ee9801529e925c173b2e9841dc10306e
[platform/upstream/dotnet/runtime.git] /
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;
5 using System.Diagnostics;
6 using System.IO;
7 using System.Runtime.InteropServices;
8 using System.Text.RegularExpressions;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Microsoft.Extensions.Logging;
12
13 namespace Microsoft.Extensions.Hosting.IntegrationTesting
14 {
15     /// <summary>
16     /// Deployer for WebListener and Kestrel.
17     /// </summary>
18     public class SelfHostDeployer : ApplicationDeployer
19     {
20         private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down.";
21
22         public Process HostProcess { get; private set; }
23
24         public SelfHostDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
25             : base(deploymentParameters, loggerFactory)
26         {
27         }
28
29         public override async Task<DeploymentResult> DeployAsync()
30         {
31             using (Logger.BeginScope("SelfHost.Deploy"))
32             {
33                 // Start timer
34                 StartTimer();
35
36                 if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr
37                         && DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x86)
38                 {
39                     // Publish is required to rebuild for the right bitness
40                     DeploymentParameters.PublishApplicationBeforeDeployment = true;
41                 }
42
43                 if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr
44                         && DeploymentParameters.ApplicationType == ApplicationType.Standalone)
45                 {
46                     // Publish is required to get the correct files in the output directory
47                     DeploymentParameters.PublishApplicationBeforeDeployment = true;
48                 }
49
50                 if (DeploymentParameters.PublishApplicationBeforeDeployment)
51                 {
52                     DotnetPublish();
53                 }
54
55                 // Launch the host process.
56                 var hostExitToken = await StartSelfHostAsync();
57
58                 Logger.LogInformation("Application ready");
59
60                 return new DeploymentResult(
61                     LoggerFactory,
62                     DeploymentParameters,
63                     contentRoot: DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath,
64                     hostShutdownToken: hostExitToken);
65             }
66         }
67
68         protected async Task<CancellationToken> StartSelfHostAsync()
69         {
70             using (Logger.BeginScope("StartSelfHost"))
71             {
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" : "");
77
78                 if (DeploymentParameters.PublishApplicationBeforeDeployment)
79                 {
80                     workingDirectory = DeploymentParameters.PublishedApplicationRootPath;
81                 }
82                 else
83                 {
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;
91                 }
92
93                 var executable = Path.Combine(workingDirectory, DeploymentParameters.ApplicationName + executableExtension);
94
95                 if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable)
96                 {
97                     executableName = GetDotNetExeForArchitecture();
98                     executableArgs = executable;
99                 }
100                 else
101                 {
102                     executableName = executable;
103                 }
104
105                 Logger.LogInformation($"Executing {executableName} {executableArgs}");
106
107                 var startInfo = new ProcessStartInfo
108                 {
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
118                 };
119
120                 AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables);
121
122                 var started = new TaskCompletionSource<object>();
123
124                 HostProcess = new Process() { StartInfo = startInfo };
125                 HostProcess.EnableRaisingEvents = true;
126                 HostProcess.OutputDataReceived += (sender, dataArgs) =>
127                 {
128                     if (string.Equals(dataArgs.Data, ApplicationStartedMessage))
129                     {
130                         started.TrySetResult(null);
131                     }
132                 };
133                 var hostExitTokenSource = new CancellationTokenSource();
134                 HostProcess.Exited += (sender, e) =>
135                 {
136                     Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id);
137
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}"));
140
141                     TriggerHostShutdown(hostExitTokenSource);
142                 };
143
144                 try
145                 {
146                     HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger);
147                 }
148                 catch (Exception ex)
149                 {
150                     Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString());
151                 }
152
153                 if (HostProcess.HasExited)
154                 {
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");
157                 }
158
159                 Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id);
160
161                 // Host may not write startup messages, in which case assume it started
162                 if (DeploymentParameters.StatusMessagesEnabled)
163                 {
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));
168                 }
169
170                 return hostExitTokenSource.Token;
171             }
172         }
173
174         public override void Dispose()
175         {
176             using (Logger.BeginScope("SelfHost.Dispose"))
177             {
178                 ShutDownIfAnyHostProcess(HostProcess);
179
180                 if (DeploymentParameters.PublishApplicationBeforeDeployment)
181                 {
182                     CleanPublishedOutput();
183                 }
184
185                 InvokeUserApplicationCleanup();
186
187                 StopTimer();
188             }
189         }
190     }
191 }