/// <summary> /// Retry an action while the exception meets the condition during the maximum wait specified /// </summary> /// <param name="task"></param> /// <param name="condition"></param> /// <param name="maxWait">Max milliseconds for the operation to complete</param> /// <param name="logger"></param> /// <param name="minRetries"></param> public static void RetryWhile( Action task, Func <Exception, bool> condition, int maxWait, ILoggerInterface logger, int minRetries = 2) { Stopwatch sw = Stopwatch.StartNew(); sw.Start(); int sleep = 250; int sleepStep = 400; int failCount = 0; while (true) { try { task(); if (failCount > 0) { logger?.LogInfo(true, "Operation completed."); } return; } catch (Exception e) { // If the looping condition is not met, throw the exception. if (!condition(e)) { throw; } // If we have reached the maximum wait limit plus we have failed at least once, abort. if (sw.ElapsedMilliseconds > maxWait && failCount >= minRetries) { throw new Exception($"Transient error did not go away after waiting for {maxWait}ms and failing {failCount} times...", e); } failCount++; string errorMessage = e.Message; if (e is AggregateException aggregateException) { errorMessage += "(" + string.Join( ", ", aggregateException.InnerExceptions.Select((i) => i.Message)) + ")"; } logger?.LogInfo(true, "Found transient error: {0}", errorMessage); logger?.LogInfo(true, "Retrying operation..."); Thread.Sleep(sleep); sleep = sleep + sleepStep; } } }
/// <summary> /// Moves a directory (MOVE) if in same drive, or copies and deletes if between drives /// as MOVE operation is not supported in such scenario. Supports long path names. /// </summary> /// <param name="source"></param> /// <param name="destination"></param> /// <param name="logger"></param> /// <param name="ignoreOnDeployPattern"></param> public static void MoveDirectory(string source, string destination, ILoggerInterface logger, string ignoreOnDeployPattern = null) { try { source = EnsureLongPathSupportIfAvailable(source); destination = EnsureLongPathSupportIfAvailable(destination); RetryWhile( () => { Directory.Move(source, destination); }, // Retry while access to the path is denied, in move operations // this might happen due to files being scanned by an antivirus // or other transient locks (e) => Convert.ToString((uint)e.HResult) == "2147942405", 10000, logger); } catch (IOException e) { if (e.HResult != -2146232800) { throw; } logger.LogInfo( true, $"Move operation cannot complete because source '{source}' and destination '{destination}' are on same drive, falling back to copy."); CopyFilesRecursivelyFast(source, destination, false, ignoreOnDeployPattern, logger); Directory.Delete(source, true); } }
/// <summary> /// Get an instance of Application /// </summary> /// <param name="parentLogger">Logger implementation</param> public Application(ILoggerInterface parentLogger) { NewRelic.Api.Agent.NewRelic.SetApplicationName("IisChef"); NewRelicAgentExtensions.AddCustomParameter("server", Environment.MachineName); NewRelicAgentExtensions.AddCustomParameter("user", Environment.UserName); BindingRedirectHandler.DoBindingRedirects(AppDomain.CurrentDomain); ServicePointManager.Expect100Continue = true; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3; // Check current account var identity = WindowsIdentity.GetCurrent(); var principal = new WindowsPrincipal(identity); parentLogger.LogInfo(false, $"Chef app started with identity '{identity.Name}'"); if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) { parentLogger.LogError("Not running under full admin privileges."); if (Debugger.IsAttached) { throw new Exception("You must run the deployer with full privileges."); } } // Use the parent logger, at least until we can build a file based one... this.Logger = parentLogger; }
/// <summary> /// Get a setting /// </summary> /// <typeparam name="TType"></typeparam> /// <param name="name"></param> /// <param name="defaultValue"></param> /// <param name="logger"></param> /// <param name="isEnum"></param> /// <returns></returns> public TType GetSettingPersistent <TType>(string name, TType defaultValue, ILoggerInterface logger, bool isEnum = false) { this.privateDataPersistent = this.privateDataPersistent ?? new Dictionary <string, object>(); TType result = defaultValue; if (!this.privateDataPersistent.ContainsKey(name)) { return(defaultValue); } try { if (isEnum) { result = (TType)Enum.Parse(typeof(TType), Convert.ToString(this.privateDataPersistent[name])); } else { result = (TType)this.privateDataPersistent[name]; } } catch (Exception e) { logger.LogInfo(false, "source value: '" + Convert.ToString(this.privateDataPersistent[name]) + "'"); logger.LogException(e); } return(result); }
/// <summary> /// Add permissions to a directory if missing /// </summary> /// <param name="identity"></param> /// <param name="directory"></param> /// <param name="logger"></param> public static void RemoveAccessRulesForIdentity( IdentityReference identity, string directory, ILoggerInterface logger) { var directoryInfo = new DirectoryInfo(directory); // Get a DirectorySecurity object that represents the current security settings. DirectorySecurity dSecurity = directoryInfo.GetAccessControl(); bool removed = false; var rules = dSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier)); foreach (AuthorizationRule r in rules) { if (r.IdentityReference == identity) { var currentRule = (FileSystemAccessRule)r; dSecurity.RemoveAccessRule(currentRule); removed = true; } } if (removed) { directoryInfo.SetAccessControl(dSecurity); } else { logger.LogInfo(true, "Could not find any rule to remove for identity {0}", identity.Value); } }
/// <summary> /// Ensure user is in group /// </summary> /// <param name="userPrincipalName"></param> /// <param name="groupname"></param> /// <param name="logger"></param> /// <param name="acp"></param> public static void EnsureUserInGroup( string userPrincipalName, string groupname, ILoggerInterface logger, AccountManagementPrincipalContext acp) { logger.LogInfo(true, $"Ensure user '{userPrincipalName}' in group '{groupname}'"); UserPrincipal user = SearchUser(userPrincipalName, acp, out var userContext); if (user == null) { userContext?.Dispose(); throw new Exception($"User '{userPrincipalName}' not found."); } GroupPrincipal group = SearchGroup(groupname, acp, out var groupContext); if (group == null) { userContext?.Dispose(); groupContext?.Dispose(); throw new Exception($"Group '{groupname}' not found."); } logger.LogWarning(false, $"Found group '{group.Name}' '{group.Sid}' in context '{groupContext.ConnectedServer}' and '{groupContext.ContextType}'"); foreach (Principal member in group.GetMembers(true)) { if (member.SamAccountName == user.SamAccountName) { logger.LogInfo(true, $"User already in group '{groupname}'"); return; } } group.Members.Add(user); group.Save(); userContext.Dispose(); groupContext.Dispose(); logger.LogInfo(true, $"Added user '{userPrincipalName}' to group '{groupname}'"); }
/// <summary> /// Delete a directory /// </summary> /// <param name="strDir">Directory to delete</param> /// <param name="logger"></param> /// <param name="closeProcesses">Force a process close if it cannot be deleted (i.e. in use)</param> /// <param name="waitTimeIfInUse">If in-use, time to wait (in seconds) before either failing or closing all processes if forceCloseProcesses is true.</param> private static void DoDeleteDirectory( string strDir, ILoggerInterface logger, List <string> closeProcesses = null, int waitTimeIfInUse = 10) { if (string.IsNullOrWhiteSpace(strDir)) { logger.LogWarning(true, "Empty directory name provided DoDeleteDirectory, skipping."); return; } ValidateDirectoryDepthDeletion(strDir); strDir = EnsureLongPathSupportIfAvailable(strDir); if (!Directory.Exists(strDir)) { return; } logger.LogInfo(true, "Removing directory {0} with close processes {1}", strDir, closeProcesses == null ? string.Empty : string.Join(", ", closeProcesses)); if (closeProcesses?.Any() == true) { var processes = UtilsProcess.GetPathProcessesInfo(strDir, logger, true); foreach (var p in processes.AsIterable()) { logger.LogWarning( false, "The following process might be blocking files in the directory: {0}", p.CommandLine); } if (processes.Any()) { UtilsProcess.ClosePathProcesses(strDir, closeProcesses, logger); } } RetryWhile( () => { DeleteDirectoryAndRemovePermissionsIfNeeded(strDir); }, ExceptionIsAccessDeniedOrFileInUse, waitTimeIfInUse * 1000, logger); if (Directory.Exists(strDir)) { throw new Exception($"Could not completely delete directory '{strDir}', see log for details."); } }
/// <summary> /// Ensure user not in group /// </summary> /// <param name="userPrincipalName"></param> /// <param name="groupname"></param> /// <param name="logger"></param> /// <param name="acp"></param> public static void EnsureUserNotInGroup( string userPrincipalName, string groupname, ILoggerInterface logger, AccountManagementPrincipalContext acp) { logger.LogInfo(true, $"Ensure user '{userPrincipalName}' NOT in group {groupname}"); UserPrincipal up = SearchUser(userPrincipalName, acp, out var userContext); if (up == null) { userContext?.Dispose(); throw new Exception($"User '{userPrincipalName}' not found."); } GroupPrincipal group = SearchGroup(groupname, acp, out var groupContext); if (group == null) { userContext?.Dispose(); groupContext?.Dispose(); throw new Exception($"Group '{groupname}' not found."); } foreach (Principal member in group.GetMembers(true)) { if (member.SamAccountName == up.SamAccountName) { group.Members.Remove(member); group.Save(); logger.LogInfo(true, $"Removed user '{userPrincipalName}' from group '{groupname}'"); return; } } logger.LogInfo(true, $"User '{userPrincipalName}' not found in group '{groupname}'"); }
/// <summary> /// Wait for an operation to complete /// </summary> /// <param name="condition"></param> /// <param name="maxWaitMilliseconds"></param> /// <param name="waitMessage"></param> /// <param name="logger"></param> /// <returns></returns> public static bool WaitWhile( Func <bool> condition, int maxWaitMilliseconds, string waitMessage, ILoggerInterface logger) { int sleep = 500; int sleepStep = 1000; Stopwatch sw; sw = Stopwatch.StartNew(); sw.Start(); while (condition() && sw.ElapsedMilliseconds < maxWaitMilliseconds) { logger.LogInfo(true, waitMessage); Thread.Sleep(sleep); sleep += sleepStep; } return(!condition()); }
/// <summary>Adds a privilege to an account. Only works for local accounts.</summary> /// <param name="accountName">Name of an account - "domain\account" or only "account"</param> /// <param name="privilegeName">Name of the privilege (ms-help://MS.VSCC/MS.MSDNVS.1031/security/accctrl_96lv.htm")</param> /// <param name="logger"></param> /// <returns>The windows error code returned by LsaAddAccountRights</returns> public static long SetRight(string accountName, string privilegeName, ILoggerInterface logger) { ValidatePrivilege(privilegeName); logger.LogInfo(true, $"SetRight '{privilegeName}' for account '{accountName}'"); long winErrorCode = 0; // contains the last error var rights = new List <string>(); // pointer an size for the SID IntPtr sid = IntPtr.Zero; int sidSize = 0; // StringBuilder and size for the domain name StringBuilder domainName = new StringBuilder(); int nameSize = 0; // account-type variable for lookup int accountType = 0; // get required buffer size Advapi32Extern.LookupAccountName(string.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); // allocate buffers domainName = new StringBuilder(nameSize); sid = Marshal.AllocHGlobal(sidSize); // lookup the SID for the account bool result = Advapi32Extern.LookupAccountName(string.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); // say what you're doing logger.LogInfo(true, "LookupAccountName result = " + result); logger.LogInfo(true, $"IsValidSid: {Advapi32Extern.IsValidSid(sid)}"); logger.LogInfo(true, $"LookupAccountName succedded: [domainName='{domainName}']"); if (!result) { winErrorCode = Advapi32Extern.GetLastError(); logger.LogInfo(false, "LookupAccountName failed: " + winErrorCode); return(winErrorCode); } // initialize an empty unicode-string Advapi32Extern.LSA_UNICODE_STRING systemName = new Advapi32Extern.LSA_UNICODE_STRING(); // initialize a pointer for the policy handle IntPtr policyHandle = IntPtr.Zero; // these attributes are not used, but LsaOpenPolicy wants them to exists Advapi32Extern.LSA_OBJECT_ATTRIBUTES objectAttributes = new Advapi32Extern.LSA_OBJECT_ATTRIBUTES(); objectAttributes.Length = 0; objectAttributes.RootDirectory = IntPtr.Zero; objectAttributes.Attributes = 0; objectAttributes.SecurityDescriptor = IntPtr.Zero; objectAttributes.SecurityQualityOfService = IntPtr.Zero; // get a policy handle logger.LogInfo(true, "OpenPolicy started"); int resultPolicy = Advapi32Extern.LsaOpenPolicy(ref systemName, ref objectAttributes, Advapi32Extern.Access, out policyHandle); winErrorCode = Advapi32Extern.LsaNtStatusToWinError(resultPolicy); if (winErrorCode != 0) { logger.LogInfo(false, "OpenPolicy failed: " + winErrorCode); return(winErrorCode); } IntPtr userRightsPtr = IntPtr.Zero; int countOfRights = 0; logger.LogInfo(true, "LsaEnumerateAccountRights started"); int resultEnumerate = Advapi32Extern.LsaEnumerateAccountRights(policyHandle, sid, out userRightsPtr, out countOfRights); winErrorCode = Advapi32Extern.LsaNtStatusToWinError(resultEnumerate); if (winErrorCode != 0 && winErrorCode != 2) { logger.LogInfo(false, "LsaEnumerateAccountRights failed: " + winErrorCode); return(winErrorCode); } // Code 2 means no privileges if (winErrorCode == 0) { long ptr = userRightsPtr.ToInt64(); Advapi32Extern.LSA_UNICODE_STRING userRight; for (int i = 0; i < countOfRights; i++) { userRight = (Advapi32Extern.LSA_UNICODE_STRING)Marshal.PtrToStructure(userRightsPtr, typeof(Advapi32Extern.LSA_UNICODE_STRING)); string userRightStr = Marshal.PtrToStringAuto(userRight.Buffer); rights.Add(userRightStr); ptr += Marshal.SizeOf(userRight); } if (rights.Contains(privilegeName)) { logger.LogInfo(false, $"Account already has right '{privilegeName}'"); return(winErrorCode); } } // Now that we have the SID an the policy, // we can add rights to the account. // initialize an unicode-string for the privilege name Advapi32Extern.LSA_UNICODE_STRING[] userRights = new Advapi32Extern.LSA_UNICODE_STRING[1]; userRights[0] = new Advapi32Extern.LSA_UNICODE_STRING(); userRights[0].Buffer = Marshal.StringToHGlobalUni(privilegeName); userRights[0].Length = (ushort)(privilegeName.Length * UnicodeEncoding.CharSize); userRights[0].MaximumLength = (ushort)((privilegeName.Length + 1) * UnicodeEncoding.CharSize); // add the right to the account logger.LogInfo(true, "LsaAddAccountRights started"); int res = Advapi32Extern.LsaAddAccountRights(policyHandle, sid, userRights, 1); winErrorCode = Advapi32Extern.LsaNtStatusToWinError(res); if (winErrorCode != 0) { logger.LogInfo(false, "LsaAddAccountRights failed: " + winErrorCode); return(winErrorCode); } rights.Add(privilegeName); Advapi32Extern.LsaClose(policyHandle); Advapi32Extern.FreeSid(sid); return(winErrorCode); }
/// <summary> /// Creates a link (symlink or junction) /// </summary> /// <param name="mountPath">Path where the symlink or junction will be created.</param> /// <param name="mountDestination">Path the JUNCTION points to.</param> /// <param name="logger"></param> /// <param name="persistOnDeploy">If true, any files in the repo are synced to the content folder.</param> /// <param name="overWrite">If a junction or link already exists, overwrite it</param> /// <param name="linkType">Use this to force usage of symlinks. Otherwise junction/symlink is chosen by the method internally.</param> /// <returns></returns> public static void EnsureLink( string mountPath, string mountDestination, ILoggerInterface logger, bool persistOnDeploy, bool overWrite = false, LinkTypeRequest linkType = LinkTypeRequest.Auto) { var linkmanager = ReparsePointFactory.Create(); // On a local folder based deployment we might be redeploying on top of same application... if (Directory.Exists(mountPath)) { logger.LogWarning(true, "Mount destination already exists: {0}", mountPath); if (linkmanager.GetLinkType(mountPath) == LinkType.Junction || linkmanager.GetLinkType(mountPath) == LinkType.Symbolic) { logger.LogInfo(true, "Mount destination is junction, grabbing attributes."); var atts = linkmanager.GetLink(mountPath); logger.LogInfo(true, "Mount destination attributes: {0}", Newtonsoft.Json.JsonConvert.SerializeObject(atts)); var currentTarget = new DirectoryInfo(atts.Target); var requiredTarget = new DirectoryInfo(mountDestination); if (currentTarget.FullName == requiredTarget.FullName || overWrite) { // Remove it, it will be recreated anyways. Directory.Delete(mountPath); } else { // Something already exists. And it is NOT a junction equivalent to what // we are asking for. throw new Exception($"Could not mount junction because a directory or junction already exists at the junction source path: {mountPath}"); } } else { bool existingDirectoryHasFiles = Directory.EnumerateFiles(mountPath, "*", SearchOption.AllDirectories).Any(); // If the mountpath exists, but has nothing in it, delete it to make this process more error-proof. if (Directory.Exists(mountPath)) { if (persistOnDeploy) { // Copy any files, and then delete UtilsSystem.CopyFilesRecursivelyFast(mountPath, mountDestination, true, null, logger); Directory.Delete(mountPath, true); } else { if (!existingDirectoryHasFiles) { // Delete so we can junction Directory.Delete(mountPath, true); } } } } } // Create junction will fail if the physicial folder exists at the junction target // so the previous logic takes care of that...7 bool useSymlinkInsteadOfJunction; switch (linkType) { case LinkTypeRequest.Auto: useSymlinkInsteadOfJunction = mountDestination.StartsWith("\\"); break; case LinkTypeRequest.Junction: useSymlinkInsteadOfJunction = false; break; case LinkTypeRequest.Symlink: useSymlinkInsteadOfJunction = true; break; default: throw new NotSupportedException(); } logger.LogInfo(true, $"Creating {(useSymlinkInsteadOfJunction ? "symlink" : "junction")} '{mountPath}' => '{mountDestination}'"); // For remote drives, junctions will not work // https://helpcenter.netwrix.com/Configure_IT_Infrastructure/File_Servers/Enable_Symlink.html linkmanager.CreateLink(mountPath, mountDestination, useSymlinkInsteadOfJunction ? LinkType.Symbolic : LinkType.Junction); }
/// <summary> /// IIS is very bad at detecting and handling changes in certificates stored in the /// central certificate store, use this method to ensure that a hostname bound /// to a SSL termination is properly updated throughout IIS /// /// https://docs.microsoft.com/en-us/iis/get-started/whats-new-in-iis-85/certificate-rebind-in-iis85 /// https://delpierosysadmin.wordpress.com/2015/02/23/iis-8-5-enable-automatic-rebind-of-renewed-certificate-via-command-line/ /// </summary> public static void EnsureCertificateInCentralCertificateStoreIsRebound(string hostname, ILoggerInterface logger) { Dictionary <string, List <Binding> > temporaryBindings = new Dictionary <string, List <Binding> >(); using (var sm = new ServerManager()) { // Al sites that have an SSL termination bound to this hostname var sites = UtilsSystem.QueryEnumerable( sm.Sites, (s) => s.Bindings.Any(i => i.Protocol == "https" && hostname.Equals(i.Host, StringComparison.CurrentCultureIgnoreCase)), (s) => s, (s) => s.Name, logger).ToList(); // Remove temporarily foreach (var site in sites) { foreach (var binding in site.Bindings.Where((i) => i.Protocol == "https" && hostname.Equals(i.Host, StringComparison.CurrentCultureIgnoreCase)).ToList()) { if (!temporaryBindings.ContainsKey(site.Name)) { temporaryBindings[site.Name] = new List <Binding>(); } logger.LogInfo(true, "Removed binding {0} from site {1}", binding.BindingInformation, site.Name); temporaryBindings[site.Name].Add(binding); site.Bindings.Remove(binding); } } CommitChanges(sm); } // This wait here helps... Thread.Sleep(2000); // Now restore... using (var sm = new ServerManager()) { foreach (var siteName in temporaryBindings.Keys) { var site = FindSiteWithName(sm, siteName, logger).Single(); foreach (var binding in temporaryBindings[siteName]) { var b = site.Bindings.Add(binding.BindingInformation, binding.Protocol); b.SslFlags = binding.SslFlags; b.CertificateStoreName = binding.CertificateStoreName; b.UseDsMapper = binding.UseDsMapper; logger.LogInfo(true, "Restored binding {0} to site {1}", binding.BindingInformation, site.Name); } } CommitChanges(sm); } // This wait here helps also... Thread.Sleep(2000); }
/// <summary> /// Find a certificate in IIS central certificate store /// </summary> /// <param name="hostName"></param> /// <param name="logger"></param> /// <param name="certificatePath"></param> /// <returns></returns> public static X509Certificate2 FindCertificateInCentralCertificateStore( string hostName, ILoggerInterface logger, out string certificatePath) { string centralStorePath = UtilsIis.CentralStorePath(logger); FileInfo certificateFile = null; // Look for a certificate file that includes wildcard matching logic // https://serverfault.com/questions/901494/iis-wildcard-https-binding-with-centralized-certificate-store var hostNameParts = hostName.Split(".".ToCharArray()).Reverse().ToList(); foreach (var f in new DirectoryInfo(centralStorePath).EnumerateFiles()) { // Check if this certificate file is valid for the hostname... var certNameParts = Path.GetFileNameWithoutExtension(f.FullName).Split(".".ToCharArray()).Reverse() .ToList(); // This won't allow for nested subdomain with wildcards, but it's a good starting point // i.e. a hostname such as "a.mytest.mydomain.com" won't be matched to a certifica // such as "_.mydomain.com" // but "mytest.mydomain.com" will match to "_.mydomain.com". if (certNameParts.Count != hostNameParts.Count) { continue; } bool isMatch = true; for (int x = 0; x < hostNameParts.Count; x++) { if (hostNameParts[x] == "*" || certNameParts[x] == "_") { continue; } if (hostNameParts[x] != certNameParts[x]) { isMatch = false; break; } } if (isMatch) { certificateFile = f; break; } } certificatePath = certificateFile?.FullName; // This is null on purpose. string certificatePassword = null; X509Certificate2Collection collection = new X509Certificate2Collection(); if (certificateFile != null) { logger.LogInfo(true, "Found potential certificate matching file at {0}", certificateFile.FullName); try { // Usamos ephemeral keyset para que no almacene claves en la máquina todo el tiempo... collection.Import(certificateFile.FullName, certificatePassword, X509KeyStorageFlags.EphemeralKeySet); var originalCert = collection[0]; logger.LogInfo(true, "Certificate IssuerName '{0}'", originalCert.IssuerName.Name); logger.LogInfo(true, "Certificate FriendlyName '{0}'", originalCert.FriendlyName); logger.LogInfo(true, "Certificate SubjectName '{0}'", originalCert.SubjectName.Name); logger.LogInfo(true, "Certificate NotBefore '{0}'", originalCert.NotBefore.ToString("HH:mm:ss yyyy/MM/dd")); return(originalCert); } catch (Exception e) { logger.LogWarning(false, $"Error importing certificate: '{certificateFile.FullName}'." + e.Message); } } return(null); }
private static List <Handle> GetProcessesThatBlockPathHandle(string path, ILoggerInterface logger, bool logDetails = false) { if (!File.Exists(path) && !Directory.Exists(path)) { return(new List <Handle>()); } string key = "SOFTWARE\\Sysinternals\\Handle"; string name = "EulaAccepted"; // This Utility has an EULA GUI on first run... try to avoid that // by manually setting the registry int?eulaaccepted64 = (int?)UtilsRegistry.GetRegistryKeyValue64(RegistryHive.CurrentUser, key, name, null); int?eulaaccepted32 = (int?)UtilsRegistry.GetRegistryKeyValue32(RegistryHive.CurrentUser, key, name, null); bool eulaaccepted = (eulaaccepted32 == 1 && eulaaccepted64 == 1); if (!eulaaccepted) { UtilsRegistry.SetRegistryValue(RegistryHive.CurrentUser, key, name, 1, RegistryValueKind.DWord); } // Normalize the path, to ensure that long path is not used, otherwise handle.exe won't work as expected string fileName = UtilsSystem.RemoveLongPathSupport(path); List <Handle> result = new List <Handle>(); string outputTool = string.Empty; // Gather the handle.exe from the embeded resource and into a temp file var handleexe = UtilsSystem.GetTempPath("handle") + Guid.NewGuid().ToString().Replace("-", "_") + ".exe"; UtilsSystem.EmbededResourceToFile(Assembly.GetExecutingAssembly(), "_Resources.Handle.exe", handleexe); try { using (Process tool = new Process()) { tool.StartInfo.FileName = handleexe; tool.StartInfo.Arguments = fileName; tool.StartInfo.UseShellExecute = false; tool.StartInfo.Verb = "runas"; tool.StartInfo.RedirectStandardOutput = true; tool.Start(); outputTool = tool.StandardOutput.ReadToEnd(); tool.WaitForExit(1000); if (!tool.HasExited) { tool.Kill(); } } } catch (Exception e) { logger.LogException(e, EventLogEntryType.Warning); } finally { UtilsSystem.DeleteFile(handleexe, logger, 5); } string matchPattern = @"(?<=\s+pid:\s+)\b(\d+)\b(?=\s+)"; foreach (Match match in Regex.Matches(outputTool, matchPattern)) { if (int.TryParse(match.Value, out var pid)) { if (result.All(i => i.pid != pid)) { result.Add(new Handle() { pid = pid }); } } } if (result.Any() && logDetails) { logger?.LogInfo(true, outputTool); } return(result); }
/// <summary> /// Closes all the handles that block any files in the specified path /// </summary> /// <param name="path"></param> /// <param name="allowedProcesses">List of whitelisted processes</param> /// <param name="logger"></param> public static void ClosePathProcesses( string path, List <string> allowedProcesses, ILoggerInterface logger) { if (string.IsNullOrWhiteSpace(path)) { return; } // Make sure the path exists if (!File.Exists(path) && !Directory.Exists(path)) { return; } // Load list of processes that block directory var processes = GetPathProcessesInfo(path, logger); // Filter the whitelisted string regex = string.Join("|", allowedProcesses); var processesThatWillBeClosed = processes.Where((i) => i.MainModulePath != null && Regex.IsMatch(i.MainModulePath, regex)).ToList(); if (!processesThatWillBeClosed.Any()) { return; } // Message of processes that will not be closed var processesThatWillNotBeClosed = processes.Except(processesThatWillBeClosed).ToList(); if (processesThatWillNotBeClosed.Any()) { logger.LogWarning(true, "The following processes are not whitelisted and will not be closed {0}", string.Join(", ", processesThatWillNotBeClosed.Select((i) => i.ProcessName))); } // Grab the actual process instances var processesInstances = GetProcessInstance(processesThatWillBeClosed); // First kill al the processes. foreach (var p in processesInstances) { try { logger.LogInfo(true, "Killing process: {0}", p.ProcessName); if (!p.HasExited) { p.Kill(); p.WaitForExit(3000); } } catch (Exception e) { logger.LogException(e, EventLogEntryType.Warning); } } // Even though the processes have exited, handles take a while to be released Thread.Sleep(500); foreach (var p in processesInstances) { bool hasClosed = UtilsSystem.WaitWhile(() => !p.HasExited, 15000, $"Waiting for process {p.ProcessName} to close.", logger); logger.LogInfo(true, "Process {0} has closed: {1}", p.ProcessName, hasClosed); } }
/// <summary> /// Copy files recursively. /// </summary> /// <param name="source"></param> /// <param name="target"></param> /// <param name="overwrite"></param> /// <param name="ignoreOnDeployPattern"></param> /// <param name="logger"></param> private static void DoCopyFilesRecursivelyFast( DirectoryInfo source, DirectoryInfo target, bool overwrite, string ignoreOnDeployPattern, ILoggerInterface logger) { var files = source.EnumerateFiles("*", SearchOption.AllDirectories); ParallelOptions pop = new ParallelOptions(); // The bottle neck here is disk rather than CPU... but number of CPU's is a good measure // of how powerful the target machine might be... pop.MaxDegreeOfParallelism = (int)Math.Ceiling(Environment.ProcessorCount * 1.5); logger.LogInfo(true, "Copying files from {1} with {0} threads.", pop.MaxDegreeOfParallelism, source.FullName); var ignoreOnDeployRegex = string.IsNullOrWhiteSpace(ignoreOnDeployPattern) ? null : new Regex(ignoreOnDeployPattern); int fileCount = 0; int dirCount = 0; Stopwatch sw = new Stopwatch(); sw.Start(); Parallel.ForEach(files, pop, (i) => { try { var dir = i.Directory.FullName; var relativeDir = dir.Substring(source.FullName.Length, dir.Length - source.FullName.Length) .TrimStart("\\".ToCharArray()); var relativeFile = Path.Combine(relativeDir, i.Name); var destDir = Path.Combine(target.FullName, relativeDir); var destFile = new FileInfo(Path.Combine(destDir, i.Name)); if (ignoreOnDeployRegex?.IsMatch(relativeFile) == true) { return; } if (!Directory.Exists(destDir)) { Directory.CreateDirectory(destDir); Interlocked.Add(ref dirCount, 1); } if ((!destFile.Exists) || (destFile.Exists && overwrite)) { i.CopyTo(destFile.FullName, true); Interlocked.Add(ref fileCount, 1); } } catch (Exception e) { // Reserved names cannot be copied... if (InvalidWindowsFileNames.Contains(i.Name.ToLower())) { logger.LogWarning(true, "Skipped invalid file: " + i.FullName); } else { throw new Exception("Error copying file: " + i.FullName, e); } } if (sw.ElapsedMilliseconds > 2000) { lock (string.Intern("utils-system-filecopy-fast")) { if (sw.ElapsedMilliseconds > 2000) { try { int leftPos = Console.CursorLeft - 80; Console.SetCursorPosition(leftPos >= 0 ? leftPos : 0, Console.CursorTop); Console.Write($"Copied {fileCount} files.".PadRight(80, " ".ToCharArray().First())); } catch (IOException) { // ignored, the console might no always be available (i.e. in a service) // Exception Type: 'System.IO.IOException // ExceptionMessage: The handle is invalid. } sw.Restart(); } } } }); logger.LogInfo(true, "Copied {0} files and {1} directories.", fileCount, dirCount); }
/// <summary> /// Downloads (And extracts) single artifacts from jobs. /// </summary> /// <param name="applicationId"></param> /// <param name="build"></param> /// <param name="artifactRegex"></param> /// <param name="destinationPath"></param> /// <param name="logger"></param> public void DownloadSingleArtifactFromBuild( string applicationId, Build build, string artifactRegex, string destinationPath, ILoggerInterface logger) { UtilsSystem.EnsureDirectoryExists(destinationPath, true); // Use the first job in the build... var job = build.jobs.First(); var artifact = this.FindDefaultArtifactForBuild(job, build, artifactRegex); var filename = artifact.fileName; var extension = Path.GetExtension(filename); string downloadTemporaryDir = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(this.TempDir, "_appveyor", "dld", applicationId), true); int artifactRetentionNum = 5; int artifactAgeHoursForStale = 24; // Do not touch the latest artifactRetentionNum artifacts or artifacts that are not older than artifactAgeHoursForStale hours var staleFiles = Directory.EnumerateFiles(downloadTemporaryDir) .Select((i) => new FileInfo(i)) .Where((i) => i.Extension.Equals(".zip", StringComparison.CurrentCultureIgnoreCase)) .OrderByDescending((i) => i.CreationTimeUtc) .Skip(artifactRetentionNum) .Where((i) => (DateTime.UtcNow - i.LastWriteTime).TotalHours > artifactAgeHoursForStale) .ToList(); foreach (var f in staleFiles) { // Make this fail proof, it's just a cleanup. try { this.Logger.LogInfo(true, "Removing stale artifact cache file {0}", f.FullName); f.Delete(); } catch { // ignored } } // Use a short hash as the temporary file name, because long paths can have issues... var tmpFile = UtilsSystem.CombinePaths(downloadTemporaryDir, UtilsEncryption.GetShortHash(JsonConvert.SerializeObject(build) + filename) + extension); if (Path.GetExtension(tmpFile)?.ToLower() != ".zip") { throw new NotImplementedException("AppVeyor artifacts should only be Zip Files."); } if (!File.Exists(tmpFile)) { // Use an intermediate .tmp file just in case the files does not finish to download, // if it exists, clear it. string tmpFileDownload = tmpFile + ".tmp"; if (File.Exists(tmpFileDownload)) { UtilsSystem.RetryWhile(() => File.Delete(tmpFileDownload), (e) => true, 4000, this.Logger); } var url = $"/api/buildjobs/{job.jobId}/artifacts/{filename}"; logger.LogInfo(true, "Downloading artifact from: '{0}' to '{1}'", url, tmpFileDownload); this.ExecuteApiCallToFile(url, tmpFileDownload); // Rename to the final cached artifact file logger.LogInfo(true, "Download succesful, moving to '{0}'", tmpFile); UtilsSystem.RetryWhile(() => File.Move(tmpFileDownload, tmpFile), (e) => true, 4000, this.Logger); } else { logger.LogInfo(true, "Skipping artifact download, already in local cache: {0}", tmpFile); } logger.LogInfo(true, "Unzipping {1} file to '{0}'...", destinationPath, UtilsSystem.BytesToString(new FileInfo(tmpFile).Length)); ZipFile.ExtractToDirectory(tmpFile, destinationPath); logger.LogInfo(true, "Unzipping finished."); }
/// <summary> /// Create a user or return one if it does not exist. /// </summary> /// <param name="identity"></param> /// <param name="password"></param> /// <param name="displayName"></param> /// <param name="logger"></param> /// <param name="acp"></param> /// <returns></returns> public static UserPrincipal EnsureUserExists( string identity, string password, string displayName, ILoggerInterface logger, AccountManagementPrincipalContext acp) { var parsedUserName = new FqdnNameParser(identity); if (parsedUserName.UserPrincipalName.Length > 64) { throw new Exception($"Windows account userPrincipalName '{parsedUserName.UserPrincipalName}' cannot be longer than 64 characters."); } using (PrincipalContext pc = BuildPrincipal(acp)) { UserPrincipal up = FindUser(identity, pc); string samAccountName = parsedUserName.SamAccountName; logger.LogInfo(false, $"Ensure windows account exists '{samAccountName}@{password}' with userPrincipal '{identity}'"); if (up == null) { up = new UserPrincipal(pc, samAccountName, password, true); } else { logger.LogInfo(true, $"Found account IsAccountLockedOut={up.IsAccountLockedOut()}, SamAccountName={up.SamAccountName}"); // Make sure we have the latest password, just in case // the pwd algorithm generation changes... try { up.SetPassword(password); } catch (Exception e) { logger.LogWarning(true, "Cannot update password for account: " + e.Message + e.InnerException?.Message); } } up.UserCannotChangePassword = true; up.PasswordNeverExpires = true; up.Enabled = true; up.DisplayName = displayName; up.Description = parsedUserName.UserPrincipalName; // If we are in a domain, assign the user principal name if (pc.ContextType == ContextType.Domain) { logger.LogInfo(true, "Setting UserPrincipalName to '{0}'", parsedUserName.UserPrincipalName); up.UserPrincipalName = parsedUserName.UserPrincipalName + "@" + parsedUserName.DomainName.ToLower(); } if (up.IsAccountLockedOut()) { try { up.UnlockAccount(); } catch (Exception e) { logger.LogWarning(true, "Cannot unlock account: " + e.Message + e.InnerException?.Message); } } try { up.Save(); } catch (Exception e) { logger.LogException(new Exception("Error while saving user", e), EventLogEntryType.Warning); // Sometimes it crashes, but everything was OK (weird?) // so we check again if the user has been created Thread.Sleep(500); up = FindUser(identity, pc); if (up == null) { // Rethrow the original, whatever it was. ExceptionDispatchInfo.Capture(e).Throw(); } } return(up); } }