IISDeployer.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Linq;
  8. using System.ServiceProcess;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using System.Xml.Linq;
  12. using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
  13. using Microsoft.Extensions.Logging;
  14. using Microsoft.Web.Administration;
  15. namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
  16. {
  17. /// <summary>
  18. /// Deployer for IIS.
  19. /// </summary>
  20. public class IISDeployer : IISDeployerBase
  21. {
  22. private const string DetailedErrorsEnvironmentVariable = "ASPNETCORE_DETAILEDERRORS";
  23. private static readonly TimeSpan _timeout = TimeSpan.FromSeconds(60);
  24. private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(200);
  25. private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource();
  26. private string _configPath;
  27. private string _debugLogFile;
  28. public Process HostProcess { get; set; }
  29. public IISDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
  30. : base(new IISDeploymentParameters(deploymentParameters), loggerFactory)
  31. {
  32. }
  33. public IISDeployer(IISDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
  34. : base(deploymentParameters, loggerFactory)
  35. {
  36. }
  37. public override void Dispose()
  38. {
  39. Dispose(gracefulShutdown: false);
  40. }
  41. public override void Dispose(bool gracefulShutdown)
  42. {
  43. Stop();
  44. TriggerHostShutdown(_hostShutdownToken);
  45. GetLogsFromFile();
  46. CleanPublishedOutput();
  47. InvokeUserApplicationCleanup();
  48. StopTimer();
  49. }
  50. public override Task<DeploymentResult> DeployAsync()
  51. {
  52. using (Logger.BeginScope("Deployment"))
  53. {
  54. StartTimer();
  55. if (string.IsNullOrEmpty(DeploymentParameters.ServerConfigTemplateContent))
  56. {
  57. DeploymentParameters.ServerConfigTemplateContent = File.ReadAllText("IIS.config");
  58. }
  59. // For now, only support using published output
  60. DeploymentParameters.PublishApplicationBeforeDeployment = true;
  61. // Move ASPNETCORE_DETAILEDERRORS to web config env variables
  62. if (IISDeploymentParameters.EnvironmentVariables.ContainsKey(DetailedErrorsEnvironmentVariable))
  63. {
  64. IISDeploymentParameters.WebConfigBasedEnvironmentVariables[DetailedErrorsEnvironmentVariable] =
  65. IISDeploymentParameters.EnvironmentVariables[DetailedErrorsEnvironmentVariable];
  66. IISDeploymentParameters.EnvironmentVariables.Remove(DetailedErrorsEnvironmentVariable);
  67. }
  68. // Do not override settings set on parameters
  69. if (!IISDeploymentParameters.HandlerSettings.ContainsKey("debugLevel") &&
  70. !IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile"))
  71. {
  72. _debugLogFile = Path.GetTempFileName();
  73. IISDeploymentParameters.HandlerSettings["debugLevel"] = "file";
  74. IISDeploymentParameters.HandlerSettings["debugFile"] = _debugLogFile;
  75. }
  76. DotnetPublish();
  77. var contentRoot = DeploymentParameters.PublishedApplicationRootPath;
  78. RunWebConfigActions(contentRoot);
  79. var uri = TestUriHelper.BuildTestUri(ServerType.IIS, DeploymentParameters.ApplicationBaseUriHint);
  80. StartIIS(uri, contentRoot);
  81. // Warm up time for IIS setup.
  82. Logger.LogInformation("Successfully finished IIS application directory setup.");
  83. return Task.FromResult<DeploymentResult>(new IISDeploymentResult(
  84. LoggerFactory,
  85. IISDeploymentParameters,
  86. applicationBaseUri: uri.ToString(),
  87. contentRoot: contentRoot,
  88. hostShutdownToken: _hostShutdownToken.Token,
  89. hostProcess: HostProcess
  90. ));
  91. }
  92. }
  93. protected override IEnumerable<Action<XElement, string>> GetWebConfigActions()
  94. {
  95. yield return WebConfigHelpers.AddOrModifyAspNetCoreSection(
  96. key: "hostingModel",
  97. value: DeploymentParameters.HostingModel.ToString());
  98. yield return (element, _) => {
  99. var aspNetCore = element
  100. .Descendants("system.webServer")
  101. .Single()
  102. .GetOrAdd("aspNetCore");
  103. // Expand path to dotnet because IIS process would not inherit PATH variable
  104. if (aspNetCore.Attribute("processPath")?.Value.StartsWith("dotnet") == true)
  105. {
  106. aspNetCore.SetAttributeValue("processPath", DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture));
  107. }
  108. };
  109. yield return WebConfigHelpers.AddOrModifyHandlerSection(
  110. key: "modules",
  111. value: DeploymentParameters.AncmVersion.ToString());
  112. foreach (var action in base.GetWebConfigActions())
  113. {
  114. yield return action;
  115. }
  116. }
  117. private void GetLogsFromFile()
  118. {
  119. try
  120. {
  121. // Handle cases where debug file is redirected by test
  122. var debugLogLocations = new List<string>();
  123. if (IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile"))
  124. {
  125. debugLogLocations.Add(IISDeploymentParameters.HandlerSettings["debugFile"]);
  126. }
  127. if (DeploymentParameters.EnvironmentVariables.ContainsKey("ASPNETCORE_MODULE_DEBUG_FILE"))
  128. {
  129. debugLogLocations.Add(DeploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG_FILE"]);
  130. }
  131. // default debug file name
  132. debugLogLocations.Add("aspnetcore-debug.log");
  133. foreach (var debugLogLocation in debugLogLocations)
  134. {
  135. if (string.IsNullOrEmpty(debugLogLocation))
  136. {
  137. continue;
  138. }
  139. var file = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, debugLogLocation);
  140. if (File.Exists(file))
  141. {
  142. var lines = File.ReadAllLines(file);
  143. if (!lines.Any())
  144. {
  145. Logger.LogInformation($"Debug log file {file} found but was empty");
  146. continue;
  147. }
  148. foreach (var line in lines)
  149. {
  150. Logger.LogInformation(line);
  151. }
  152. return;
  153. }
  154. }
  155. // ANCM V1 does not support logs
  156. if (DeploymentParameters.AncmVersion == AncmVersion.AspNetCoreModuleV2)
  157. {
  158. throw new InvalidOperationException($"Unable to find non-empty debug log files. Tried: {string.Join(", ", debugLogLocations)}");
  159. }
  160. }
  161. finally
  162. {
  163. if (File.Exists(_debugLogFile))
  164. {
  165. File.Delete(_debugLogFile);
  166. }
  167. }
  168. }
  169. public void StartIIS(Uri uri, string contentRoot)
  170. {
  171. // Backup currently deployed apphost.config file
  172. using (Logger.BeginScope("StartIIS"))
  173. {
  174. var port = uri.Port;
  175. if (port == 0)
  176. {
  177. throw new NotSupportedException("Cannot set port 0 for IIS.");
  178. }
  179. AddTemporaryAppHostConfig(contentRoot, port);
  180. WaitUntilSiteStarted();
  181. }
  182. }
  183. private void WaitUntilSiteStarted()
  184. {
  185. ServiceController serviceController = new ServiceController("w3svc");
  186. Logger.LogInformation("W3SVC status " + serviceController.Status);
  187. if (serviceController.Status != ServiceControllerStatus.Running &&
  188. serviceController.Status != ServiceControllerStatus.StartPending)
  189. {
  190. Logger.LogInformation("Starting W3SVC");
  191. serviceController.Start();
  192. serviceController.WaitForStatus(ServiceControllerStatus.Running, _timeout);
  193. }
  194. RetryServerManagerAction(serverManager =>
  195. {
  196. var site = serverManager.Sites.Single();
  197. var appPool = serverManager.ApplicationPools.Single();
  198. if (appPool.State != ObjectState.Started && appPool.State != ObjectState.Starting)
  199. {
  200. var state = appPool.Start();
  201. Logger.LogInformation($"Starting pool, state: {state.ToString()}");
  202. }
  203. if (site.State != ObjectState.Started && site.State != ObjectState.Starting)
  204. {
  205. var state = site.Start();
  206. Logger.LogInformation($"Starting site, state: {state.ToString()}");
  207. }
  208. if (site.State != ObjectState.Started)
  209. {
  210. throw new InvalidOperationException("Site not started yet");
  211. }
  212. var workerProcess = appPool.WorkerProcesses.SingleOrDefault();
  213. if (workerProcess == null)
  214. {
  215. throw new InvalidOperationException("Site is started but no worked process found");
  216. }
  217. HostProcess = Process.GetProcessById(workerProcess.ProcessId);
  218. // Ensure w3wp.exe is killed if test process termination is non-graceful.
  219. // Prevents locked files when stop debugging unit test.
  220. ProcessTracker.Add(HostProcess);
  221. // cache the process start time for verifying log file name.
  222. var _ = HostProcess.StartTime;
  223. Logger.LogInformation("Site has started.");
  224. });
  225. }
  226. private void AddTemporaryAppHostConfig(string contentRoot, int port)
  227. {
  228. _configPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("D"));
  229. var appHostConfigPath = Path.Combine(_configPath, "applicationHost.config");
  230. Directory.CreateDirectory(_configPath);
  231. var config = XDocument.Parse(DeploymentParameters.ServerConfigTemplateContent ?? File.ReadAllText("IIS.config"));
  232. ConfigureAppHostConfig(config.Root, contentRoot, port);
  233. config.Save(appHostConfigPath);
  234. RetryServerManagerAction(serverManager =>
  235. {
  236. var redirectionConfiguration = serverManager.GetRedirectionConfiguration();
  237. var redirectionSection = redirectionConfiguration.GetSection("configurationRedirection");
  238. redirectionSection.Attributes["enabled"].Value = true;
  239. redirectionSection.Attributes["path"].Value = _configPath;
  240. serverManager.CommitChanges();
  241. });
  242. }
  243. private void ConfigureAppHostConfig(XElement config, string contentRoot, int port)
  244. {
  245. ConfigureModuleAndBinding(config, contentRoot, port);
  246. // In IISExpress system.webServer/modules in under location element
  247. config
  248. .RequiredElement("system.webServer")
  249. .RequiredElement("modules")
  250. .GetOrAdd("add", "name", DeploymentParameters.AncmVersion.ToString());
  251. var pool = config
  252. .RequiredElement("system.applicationHost")
  253. .RequiredElement("applicationPools")
  254. .RequiredElement("add");
  255. if (DeploymentParameters.EnvironmentVariables.Any())
  256. {
  257. var environmentVariables = pool
  258. .GetOrAdd("environmentVariables");
  259. foreach (var tuple in DeploymentParameters.EnvironmentVariables)
  260. {
  261. environmentVariables
  262. .GetOrAdd("add", "name", tuple.Key)
  263. .SetAttributeValue("value", tuple.Value);
  264. }
  265. }
  266. if (DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x86)
  267. {
  268. pool.SetAttributeValue("enable32BitAppOnWin64", "true");;
  269. }
  270. RunServerConfigActions(config, contentRoot);
  271. }
  272. private void Stop()
  273. {
  274. try
  275. {
  276. RetryServerManagerAction(serverManager =>
  277. {
  278. var site = serverManager.Sites.SingleOrDefault();
  279. if (site == null)
  280. {
  281. throw new InvalidOperationException("Site not found");
  282. }
  283. if (site.State != ObjectState.Stopped && site.State != ObjectState.Stopping)
  284. {
  285. var state = site.Stop();
  286. Logger.LogInformation($"Stopping site, state: {state.ToString()}");
  287. }
  288. var appPool = serverManager.ApplicationPools.SingleOrDefault();
  289. if (appPool == null)
  290. {
  291. throw new InvalidOperationException("Application pool not found");
  292. }
  293. if (appPool.State != ObjectState.Stopped && appPool.State != ObjectState.Stopping)
  294. {
  295. var state = appPool.Stop();
  296. Logger.LogInformation($"Stopping pool, state: {state.ToString()}");
  297. }
  298. if (site.State != ObjectState.Stopped)
  299. {
  300. throw new InvalidOperationException("Site not stopped yet");
  301. }
  302. try
  303. {
  304. if (appPool.WorkerProcesses != null &&
  305. appPool.WorkerProcesses.Any(wp =>
  306. wp.State == WorkerProcessState.Running ||
  307. wp.State == WorkerProcessState.Stopping))
  308. {
  309. throw new InvalidOperationException("WorkerProcess not stopped yet");
  310. }
  311. }
  312. // If WAS was stopped for some reason appPool.WorkerProcesses
  313. // would throw UnauthorizedAccessException.
  314. // check if it's the case and continue shutting down deployer
  315. catch (UnauthorizedAccessException)
  316. {
  317. var serviceController = new ServiceController("was");
  318. if (serviceController.Status != ServiceControllerStatus.Stopped)
  319. {
  320. throw;
  321. }
  322. }
  323. if (!HostProcess.HasExited)
  324. {
  325. throw new InvalidOperationException("Site is stopped but host process is not");
  326. }
  327. Logger.LogInformation($"Site has stopped successfully.");
  328. });
  329. }
  330. finally
  331. {
  332. // Undo redirection.config changes unconditionally
  333. RetryServerManagerAction(serverManager =>
  334. {
  335. var redirectionConfiguration = serverManager.GetRedirectionConfiguration();
  336. var redirectionSection = redirectionConfiguration.GetSection("configurationRedirection");
  337. redirectionSection.Attributes["enabled"].Value = false;
  338. serverManager.CommitChanges();
  339. if (Directory.Exists(_configPath))
  340. {
  341. Directory.Delete(_configPath, true);
  342. }
  343. });
  344. }
  345. }
  346. private void RetryServerManagerAction(Action<ServerManager> action)
  347. {
  348. List<Exception> exceptions = null;
  349. var sw = Stopwatch.StartNew();
  350. int retryCount = 0;
  351. while (sw.Elapsed < _timeout)
  352. {
  353. try
  354. {
  355. using (var serverManager = new ServerManager())
  356. {
  357. action(serverManager);
  358. }
  359. return;
  360. }
  361. catch (Exception ex)
  362. {
  363. if (exceptions == null)
  364. {
  365. exceptions = new List<Exception>();
  366. }
  367. exceptions.Add(ex);
  368. }
  369. retryCount++;
  370. Thread.Sleep(_retryDelay);
  371. }
  372. throw new AggregateException($"Operation did not succeed after {retryCount} retries", exceptions.ToArray());
  373. }
  374. }
  375. }