Functions/GenXdev.FileSystem/Find-Item.Utilities.cs
// ################################################################################
// Part of PowerShell module : GenXdev.FileSystem // Original cmdlet filename : Find-Item.Utilities.cs // Original author : René Vaessen / GenXdev // Version : 1.276.2025 // ################################################################################ // MIT License // // Copyright 2021-2025 GenXdev // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // ################################################################################ using StreamRegex.Extensions.Core; using StreamRegex.Extensions.RegexExtensions; using System.Collections.Concurrent; using System.Collections.ObjectModel; using System.Data.Common; using System.Diagnostics; using System.IO; using System.Management; using System.Management.Automation; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Xml.Linq; using Windows.ApplicationModel.Calls; public partial class FindItem : PSCmdlet { /// <summary> /// Normalizes paths by removing long path prefixes for non-filesystem use. /// </summary> /// <param name="path">The path to normalize.</param> /// <returns>The normalized path.</returns> protected string NormalizePathForNonFileSystemUse(string path) { // force paths internally to have backslashes path = path.Replace("/", "\\"); // path references user home directory? if (path == "~" || path.StartsWith("~\\")) { string home = this.SessionState.Path.GetResolvedPSPathFromPSPath("~")[0].Path; path = path == "~" ? home : Path.Combine(home, path.Substring(2)); } // check for unc long path prefix if (path.StartsWith(@"\\?\UNC\", StringComparison.InvariantCultureIgnoreCase)) { // remove prefix and adjust string result = @"\\" + path.Substring(8); return result; } // check for local long path prefix if (path.StartsWith(@"\\?\")) { // remove prefix string result = path.Substring(4); return result; } // check for alternate unc prefix if (path.StartsWith(@"\??\UNC\", StringComparison.InvariantCultureIgnoreCase)) { // remove prefix and adjust string result = @"\\" + path.Substring(8); return result; } // check for alternate local prefix if (path.StartsWith(@"\??\")) { // remove prefix string result = path.Substring(4); return result; } // return unchanged if no prefix return path; } /// <summary> /// Gets the number of physical cores for default parallelism /// </summary> /// <returns>The count of physical cores.</returns> public int GetCoreCount() { // initialize core count int totalPhysicalCores = 0; // query wmi for processors using (var searcher = new ManagementObjectSearcher("SELECT NumberOfCores FROM Win32_Processor")) { // sum cores from each processor foreach (var item in searcher.Get()) { totalPhysicalCores += Convert.ToInt32(item["NumberOfCores"]); } } // return total cores return totalPhysicalCores; } /// <summary> /// Gets available RAM in bytes for resource calculations /// </summary> /// <returns>Available bytes of RAM.</returns> protected long GetFreeRamInBytes() { // use performance counter for memory using (var counter = new PerformanceCounter("Memory", "Available Bytes")) { // get current value long result = (long)counter.NextValue(); return result; } } /// <summary> /// Lists disk shares on a machine using UNC paths. /// </summary> /// <param name="machineName">The machine to query.</param> /// <returns>Array of share names.</returns> public static string[] ListDiskSharesUNC(string machineName) { // attempt to query shares try { // create wmi query for shares var query = new ObjectQuery("SELECT * FROM Win32_Share"); // set scope to machine var scope = new ManagementScope($@"\\{machineName}\root\cimv2"); // connect to scope scope.Connect(); // execute query var searcher = new ManagementObjectSearcher(scope, query); // get results var shares = searcher.Get(); // prepare list for unc paths var uncPaths = new List<string>(); // process each share foreach (ManagementObject share in shares) { // get type value uint typeValue = Convert.ToUInt32(share["Type"]); // get share name string name = share["Name"]?.ToString() ?? ""; // check if disk share bool isDisk = (typeValue & 0xFFFF) == 0; // add if disk and named if (isDisk && !string.IsNullOrEmpty(name)) { uncPaths.Add(name); } } // return as array return uncPaths.ToArray<string>(); } catch { // silent error handling // Console.WriteLine($"Error: {ex.Message}"); // Uncomment for // debugging } // return empty on failure return new string[0]; } /// <summary> /// Adds a path to the search queue and updates counters. /// </summary> /// <param name="path">The path to enqueue.</param> protected void AddToSearchQueue(string path) { if (UseVerboseOutput) { VerboseQueue.Enqueue($"AddToSearchQueue: '{path}'"); } // add to directory queue DirQueue.Enqueue(path); // increment queued count Interlocked.Increment(ref dirsQueued); // wait.. if (!isStarted) return; // add workers if needed AddWorkerTasksIfNeeded(cts.Token); } /// <summary> /// Gets drives to search based on parameters. /// </summary> /// <returns>Enumerable of drives.</returns> protected IEnumerable<string> GetRootsToSearch() { // handle all drives switch if (this.AllDrives.IsPresent) { var combinedDrives = DriveInfo.GetDrives() .Where(q => q.IsReady && (IncludeOpticalDiskDrives || q.DriveType != DriveType.CDRom) && q.DriveType != DriveType.Unknown) .Select(q => char.ToUpperInvariant(q.Name[0])) .Union(SearchDrives .Where(q => !string.IsNullOrWhiteSpace(q)) .Select(q => char.ToUpperInvariant(q[0])) .Union(DriveLetter .Where(c => !char.IsWhiteSpace(c)) .Select(c => char.ToUpperInvariant(c)) ) ); foreach (var drive in combinedDrives) { yield return (drive + ":\\"); } } else { var drives = SearchDrives .Where(q => !string.IsNullOrWhiteSpace(q)) .Select(q => char.ToUpperInvariant(q[0])) .Union(DriveLetter .Where(c => !char.IsWhiteSpace(c)) .Select(c => char.ToUpperInvariant(c)) ); foreach (var drive in drives) { yield return (drive + ":\\"); } } } /// <summary> /// Adds worker tasks if below parallelism limit. /// </summary> protected void AddWorkerTasksIfNeeded(CancellationToken ctx) { if (ctx.IsCancellationRequested) return; // get count of active matches long fileMatchesCount = Interlocked.Read(ref this.fileMatchesActive); // get count of directories left long dirsLeft = Interlocked.Read(ref dirsQueued); // get count of files found long fileOutputCount = Interlocked.Read(ref filesFound); // lock for worker management lock (WorkersLock) { // remove completed workers Workers.RemoveAll(w => w.IsCompleted); // count active workers var currentNrOfDirectoryProcessors = Interlocked.Read(ref this.directoryProcessors); // calculate needed matching workers var requestedDirectoryProcessors = Math.Min( MaxDegreeOfParallelism, DirQueue.Count ); // determine missing workers long missingDirectoryProcessors = Math.Min( requestedDirectoryProcessors - currentNrOfDirectoryProcessors, (FileContentMatchQueue.Count <= PatternMatcherOptions.BufferSize / 125) ? Int32.MaxValue : 0 ); // add missing workers // List to hold worker tasks while (missingDirectoryProcessors-- > 0) { // Start worker tasks for parallel directory processing AddWorkerTask(Workers, false, ctx); } // count active workers var currentNrOfMatchingProcessors = Interlocked.Read(ref this.matchProcessors); // calculate needed matching workers var requestedMatchingProcessors = Math.Min(MaxDegreeOfParallelism, FileContentMatchQueue.Count); // determine missing workers var missingMatchingProcessors = requestedMatchingProcessors - currentNrOfMatchingProcessors; // add missing workers // List to hold worker tasks while (missingMatchingProcessors-- > 0) { // Start worker tasks for parallel directory processing AddWorkerTask(Workers, true, ctx); } } } /// <summary> /// Adds a single worker task to the list. /// </summary> /// <param name="workers">The list of workers.</param> protected void AddWorkerTask(List<Task> workers, bool contentMatcher, CancellationToken ctx) { if (UseVerboseOutput) { string str = contentMatcher ? "content matcher" : "directory processor"; VerboseQueue.Enqueue($"Start new {str} worker"); } // need to add a content matcher task? if (contentMatcher) { // update counters Interlocked.Increment(ref matchProcessors); // add worker workers.Add(Task.Run(async () => { try { string filePath; while (FileContentMatchQueue.TryDequeue(out filePath) && !ctx.IsCancellationRequested) { // try processing try { await FileContentProcessor(filePath, ctx); } catch (Exception ex) { // log failure if verbose if (UseVerboseOutput) { VerboseQueue.Enqueue($"Worker task failed: {ex.Message}"); } } finally { if (UseVerboseOutput) { string str = contentMatcher ? "Content matcher" : "Directory processor"; VerboseQueue.Enqueue($"{str} worker stopped"); } AddWorkerTasksIfNeeded(ctx); } } } finally { Interlocked.Decrement(ref matchProcessors); AddWorkerTasksIfNeeded(ctx); } }, ctx)); return; } Interlocked.Increment(ref directoryProcessors); // add new task workers.Add(Task.Run(() => { // try processing try { // run directory processor DirectoryProcessor(cts!.Token); } catch (Exception ex) { // log failure if verbose if (UseVerboseOutput) { VerboseQueue.Enqueue($"Worker task failed: {ex.Message}"); } } finally { if (UseVerboseOutput) { string str = contentMatcher ? "Content matcher" : "Directory processor"; VerboseQueue.Enqueue($"{str} worker stopped"); } Interlocked.Decrement(ref directoryProcessors); } })); } /// <summary> /// Checks if all workers are completed. /// </summary> /// <returns>True if all completed.</returns> protected bool AllWorkersCompleted() { // lock for check lock (WorkersLock) { // check for any incomplete bool result = !(Workers.Any(w => !w.IsCompleted)); return result; } } /// <summary> /// Executes a PowerShell script and returns the result of type T, handling /// any errors that occur. /// </summary> /// <param name="script">The script to execute.</param> /// <returns>The result as type T.</returns> protected T InvokeScript<T>(string script) { // invoke and collect results // Invoke the script and collect results Collection<PSObject> results = InvokeCommand.InvokeScript(script); // check if results match T // Return the result if it matches type T if (results is T) { return (T)(object)results; } // check first result base if (results.Count > 0 && results[0].BaseObject is T) { return (T)results[0].BaseObject; } // return default on failure return default(T); } // Local method to calculate current depth for recursion limit check /// <summary> /// Calculates current recursion depth and limit. /// </summary> /// <param name="CurrentLocation">The current path.</param> /// <param name="IsUncPath">If UNC path.</param> /// <param name="CurrentRecursionDepth">Output depth.</param> /// <param name="CurrentRecursionLimit">Output limit.</param> void GetCurrentDepthParameters(string CurrentLocation, bool IsUncPath, out int CurrentRecursionDepth, out int CurrentRecursionLimit) { // get relative path // Compute relative path to determine depth from base var relativePath = Path.GetRelativePath(RelativeBasePath, CurrentLocation); // adjust leading dot separator if (relativePath.StartsWith("." + Path.DirectorySeparatorChar) || relativePath.StartsWith("." + Path.AltDirectorySeparatorChar)) { relativePath = relativePath.Substring(2); } else if (relativePath.StartsWith(".." + Path.DirectorySeparatorChar) || relativePath.StartsWith(".." + Path.AltDirectorySeparatorChar)) { relativePath = relativePath.Substring(3); } // count directory levels // Count directory levels in the relative path CurrentRecursionDepth = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Length; // set offset based on path type // Adjust limit based on path type (UNC or local) var offset = IsUncPath ? 4 : 2; // Account for \\server\share\ or C:\ // compute limit CurrentRecursionLimit = MaxRecursionDepth <= 0 ? 0 : Path.IsPathRooted(relativePath) ? (MaxRecursionDepth + offset) : MaxRecursionDepth; } /// <summary> /// Formats 64-bits integers to string with a semi fixed width /// </summary> /// <param name="nr">Number to format</param> /// <param name="padLeft">When set, will pad spaces to the left side</param> /// <returns></returns> private string formatStat(long nr, bool padLeft) { var s = nr.ToString(); // snap to sizes of 4 int steps = 3; int length = s.Length + (steps - (s.Length % steps)); return padLeft ? (s.PadLeft(length)) : (s.PadRight(length)); } /// <summary> /// Handles progress queue dequeuing. /// </summary> /// <param name="all">If to process all.</param> private void UpdateProgressStatus(bool force = false) { // get and convert last timestamp var now = DateTime.UtcNow; long lastProgress = Interlocked.Read(ref this.lastProgress); var time = DateTime.FromBinary(lastProgress); // too soon/frequent? if (!force && now - time < TimeSpan.FromMilliseconds(250)) return; // set current timestamp as the new checkpoint lastProgress = now.ToBinary(); Interlocked.Exchange(ref lastProgress, lastProgress); // get output kind bool outputtingFiles = FilesAndDirectories.IsPresent || !Directory.IsPresent; // get processing actions bool matchingContent = outputtingFiles && (Content != ".*" && !string.IsNullOrWhiteSpace(Content)); // determine if including files based on switches and pattern bool andFiles = outputtingFiles && matchingContent; // get counters long fileMatchesCount = Interlocked.Read(ref this.fileMatchesActive); long dirsDone = Interlocked.Read(ref dirsCompleted); long dirsLeft = Interlocked.Read(ref dirsQueued); long fileMatchesLeft = Interlocked.Read(ref matchesQueued); long fileOutputCount = Interlocked.Read(ref filesFound); long directoryProcessorsCount = Interlocked.Read(ref directoryProcessors); long matchProcessorsCount = Interlocked.Read(ref matchProcessors); long fileMatchesStartedCount = Interlocked.Read(ref fileMatchesStarted); long fileMatchesCompletedCount = Interlocked.Read(ref fileMatchesCompleted); long queuedMatchesCount = FileContentMatchQueue.Count; // calculate percent complete double ratio = (dirsDone + fileMatchesCompletedCount) / Math.Max(1d, (dirsLeft + fileMatchesLeft)); // calculate completion percentage for progress int progressPercent = (int)Math.Round( Math.Min(100, Math.Max(0, ratio ) * 100d ), 0 ); // create progress record var record = new ProgressRecord(0, "Find-Item", ( "Scanning directories" + (andFiles ? " and found file contents" : "") )) { // set percent complete PercentComplete = progressPercent, // set status description with counts StatusDescription = ( "Directories: " + formatStat(dirsDone, true) + "/" + formatStat(DirQueue.Count, false) + " [" + formatStat(directoryProcessorsCount, false) + "] | Found: " + formatStat(fileOutputCount, false) + (matchingContent ? " | Matched: " + formatStat(fileMatchesStartedCount, true) + "/" + formatStat(queuedMatchesCount, false) + " [" + formatStat(matchProcessors, false) + "]" : string.Empty) ), // set current operation message CurrentOperation = ( outputtingFiles && matchingContent ? ( dirsLeft - dirsDone == 0 ? "Searching for more files and matching file content" : dirsLeft == 0 ? "Searching for files to match" : "Matching file contents" ) : ( outputtingFiles ? ( filesFound == 0 ? "Searching for files" : "Searching for more files" ) : "Searching for matching directories" ) ) }; // write the progress record WriteProgress(record); } } |