Functions/GenXdev.FileSystem/PSGenXdevCmdlet.cs
// ################################################################################
// Part of PowerShell module : GenXdev.FileSystem // Original cmdlet filename : PSGenXdevCmdlet.cs // Original author : René Vaessen / GenXdev // Version : 1.3066.2025 // ################################################################################ // Copyright (c) René Vaessen / GenXdev // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ################################################################################ using System.Collections; using System.Collections.Concurrent; using System.Collections.ObjectModel; using System.Management.Automation; using System.Net; using Microsoft.PowerShell.Commands; using System.Text.Json; using System.Text; using System.IO; public abstract partial class PSGenXdevCmdlet : PSCmdlet { internal static readonly ConcurrentDictionary<string, CommandInfo> CommandInfoCache = new ConcurrentDictionary<string, CommandInfo>(StringComparer.OrdinalIgnoreCase); private static readonly ScriptBlock WriteJsonAtomicScript = ScriptBlock.Create(@" param( [string]$FilePath, [hashtable]$Data, [int]$MaxRetries, [int]$RetryDelayMs ) GenXdev.FileSystem\WriteJsonAtomic ` -FilePath $FilePath ` -Data $Data ` -MaxRetries $MaxRetries ` -RetryDelayMs $RetryDelayMs "); private static readonly ScriptBlock ReadJsonWithRetryScript = ScriptBlock.Create(@" param( [string]$FilePath, [int]$MaxRetries, [int]$RetryDelayMs, [switch]$AsHashtable ) GenXdev.FileSystem\ReadJsonWithRetry ` -FilePath $FilePath ` -MaxRetries $MaxRetries ` -RetryDelayMs $RetryDelayMs ` -AsHashtable:$AsHashtable "); protected Hashtable CopyIdenticalParamValues(string CmdletName) { // Get command info for the target function var functionInfo = GetCachedCommandInfo(CmdletName); if (functionInfo?.Parameters == null) { throw new ArgumentException($"Function '{CmdletName}' not found"); } var results = new Hashtable(); var defaults = CreateDefaultsHashtable(); // Convert bound parameters to dictionary var boundParamsDict = ConvertToParameterDictionary(this.MyInvocation.BoundParameters); // Process each parameter of the target function foreach (var parameterKvp in functionInfo.Parameters) { var paramName = parameterKvp.Key; var paramInfo = parameterKvp.Value; if (boundParamsDict.ContainsKey(paramName)) { var paramValue = boundParamsDict[paramName]; // Handle switch parameters if (paramInfo.ParameterType == typeof(SwitchParameter)) { if (IsTrue(paramValue)) { results[paramName] = paramValue; } } else { results[paramName] = paramValue; } } else { // Use default values if (paramInfo.ParameterType != typeof(SwitchParameter)) { var defaultValue = defaults[paramName]; if (defaultValue != null) { results[paramName] = defaultValue; } } else { var defaultValue = defaults[paramName]; if (IsTrue(defaultValue)) { results[paramName] = true; } } } } return results; } /// <summary> /// Convert object to JSON using System.Text.Json /// </summary> protected string ConvertToJson(object obj, int depth = 20) { var options = new JsonSerializerOptions { WriteIndented = true, MaxDepth = depth }; return JsonSerializer.Serialize(obj, options); } /// <summary> /// Convert JSON to object array using System.Text.Json /// </summary> protected object[] ConvertFromJson(string json) { var options = new JsonSerializerOptions { MaxDepth = 20 }; var obj = JsonSerializer.Deserialize<object>(json, options); return new object[] { obj }; } /// <summary> /// Convert JSON to typed array using System.Text.Json /// </summary> protected T[] ConvertFromJson<T>(string json) { var options = new JsonSerializerOptions { MaxDepth = 20 }; var obj = JsonSerializer.Deserialize<T>(json, options); return new T[] { obj }; } /// <summary> /// Writes data to a JSON file atomically /// </summary> protected void WriteJsonAtomic(string filePath, Hashtable data, int maxRetries = 10, int retryDelayMs = 200) { WriteJsonAtomicScript.Invoke(filePath, data, maxRetries, retryDelayMs); } /// <summary> /// Reads JSON file with retry logic /// </summary> protected object ReadJsonWithRetry(string filePath, int maxRetries = 10, int retryDelayMs = 200, bool asHashtable = false) { Collection<PSObject> results = ReadJsonWithRetryScript.Invoke( filePath, maxRetries, retryDelayMs, new SwitchParameter(asHashtable) ); if (results == null || results.Count == 0) { return asHashtable ? new Hashtable() : null; } return results[0]?.BaseObject; } /// <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, params object[] args) { // execute the PowerShell script and collect all output objects Collection<PSObject> results = InvokeCommand.InvokeScript(script, args); // check if the entire results collection is of type T // handles cases where script returns a single collection if (results is T) { return (T)(object)results; } // check if first result's base object is of type T // handles cases where script returns wrapped PSObjects if (results.Count > 0 && results[0].BaseObject is T) { return (T)results[0].BaseObject; } // return default value if no matching type found return default(T); } protected IEnumerable<T> InvokeCmdlet<T>( string Cmdlet, Hashtable parameters = null, bool includeIdenticalParamValues = false, params string[] paramsToExclude ) { StringBuilder script = new StringBuilder(); script.AppendLine("param($invocationArgs)"); script.AppendLine("function go {"); script.AppendLine(" param("); bool first = true; parameters = parameters ?? new Hashtable(); if (includeIdenticalParamValues) { var old = parameters; parameters = CopyIdenticalParamValues(Cmdlet); foreach (DictionaryEntry entry in old) { parameters[entry.Key] = entry.Value; } } // filter parameters collection if (paramsToExclude != null && paramsToExclude.Length > 0) { var filtered = new Hashtable(); foreach (DictionaryEntry entry in parameters) { if (!Array.Exists(paramsToExclude, p => p.Equals(entry.Key.ToString(), StringComparison.OrdinalIgnoreCase))) { filtered[entry.Key] = entry.Value; } } parameters = filtered; } foreach (DictionaryEntry entry in parameters) { if (!first) { script.AppendLine(", "); } script.AppendFormat("${0}", entry.Key); first = false; } script.AppendLine(") "); script.AppendFormat("{0} ", Cmdlet); first = true; foreach (DictionaryEntry entry in parameters) { if (!first) { script.Append(" "); } script.AppendFormat("-{0}:${0}", entry.Key); first = false; } script.AppendLine(" ; "); script.AppendLine("} "); script.AppendLine("go @invocationArgs ; "); var scriptBlock = ScriptBlock.Create(script.ToString()); foreach (var result in scriptBlock.Invoke(parameters)) { if (result is T obj1) { yield return obj1; } else if (result?.BaseObject is T obj2) { yield return obj2; } } } protected T InvokeCmdletSingle<T>( string Cmdlet, Hashtable parameters = null, bool includeIdenticalParamValues = false, params string[] paramsToExclude ) { foreach (var result in InvokeCmdlet<T>(Cmdlet, parameters, includeIdenticalParamValues, paramsToExclude)) { return result; } return default(T); } protected System.Collections.Generic.List<T> InvokeCmdletList<T>( string Cmdlet, Hashtable parameters = null, bool includeIdenticalParamValues = false, params string[] paramsToExclude ) { var list = new List<T>(); foreach (var result in InvokeCmdlet<T>(Cmdlet, parameters, includeIdenticalParamValues, paramsToExclude)) { list.Add(result); } return list; } #region Private protected CommandInfo GetCachedCommandInfo(string functionName) { if (CommandInfoCache.TryGetValue(functionName, out var cachedInfo)) { return cachedInfo; } var getCommandScript = $"Microsoft.PowerShell.Core\\Get-Command -Name '{functionName}' -ErrorAction SilentlyContinue"; var commandResults = InvokeCommand.InvokeScript(getCommandScript); CommandInfo commandInfo = null; if (commandResults?.Any() == true) { commandInfo = commandResults.FirstOrDefault()?.BaseObject as CommandInfo; } CommandInfoCache.TryAdd(functionName, commandInfo); return commandInfo; } protected string ExpandPath(string Path, bool CreateDirectory = false, bool CreateFile = false, bool DeleteExistingFile = false, bool FileMustExist = false, bool DirectoryMustExist = false) { // Build the Expand-Path command with conditional parameters var scriptBuilder = new System.Text.StringBuilder(); scriptBuilder.Append("param($Path) GenXdev.FileSystem\\Expand-Path -FilePath $Path"); if (CreateDirectory) { scriptBuilder.Append(" -CreateDirectory"); } if (CreateFile) { scriptBuilder.Append(" -CreateFile"); } if (DeleteExistingFile) { scriptBuilder.Append(" -DeleteExistingFile"); } if (FileMustExist) { scriptBuilder.Append(" -FileMustExist"); } if (DirectoryMustExist) { scriptBuilder.Append(" -DirectoryMustExist"); } var expandPathScript = ScriptBlock.Create(scriptBuilder.ToString()); var result = expandPathScript.Invoke(Path); if (result?.Count > 0 && result[0]?.BaseObject != null) { return result[0].BaseObject.ToString(); } return Path; } /// <summary> /// Confirms user consent for installing third-party software /// </summary> protected bool ConfirmInstallationConsent(string applicationName, string source, string description = null, string publisher = null, bool forceConsent = false, bool consentToThirdPartySoftwareInstallation = false) { var scriptBuilder = new System.Text.StringBuilder(); scriptBuilder.Append("param($ApplicationName, $Source, $Description, $Publisher, $ForceConsent, $ConsentToThirdPartySoftwareInstallation) "); scriptBuilder.Append("GenXdev.FileSystem\\Confirm-InstallationConsent "); scriptBuilder.Append("-ApplicationName $ApplicationName "); scriptBuilder.Append("-Source $Source"); if (!string.IsNullOrEmpty(description)) { scriptBuilder.Append(" -Description $Description"); } if (!string.IsNullOrEmpty(publisher)) { scriptBuilder.Append(" -Publisher $Publisher"); } if (forceConsent) { scriptBuilder.Append(" -ForceConsent"); } if (consentToThirdPartySoftwareInstallation) { scriptBuilder.Append(" -ConsentToThirdPartySoftwareInstallation"); } var confirmConsentScript = ScriptBlock.Create(scriptBuilder.ToString()); var result = confirmConsentScript.Invoke( applicationName, source, description ?? "This software is required for certain features in the GenXdev modules.", publisher ?? "Third-party vendor", forceConsent, consentToThirdPartySoftwareInstallation ); if (result?.Count > 0 && result[0]?.BaseObject != null) { return (bool)result[0].BaseObject; } return false; } protected Hashtable CreateDefaultsHashtable() { var defaultsHash = new Hashtable(); var cmdletType = this.GetType(); try { // Create a new instance to get default values var newInstance = System.Activator.CreateInstance(cmdletType); foreach (var property in cmdletType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)) { // Only consider properties that are cmdlet parameters if (property.CanRead && property.CanWrite && System.Attribute.IsDefined(property, typeof(ParameterAttribute))) { try { var value = property.GetValue(newInstance); if (value != null) { defaultsHash[property.Name] = value; } } catch { // Skip properties that can't be accessed } } } } catch { // If we can't create instance or access properties, return empty defaults } return defaultsHash; } private Dictionary<string, object> ConvertToParameterDictionary(object boundParamsObject) { var result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); if (boundParamsObject is IDictionary dict) { foreach (DictionaryEntry entry in dict) { if (entry.Key is string key) { result[key] = entry.Value; } } } else if (boundParamsObject is PSObject psObj) { foreach (var property in psObj.Properties) { result[property.Name] = property.Value; } } return result; } protected bool IsTrue(object value) { if (value == null) return false; if (value is bool boolValue) return boolValue; if (value is SwitchParameter switchParam) return switchParam.ToBool(); return false; } protected string GetGenXdevAppDataPath(string additional = null) { if (string.IsNullOrWhiteSpace(additional)) { return ExpandPath( Path.Combine( Environment.GetEnvironmentVariable("LOCALAPPDATA"), "GenXdev.PowerShell" ) + "\\", CreateDirectory: true, DeleteExistingFile: true ); } return ExpandPath( Path.Combine( Environment.GetEnvironmentVariable("LOCALAPPDATA"), "GenXdev.PowerShell", additional ) + "\\", CreateDirectory: true, DeleteExistingFile: true ); } protected string GetGenXdevModuleBase(string ModuleName) { return ExpandPath(( System.IO.Path.GetDirectoryName( InvokeScript<string>("(Get-Module '" + ModuleName + "').Path") ) + "\\"), CreateDirectory: true, DeleteExistingFile: true ); } protected string GetGenXdevModulesBase() { return ExpandPath( Path.Combine( GetGenXdevModuleBase("GenXdev.FileSystem"), "..", ".." ) + "\\", CreateDirectory: true, DeleteExistingFile: true ); } protected string GetPowerShellProfilePath() { return ExpandPath( System.IO.Path.GetDirectoryName( InvokeScript<string>("$Profile") ) + "\\", CreateDirectory: true, DeleteExistingFile: true ); } protected string GetPowerShellScriptsPath() { return ExpandPath( Path.Combine( GetPowerShellProfilePath(), "Scripts" ) + "\\", CreateDirectory: true, DeleteExistingFile: true ); } /// <summary> /// Sets a global variable in the PowerShell session /// </summary> /// <param name="name">Variable name</param> /// <param name="value">Variable value</param> protected void SetGlobalVariable(string name, object value) { var setVariableScript = ScriptBlock.Create( "param($name, $value) " + "Microsoft.PowerShell.Utility\\Set-Variable " + "-Scope Global -Name $name -Value $value"); setVariableScript.Invoke(name, value); } #endregion } |