src/InstallHelper.cs

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using static System.Environment;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.PowerShell.PowerShellGet.RepositorySettings;
using MoreLinq.Extensions;
using Newtonsoft.Json;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Packaging.Core;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
 
namespace Microsoft.PowerShell.PowerShellGet.Cmdlets
{
    /// <summary>
    /// Find helper class
    /// </summary>
    class InstallHelper : PSCmdlet
    {
        private CancellationToken cancellationToken;
        private readonly bool update;
        private readonly PSCmdlet cmdletPassedIn;
 
        // This will be a list of all the repository caches
        public static readonly List<string> RepoCacheFileName = new List<string>();
        public static readonly string RepositoryCacheDir = Path.Combine(Environment.GetFolderPath(SpecialFolder.LocalApplicationData), "PowerShellGet", "RepositoryCache");
        public static readonly string osPlatform = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
        private string programFilesPath;
        private string myDocumentsPath;
        private string psPath;
        private string psModulesPath;
        private string psScriptsPath;
        private string psInstalledScriptsInfoPath;
        private List<string> psModulesPathAllDirs;
        private List<string> psScriptsPathAllDirs;
        private List<string> pkgsLeftToInstall;
 
 
        public InstallHelper(bool update, CancellationToken cancellationToken, PSCmdlet cmdletPassedIn)
        {
            this.update = update;
            this.cancellationToken = cancellationToken;
            this.cmdletPassedIn = cmdletPassedIn;
        }
 
        public void ProcessInstallParams(string[] _name, string _version, bool _prerelease, string[] _repository, string _scope, bool _acceptLicense, bool _quiet, bool _reinstall, bool _force, bool _trustRepository, bool _noClobber, PSCredential _credential, string _requiredResourceFile, string _requiredResourceJson, Hashtable _requiredResourceHash)
        {
            cmdletPassedIn.WriteDebug(string.Format("Parameters passed in >>> Name: '{0}'; Version: '{1}'; Prerelease: '{2}'; Repository: '{3}'; Scope: '{4}'; AcceptLicense: '{5}'; Quiet: '{6}'; Reinstall: '{7}'; TrustRepository: '{8}'; NoClobber: '{9}';", string.Join(",", _name), _version != null ? _version:string.Empty, _prerelease.ToString(), _repository != null ? string.Join(",", _repository):string.Empty, _scope != null ? _scope:string.Empty, _acceptLicense.ToString(), _quiet.ToString(), _reinstall.ToString(), _trustRepository.ToString(), _noClobber.ToString()));
 
            var consoleIsElevated = false;
            var isWindowsPS = false;
            cmdletPassedIn.WriteDebug("Entering InstallHelper::ProcessInstallParams");
#if NET472
            // If WindowsPS
            var id = System.Security.Principal.WindowsIdentity.GetCurrent();
            consoleIsElevated = (id.Owner != id.User);
            isWindowsPS = true;
 
            myDocumentsPath = Path.Combine(Environment.GetFolderPath(SpecialFolder.MyDocuments), "WindowsPowerShell");
            programFilesPath = Path.Combine(Environment.GetFolderPath(SpecialFolder.ProgramFiles), "WindowsPowerShell");
#else
            // If PS6+ on Windows
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                var id = System.Security.Principal.WindowsIdentity.GetCurrent();
                consoleIsElevated = (id.Owner != id.User);
                 
                myDocumentsPath = Path.Combine(Environment.GetFolderPath(SpecialFolder.MyDocuments), "PowerShell");
                programFilesPath = Path.Combine(Environment.GetFolderPath(SpecialFolder.ProgramFiles), "PowerShell");
            }
            else
            {
                // Paths are the same for both Linux and MacOS
                myDocumentsPath = Path.Combine(Environment.GetFolderPath(SpecialFolder.LocalApplicationData), "powershell");
                programFilesPath = Path.Combine("/usr", "local", "share", "powershell");
 
                using (System.Management.Automation.PowerShell pwsh = System.Management.Automation.PowerShell.Create())
                {
                    var uID = pwsh.AddScript("id -u").Invoke();
                    foreach (var item in uID)
                    {
                        cmdletPassedIn.WriteDebug(string.Format("UID is: '{0}'", item));
                        consoleIsElevated = (String.Equals(item.ToString(), "0"));
                    }
                    
                }
            }
#endif
            cmdletPassedIn.WriteDebug(string.Format("Console is elevated: '{0}'", consoleIsElevated));
            cmdletPassedIn.WriteDebug(string.Format("Console is Windows PowerShell: '{0}'", isWindowsPS));
            cmdletPassedIn.WriteDebug(string.Format("Current user scope installation path: '{0}'", myDocumentsPath));
            cmdletPassedIn.WriteDebug(string.Format("All users scope installation path: '{0}'", programFilesPath));
 
            // If Scope is AllUsers and there is no console elevation
            if (!string.IsNullOrEmpty(_scope) && _scope.Equals("AllUsers", StringComparison.OrdinalIgnoreCase) && !consoleIsElevated)
            {
                // Throw an error when Install-PSResource is used as a non-admin user and '-Scope AllUsers'
                throw new System.ArgumentException(string.Format(CultureInfo.InvariantCulture, "Install-PSResource requires admin privilege for AllUsers scope."));
            }
 
            // If no scope is specified (whether or not the console is elevated) default installation will be to CurrentUser
            // If running under admin on Windows with PowerShell less than PS6, default will be AllUsers
            if (string.IsNullOrEmpty(_scope))
            {
                // If non-Windows or non-elevated default scope will be current user
                _scope = "CurrentUser";
 
                // If Windows and elevated default scope will be all users
                if (isWindowsPS && consoleIsElevated)
                {
                    _scope = "AllUsers";
                }
            }
            cmdletPassedIn.WriteVerbose(string.Format("Scope is: {0}", _scope));
 
            psPath = string.Equals(_scope, "AllUsers") ? programFilesPath : myDocumentsPath;
            psModulesPath = Path.Combine(psPath, "Modules");
            psScriptsPath = Path.Combine(psPath, "Scripts");
            psInstalledScriptsInfoPath = Path.Combine(psScriptsPath, "InstalledScriptInfos");
 
            cmdletPassedIn.WriteDebug("Checking to see if paths exist");
            cmdletPassedIn.WriteDebug(string.Format("Path: '{0}' >>> exists? '{1}'", psModulesPath, Directory.Exists(psModulesPath)));
            cmdletPassedIn.WriteDebug(string.Format("Path: '{0}' >>> exists? '{1}'", psScriptsPath, Directory.Exists(psScriptsPath)));
            cmdletPassedIn.WriteDebug(string.Format("Path: '{0}' >>> exists? '{1}'", psInstalledScriptsInfoPath, Directory.Exists(psInstalledScriptsInfoPath)));
 
 
            // Create PowerShell modules and scripts paths if they don't already exist
            if (!Directory.Exists(psModulesPath))
            {
                cmdletPassedIn.WriteVerbose(string.Format("Newly created PowerShell modules path is: {0}", psModulesPath));
                Directory.CreateDirectory(psModulesPath);
            }
            if (!Directory.Exists(psScriptsPath))
            {
                cmdletPassedIn.WriteVerbose(string.Format("Newly created PowerShell scripts path is: {0}", psScriptsPath));
                Directory.CreateDirectory(psScriptsPath);
            }
            if (!Directory.Exists(psInstalledScriptsInfoPath))
            {
                cmdletPassedIn.WriteVerbose(string.Format("Newly created PowerShell installed scripts info path is: {0}", psInstalledScriptsInfoPath));
                Directory.CreateDirectory(psInstalledScriptsInfoPath);
            }
 
            psModulesPathAllDirs = (Directory.GetDirectories(psModulesPath)).ToList();
            // Get the script metadata XML files from the 'InstalledScriptInfos' directory
            psScriptsPathAllDirs = (Directory.GetFiles(psInstalledScriptsInfoPath)).ToList();
 
            Dictionary<string, PkgParams> pkgsinJson = new Dictionary<string, PkgParams>();
            Dictionary<string, string> jsonPkgsNameVersion = new Dictionary<string, string>();
 
            if (_requiredResourceFile != null)
            {
                var resolvedReqResourceFile = SessionState.Path.GetResolvedPSPathFromPSPath(_requiredResourceFile).FirstOrDefault().Path;
                cmdletPassedIn.WriteDebug(String.Format("Resolved required resource file path is: '{0}'", resolvedReqResourceFile));
 
                if (!File.Exists(resolvedReqResourceFile))
                {
                    var exMessage = String.Format("The RequiredResourceFile does not exist. Please try specifying a path to a valid .json or .psd1 file");
                    var ex = new ArgumentException(exMessage);
                    var RequiredResourceFileDoesNotExist = new ErrorRecord(ex, "RequiredResourceFileDoesNotExist", ErrorCategory.ObjectNotFound, null);
 
                    cmdletPassedIn.ThrowTerminatingError(RequiredResourceFileDoesNotExist);
                }
 
                if (resolvedReqResourceFile.EndsWith(".psd1"))
                {
                    // TODO: implement after implementing publish
                    throw new Exception("This feature is not yet implemented");
                    return;
                }
                else if (resolvedReqResourceFile.EndsWith(".json"))
                {
                    // If json file
                    using (StreamReader sr = new StreamReader(resolvedReqResourceFile))
                    {
                        _requiredResourceJson = sr.ReadToEnd();
                    }
 
                    try
                    {
                        pkgsinJson = JsonConvert.DeserializeObject<Dictionary<string, PkgParams>>(_requiredResourceJson, new JsonSerializerSettings { MaxDepth = 6 });
                    }
                    catch (Exception e)
                    {
                        var exMessage = String.Format("Argument for parameter -RequiredResource is not in proper json format. Make sure argument is either a hashtable or a json object.");
                        var ex = new ArgumentException(exMessage);
                        var RequiredResourceNotInProperJsonFormat = new ErrorRecord(ex, "RequiredResourceNotInProperJsonFormat", ErrorCategory.ObjectNotFound, null);
 
                        cmdletPassedIn.ThrowTerminatingError(RequiredResourceNotInProperJsonFormat);
                    }
                }
                else
                {
                   // throw new Exception("The RequiredResourceFile does not have the proper file extension. Please try specifying a path to a valid .json or .psd1 file");
                    var exMessage = String.Format("The RequiredResourceFile does not have the proper file extension. Please try specifying a path to a valid .json or .psd1 file");
                    var ex = new ArgumentException(exMessage);
                    var RequiredResourceFileExtensionError = new ErrorRecord(ex, "RequiredResourceFileExtensionError", ErrorCategory.ObjectNotFound, null);
 
                    cmdletPassedIn.ThrowTerminatingError(RequiredResourceFileExtensionError);
                }
            }
 
            if (_requiredResourceHash != null)
            {
                string jsonString = "";
                try
                {
                    jsonString = _requiredResourceHash.ToJson();
                }
                catch (Exception e)
                {
                    var exMessage = String.Format("Argument for parameter -RequiredResource is not in proper json format. Make sure argument is either a hashtable or a json object.");
                    var ex = new ArgumentException(exMessage);
                    var RequiredResourceNotInProperJsonFormat = new ErrorRecord(ex, "RequiredResourceNotInProperJsonFormat", ErrorCategory.ObjectNotFound, null);
 
                    cmdletPassedIn.ThrowTerminatingError(RequiredResourceNotInProperJsonFormat);
                }
 
                PkgParams pkg = null;
                try
                {
                    pkg = JsonConvert.DeserializeObject<PkgParams>(jsonString, new JsonSerializerSettings { MaxDepth = 6 });
                }
                catch (Exception e)
                {
                    var exMessage = String.Format("Argument for parameter -RequiredResource is not in proper json format. Make sure argument is either a hashtable or a json object.");
                    var ex = new ArgumentException(exMessage);
                    var RequiredResourceNotInProperJsonFormat = new ErrorRecord(ex, "RequiredResourceNotInProperJsonFormat", ErrorCategory.ObjectNotFound, null);
 
                    cmdletPassedIn.ThrowTerminatingError(RequiredResourceNotInProperJsonFormat);
                }
 
                ProcessRepositories(new string[] { pkg.Name }, pkg.Version, pkg.Prerelease, new string[] { pkg.Repository }, pkg.Scope, pkg.AcceptLicense, pkg.Quiet, pkg.Reinstall, pkg.Force, pkg.TrustRepository, pkg.NoClobber, pkg.Credential);
 
                return;
            }
 
            if (_requiredResourceJson != null)
            {
                if (!pkgsinJson.Any())
                {
                    try
                    {
                        pkgsinJson = JsonConvert.DeserializeObject<Dictionary<string, PkgParams>>(_requiredResourceJson, new JsonSerializerSettings { MaxDepth = 6 });
                    }
                    catch (Exception e)
                    {
                        try
                        {
                            jsonPkgsNameVersion = JsonConvert.DeserializeObject<Dictionary<string, string>>(_requiredResourceJson, new JsonSerializerSettings { MaxDepth = 6 });
                        }
                        catch (Exception e2)
                        {
                            var exMessage = String.Format("Argument for parameter -RequiredResource is not in proper json format. Make sure argument is either a hashtable or a json object.");
                            var ex = new ArgumentException(exMessage);
                            var RequiredResourceNotInProperJsonFormat = new ErrorRecord(ex, "RequiredResourceNotInProperJsonFormat", ErrorCategory.ObjectNotFound, null);
 
                            cmdletPassedIn.ThrowTerminatingError(RequiredResourceNotInProperJsonFormat);
                        }
                    }
                }
 
                foreach (var pkg in jsonPkgsNameVersion)
                {
                    ProcessRepositories(new string[] { pkg.Key }, pkg.Value, false, null, null, false, false, false, false, false, false, null);
                }
 
                foreach (var pkg in pkgsinJson)
                {
                    ProcessRepositories(new string[] { pkg.Key }, pkg.Value.Version, pkg.Value.Prerelease, new string[] { pkg.Value.Repository }, pkg.Value.Scope, pkg.Value.AcceptLicense, pkg.Value.Quiet, pkg.Value.Reinstall, pkg.Value.Force, pkg.Value.TrustRepository, pkg.Value.NoClobber, pkg.Value.Credential);
                }
                return;
            }
 
            ProcessRepositories(_name, _version, _prerelease, _repository, _scope, _acceptLicense, _quiet, _reinstall, _force, _trustRepository, _noClobber, _credential);
        }
 
        public void ProcessRepositories(string[] packageNames, string version, bool prerelease, string[] repository, string scope, bool acceptLicense, bool quiet, bool reinstall, bool force, bool trustRepository, bool noClobber, PSCredential credential)
        {
            var r = new RespositorySettings();
            var listOfRepositories = r.Read(repository);
 
            pkgsLeftToInstall = packageNames.ToList();
 
            var yesToAll = false;
            var noToAll = false;
            var repositoryIsNotTrusted = "Untrusted repository";
            var queryInstallUntrustedPackage = "You are installing the modules from an untrusted repository. If you trust this repository, change its Trusted value by running the Set-PSResourceRepository cmdlet. Are you sure you want to install the PSresource from '{0}' ?";
 
            foreach (var repo in listOfRepositories)
            {
                var sourceTrusted = false;
 
                string repoName = repo.Properties["Name"].Value.ToString();
 
                cmdletPassedIn.WriteDebug(string.Format("Attempting to search for packages in '{0}'", repoName));
                if (string.Equals(repo.Properties["Trusted"].Value.ToString(), "false", StringComparison.InvariantCultureIgnoreCase) && !trustRepository && !force)
                {
                    cmdletPassedIn.WriteDebug("Checking if untrusted repository should be used");
 
                    if (!(yesToAll || noToAll))
                    {
                        // Prompt for installation of package from untrusted repository
                        var message = string.Format(CultureInfo.InvariantCulture, queryInstallUntrustedPackage, repoName);
                        sourceTrusted = cmdletPassedIn.ShouldContinue(message, repositoryIsNotTrusted, true, ref yesToAll, ref noToAll);
                    }
                }
                else
                {
                    sourceTrusted = true;
                }
 
                if (sourceTrusted || yesToAll)
                {
                    cmdletPassedIn.WriteDebug("Untrusted repository accepted as trusted source.");
                    // Try to install-- returns any pkgs that weren't found
                    // If it can't find the pkg in one repository, it'll look for it in the next repo in the list
                    var returnedPkgsNotInstalled = InstallPkgs(repoName, repo.Properties["Url"].Value.ToString(), pkgsLeftToInstall, packageNames, version, prerelease, scope, acceptLicense, quiet, reinstall, force, trustRepository, noClobber, credential);
                    if (!pkgsLeftToInstall.Any())
                    {
                        return;
                    }
                    pkgsLeftToInstall = returnedPkgsNotInstalled;
                }
            }
        }
 
        // Installing a package will have a transactional behavior:
        // Package and its dependencies will be saved into a tmp folder
        // and will only be properly installed if all dependencies are found successfully.
        // Once package is installed, we want to resolve and install all dependencies.
        public List<string> InstallPkgs(string repositoryName, string repositoryUrl, List<string> pkgsLeftToInstall, string[] packageNames, string version, bool prerelease, string scope, bool acceptLicense, bool quiet, bool reinstall, bool force, bool trustRepository, bool noClobber, PSCredential credential)
        {
            PackageSource source = new PackageSource(repositoryUrl);
 
            if (credential != null)
            {
                string password = new NetworkCredential(string.Empty, credential.Password).Password;
                source.Credentials = PackageSourceCredential.FromUserInput(repositoryUrl, credential.UserName, password, true, null);
            }
 
            var provider = FactoryExtensionsV3.GetCoreV3(NuGet.Protocol.Core.Types.Repository.Provider);
            SourceRepository repository = new SourceRepository(source, provider);
            SearchFilter filter = new SearchFilter(prerelease);
            SourceCacheContext context = new SourceCacheContext();
 
            // TODO: proper error handling here
            PackageMetadataResource resourceMetadata = null;
            try
            {
                resourceMetadata = repository.GetResourceAsync<PackageMetadataResource>().GetAwaiter().GetResult();
            }
            catch
            {
            }
            if (resourceMetadata == null)
            {
              cmdletPassedIn.WriteVerbose(string.Format("Error retreiving resource repository from '{0}'. Try running command again with -Credential", repositoryName));
 
                return pkgsLeftToInstall;
            }
 
            foreach (var pkgName in packageNames)
            {
                IPackageSearchMetadata filteredFoundPkgs = null;
 
                VersionRange versionRange = null;
                if (version == null)
                {
                    // ensure that the latst version is returned first (the ordering of versions differ
                    // TODO: proper error handling
                    try
                    {
                        filteredFoundPkgs = (resourceMetadata.GetMetadataAsync(pkgName, prerelease, false, context, NullLogger.Instance, cancellationToken).GetAwaiter().GetResult()
                            .OrderByDescending(p => p.Identity.Version, VersionComparer.VersionRelease)
                            .FirstOrDefault());
 
                            // Check if exact version
                            NuGetVersion nugetVersion;
                            NuGetVersion.TryParse(filteredFoundPkgs.Identity.Version.ToString(), out nugetVersion);
 
                            if (nugetVersion != null)
                            {
                                versionRange = new VersionRange(nugetVersion, true, nugetVersion, true, null, null);
                            }
                    }
                    catch
                    {
                    }
 
                    if (filteredFoundPkgs == null)
                    {
                        cmdletPassedIn.WriteVerbose(String.Format("Could not find package '{0}' in repository '{1}'", pkgName, repositoryName));
 
                        return pkgsLeftToInstall;
                    }
                }
                else
                {
                    // Check if exact version
                    NuGetVersion nugetVersion;
                    NuGetVersion.TryParse(version, out nugetVersion);
 
                    if (nugetVersion != null)
                    {
                        // Exact version
                        versionRange = new VersionRange(nugetVersion, true, nugetVersion, true, null, null);
                    }
                    else
                    {
                        // Check if version range
                        versionRange = VersionRange.Parse(version);
                    }
                    cmdletPassedIn.WriteDebug(string.Format("Parsed version is: '{0}'", versionRange.ToString()));
 
                    // Search for packages within a version range
                    // ensure that the latest version is returned first (the ordering of versions differ
                    filteredFoundPkgs = (resourceMetadata.GetMetadataAsync(pkgName, prerelease, false, context, NullLogger.Instance, cancellationToken).GetAwaiter().GetResult()
                        .Where(p => versionRange.Satisfies(p.Identity.Version))
                        .OrderByDescending(p => p.Identity.Version, VersionComparer.VersionRelease)
                        .FirstOrDefault());
                }
 
                List<IPackageSearchMetadata> foundDependencies = new List<IPackageSearchMetadata>();
 
                // Found package to install, now search for dependencies
                if (filteredFoundPkgs != null)
                {
                    // TODO: improve dependency search
                    // This function recursively finds all dependencies
                    foundDependencies.AddRange(FindDependenciesFromSource(filteredFoundPkgs, resourceMetadata, context, prerelease, reinstall));
                }
 
                // Check which pkgs actually need to be installed
                List<IPackageSearchMetadata> pkgsToInstall = new List<IPackageSearchMetadata>();
                pkgsToInstall.Add(filteredFoundPkgs);
                pkgsToInstall.AddRange(foundDependencies);
 
                // We have a list of everything that needs to be installed
                // Check the system to see if that particular package AND package version is there
                // If it is, remove it from the list of pkgs to install
                if (versionRange != null)
                {
                    foreach (var name in packageNames)
                    {
                        var pkgDirName = Path.Combine(psModulesPath, name);
                        var pkgDirNameScript = Path.Combine(psInstalledScriptsInfoPath, name + "_InstalledScriptInfo.xml");
 
                        // Check to see if the package dir exists in the Modules path or if the script metadata file exists in the Scripts path
                        if (psModulesPathAllDirs.Contains(pkgDirName, StringComparer.OrdinalIgnoreCase)
                            || psScriptsPathAllDirs.Contains(pkgDirNameScript, StringComparer.OrdinalIgnoreCase))
                        {
                            // Then check to see if the package version exists in the Modules path
                            var pkgDirVersion = new List<string>();
                            try
                            {
                                pkgDirVersion = (Directory.GetDirectories(pkgDirName)).ToList();
                            }
                            catch { }
 
                            List<string> pkgVersion = new List<string>();
                            foreach (var path in pkgDirVersion)
                            {
                                pkgVersion.Add(Path.GetFileName(path));
                            }
 
                            // Remove any pkg versions that are not formatted correctly, eg: 2.2.4.1x
                            String[] pkgVersions = pkgVersion.ToArray();
 
                            foreach (var installedPkgVer in pkgVersions)
                            {
                                if (!NuGetVersion.TryParse(installedPkgVer, out NuGetVersion pkgVer))
                                {
                                    pkgVersion.Remove(installedPkgVer);
                                }
                            }
 
                            // These are all the packages already installed
                            var pkgsAlreadyInstalled = pkgVersion.FindAll(p => versionRange.Satisfies(NuGetVersion.Parse(p)));
 
                            if (pkgsAlreadyInstalled.Any() && !reinstall)
                            {
                                // Remove the pkg from the list of pkgs that need to be installed
                                var pkgsToRemove = pkgsToInstall.Find(p => string.Equals(p.Identity.Id, name, StringComparison.CurrentCultureIgnoreCase));
 
                                pkgsToInstall.Remove(pkgsToRemove);
                                pkgsLeftToInstall.Remove(name);
                            }
                        }
                        else if (update)
                        {
                            // Not installed throw terminating error
                            var exMessage = String.Format("Module '{0}' was not updated because no valid module was found in the module directory.Verify that the module is located in the folder specified by $env: PSModulePath.", name);
                            var ex = new ArgumentException(exMessage); // System.ArgumentException vs PSArgumentException
                            var moduleNotInstalledError = new ErrorRecord(ex, "ModuleNotInstalled", ErrorCategory.ObjectNotFound, null);
 
                            cmdletPassedIn.ThrowTerminatingError(moduleNotInstalledError);
                        }
                    }
                }
                else
                {
                    foreach (var name in packageNames)
                    {
                        // case sensitivity issues here!
                        var dirName = Path.Combine(psModulesPath, name);
                        // Example script metadata file name format: 'install-kubectl_InstalledScriptInfo.xml'
                        var dirNameScript = Path.Combine(psInstalledScriptsInfoPath, name + "_InstalledScriptInfo.xml");
 
                        // Check to see if the package dir exists in the path
                        if (psModulesPathAllDirs.Contains(dirName, StringComparer.OrdinalIgnoreCase)
                             || psScriptsPathAllDirs.Contains(dirNameScript, StringComparer.OrdinalIgnoreCase))
                        {
                            // then check to see if the package/script exists in the path
                            if ((Directory.Exists(dirName) || Directory.Exists(dirNameScript)) && !reinstall && !update)
                            {
                                // Remove the pkg from the list of pkgs that need to be installed
                                //case sensitivity here
                                var pkgsToRemove = pkgsToInstall.Find(p => string.Equals(p.Identity.Id, name, StringComparison.CurrentCultureIgnoreCase));
 
                                pkgsToInstall.Remove(pkgsToRemove);
                                pkgsLeftToInstall.Remove(name);
                            }
                            // if update, check to see if that particular version is in the path..
                            // check script version matching too
                        }
                        else if (update)
                        {
                            // Throw module or script not installed terminating error
                            var exMessage = String.Format("Module '{0}' was not updated because no valid module was found in the module directory.Verify that the module is located in the folder specified by $env: PSModulePath.", name);
                            var ex = new ArgumentException(exMessage);
                            var moduleNotInstalledError = new ErrorRecord(ex, "ModuleNotInstalled", ErrorCategory.ObjectNotFound, null);
 
                            cmdletPassedIn.ThrowTerminatingError(moduleNotInstalledError);
                        }
                    }
                }
 
                // Remove any null pkgs
                pkgsToInstall.Remove(null);
 
                var tempInstallPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
                var dir = Directory.CreateDirectory(tempInstallPath); // should check it gets created properly
                                                                       // To delete file attributes from the existing ones get the current file attributes first and use AND (&) operator
                                                                       // with a mask (bitwise complement of desired attributes combination).
                dir.Attributes = dir.Attributes & ~FileAttributes.ReadOnly;
 
                // Install everything to a temp path
                foreach (var p in pkgsToInstall)
                {
                    cmdletPassedIn.WriteVerbose(string.Format("Begin installing package: '{0}'", p.Identity.Id));
 
                    if (!quiet)
                    {
                        int i = 1;
                        int j = 1;
                        /****************************
                        * START PACKAGE INSTALLATION -- start progress bar
                        *****************************/
                        // Write-Progress -Activity "Search in Progress" - Status "$i% Complete:" - PercentComplete $i
 
                        int activityId = 0;
                        string activity = "";
                        string statusDescription = "";
 
                        if (packageNames.ToList().Contains(p.Identity.Id))
                        {
                            // If the pkg exists in one of the names passed in, then we wont include it as a dependent package
                            activityId = 0;
                            activity = string.Format("Installing {0}...", p.Identity.Id);
                            statusDescription = string.Format("{0}% Complete:", i++);
 
                            j = 1;
                        }
                        else
                        {
                            // Child process
                            // Installing dependent package
                            activityId = 1;
                            activity = string.Format("Installing dependent package {0}...", p.Identity.Id);
                            statusDescription = string.Format("{0}% Complete:", j);
                        }
 
                        var progressRecord = new ProgressRecord(activityId, activity, statusDescription);
                        cmdletPassedIn.WriteProgress(progressRecord);
                    }
 
                    var pkgIdentity = new PackageIdentity(p.Identity.Id, p.Identity.Version);
 
                    var resource = new DownloadResourceV2FeedProvider();
                    var cacheContext = new SourceCacheContext();
                    var downloadResource = repository.GetResourceAsync<DownloadResource>().GetAwaiter().GetResult();
 
                    var result = downloadResource.GetDownloadResourceResultAsync(
                        pkgIdentity,
                        new PackageDownloadContext(cacheContext),
                        tempInstallPath,
                        logger: NullLogger.Instance,
                        CancellationToken.None).GetAwaiter().GetResult();
 
                    cmdletPassedIn.WriteDebug(string.Format("Successfully able to download package from source to: '{0}'", tempInstallPath));
 
                    // Need to close the .nupkg
                    result.Dispose();
 
 
                    // ACCEPT LICENSE
                    // Prompt if module requires license acceptance
                    // Need to read from .psd1
                    var newVersion = p.Identity.Version.ToString();
                    if (p.Identity.Version.IsPrerelease)
                    {
                        newVersion = p.Identity.Version.ToString().Substring(0, p.Identity.Version.ToString().IndexOf('-'));
                    }
 
                    var modulePath = Path.Combine(tempInstallPath, pkgIdentity.Id, newVersion);
                    var moduleManifest = Path.Combine(modulePath, pkgIdentity.Id + ".psd1");
                    var requireLicenseAcceptance = false;
 
                    if (File.Exists(moduleManifest))
                    {
                        using (StreamReader sr = new StreamReader(moduleManifest))
                        {
                            var text = sr.ReadToEnd();
 
                            var pattern = "RequireLicenseAcceptance\\s*=\\s*\\$true";
                            var patternToSkip1 = "#\\s*RequireLicenseAcceptance\\s*=\\s*\\$true";
                            var patternToSkip2 = "\\*\\s*RequireLicenseAcceptance\\s*=\\s*\\$true";
 
                            Regex rgx = new Regex(pattern);
 
                            if (rgx.IsMatch(pattern) && !rgx.IsMatch(patternToSkip1) && !rgx.IsMatch(patternToSkip2))
                            {
                                requireLicenseAcceptance = true;
                            }
                        }
 
                        if (requireLicenseAcceptance)
                        {
                            // If module requires license acceptance and -AcceptLicense is not passed in, display prompt
                            if (!acceptLicense)
                            {
                                var PkgTempInstallPath = Path.Combine(tempInstallPath, p.Identity.Id, newVersion);
                                var LicenseFilePath = Path.Combine(PkgTempInstallPath, "License.txt");
 
                                if (!File.Exists(LicenseFilePath))
                                {
                                    var exMessage = "License.txt not Found. License.txt must be provided when user license acceptance is required.";
                                    var ex = new ArgumentException(exMessage); // System.ArgumentException vs PSArgumentException
                                    var acceptLicenseError = new ErrorRecord(ex, "LicenseTxtNotFound", ErrorCategory.ObjectNotFound, null);
 
                                    cmdletPassedIn.ThrowTerminatingError(acceptLicenseError);
                                }
 
                                // Otherwise read LicenseFile
                                string licenseText = System.IO.File.ReadAllText(LicenseFilePath);
                                var acceptanceLicenseQuery = $"Do you accept the license terms for module '{p.Identity.Id}'.";
                                var message = licenseText + "`r`n" + acceptanceLicenseQuery;
 
                                var title = "License Acceptance";
                                var yesToAll = false;
                                var noToAll = false;
                                var shouldContinueResult = ShouldContinue(message, title, true, ref yesToAll, ref noToAll);
 
                                if (yesToAll)
                                {
                                    acceptLicense = true;
                                }
                            }
 
                            // Check if user agreed to license terms, if they didn't then throw error, otherwise continue to install
                            if (!acceptLicense)
                            {
                                var message = $"License Acceptance is required for module '{p.Identity.Id}'. Please specify '-AcceptLicense' to perform this operation.";
                                var ex = new ArgumentException(message); // System.ArgumentException vs PSArgumentException
                                var acceptLicenseError = new ErrorRecord(ex, "ForceAcceptLicense", ErrorCategory.InvalidArgument, null);
 
                                cmdletPassedIn.ThrowTerminatingError(acceptLicenseError);
                            }
                        }
                    }
 
                    var dirNameVersion = Path.Combine(tempInstallPath, p.Identity.Id.ToLower(), p.Identity.Version.ToNormalizedString().ToLower());
                    var nupkgSHAToDelete = Path.Combine(dirNameVersion, (p.Identity.ToString() + ".nupkg.sha512").ToLower());
                    var nuspecToDelete = Path.Combine(dirNameVersion, (p.Identity.Id + ".nuspec").ToLower());
                    var nupkgToDelete = Path.Combine(dirNameVersion, (p.Identity.ToString() + ".nupkg").ToLower());
 
                    cmdletPassedIn.WriteDebug(string.Format("Deleting '{0}'", nupkgSHAToDelete));
                    File.Delete(nupkgSHAToDelete);
                    cmdletPassedIn.WriteDebug(string.Format("Deleting '{0}'", nuspecToDelete));
                    File.Delete(nuspecToDelete);
                    cmdletPassedIn.WriteDebug(string.Format("Deleting '{0}'", nupkgToDelete));
                    File.Delete(nupkgToDelete);
 
                    var scriptPath = Path.Combine(dirNameVersion, (p.Identity.Id.ToString() + ".ps1"));
                    var isScript = File.Exists(scriptPath) ? true : false;
 
                    if (!Directory.Exists(dirNameVersion))
                    {
                        cmdletPassedIn.WriteDebug(string.Format("Directory does not exist, creating directory: '{0}'", dirNameVersion));
                        Directory.CreateDirectory(dirNameVersion);
                    }
 
                    // Create PSGetModuleInfo.xml
                    var fullinstallPath = isScript ? Path.Combine(dirNameVersion, (p.Identity.Id + "_InstalledScriptInfo.xml"))
                        : Path.Combine(dirNameVersion, "PSGetModuleInfo.xml");
 
                    // Create XMLs
                    using (StreamWriter sw = new StreamWriter(fullinstallPath))
                    {
                        var tags = p.Tags.Split(' ');
 
                        var module = tags.Contains("PSModule") ? "Module" : null;
                        var script = tags.Contains("PSScript") ? "Script" : null;
 
                        List<string> includesDscResource = new List<string>();
                        List<string> includesCommand = new List<string>();
                        List<string> includesFunction = new List<string>();
                        List<string> includesRoleCapability = new List<string>();
                        List<string> filteredTags = new List<string>();
 
                        var psDscResource = "PSDscResource_";
                        var psCommand = "PSCommand_";
                        var psFunction = "PSFunction_";
                        var psRoleCapability = "PSRoleCapability_";
 
                        foreach (var tag in tags)
                        {
                            if (tag.StartsWith(psDscResource))
                            {
                                includesDscResource.Add(tag.Remove(0, psDscResource.Length));
                            }
                            else if (tag.StartsWith(psCommand))
                            {
                                includesCommand.Add(tag.Remove(0, psCommand.Length));
                            }
                            else if (tag.StartsWith(psFunction))
                            {
                                includesFunction.Add(tag.Remove(0, psFunction.Length));
                            }
                            else if (tag.StartsWith(psRoleCapability))
                            {
                                includesRoleCapability.Add(tag.Remove(0, psRoleCapability.Length));
                            }
                            else if (!tag.StartsWith("PSWorkflow_") && !tag.StartsWith("PSCmdlet_") && !tag.StartsWith("PSIncludes_")
                                && !tag.Equals("PSModule") && !tag.Equals("PSScript"))
                            {
                                filteredTags.Add(tag);
                            }
                        }
 
                        // If NoClobber is specified, ensure command clobbering does not happen
                        if (noClobber)
                        {
                            // This is a primitive implementation
                            // 1) get all possible paths
                            // 2) search all modules and compare
                            /// Cannot uninstall a module if another module is dependent on it
 
                            using (System.Management.Automation.PowerShell pwsh = System.Management.Automation.PowerShell.Create())
                            {
                                // Get all modules
                                var results = pwsh.AddCommand("Get-Module").AddParameter("ListAvailable").Invoke();
 
                                // Structure of LINQ call:
                                // Results is a collection of PSModuleInfo objects that contain a property listing module commands, "ExportedCommands".
                                // ExportedCommands is collection of PSModuleInfo objects that need to be iterated through to see if any of them are the command we're trying to install
                                // If anything from the final call gets returned, there is a command clobber with this pkg.
 
                                List<IEnumerable<PSObject>> pkgsWithCommandClobber = new List<IEnumerable<PSObject>>();
                                foreach (string command in includesCommand)
                                {
                                    pkgsWithCommandClobber.Add(results.Where(pkg => ((ReadOnlyCollection<PSModuleInfo>)pkg.Properties["ExportedCommands"].Value).Where(ec => ec.Name.Equals(command, StringComparison.InvariantCultureIgnoreCase)).Any()));
                                }
                                if (pkgsWithCommandClobber.Any())
                                {
                                    var uniqueCommandNames = (pkgsWithCommandClobber.Select(cmd => cmd.ToString()).Distinct()).ToArray();
                                    string strUniqueCommandNames = string.Join(",", uniqueCommandNames);
 
                                    throw new System.ArgumentException(string.Format(CultureInfo.InvariantCulture, "Command(s) with name(s) '{0}' is already available on this system. Installing '{1}' may override the existing command. If you still want to install '{1}', remove the -NoClobber parameter.", strUniqueCommandNames, p.Identity.Id));
                                }
                            }
                        }
 
                        Dictionary<string, List<string>> includes = new Dictionary<string, List<string>> {
                            { "DscResource", includesDscResource },
                            { "Command", includesCommand },
                            { "Function", includesFunction },
                            { "RoleCapability", includesRoleCapability }
                        };
 
                        Dictionary<string, VersionRange> dependencies = new Dictionary<string, VersionRange>();
                        foreach (var depGroup in p.DependencySets)
                        {
                            PackageDependency depPkg = depGroup.Packages.FirstOrDefault();
                            dependencies.Add(depPkg.Id, depPkg.VersionRange);
                        }
 
                        var psGetModuleInfoObj = new PSObject();
                        // TODO: Add release notes
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Name", p.Identity.Id));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Version", p.Identity.Version));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Type", module != null ? module : (script != null ? script : null)));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Description", p.Description));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Author", p.Authors));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("CompanyName", p.Owners));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("PublishedDate", p.Published));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("InstalledDate", System.DateTime.Now));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("LicenseUri", p.LicenseUrl));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("ProjectUri", p.ProjectUrl));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("IconUri", p.IconUrl));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Includes", includes.ToList()));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("PowerShellGetFormatVersion", "3"));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Dependencies", dependencies.ToList()));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("RepositorySourceLocation", repositoryUrl));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("Repository", repositoryUrl));
                        psGetModuleInfoObj.Members.Add(new PSNoteProperty("InstalledLocation", null));
 
                        psGetModuleInfoObj.TypeNames.Add("Microsoft.PowerShell.Commands.PSRepositoryItemInfo");
 
                        var serializedObj = PSSerializer.Serialize(psGetModuleInfoObj);
 
                        sw.Write(serializedObj);
                    }
 
                    // Copy to proper path
                    var installPath = isScript ? psScriptsPath : psModulesPath;
 
                    var newPath = isScript ? installPath
                        : Path.Combine(installPath, p.Identity.Id.ToString());
                    cmdletPassedIn.WriteDebug(string.Format("Installation path is: '{0}'", newPath));
 
                    // If script, just move the files over, if module, move the version directory over
                    var tempModuleVersionDir = isScript ? dirNameVersion //Path.Combine(tempInstallPath, p.Identity.Id, p.Identity.Version.ToNormalizedString())
                        : Path.Combine(tempInstallPath, p.Identity.Id.ToLower());
                     cmdletPassedIn.WriteVerbose(string.Format("Full installation path is: '{0}'", tempModuleVersionDir));
 
                    if (isScript)
                    {
 
                        var scriptXML = p.Identity.Id + "_InstalledScriptInfo.xml";
                        cmdletPassedIn.WriteDebug(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(psScriptsPath, "InstalledScriptInfos", scriptXML))));
                        if (File.Exists(Path.Combine(psScriptsPath, "InstalledScriptInfos", scriptXML)))
                        {
                            cmdletPassedIn.WriteDebug(string.Format("Deleting script metadata XML"));
                            File.Delete(Path.Combine(psScriptsPath, "InstalledScriptInfos", scriptXML));
                        }
 
                        cmdletPassedIn.WriteDebug(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(newPath, p.Identity.Id + ".ps1"))));
                        if (File.Exists(Path.Combine(newPath, p.Identity.Id + ".ps1")))
                        {
                            cmdletPassedIn.WriteDebug(string.Format("Deleting script file"));
                            File.Delete(Path.Combine(newPath, p.Identity.Id + ".ps1"));
                        }
 
 
                        cmdletPassedIn.WriteDebug(string.Format("Moving '{0}' to '{1}'", Path.Combine(dirNameVersion, scriptXML), Path.Combine(psScriptsPath, "InstalledScriptInfos", scriptXML)));
                        File.Move(Path.Combine(dirNameVersion, scriptXML), Path.Combine(psScriptsPath, "InstalledScriptInfos", scriptXML));
 
                        cmdletPassedIn.WriteDebug(string.Format("Moving '{0}' to '{1}'", scriptPath, Path.Combine(newPath, p.Identity.Id + ".ps1")));
                        File.Move(scriptPath, Path.Combine(newPath, p.Identity.Id + ".ps1"));
                    }
                    else
                    {
                        if (!Directory.Exists(newPath))
                        {
                            try
                            {
                                cmdletPassedIn.WriteDebug(string.Format("Attempting to move '{0}' to '{1}", tempModuleVersionDir, newPath));
                                Directory.Move(tempModuleVersionDir, newPath);
                            }
                            catch (Exception e)
                            {
                                throw new Exception(e.Message);
                            }
                        }
                        else
                        {
                            tempModuleVersionDir = Path.Combine(tempModuleVersionDir, p.Identity.Version.ToString());
                            cmdletPassedIn.WriteDebug(string.Format("Temporary module version directory is: '{0}'", tempModuleVersionDir));
 
                            var newVersionPath = Path.Combine(newPath, newVersion);
                            cmdletPassedIn.WriteDebug(string.Format("Path for module version directory installation is: '{0}'", newVersionPath));
 
 
                            if (Directory.Exists(newVersionPath))
                            {
                                // Delete the directory path before replacing it with the new module
                                try
                                {
                                    cmdletPassedIn.WriteDebug(string.Format("Attempting to delete '{0}'", newVersionPath));
                                    Directory.Delete(newVersionPath, true);
                                }
                                catch (Exception e)
                                {
                                    throw new Exception(e.Message);
                                }
                            }
 
                            try
                            {
                                cmdletPassedIn.WriteDebug(string.Format("Attempting to move '{0}' to '{1}", newPath, newVersion));
                                Directory.Move(tempModuleVersionDir, Path.Combine(newPath, newVersion));
                            }
                            catch (Exception e)
                            {
                                throw new Exception(e.Message);
                            }
                        }
                    }
 
                    cmdletPassedIn.WriteVerbose(String.Format("Successfully installed package {0}", p.Identity.Id));
                    
                    try
                    {
                        cmdletPassedIn.WriteDebug(string.Format("Attempting to delete '{0}'", tempInstallPath));
                        Directory.Delete(tempInstallPath, true);
                    }
                    catch (Exception e)
                    {
                        throw new Exception(e.Message);
                    }
 
                    pkgsLeftToInstall.Remove(pkgName);
                }
            }
 
            return pkgsLeftToInstall;
        }
 
 
        private List<IPackageSearchMetadata> FindDependenciesFromSource(IPackageSearchMetadata pkg, PackageMetadataResource pkgMetadataResource, SourceCacheContext srcContext, bool prerelease, bool reinstall)
        {
            // Dependency resolver
            // This function is recursively called
            // Call the findpackages from source helper (potentially generalize this so it's finding packages from source or cache)
            List<IPackageSearchMetadata> foundDependencies = new List<IPackageSearchMetadata>();
 
            // 1) Check the dependencies of this pkg
            // 2) For each dependency group, search for the appropriate name and version
            // A dependency group includes all the dependencies for a particular framework
            foreach (var dependencyGroup in pkg.DependencySets)
            {
                foreach (var pkgDependency in dependencyGroup.Packages)
                {
                    // a) Check that the appropriate pkg dependencies exist
                    // Returns all versions from a single package id.
                    var dependencies = pkgMetadataResource.GetMetadataAsync(pkgDependency.Id, prerelease, true, srcContext, NullLogger.Instance, cancellationToken).GetAwaiter().GetResult();
 
                    // b) Check if the appropriate verion range exists (if version exists, then add it to the list to return)
                    VersionRange versionRange = null;
                    try
                    {
                        versionRange = VersionRange.Parse(pkgDependency.VersionRange.OriginalString);
                    }
                    catch
                    {
                        var exMessage = String.Format("Error parsing version range");
                        var ex = new ArgumentException(exMessage);
                        var ErrorParsingVersionRange = new ErrorRecord(ex, "ErrorParsingVersionRange", ErrorCategory.ParserError, null);
 
                        cmdletPassedIn.ThrowTerminatingError(ErrorParsingVersionRange);
                    }
 
                    // If no version/version range is specified the we just return the latest version
                    IPackageSearchMetadata depPkgToReturn = (versionRange == null ?
                        dependencies.FirstOrDefault() :
                        dependencies.Where(v => versionRange.Satisfies(v.Identity.Version)).FirstOrDefault());
 
                    // If the pkg already exists on the system, don't add it to the list of pkgs that need to be installed
                    var dirName = Path.Combine(psModulesPath, pkgDependency.Id);
                    var dependencyAlreadyInstalled = false;
 
                    // Check to see if the package dir exists in the path
                    if (psModulesPathAllDirs.Contains(dirName, StringComparer.OrdinalIgnoreCase))
                    {
                        // Then check to see if the package exists in the path
                        if (Directory.Exists(dirName))
                        {
                            var pkgDirVersion = (Directory.GetDirectories(dirName)).ToList();
                            List<string> pkgVersion = new List<string>();
                            foreach (var path in pkgDirVersion)
                            {
                                pkgVersion.Add(Path.GetFileName(path));
                            }
 
                            // These are all the packages already installed
                            NuGetVersion ver;
                            var pkgsAlreadyInstalled = pkgVersion.FindAll(p => NuGetVersion.TryParse(p, out ver) && versionRange.Satisfies(ver));
 
                            if (pkgsAlreadyInstalled.Any() && !reinstall)
                            {
                                // Don't add the pkg to the list of pkgs that need to be installed
                                dependencyAlreadyInstalled = true;
                            }
                        }
                    }
 
                    if (!dependencyAlreadyInstalled)
                    {
                        foundDependencies.Add(depPkgToReturn);
                    }
 
                    // Search for any dependencies the pkg has
                    foundDependencies.AddRange(FindDependenciesFromSource(depPkgToReturn, pkgMetadataResource, srcContext, prerelease, reinstall));
                }
            }
 
            return foundDependencies;
        }
    }
}