Functions/GenXdev.FileSystem/Find-Item.Initialization.cs

// ################################################################################
// Part of PowerShell module : GenXdev.FileSystem
// Original cmdlet filename : Find-Item.Initialization.cs
// Original author : René Vaessen / GenXdev
// Version : 1.278.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 System.Collections.Concurrent;
using System.Management.Automation;
 
namespace GenXdev.FileSystem
{
 
    /// <summary>
    /// Handles initialization for the FindItem cmdlet.
    /// </summary>
    public partial class FindItem : PSCmdlet
    {
        /// <summary>
        /// set up parallelism based on user input or defaults
        /// </summary>
        private void InitializeParallelismConfiguration()
        {
            // determine actions
            usingSelectString = !String.IsNullOrEmpty(Content) && (Content != ".*" || SimpleMatch.IsPresent) &&
                 (Content != "*" || !SimpleMatch.IsPresent);
 
            // how many files at once? what user wants, or default to core count
            baseTargetWorkerCount =
                MaxDegreeOfParallelism <= 0 ?
                GetCoreCount() :
                Math.Max(1, MaxDegreeOfParallelism / 2);
 
            int baseLength = baseMemoryPerWorker / 125;
            bool buffersFull = FileContentMatchQueue.Count <= baseLength ||
                                VerboseQueue.Count <= baseLength ||
                                DirQueue.Count <= baseLength ||
                                OutputQueue.Count <= baseLength;
 
            // stop finding files when the queues get too full
            maxDirectoryWorkersInParallel = () =>
                            // we are thinking about the memory being used
                            // for storing paths to process
                            // stop finding more files when queues get too full
                            buffersFull ?
                            Math.Min(DirQueue.Count, baseTargetWorkerCount) : 0;
 
 
            // dynamicly scale according to how full buffers are getting
            maxMatchWorkersInParallel = () =>
            Math.Min(
                Math.Min(
                        // idealy as much as possible
                        FileContentMatchQueue.Count,
                        // if user wants one we take one
                        MaxDegreeOfParallelism == 1 ? 1 : int.MaxValue
                ),
                Math.Max(
                    Math.Min(
                        // idealy as much as possible
                        FileContentMatchQueue.Count,
                        // limit to twice the target worker count
                        baseTargetWorkerCount * 2
                        // and share it with directory workers
                        // so when they get throttled, we can use more for matching
                        ) - maxDirectoryWorkersInParallel(),
                    Math.Min(
                        // idealy as much as possible
                        FileContentMatchQueue.Count,
                        // limit to twice the target worker count
                        baseTargetWorkerCount
                    )
                )
            );
 
            // we will only temporarily change the thread pool size
            ThreadPool.GetMaxThreads(out this.oldMaxWorkerThread, out this.oldMaxCompletionPorts);
 
            int workerThreads = Math.Max(1, Math.Max(this.oldMaxWorkerThread, maxDirectoryWorkersInParallel() * 2));
            int completionPortThreads = Math.Max(1, Math.Max(this.oldMaxCompletionPorts, maxMatchWorkersInParallel()));
 
            // increase thread pool size if needed
            ThreadPool.SetMaxThreads(
 
                // worker threads
                // used for Directory processors and Match processors
                workerThreads,
 
                // used after async IO operations
                completionPortThreads
             );
 
            if (UseVerboseOutput)
            {
                VerboseQueue.Enqueue($"Max worker threads set to {workerThreads}, completion port threads set to {completionPortThreads}");
                VerboseQueue.Enqueue($"Base target worker count: {baseTargetWorkerCount}");
                VerboseQueue.Enqueue($"Using content matching: {usingSelectString}");
                if (usingSelectString)
                {
                    VerboseQueue.Enqueue($"Max match workers in parallel: {maxMatchWorkersInParallel()}");
                }
                VerboseQueue.Enqueue($"Max directory workers in parallel: {maxDirectoryWorkersInParallel()}");
            }
        }
 
        /// <summary>
        /// initialize provided names for searching
        /// </summary>
        private void InitializeProvidedNames()
        {
            // process each unique search mask provided
            if (Name != null && Name.Length > 0)
            {
                // loop through each mask
                foreach (var name in Name)
                {
                    // check if mask already processed to avoid duplicates
                    if (VisitedNodes.TryAdd("start;" + name, true))
                    {
 
                        // log processing of mask if verbose enabled
                        if (UseVerboseOutput)
                        {
                            VerboseQueue.Enqueue($"Processing name: {name}");
                        }
 
                        // prepare search starting point
                        InitializeSearchDirectory(name);
                    }
                    else if (UseVerboseOutput)
                    {
 
                        // log skipping duplicate mask
                        WriteWarning($"Skipping duplicate name: {name}");
                    }
                }
            }
        }
 
        /// <summary>
        /// Sets up cancellation token with optional timeout.
        /// </summary>
        protected void InitializeCancellationToken()
        {
 
            // create token source
            // Set up cancellation with optional timeout
            cts = new CancellationTokenSource();
 
            // apply timeout if specified
            if (TimeoutSeconds.HasValue)
            {
                cts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds.Value));
 
                // log timeout if verbose
                if (UseVerboseOutput)
                {
                    VerboseQueue.Enqueue($"Search timeout set to {TimeoutSeconds.Value} seconds");
                }
            }
        }
 
        /// <summary>
        /// Configures buffering for pattern matching.
        /// </summary>
        protected void InitializeBufferingConfiguration()
        {
 
            // calculate max file size from ram
            // Calculate max file size based on available RAM to prevent memory
            // issues
            baseMemoryPerWorker = Math.Max(
                // minimal 2MB
                1024 * 1024 * 2,
                Math.Min(
                    // max 10 mb
                    1024 * 1024 * 10,
 
                        (int)Math.Round(
 
                            // shoot for approximately max 5% of free ram available
                            // for this user invoced PowerShell search
                            (
                                GetFreeRamInBytes() * 0.05d
 
                             ) / Math.Max(1, Convert.ToDouble(baseTargetWorkerCount)),
                            0
                        )
                )
            );
 
            if (UseVerboseOutput)
            {
                VerboseQueue.Enqueue($"Base memory per worker set to {baseMemoryPerWorker / (1024 * 1024)} MB");
            }
        }
 
        /// <summary>
        /// Resolves the relative base directory.
        /// </summary>
        protected void InitializeRelativeBaseDir()
        {
 
            // declare base dir
            string baseDir;
 
            // check if base path provided
            if (!string.IsNullOrEmpty(RelativeBasePath))
            {
 
                // use full path if rooted
                if (Path.IsPathRooted(RelativeBasePath))
                {
                    baseDir = Path.GetFullPath(RelativeBasePath);
                }
                else
                {
 
                    // combine with current if relative
                    baseDir = Path.GetFullPath(Path.Combine(CurrentDirectory, RelativeBasePath));
                }
            }
            else
            {
 
                // default to current
                baseDir = Path.GetFullPath(CurrentDirectory);
            }
 
            // set relative base
            RelativeBasePath = baseDir;
 
            if (UseVerboseOutput)
            {
                VerboseQueue.Enqueue($"Using relative base directory: {RelativeBasePath}");
            }
        }
 
        /// <summary>
        /// Sets the current directory safely.
        /// </summary>
        protected void InitializeCurrentDirectory()
        {
            // get powershell location
            // Get current PowerShell location for base directory
            var psLocation = InvokeScript<string>("(Get-Location).Path");
 
            // declare safe dir
            string safeDir = null;
 
            // declare validity
            bool isValid = false;
 
            // try to validate
            try
            {
 
                // get full path
                // Validate and get full path
                safeDir = Path.GetFullPath(psLocation);
 
                // check existence
                isValid = System.IO.Directory.Exists(safeDir);
            }
            catch
            {
 
                // log invalid if verbose
                if (UseVerboseOutput)
                {
                    VerboseQueue.Enqueue($"Invalid current directory path: {psLocation}");
                }
 
                // set invalid
                isValid = false;
            }
 
            // use if valid
            if (isValid)
            {
                CurrentDirectory = safeDir;
 
                // log if verbose
                if (UseVerboseOutput)
                {
                    VerboseQueue.Enqueue($"Using current directory: {CurrentDirectory}");
                }
            }
            else
            {
 
                // default to profile
                // Default to user profile if current directory invalid
                CurrentDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 
                // log default if verbose
                if (UseVerboseOutput)
                {
                    VerboseQueue.Enqueue($"Defaulted to user profile directory: {CurrentDirectory}");
                }
            }
        }
 
        /// <summary>
        /// Sets up exclude patterns for wildcards.
        /// </summary>
        protected void InitializeExcludePatterns()
        {
 
            // set options based on casing
            this.CurrentWildCardOptions = CaseNameMatching == MatchCasing.PlatformDefault ? (
                         // windows or linux based?
                         Environment.OSVersion.Platform == PlatformID.Win32NT ?
                         WildcardOptions.IgnoreCase |
                         WildcardOptions.CultureInvariant : WildcardOptions.CultureInvariant
                    ) : CaseNameMatching == MatchCasing.CaseInsensitive ? (
                       WildcardOptions.IgnoreCase |
                       WildcardOptions.CultureInvariant
                    ) : WildcardOptions.CultureInvariant;
 
            // create file exclude patterns
            FileExcludePatterns = Exclude.Select(
                p => WildcardPattern.Get(p, CurrentWildCardOptions)
            ).ToArray();
 
            // create dir exclude patterns
            DirectoryExcludePatterns = Exclude.Select(
                p => WildcardPattern.Get(p.EndsWith("\\") ? p.TrimEnd('\\') : p, CurrentWildCardOptions)
            ).ToArray();
 
            // default git exclusion if none
            if (DirectoryExcludePatterns.Length == 0)
            {
                DirectoryExcludePatterns = new WildcardPattern[1] { new WildcardPattern("*\\.git", CurrentWildCardOptions) };
            }
 
            if (UseVerboseOutput)
            {
                VerboseQueue.Enqueue($"Using {FileExcludePatterns.Length} file exclude patterns and {DirectoryExcludePatterns.Length} directory exclude patterns.");
                foreach (var pattern in Exclude)
                {
                    VerboseQueue.Enqueue($" Exclude pattern: '{pattern}'");
                }
 
                if (DirectoryExcludePatterns.Length == 1 && Exclude.Length == 0)
                {
                    VerboseQueue.Enqueue($" Defaulted to exclude pattern: '*.git'");
                }
 
                if (usingSelectString && !IncludeNonTextFileMatching.IsPresent)
                {
                    VerboseQueue.Enqueue($"Skipping non text file content based ono file-extensions, use -IncludeNonTextFileMatching to disable");
                }
            }
        }
 
        /// <summary>
        /// Initializes wildcard matcher and deduplicates excludes.
        /// </summary>
        protected void InitializeWildcardMatcher()
        {
 
            // deduplicate excludes if present
            if (Exclude != null && Exclude.Length > 0)
            {
                Exclude = Exclude.Distinct().ToArray();
            }
            else
            {
 
                // set empty if none
                Exclude = Array.Empty<string>();
            }
        }
 
        /// <summary>
        /// Sets up visited nodes dictionary with appropriate comparer.
        /// </summary>
        protected void InitializeVisitedNodes()
        {
 
            // create dictionary with casing comparer
            VisitedNodes = new ConcurrentDictionary<string, bool>(
                comparer: (
                       this.CaseNameMatching == MatchCasing.PlatformDefault ?
                       (
                            // windows or linux based?
                            Environment.OSVersion.Platform == PlatformID.Win32NT ?
                            StringComparer.OrdinalIgnoreCase :
                            StringComparer.Ordinal
                       ) : (
 
                            this.CaseNameMatching == MatchCasing.CaseInsensitive ?
                                StringComparer.OrdinalIgnoreCase :
                                StringComparer.Ordinal
                       )
                    )
            );
        }
 
        /// <summary>
        /// Initializes verbose output setting
        /// </summary>
        protected void InitializeVerboseOutput()
        {
 
            // attempt to set verbose
            try
            {
 
                // check verbose switch
                // Check for -Verbose switch
                bool verboseSwitch = MyInvocation.BoundParameters.ContainsKey("Verbose");
 
                // check preference if no switch
                // Fall back to VerbosePreference if no switch
                if (!verboseSwitch)
                {
 
                    // get preference value
                    var verbosePref = InvokeScript<string>("Write-Output ($VerbosePreference)");
 
                    // evaluate preference
                    verboseSwitch = !string.IsNullOrEmpty(verbosePref) &&
                                   !verbosePref.Equals("SilentlyContinue", StringComparison.OrdinalIgnoreCase);
                }
 
                // set flag
                UseVerboseOutput = verboseSwitch;
            }
            catch
            {
 
                // default to false on error
                UseVerboseOutput = false;
            }
        }
 
        /// <summary>
        /// Prepares search directory from mask.
        /// </summary>
        /// <param name="name">The search mask.</param>
        protected void InitializeSearchDirectory(string name)
        {
            // normalize separators
            // Normalize separators to backslashes for consistency
            name = name.Replace("/", "\\");
 
            // normalize path
            // Normalize the path part for processing
            var normPath = NormalizePathForNonFileSystemUse(name);
 
            // adjust trailing recurse
            // Adjust for trailing recursive patterns
            if (RecurseEndPatternWithSlashAtStartMatcher.IsMatch(normPath))
            {
                normPath += "\\";
            }
 
            // determine path types
            // Determine path type for correct handling
            bool isRooted = Path.IsPathRooted(normPath);
            bool isUncPath = normPath.StartsWith(@"\\");
 
            bool isRelative = !isRooted && !isUncPath && !normPath.StartsWith("~");
 
            // add it
            if (UseVerboseOutput) { VerboseQueue.Enqueue($"Adding name: '{name}'"); }
            AddToSearchQueue(name);
 
            // add more roots?
            if (isRelative && Root != null && Root.Length > 0)
            {
                foreach (string path in Root)
                {
                    if (UseVerboseOutput) { VerboseQueue.Enqueue($"Adding for path '{path}' with name: '{name}'"); }
                    AddToSearchQueue(Path.Combine(path, name));
                }
            }
 
            // Process search for each specified drive
            foreach (var root in GetRootsToSearch())
            {
                if (UseVerboseOutput) { VerboseQueue.Enqueue($"Adding for root '{root}' with name: '{name}'"); }
                AddToSearchQueue(Path.Combine(root, name));
            }
        }
    }
}