// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.IntegrationTesting { public class RemoteWindowsDeployer : ApplicationDeployer { /// /// Example: If the share path is '\\dir1\dir2', then this returns the full path to the /// deployed folder. Example: '\\dir1\dir2\048f6c99-de3e-488a-8020-f9eb277818d9' /// private string _deployedFolderPathInFileShare; private readonly RemoteWindowsDeploymentParameters _deploymentParameters; private bool _isDisposed; private static readonly Lazy _scripts = new Lazy(() => CopyEmbeddedScriptFilesToDisk()); public RemoteWindowsDeployer(RemoteWindowsDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) : base(deploymentParameters, loggerFactory) { _deploymentParameters = deploymentParameters; if (_deploymentParameters.ServerType != ServerType.IIS && _deploymentParameters.ServerType != ServerType.Kestrel && _deploymentParameters.ServerType != ServerType.HttpSys) { throw new InvalidOperationException($"Server type {_deploymentParameters.ServerType} is not supported for remote deployment." + $" Supported server types are {nameof(ServerType.Kestrel)}, {nameof(ServerType.IIS)} and {nameof(ServerType.HttpSys)}"); } if (string.IsNullOrEmpty(_deploymentParameters.ServerName)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerName}' for {nameof(RemoteWindowsDeploymentParameters.ServerName)}"); } if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountName)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountName}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountName)}." + " Account credentials are required to enable creating a powershell session to the remote server."); } if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountPassword)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountPassword}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountPassword)}." + " Account credentials are required to enable creating a powershell session to the remote server."); } if (_deploymentParameters.ApplicationType == ApplicationType.Portable && string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.DotnetRuntimePath}' for {nameof(RemoteWindowsDeploymentParameters.DotnetRuntimePath)}. " + "It must be non-empty for portable apps."); } if (string.IsNullOrEmpty(_deploymentParameters.RemoteServerFileSharePath)) { throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.RemoteServerFileSharePath)}." + " . A file share is required to copy the application's published output."); } if (string.IsNullOrEmpty(_deploymentParameters.ApplicationBaseUriHint)) { throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.ApplicationBaseUriHint)}."); } } public override async Task DeployAsync() { using (Logger.BeginScope("Deploy")) { if (_isDisposed) { throw new ObjectDisposedException("This instance of deployer has already been disposed."); } // Publish the app to a local temp folder on the machine where the test is running DotnetPublish(); if (_deploymentParameters.ServerType == ServerType.IIS) { UpdateWebConfig(); } var folderId = Guid.NewGuid().ToString(); _deployedFolderPathInFileShare = Path.Combine(_deploymentParameters.RemoteServerFileSharePath, folderId); DirectoryCopy( _deploymentParameters.PublishedApplicationRootPath, _deployedFolderPathInFileShare, copySubDirs: true); Logger.LogInformation($"Copied the locally published folder to the file share path '{_deployedFolderPathInFileShare}'"); await RunScriptAsync("StartServer"); return new DeploymentResult( LoggerFactory, DeploymentParameters, DeploymentParameters.ApplicationBaseUriHint); } } public override void Dispose() { using (Logger.BeginScope("Dispose")) { if (_isDisposed) { return; } _isDisposed = true; try { Logger.LogInformation($"Stopping the application on the server '{_deploymentParameters.ServerName}'"); RunScriptAsync("StopServer").Wait(); } catch (Exception ex) { Logger.LogWarning(0, "Failed to stop the server.", ex); } try { Logger.LogInformation($"Deleting the deployed folder '{_deployedFolderPathInFileShare}'"); Directory.Delete(_deployedFolderPathInFileShare, recursive: true); } catch (Exception ex) { Logger.LogWarning(0, $"Failed to delete the deployed folder '{_deployedFolderPathInFileShare}'.", ex); } try { Logger.LogInformation($"Deleting the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'"); Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, recursive: true); } catch (Exception ex) { Logger.LogWarning(0, $"Failed to delete the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'.", ex); } } } private void UpdateWebConfig() { var webConfigFilePath = Path.Combine(_deploymentParameters.PublishedApplicationRootPath, "web.config"); var webConfig = XDocument.Load(webConfigFilePath); var aspNetCoreSection = webConfig.Descendants("aspNetCore") .Single(); // if the dotnet runtime path is specified, update the published web.config file to have that path if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) { aspNetCoreSection.SetAttributeValue( "processPath", Path.Combine(_deploymentParameters.DotnetRuntimePath, "dotnet.exe")); } var environmentVariablesSection = aspNetCoreSection.Elements("environmentVariables").FirstOrDefault(); if (environmentVariablesSection == null) { environmentVariablesSection = new XElement("environmentVariables"); aspNetCoreSection.Add(environmentVariablesSection); } foreach (var envVariablePair in _deploymentParameters.EnvironmentVariables) { var environmentVariable = new XElement("environmentVariable"); environmentVariable.SetAttributeValue("name", envVariablePair.Key); environmentVariable.SetAttributeValue("value", envVariablePair.Value); environmentVariablesSection.Add(environmentVariable); } if(Logger.IsEnabled(LogLevel.Trace)) { Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", webConfig.ToString()); } using (var fileStream = File.Open(webConfigFilePath, FileMode.Open)) { webConfig.Save(fileStream); } } private async Task RunScriptAsync(string serverAction) { using (Logger.BeginScope($"RunScript:{serverAction}")) { var remotePSSessionHelperScript = _scripts.Value.RemotePSSessionHelper; string executablePath = null; string executableParameters = null; var applicationName = new DirectoryInfo(DeploymentParameters.ApplicationPath).Name; if (DeploymentParameters.ApplicationType == ApplicationType.Portable) { executablePath = "dotnet.exe"; executableParameters = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".dll"); } else { executablePath = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".exe"); } var parameterBuilder = new StringBuilder(); parameterBuilder.Append($"\"{remotePSSessionHelperScript}\""); parameterBuilder.Append($" -serverName {_deploymentParameters.ServerName}"); parameterBuilder.Append($" -accountName {_deploymentParameters.ServerAccountName}"); parameterBuilder.Append($" -accountPassword {_deploymentParameters.ServerAccountPassword}"); parameterBuilder.Append($" -deployedFolderPath {_deployedFolderPathInFileShare}"); if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) { parameterBuilder.Append($" -dotnetRuntimePath \"{_deploymentParameters.DotnetRuntimePath}\""); } parameterBuilder.Append($" -executablePath \"{executablePath}\""); if (!string.IsNullOrEmpty(executableParameters)) { parameterBuilder.Append($" -executableParameters \"{executableParameters}\""); } parameterBuilder.Append($" -serverType {_deploymentParameters.ServerType}"); parameterBuilder.Append($" -serverAction {serverAction}"); parameterBuilder.Append($" -applicationBaseUrl {_deploymentParameters.ApplicationBaseUriHint}"); var environmentVariables = string.Join("`,", _deploymentParameters.EnvironmentVariables.Select(envVariable => $"{envVariable.Key}={envVariable.Value}")); parameterBuilder.Append($" -environmentVariables \"{environmentVariables}\""); var startInfo = new ProcessStartInfo { FileName = "powershell.exe", Arguments = parameterBuilder.ToString(), UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, RedirectStandardInput = true }; using (var runScriptsOnRemoteServerProcess = new Process() { StartInfo = startInfo }) { runScriptsOnRemoteServerProcess.EnableRaisingEvents = true; runScriptsOnRemoteServerProcess.Exited += (sender, exitedArgs) => { Logger.LogInformation($"[{_deploymentParameters.ServerName} {serverAction} stdout]: script complete"); }; runScriptsOnRemoteServerProcess.StartAndCaptureOutAndErrToLogger(serverAction, Logger); // Wait a second for the script to run or fail. The StartServer script will only terminate when the Deployer is disposed, // so we don't want to wait for it to terminate here because it would deadlock. await Task.Delay(TimeSpan.FromMinutes(1)); if (runScriptsOnRemoteServerProcess.HasExited && runScriptsOnRemoteServerProcess.ExitCode != 0) { throw new Exception($"Failed to execute the script on '{_deploymentParameters.ServerName}'."); } } } } private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) { var dir = new DirectoryInfo(sourceDirName); if (!dir.Exists) { throw new DirectoryNotFoundException( "Source directory does not exist or could not be found: " + sourceDirName); } var dirs = dir.GetDirectories(); if (!Directory.Exists(destDirName)) { Directory.CreateDirectory(destDirName); } var files = dir.GetFiles(); foreach (var file in files) { var temppath = Path.Combine(destDirName, file.Name); file.CopyTo(temppath, false); } if (copySubDirs) { foreach (var subdir in dirs) { var temppath = Path.Combine(destDirName, subdir.Name); DirectoryCopy(subdir.FullName, temppath, copySubDirs); } } } private static Scripts CopyEmbeddedScriptFilesToDisk() { var embeddedFileNames = new[] { "RemotePSSessionHelper.ps1", "StartServer.ps1", "StopServer.ps1" }; // Copy the scripts from this assembly's embedded resources to the temp path on the machine where these // tests are being run var assembly = typeof(RemoteWindowsDeployer).GetTypeInfo().Assembly; var embeddedFileProvider = new EmbeddedFileProvider( assembly, $"{assembly.GetName().Name}.Deployers.RemoteWindowsDeployer"); var filesOnDisk = new string[embeddedFileNames.Length]; for (var i = 0; i < embeddedFileNames.Length; i++) { var embeddedFileName = embeddedFileNames[i]; var physicalFilePath = Path.Combine(Path.GetTempPath(), embeddedFileName); var sourceStream = embeddedFileProvider .GetFileInfo(embeddedFileName) .CreateReadStream(); using (sourceStream) { var destinationStream = File.Create(physicalFilePath); using (destinationStream) { sourceStream.CopyTo(destinationStream); } } filesOnDisk[i] = physicalFilePath; } var scripts = new Scripts(filesOnDisk[0], filesOnDisk[1], filesOnDisk[2]); return scripts; } private class Scripts { public Scripts(string remotePSSessionHelper, string startServer, string stopServer) { RemotePSSessionHelper = remotePSSessionHelper; StartServer = startServer; StopServer = stopServer; } public string RemotePSSessionHelper { get; } public string StartServer { get; } public string StopServer { get; } } } }