diff --git a/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj b/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj index 39975693f..f3215195e 100644 --- a/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj +++ b/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj @@ -58,30 +58,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Properties\SolutionVersion.cs + + + + + + + diff --git a/src/Chocolatey.PowerShell/ChocolateyCmdlet.cs b/src/Chocolatey.PowerShell/ChocolateyCmdlet.cs new file mode 100644 index 000000000..d69ef569b --- /dev/null +++ b/src/Chocolatey.PowerShell/ChocolateyCmdlet.cs @@ -0,0 +1,238 @@ +// Copyright © 2017-2019 Chocolatey Software, Inc ("Chocolatey") +// Copyright © 2015-2017 RealDimensions Software, LLC +// +// Chocolatey Professional, Chocolatey for Business, and Chocolatey Architect are licensed software. +// +// ===================================================================== +// End-User License Agreement +// Chocolatey Professional, Chocolatey for Service Providers, Chocolatey for Business, +// and/or Chocolatey Architect +// ===================================================================== +// +// IMPORTANT- READ CAREFULLY: This Chocolatey Software ("Chocolatey") End-User License Agreement +// ("EULA") is a legal agreement between you ("END USER") and Chocolatey for all Chocolatey products, +// controls, source code, demos, intermediate files, media, printed materials, and "online" or electronic +// documentation (collectively "SOFTWARE PRODUCT(S)") contained with this distribution. +// +// Chocolatey grants to you as an individual or entity, a personal, nonexclusive license to install and use the +// SOFTWARE PRODUCT(S). By installing, copying, or otherwise using the SOFTWARE PRODUCT(S), you +// agree to be bound by the terms of this EULA. If you do not agree to any part of the terms of this EULA, DO +// NOT INSTALL, USE, OR EVALUATE, ANY PART, FILE OR PORTION OF THE SOFTWARE PRODUCT(S). +// +// In no event shall Chocolatey be liable to END USER for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a result of the use or inability to use the +// SOFTWARE PRODUCT(S) (including but not limited to damages for loss of goodwill, work stoppage, computer +// failure or malfunction, or any and all other commercial damages or losses). +// +// The liability of Chocolatey to END USER for any reason and upon any cause of action related to the +// performance of the work under this agreement whether in tort or in contract or otherwise shall be limited to the +// amount paid by the END USER to Chocolatey pursuant to this agreement. +// +// ALL SOFTWARE PRODUCT(S) are licensed not sold. If you are an individual, you must acquire an individual +// license for the SOFTWARE PRODUCT(S) from Chocolatey or its authorized resellers. If you are an entity, you +// must acquire an individual license for each machine running the SOFTWARE PRODUCT(S) within your +// organization from Chocolatey or its authorized resellers. Both virtual and physical machines running the +// SOFTWARE PRODUCT(S) or benefitting from licensed features such as Package Builder or Package +// Internalizer must be counted in the SOFTWARE PRODUCT(S) licenses quantity of the organization. + +namespace Chocolatey.PowerShell +{ + using Chocolatey.PowerShell.Helpers; + using Chocolatey.PowerShell.StringResources; + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Text; + using System.Threading; + + public abstract class ChocolateyCmdlet : PSCmdlet + { + private readonly object _lock = new object(); + private readonly CancellationTokenSource _pipelineStopTokenSource = new CancellationTokenSource(); + + protected CancellationToken PipelineStopToken => _pipelineStopTokenSource.Token; + + protected Dictionary BoundParameters => MyInvocation.BoundParameters; + + protected string ErrorId => GetType().Name + "Error"; + + protected string ChocolateyInstallLocation => PowerShellHelper.GetInstallLocation(this); + + protected bool Debug => MyInvocation.BoundParameters.ContainsKey("Debug") + ? ConvertTo(MyInvocation.BoundParameters["Debug"]).ToBool() + : ConvertTo(GetVariableValue(PreferenceVariables.Debug)) != ActionPreference.SilentlyContinue; + + protected override void BeginProcessing() + { + WriteCmdletCallDebugMessage(); + } + + protected override void EndProcessing() + { + WriteCmdletCompletionDebugMessage(); + } + + protected override void StopProcessing() + { + lock (_lock) + { + _pipelineStopTokenSource.Cancel(); + } + } + + protected void WriteCmdletCallDebugMessage() + { + var logMessage = new StringBuilder() + .Append("Running ") + .Append(MyInvocation.InvocationName); + + foreach (var param in MyInvocation.BoundParameters) + { + if (param.Key == "ignoredArguments") + { + continue; + } + + var paramValue = IsEqual(param.Key, "SensitiveStatements") || IsEqual(param.Key, "Password") + ? "[REDACTED]" + : param.Value is IList list + ? string.Join(" ", list) + : LanguagePrimitives.ConvertTo(param.Value, typeof(string)); + + logMessage.Append($" -{param.Key} '{paramValue}'"); + } + + WriteDebug(logMessage.ToString()); + } + + protected void WriteCmdletCompletionDebugMessage() + { + WriteDebug($"Finishing '{MyInvocation.InvocationName}'"); + } + + protected string EnvironmentVariable(string name) + => EnvironmentHelper.GetVariable(name); + + protected string EnvironmentVariable(string name, EnvironmentVariableTarget scope) + => EnvironmentVariable(name, scope, preserveVariables: false); + + protected string EnvironmentVariable(string name, EnvironmentVariableTarget scope, bool preserveVariables) + => EnvironmentHelper.GetVariable(this, name, scope, preserveVariables); + + protected void SetEnvironmentVariable(string variable, string value) + => EnvironmentHelper.SetVariable(variable, value); + + protected void SetEnvironmentVariable(string name, string value, EnvironmentVariableTarget scope) + => EnvironmentHelper.SetVariable(name, value, scope); + + protected Collection GetChildItem(string[] path, bool recurse, bool force, bool literalPath) + => PowerShellHelper.GetChildItem(this, path, recurse, force, literalPath); + + protected Collection GetChildItem(string path, bool recurse) + => PowerShellHelper.GetChildItem(this, path, recurse); + + protected Collection GetChildItem(string path) + => PowerShellHelper.GetChildItem(this, path); + + internal Collection GetItem(string path, bool force, bool literalPath) + => PowerShellHelper.GetItem(this, path, force, literalPath); + + internal Collection GetItem(string path) + => PowerShellHelper.GetItem(this, path); + + internal Collection GetItem(string path, bool literalPath) + => PowerShellHelper.GetItem(this, path, literalPath); + + protected bool IsEqual(object first, object second) + => PowerShellHelper.IsEqual(first, second); + + protected void WriteHost(string message) + => PowerShellHelper.WriteHost(this, message); + + protected new void WriteDebug(string message) + => PowerShellHelper.WriteDebug(this, message); + + protected new void WriteVerbose(string message) + => PowerShellHelper.WriteVerbose(this, message); + + protected new void WriteWarning(string message) + => PowerShellHelper.WriteWarning(this, message); + + protected string CombinePaths(string parent, params string[] childPaths) + => PowerShellHelper.CombinePaths(this, parent, childPaths); + + protected void EnsureDirectoryExists(string directory) + => PowerShellHelper.EnsureDirectoryExists(this, directory); + + protected string GetParentDirectory(string path) + => PowerShellHelper.GetParentDirectory(this, path); + + protected static string GetFileName(string path) + => PowerShellHelper.GetFileName(path); + + protected string GetUnresolvedPath(string path) + => PowerShellHelper.GetUnresolvedPath(this, path); + + protected string? GetCurrentDirectory() + => PowerShellHelper.GetCurrentDirectory(this); + + protected FileInfo? GetFileInfoFor(string path) + => PowerShellHelper.GetFileInfoFor(this, path); + + protected string GetFullPath(string path) + => PowerShellHelper.GetFullPath(this, path); + + protected bool ItemExists(string path) + => PowerShellHelper.ItemExists(this, path); + + protected bool ContainerExists(string path) + => PowerShellHelper.ContainerExists(this, path); + + protected void CopyFile(string source, string destination, bool overwriteExisting) + => PowerShellHelper.CopyFile(this, source, destination, overwriteExisting); + + protected void DeleteFile(string path) + => PowerShellHelper.DeleteFile(this, path); + + protected Collection NewDirectory(string path) + => PowerShellHelper.NewDirectory(this, path); + + protected Collection NewFile(string path) + => PowerShellHelper.NewFile(this, path); + + protected Collection NewItem(string path, string itemType) + => PowerShellHelper.NewItem(this, path, itemType); + + protected Collection NewItem(string path, string name, string itemType) + => PowerShellHelper.NewItem(this, path, name, itemType); + + protected void SetExitCode(int exitCode) + => PowerShellHelper.SetExitCode(this, exitCode); + + protected T ConvertTo(object? value) + => PowerShellHelper.ConvertTo(value); + + protected string Replace(string input, string pattern, string replacement) + => PowerShellHelper.Replace(input, pattern, replacement); + + protected string Replace(string input, string pattern, string replacement, bool caseSensitive) + => PowerShellHelper.Replace(input, pattern, replacement, caseSensitive); + + public void RemoveItem(string path) + => PowerShellHelper.RemoveItem(this, path); + + public void RemoveItem(string path, bool recurse) + => PowerShellHelper.RemoveItem(this, path, recurse); + + public void RemoveItem(string[] path, bool recurse, bool force, bool literalPath) + => PowerShellHelper.RemoveItem(this, path, recurse, force, literalPath); + + + protected new void WriteObject(object value) + => PowerShellHelper.WriteObject(this, value); + } +} diff --git a/src/Chocolatey.PowerShell/Commands/AddChocolateyPinnedTaskbarItemCommand.cs b/src/Chocolatey.PowerShell/Commands/AddChocolateyPinnedTaskbarItemCommand.cs new file mode 100644 index 000000000..f24441b73 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/AddChocolateyPinnedTaskbarItemCommand.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Add, "ChocolateyPinnedTaskbarItem")] + public class AddChocolateyPinnedTaskbarItemCommand : ChocolateyCmdlet + { + /* + .SYNOPSIS + Creates an item in the task bar linking to the provided path. + + .NOTES + Does not work with SYSTEM, but does not error. It warns with the error + message. + + .INPUTS + None + + .OUTPUTS + None + + .PARAMETER TargetFilePath + The path to the application that should be launched when clicking on the + task bar icon. + + .PARAMETER IgnoredArguments + Allows splatting with arguments that do not apply. Do not use directly. + + .EXAMPLE + > + # This will create a Visual Studio task bar icon. + Install-ChocolateyPinnedTaskBarItem -TargetFilePath "${env:ProgramFiles(x86)}\Microsoft Visual Studio 11.0\Common7\IDE\devenv.exe" + + .LINK + Install-ChocolateyShortcut + + .LINK + Install-ChocolateyExplorerMenuItem + */ + + [Parameter(Mandatory = true, Position = 0)] + [Alias("TargetFilePath")] + public string Path { get; set; } = string.Empty; + + protected override void End() + { + const string verb = "Pin To Taskbar"; + var targetFolder = PSHelper.GetParentDirectory(this, Path); + var targetItem = PSHelper.GetFileName(Path); + + try + { + if (!PSHelper.ItemExists(this, Path)) + { + WriteWarning($"'{Path}' does not exist, not able to pin to task bar"); + return; + } + + dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application")); + var folder = shell.NameSpace(targetFolder); + var item = folder.ParseName(targetItem); + + bool verbFound = false; + foreach (var itemVerb in item.Verbs()) + { + var name = (string)itemVerb.Name; + if (name.Replace("&", string.Empty) == verb) + { + verbFound = true; + itemVerb.DoIt(); + break; + } + } + + if (!verbFound) + { + WriteHost($"TaskBar verb not found for {targetItem}. It may have already been pinned"); + } + + WriteHost($"'{Path}' has been pinned to the task bar on your desktop"); + } + catch (Exception ex) + { + WriteWarning($"Unable to create pin. Error captured was {ex.Message}."); + } + + base.EndProcessing(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/ExpandChocolateyArchiveCommand.cs b/src/Chocolatey.PowerShell/Commands/ExpandChocolateyArchiveCommand.cs new file mode 100644 index 000000000..5dada1892 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/ExpandChocolateyArchiveCommand.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Management.Automation; +using System.Net.NetworkInformation; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsData.Expand, "ChocolateyArchive", DefaultParameterSetName = "Path")] + [OutputType(typeof(string))] + public class ExpandChocolateyArchiveCommand : ChocolateyCmdlet + { + /* + +.SYNOPSIS +Unzips an archive file and returns the location for further processing. + +.DESCRIPTION +This unzips files using the 7-zip command line tool 7z.exe. +Supported archive formats are listed at: +https://sevenzip.osdn.jp/chm/general/formats.htm + +.INPUTS +None + +.OUTPUTS +Returns the passed in $destination. + +.NOTES +If extraction fails, an exception is thrown. + +If you are embedding files into a package, ensure that you have the +rights to redistribute those files if you are sharing this package +publicly (like on the community feed). Otherwise, please use +Install-ChocolateyZipPackage to download those resources from their +official distribution points. + +Will automatically call Set-PowerShellExitCode to set the package exit code +based on 7-zip's exit code. + +.PARAMETER FileFullPath +This is the full path to the zip file. If embedding it in the package +next to the install script, the path will be like +`"$(Split-Path -Parent $MyInvocation.MyCommand.Definition)\\file.zip"` + +`File` is an alias for FileFullPath. + +This can be a 32-bit or 64-bit file. This is mandatory in earlier versions +of Chocolatey, but optional if FileFullPath64 has been provided. + +.PARAMETER FileFullPath64 +Full file path to a 64-bit native installer to run. +If embedding in the package, you can get it to the path with +`"$(Split-Path -parent $MyInvocation.MyCommand.Definition)\\INSTALLER_FILE"` + +Provide this when you want to provide both 32-bit and 64-bit +installers or explicitly only a 64-bit installer (which will cause a package +install failure on 32-bit systems). + +.PARAMETER Destination +This is a directory where you would like the unzipped files to end up. +If it does not exist, it will be created. + +.PARAMETER SpecificFolder +OPTIONAL - This is a specific directory within zip file to extract. The +folder and its contents will be extracted to the destination. + +.PARAMETER PackageName +OPTIONAL - This will facilitate logging unzip activity for subsequent +uninstalls + +.PARAMETER DisableLogging +OPTIONAL - This disables logging of the extracted items. It speeds up +extraction of archives with many files. + +Usage of this parameter will prevent Uninstall-ChocolateyZipPackage +from working, extracted files will have to be cleaned up with +Remove-Item or a similar command instead. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +# Path to the folder where the script is executing +$toolsDir = (Split-Path -parent $MyInvocation.MyCommand.Definition) +Get-ChocolateyUnzip -FileFullPath "c:\someFile.zip" -Destination $toolsDir + +.LINK +Install-ChocolateyZipPackage + */ + + [Alias("File", "FileFullPath")] + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Path")] + public string Path { get; set; } = string.Empty; + + [Alias("UnzipLocation")] + [Parameter(Mandatory = true, Position = 1)] + public string Destination { get; set; } = string.Empty; + + [Parameter(Position = 2)] + public string SpecificFolder { get; set; } + + [Parameter(Position = 3)] + public string PackageName { get; set; } + + [Alias("File64", "FileFullPath64")] + [Parameter(Mandatory = true, ParameterSetName = "Path64")] + [Parameter(ParameterSetName = "Path")] + public string Path64 { get; set; } + + [Parameter] + public SwitchParameter DisableLogging { get; set; } + + protected override void End() + { + // This case should be prevented by the parameter set definitions, + // but it doesn't hurt to make absolutely sure here as well. + if (!(BoundParameters.ContainsKey(nameof(Path)) || BoundParameters.ContainsKey(nameof(Path64)))) + { + ThrowTerminatingError(new RuntimeException("Parameters are incorrect; either -Path or -Path64 must be specified.").ErrorRecord); + } + + var helper = new SevenZipHelper(this, PipelineStopToken); + helper.Run7zip(Path, Path64, PackageName, Destination, SpecificFolder, DisableLogging); + + WriteObject(Destination); + + base.EndProcessing(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetChocolateyConfigValueCommand.cs b/src/Chocolatey.PowerShell/Commands/GetChocolateyConfigValueCommand.cs new file mode 100644 index 000000000..cf067a37d --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetChocolateyConfigValueCommand.cs @@ -0,0 +1,85 @@ +using chocolatey; +using Chocolatey.PowerShell; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "ChocolateyConfigValue")] + public class GetChocolateyConfigValueCommand : ChocolateyCmdlet + { + [Parameter(Mandatory = true)] + public string ConfigKey { get; set; } + + protected override void End() + { + var result = GetConfigValue(ConfigKey); + + WriteObject(result); + } + + private string GetConfigValue(string key) + { + if (key is null) + { + return null; + } + + string configString = null; + Exception error = null; + foreach (var reader in InvokeProvider.Content.GetReader(ApplicationParameters.GlobalConfigFileLocation)) + { + try + { + var results = reader.Read(1); + if (results.Count > 0) + { + configString = PSHelper.ConvertTo(results[0]); + break; + } + } + catch (Exception ex) + { + WriteWarning($"Could not read configuration file: {ex.Message}"); + } + } + + if (configString is null) + { + // TODO: Replace RuntimeException + var exception = error is null + ? new RuntimeException("Config file is missing or empty.") + : new RuntimeException($"Config file is missing or empty. Error reading configuration file: {error.Message}", error); + ThrowTerminatingError(exception.ErrorRecord); + } + + var xmlConfig = new XmlDocument(); + xmlConfig.LoadXml(configString); + + foreach (XmlNode configEntry in xmlConfig.SelectNodes("chocolatey/config/add")) + { + var nodeKey = configEntry.Attributes["key"]; + if (nodeKey is null || !IsEqual(nodeKey.Value, ConfigKey)) + { + continue; + } + + var value = configEntry.Attributes["value"]; + if (!(value is null)) + { + // We don't support duplicate config entries; once found, we're done here. + return value.Value; + } + } + + return null; + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetChocolateyPathCommand.cs b/src/Chocolatey.PowerShell/Commands/GetChocolateyPathCommand.cs new file mode 100644 index 000000000..2aec02b3e --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetChocolateyPathCommand.cs @@ -0,0 +1,72 @@ +using Chocolatey.PowerShell; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; + +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "ChocolateyPath")] + public class GetChocolateyPathCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +Retrieve the paths available to be used by maintainers of packages. + +.DESCRIPTION +This function will attempt to retrieve the path according to the specified Path Type +to a valid location that can be used by maintainers in certain scenarios. + +.NOTES +Available in 1.2.0+. + +.INPUTS +None + +.OUTPUTS +This function outputs the full path stored accordingly with specified path type. +If no path could be found, there is no output. + +.PARAMETER pathType +The type of path that should be looked up. +Available values are: +- `PackagePath` - The path to the the package that is being installed. Typically `C:\ProgramData\chocolatey\lib\` +- `InstallPath` - The path to where Chocolatey is installed. Typically `C:\ProgramData\chocolatey` + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +$path = Get-ChocolateyPath -PathType 'PackagePath' + */ + [Parameter(Mandatory = true)] + [Alias("Type")] + public ChocolateyPathType PathType { get; set; } + + protected override void End() + { + try + { + var path = Paths.GetChocolateyPathType(this, PathType); + + if (ContainerExists(this, path)) + { + WriteObject(path); + } + } + catch (NotImplementedException error) + { + ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.NotImplemented", ErrorCategory.NotImplemented, PathType)); + } + catch (Exception error) + { + ThrowTerminatingError(new ErrorRecord(error, $"{ErrorId}.Unknown", ErrorCategory.NotSpecified, PathType)); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetEnvironmentVariableNameCommand.cs b/src/Chocolatey.PowerShell/Commands/GetEnvironmentVariableNameCommand.cs new file mode 100644 index 000000000..86bb2d63c --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetEnvironmentVariableNameCommand.cs @@ -0,0 +1,69 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; +using System.Text; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "EnvironmentVariableName")] + [OutputType(typeof(string))] + public class GetEnvironmentVariableNameCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +Gets all environment variable names. + +.DESCRIPTION +Provides a list of environment variable names based on the scope. This +can be used to loop through the list and generate names. + +.NOTES +Process dumps the current environment variable names in memory / +session. The other scopes refer to the registry values. + +.INPUTS +None + +.OUTPUTS +A list of environment variables names. + +.PARAMETER Scope +The environment variable target scope. This is `Process`, `User`, or +`Machine`. + +.EXAMPLE +Get-EnvironmentVariableNames -Scope Machine + +.LINK +Get-EnvironmentVariable + +.LINK +Set-EnvironmentVariable + */ + + [Parameter(Position = 0)] + public EnvironmentVariableTarget Scope { get; set; } + + [Parameter] + public SwitchParameter PreserveVariables { get; set; } + + + protected override void Begin() + { + // Avoid calling base.BeginProcessing() to log function call + } + + protected override void End() + { + foreach (var item in EnvironmentHelper.GetVariableNames(Scope)) + { + WriteObject(item); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetOsArchitectureWidthCommand.cs b/src/Chocolatey.PowerShell/Commands/GetOsArchitectureWidthCommand.cs new file mode 100644 index 000000000..dd8f6edd0 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetOsArchitectureWidthCommand.cs @@ -0,0 +1,61 @@ +using System; +using System.Management.Automation; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "OSArchitectureWidth")] + public class GetOsArchitectureWidthCommand : ChocolateyCmdlet + { + /* + +.SYNOPSIS +Get the operating system architecture address width. + +.DESCRIPTION +This will return the system architecture address width (probably 32 or +64 bit). If you pass a comparison, it will return true or false instead +of {`32`|`64`}. + +.NOTES +When your installation script has to know what architecture it is run +on, this simple function comes in handy. + +ARM64 architecture will automatically select 32bit width as +there is an emulator for 32 bit and there are no current plans by Microsoft to +ship 64 bit x86 emulation for ARM64. For more details, see +https://github.com/chocolatey/choco/issues/1800#issuecomment-484293844. + +.INPUTS +None + +.OUTPUTS +None + +.PARAMETER Compare +This optional parameter causes the function to return $true or $false, +depending on whether or not the bit width matches. + */ + [Parameter] + [Alias("Compare")] + [BoolStringSwitchTransform] + public int CompareTo { get; set; } + + protected override void End() + { + var bits = Environment.Is64BitProcess ? 64 : 32; + + if (BoundParameters.ContainsKey(nameof(CompareTo))) + { + WriteObject(ArchitectureWidth.Matches(CompareTo)); + } + else + { + WriteObject(ArchitectureWidth.Get()); + } + + base.EndProcessing(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetPackageParameterCommand.cs b/src/Chocolatey.PowerShell/Commands/GetPackageParameterCommand.cs new file mode 100644 index 000000000..bed86aa0f --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetPackageParameterCommand.cs @@ -0,0 +1,182 @@ +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using System.Text.RegularExpressions; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "PackageParameter")] + [OutputType(typeof(Hashtable))] + public class GetPackageParameterCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +Parses a string and returns a hash table array of those values for use +in package scripts. + +.DESCRIPTION +This looks at a string value and parses it into a hash table array for +use in package scripts. By default this will look at +`$env:ChocolateyPackageParameters` (`--params="'/ITEM:value'"`) and +`$env:ChocolateyPackageParametersSensitive` +(`--package-parameters-sensitive="'/PASSWORD:value'"` in commercial +editions). + +Learn more about using this at https://docs.chocolatey.org/en-us/guides/create/parse-packageparameters-argument + +.NOTES +If you need compatibility with older versions of Chocolatey, +take a dependency on the `chocolatey-core.extension` package which +also provides this functionality. If you are pushing to the community +package repository (https://community.chocolatey.org/packages), you are required +to take a dependency on the core extension until January 2018. How to +do this is explained at https://docs.chocolatey.org/en-us/guides/create/parse-packageparameters-argument#step-3---use-core-community-extension. + +The differences between this and the `chocolatey-core.extension` package +functionality is that the extension function can only do one string at a +time and it only looks at `$env:ChocolateyPackageParameters` by default. +It also only supports splitting by `:`, with this function you can +either split by `:` or `=`. For compatibility with the core extension, +build all docs with `/Item:Value`. + +.INPUTS +None + +.OUTPUTS +[HashTable] + +.PARAMETER Parameters +OPTIONAL - Specify a string to parse. If not set, will use +`$env:ChocolateyPackageParameters` and +`$env:ChocolateyPackageParametersSensitive` to parse values from. + +Parameters should be passed as "/NAME:value" or "/NAME=value". For +compatibility with `chocolatey-core.extension`, use `:`. + +For example `-Parameters "/ITEM1:value /ITEM2:value with spaces" + +To maintain compatibility with the prior art of the chocolatey-core.extension +function by the same name, quotes and apostrophes surrounding +parameter values will be removed. When the param is used, those items +can be added back if desired, but it's most important to ensure that +existing packages are compatible on upgrade. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply and future expansion. +Do not use directly. + +.EXAMPLE +> +# The default way of calling, uses `$env:ChocolateyPackageParameters` +# and `$env:ChocolateyPackageParametersSensitive` - this is typically +# how things are passed in from choco.exe +$pp = Get-PackageParameters + +.EXAMPLE +> +# see https://docs.chocolatey.org/en-us/guides/create/parse-packageparameters-argument +# command line call: `choco install --params "'/LICENSE:value'"` +$pp = Get-PackageParameters +# Read-Host, PromptForChoice, etc are not blocking calls with Chocolatey. +# Chocolatey has a custom PowerShell host that will time these calls +# after 30 seconds, allowing headless operation to continue but offer +# prompts to users to ask questions during installation. +if (!$pp['LICENSE']) { $pp['LICENSE'] = Read-Host 'License key?' } +# set a default if not passed +if (!$pp['LICENSE']) { $pp['LICENSE'] = '1234' } + +.EXAMPLE +> +$pp = Get-PackageParameters +if (!$pp['UserName']) { $pp['UserName'] = "$env:UserName" } +if (!$pp['Password']) { $pp['Password'] = Read-Host "Enter password for $($pp['UserName']):" -AsSecureString} +# fail the install/upgrade if not value is not determined +if (!$pp['Password']) { throw "Package needs Password to install, that must be provided in params or in prompt." } + +.EXAMPLE +> +# Pass in your own values +Get-PackageParameters -Parameters "/Shortcut /InstallDir:'c:\program files\xyz' /NoStartup" | set r +if ($r.Shortcut) {... } +Write-Host $r.InstallDir + +.LINK +Install-ChocolateyPackage + +.LINK +Install-ChocolateyInstallPackage + +.LINK +Install-ChocolateyZipPackage + */ + + private const string PackageParameterPattern = @"(?:^|\s+)\/(?[^\:\=\s)]+)(?:(?:\:|=){1}(?:\''|\""){0,1}(?.*?)(?:\''|\""){0,1}(?:(?=\s+\/)|$))?"; + private static readonly Regex _packageParameterRegex = new Regex(PackageParameterPattern, RegexOptions.Compiled); + + [Parameter(Position = 0)] + [Alias("Params")] + public string Parameters { get; set; } = string.Empty; + + protected override void End() + { + var paramStrings = new List(); + var logParams = true; + + if (!string.IsNullOrEmpty(Parameters)) + { + paramStrings.Add(Parameters); + } + else + { + WriteDebug("Parsing $env:ChocolateyPackageParameters and $env:ChocolateyPackageParametersSensitive for parameters"); + + var packageParams = EnvironmentVariable(EnvironmentVariables.ChocolateyPackageParameters); + if (!string.IsNullOrEmpty(packageParams)) + { + paramStrings.Add(packageParams); + } + + var sensitiveParams = EnvironmentVariable(EnvironmentVariables.ChocolateyPackageParametersSensitive); + if (!string.IsNullOrEmpty(sensitiveParams)) + { + logParams = false; + WriteDebug("Sensitive parameters detected, no logging of parameters."); + paramStrings.Add(sensitiveParams); + } + } + + var paramHash = new Hashtable(StringComparer.OrdinalIgnoreCase); + + foreach (var param in paramStrings) + { + foreach (Match match in _packageParameterRegex.Matches(param)) + { + var name = match.Groups["ItemKey"].Value.Trim(); + var valueGroup = match.Groups["ItemValue"]; + + object value; + if (valueGroup.Success) + { + value = valueGroup.Value.Trim(); + } + else + { + value = (object)true; + } + + if (logParams) + { + WriteDebug($"Adding package param '{name}'='{value}'"); + } + + paramHash[name] = value; + } + } + + WriteObject(paramHash); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetToolsLocationCommand.cs b/src/Chocolatey.PowerShell/Commands/GetToolsLocationCommand.cs new file mode 100644 index 000000000..1a0e40fcf --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetToolsLocationCommand.cs @@ -0,0 +1,95 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using System.Text.RegularExpressions; + +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "ToolsLocation")] + [OutputType(typeof(string))] + public class GetToolsLocationCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +Gets the top level location for tools/software installed outside of +package folders. + +.DESCRIPTION +Creates or uses an environment variable that a user can control to +communicate with packages about where they would like software that is +not installed through native installers, but doesn't make much sense +to be kept in package folders. Most software coming in packages stays +with the package itself, but there are some things that seem to fall +out of this category, like things that have plugins that are installed +into the same directory as the tool. Having that all combined in the +same package directory could get tricky. + +.NOTES +Sets an environment variable called `ChocolateyToolsLocation`. If the +older `ChocolateyBinRoot` is set, it uses the value from that and +removes the older variable. + +.INPUTS +None + +.OUTPUTS +None + */ + + private const string DriveLetterPattern = @"^\w:"; + private static readonly Regex _driveLetterRegex = new Regex(DriveLetterPattern, RegexOptions.Compiled); + + protected override void End() + { + var envToolsLocation = EnvironmentVariable(EnvironmentVariables.ChocolateyToolsLocation); + var toolsLocation = envToolsLocation; + + if (string.IsNullOrEmpty(toolsLocation)) + { + var binRoot = EnvironmentVariable(EnvironmentVariables.ChocolateyBinRoot); + + if (string.IsNullOrEmpty(binRoot)) + { + toolsLocation = CombinePaths(this, EnvironmentVariable("SYSTEMDRIVE"), "tools"); + } + else + { + toolsLocation = binRoot; + EnvironmentHelper.SetVariable(this, EnvironmentVariables.ChocolateyBinRoot, EnvironmentVariableTarget.User, string.Empty); + } + } + + if (!_driveLetterRegex.IsMatch(toolsLocation)) + { + toolsLocation = CombinePaths(this, EnvironmentVariable("SYSTEMDRIVE"), toolsLocation); + } + + if (envToolsLocation != toolsLocation) + { + try + { + EnvironmentHelper.SetVariable(this, EnvironmentVariables.ChocolateyToolsLocation, EnvironmentVariableTarget.User, toolsLocation); + } + catch (Exception e) + { + if (ProcessInformation.IsElevated()) + { + // sometimes User scope may not exist (such as with core) + EnvironmentHelper.SetVariable(this, EnvironmentVariables.ChocolateyToolsLocation, EnvironmentVariableTarget.Machine, toolsLocation); + } + else + { + ThrowTerminatingError(new RuntimeException(e.Message, e).ErrorRecord); + } + } + } + + WriteObject(toolsLocation); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetUacEnabledCommand.cs b/src/Chocolatey.PowerShell/Commands/GetUacEnabledCommand.cs new file mode 100644 index 000000000..72f538632 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetUacEnabledCommand.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; +using System.Text; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "UacEnabled")] + [OutputType(typeof(bool))] + public class GetUacEnabledCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +Determines if UAC (User Account Control) is turned on or off. + +.DESCRIPTION +This is a low level function used by Chocolatey to decide whether +prompting for elevated privileges is necessary or not. + +.NOTES +This checks the `EnableLUA` registry value to be determine the state of +a system. + +.INPUTS +None + +.OUTPUTS +System.Boolean + */ + + private const string UacRegistryPath = @"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"; + private const string UacRegistryProperty = "EnableLUA"; + + protected override void End() + { + var uacEnabled = false; + + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724832(v=vs.85).aspx + var osVersion = Environment.OSVersion.Version; + if (osVersion >= new Version(6, 0)) + { + try + { + var uacRegistryValue = InvokeProvider.Property.Get(UacRegistryPath, new Collection()).FirstOrDefault()?.Properties[UacRegistryProperty].Value; + uacEnabled = (int?)uacRegistryValue == 1; + } + catch + { + // Registry key doesn't exist, proceed with false + } + } + + WriteObject(uacEnabled); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetUninstallRegistryKeyCommand.cs b/src/Chocolatey.PowerShell/Commands/GetUninstallRegistryKeyCommand.cs new file mode 100644 index 000000000..1ed1081a3 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetUninstallRegistryKeyCommand.cs @@ -0,0 +1,184 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; +using System.Text; + +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "UninstallRegistryKey")] + public class GetUninstallRegistryKeyCommand : ChocolateyCmdlet + { + /* + +.SYNOPSIS +Retrieve registry key(s) for system-installed applications from an +exact or wildcard search. + +.DESCRIPTION +This function will attempt to retrieve a matching registry key for an +already installed application, usually to be used with a +chocolateyUninstall.ps1 automation script. + +The function also prevents `Get-ItemProperty` from failing when +handling wrongly encoded registry keys. + +.INPUTS +String + +.OUTPUTS +This function searches registry objects and returns an array +of PSCustomObject with the matched key's properties. + +Retrieve properties with dot notation, for example: +`$key.UninstallString` + + +.PARAMETER SoftwareName +Part or all of the Display Name as you see it in Programs and Features. +It should be enough to be unique. +The syntax follows the rules of the PowerShell `-like` operator, so the +`*` character is interpreted as a wildcard, which matches any (zero or +more) characters. + +If the display name contains a version number, such as "Launchy (2.5)", +it is recommended you use a fuzzy search `"Launchy (*)"` (the wildcard +`*`) so if Launchy auto-updates or is updated outside of Chocolatey, the +uninstall script will not fail. + +Take care not to abuse fuzzy/glob pattern searches. Be conscious of +programs that may have shared or common root words to prevent +overmatching. For example, "SketchUp*" would match two keys with +software names "SketchUp 2016" and "SketchUp Viewer" that are different +programs released by the same company. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +# Version match: Software name is "Gpg4Win (2.3.0)" +[array]$key = Get-UninstallRegistryKey -SoftwareName "Gpg4win (*)" +$key.UninstallString + +.EXAMPLE +> +# Fuzzy match: Software name is "Launchy 2.5" +[array]$key = Get-UninstallRegistryKey -SoftwareName "Launchy*" +$key.UninstallString + +.EXAMPLE +> +# Exact match: Software name in Programs and Features is "VLC media player" +[array]$key = Get-UninstallRegistryKey -SoftwareName "VLC media player" +$key.UninstallString + +.EXAMPLE +> +# Version match: Software name is "SketchUp 2016" +# Note that the similar software name "SketchUp Viewer" would not be matched. +[array]$key = Get-UninstallRegistryKey -SoftwareName "SketchUp [0-9]*" +$key.UninstallString + +.LINK +Install-ChocolateyPackage + +.LINK +Install-ChocolateyInstallPackage + +.LINK +Uninstall-ChocolateyPackage + */ + private const string LocalUninstallKey = @"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"; + private const string MachineUninstallKey = @"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"; + private const string MachineUninstallKey6432 = @"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"; + + + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] + public string SoftwareName { get; set; } = string.Empty; + + protected override void End() + { + if (!string.IsNullOrEmpty(SoftwareName)) + { + // TODO: Replace RuntimeException + ThrowTerminatingError(new RuntimeException($"{SoftwareName} cannot be empty for Get-UninstallRegistryKey").ErrorRecord); + } + + WriteVerbose("Retrieving all uninstall registry keys"); + var keys = GetChildItem(this, new[] { MachineUninstallKey6432, MachineUninstallKey, LocalUninstallKey }, recurse: false, force: false, literalPath: true); + WriteDebug($"Registry uninstall keys on system: {keys.Count}"); + + // Error handling check: `'Get-ItemProperty`' fails if a registry key is encoded incorrectly. + List foundKey = null; + bool warnForBadKeys = false; + for (var attempt = 1; attempt <= keys.Count;attempt++) + { + bool success = false; + var keyPaths = ConvertTo(keys.Select(k => k.Properties["PSPath"].Value)); + + try + { + foundKey = InvokeProvider.Property.Get(keyPaths, new Collection(), literalPath: true) + .Where(k => IsLike(ConvertTo(k.Properties), SoftwareName)) + .ToList(); + success = true; + } + catch + { + WriteDebug("Found bad key"); + var badKey = new List(); + foreach (var key in keys) + { + var psPath = ConvertTo(key.Properties["PSPath"].Value); + try + { + InvokeProvider.Property.Get(psPath, new Collection()); + } + catch + { + badKey.Add(key); + WriteVerbose($"Skipping bad key: {psPath}"); + } + } + + foreach (var bad in badKey) + { + keys.Remove(bad); + } + } + + if (success) + { + break; + } + + if (attempt >= 10 && !warnForBadKeys) + { + warnForBadKeys = true; + } + } + + if (warnForBadKeys) + { + WriteWarning("Found 10 or more bad registry keys. Run command again with `'--verbose --debug`' for more info."); + WriteDebug("Each key searched should correspond to an installed program. It is very unlikely to have more than a few programs with incorrectly encoded keys, if any at all. This may be indicative of one or more corrupted registry branches."); + } + + if (!(foundKey?.Count > 0)) + { + WriteWarning($"No registry key found based on '{SoftwareName}'"); + } + else + { + WriteDebug($"Found {foundKey.Count} uninstall registry key(s) with SoftwareName: '{SoftwareName}'"); + WriteObject(foundKey); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetVirusCheckValidCommand.cs b/src/Chocolatey.PowerShell/Commands/GetVirusCheckValidCommand.cs new file mode 100644 index 000000000..fa8ab6178 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetVirusCheckValidCommand.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.Get, "VirusCheckValid")] + public class GetVirusCheckValidCommand : ChocolateyCmdlet + { + /* + .SYNOPSIS + Used in Pro/Business editions. Runtime virus check against downloaded + resources. + + .DESCRIPTION + Run a runtime malware check against downloaded resources prior to + allowing Chocolatey to execute a file. This is only available + in Pro / Business editions. + + .NOTES + Only licensed editions of Chocolatey provide runtime malware protection. + + .INPUTS + None + + .OUTPUTS + None + + .PARAMETER Url + Not used + + .PARAMETER File + The full file path to the file to verify against anti-virus scanners. + + .PARAMETER IgnoredArguments + Allows splatting with arguments that do not apply. Do not use directly. + + */ + + [Parameter(Mandatory = false, Position = 0)] + public string Url { get; set; } + + [Parameter(Mandatory = false, Position = 1)] + public string File { get; set; } = string.Empty; + + protected override void End() + { + WriteDebug("No runtime virus checking built into FOSS Chocolatey. Check out Pro/Business - https://chocolatey.org/compare"); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/InstallChocolateyEnvironmentVariableCommand.cs b/src/Chocolatey.PowerShell/Commands/InstallChocolateyEnvironmentVariableCommand.cs new file mode 100644 index 000000000..65c40a6f5 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/InstallChocolateyEnvironmentVariableCommand.cs @@ -0,0 +1,136 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Install, "ChocolateyEnvironmentVariable")] + public class InstallChocolateyEnvironmentVariableCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +**NOTE:** Administrative Access Required when `-VariableType 'Machine'.` + +Creates a persistent environment variable. + +.DESCRIPTION +Install-ChocolateyEnvironmentVariable creates an environment variable +with the specified name and value. The variable is persistent and +will remain after reboots and across multiple PowerShell and command +line sessions. The variable can be scoped either to the User or to +the Machine. If Machine level scoping is specified, the command is +elevated to an administrative session. + +.NOTES +This command will assert UAC/Admin privileges on the machine when +`-VariableType Machine`. + +This will add the environment variable to the current session. + +.INPUTS +None + +.OUTPUTS +None + +.PARAMETER VariableName +The name or key of the environment variable + +.PARAMETER VariableValue +A string value assigned to the above name. + +.PARAMETER VariableType +Specifies whether this variable is to be accessible at either the +individual user level or at the Machine level. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +# Creates a User environment variable "JAVA_HOME" pointing to +# "d:\oracle\jdk\bin". +Install-ChocolateyEnvironmentVariable "JAVA_HOME" "d:\oracle\jdk\bin" + +.EXAMPLE +> +# Creates a User environment variable "_NT_SYMBOL_PATH" pointing to +# "symsrv*symsrv.dll*f:\localsymbols*http://msdl.microsoft.com/download/symbols". +# The command will be elevated to admin privileges. +Install-ChocolateyEnvironmentVariable ` + -VariableName "_NT_SYMBOL_PATH" ` + -VariableValue "symsrv*symsrv.dll*f:\localsymbols*http://msdl.microsoft.com/download/symbols" ` + -VariableType Machine + +.EXAMPLE +> +# Remove an environment variable +Install-ChocolateyEnvironmentVariable -VariableName 'bob' -VariableValue $null + +.LINK +Uninstall-ChocolateyEnvironmentVariable + +.LINK +Get-EnvironmentVariable + +.LINK +Set-EnvironmentVariable + +.LINK +Install-ChocolateyPath + */ + [Parameter(Position = 0)] + [Alias("VariableName")] + public string Name { get; set; } = string.Empty; + + [Parameter(Position = 1)] + [Alias("VariableValue")] + public string Value { get; set; } = string.Empty; + + [Parameter(Position = 2)] + [Alias("Target", "VariableType")] + public EnvironmentVariableTarget Type { get; set; } = EnvironmentVariableTarget.User; + + protected override void End() + { + if (Type == EnvironmentVariableTarget.Machine) + { + if (ProcessInformation.IsElevated()) + { + EnvironmentHelper.SetVariable(this, Name, Type, Value); + } + else + { + var helper = new StartChocolateyProcessHelper(this, PipelineStopToken); + var args = $"Install-ChocolateyEnvironmentVariable -Name '{Name}' -Value '{Value}' -Type '{Type}'"; + helper.Start(workingDirectory: null, args, sensitiveStatements: null, elevated: true, minimized: true, noSleep: true); + } + } + else + { + try + { + EnvironmentHelper.SetVariable(this, Name, Type, Value); + } + catch (Exception ex) + { + if (ProcessInformation.IsElevated()) + { + // HKCU:\Environment may not exist, which happens sometimes with Server Core. + // In this case, set it at machine scope instead. + EnvironmentHelper.SetVariable(this, Name, EnvironmentVariableTarget.Machine, Value); + } + else + { + ThrowTerminatingError(new RuntimeException(ex.Message, ex).ErrorRecord); + } + } + } + + EnvironmentHelper.SetVariable(Name, Value); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/InstallChocolateyExplorerMenuItemCommand.cs b/src/Chocolatey.PowerShell/Commands/InstallChocolateyExplorerMenuItemCommand.cs new file mode 100644 index 000000000..dabdec9d3 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/InstallChocolateyExplorerMenuItemCommand.cs @@ -0,0 +1,123 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Install, "ChocolateyExplorerMenuItem")] + public class InstallChocolateyExplorerMenuItemCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +**NOTE:** Administrative Access Required. + +Creates a windows explorer context menu item that can be associated with +a command + +.DESCRIPTION +Install-ChocolateyExplorerMenuItem can add an entry in the context menu +of Windows Explorer. The menu item is given a text label and a command. +The command can be any command accepted on the windows command line. The +menu item can be applied to either folder items or file items. + +Because this command accesses and edits the root class registry node, it +will be elevated to admin. + +.NOTES +This command will assert UAC/Admin privileges on the machine. + +Chocolatey will automatically add the path of the file or folder clicked +to the command. This is done simply by appending a %1 to the end of the +command. + +.INPUTS +None + +.OUTPUTS +None + +.PARAMETER MenuKey +A unique string to identify this menu item in the registry + +.PARAMETER MenuLabel +The string that will be displayed in the context menu + +.PARAMETER Command +A command line command that will be invoked when the menu item is +selected + +.PARAMETER Type +Specifies if the menu item should be applied to a folder or a file + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +# This will create a context menu item in Windows Explorer when any file +# is right clicked. The menu item will appear with the text "Open with +# Sublime Text 2" and will invoke sublime text 2 when selected. +$sublimeDir = (Get-ChildItem $env:ALLUSERSPROFILE\chocolatey\lib\sublimetext* | select $_.last) +$sublimeExe = "$sublimeDir\tools\sublime_text.exe" +Install-ChocolateyExplorerMenuItem "sublime" "Open with Sublime Text 2" $sublimeExe + +.EXAMPLE +> +# This will create a context menu item in Windows Explorer when any +# folder is right clicked. The menu item will appear with the text +# "Open with Sublime Text 2" and will invoke sublime text 2 when selected. +$sublimeDir = (Get-ChildItem $env:ALLUSERSPROFILE\chocolatey\lib\sublimetext* | select $_.last) +$sublimeExe = "$sublimeDir\tools\sublime_text.exe" +Install-ChocolateyExplorerMenuItem "sublime" "Open with Sublime Text 2" $sublimeExe "directory" + +.LINK +Install-ChocolateyShortcut + */ + + [Parameter(Mandatory = true, Position = 0)] + public string MenuKey { get; set; } = string.Empty; + + [Parameter(Position = 1)] + public string MenuLabel { get; set; } + + [Parameter(Position = 2)] + public string Command { get; set; } + + [Parameter(Position = 3)] + public ExplorerMenuItemType Type { get; set; } = ExplorerMenuItemType.File; + + protected override void End() + { + try + { + var key = Type == ExplorerMenuItemType.File ? "*" : "directory"; + + var elevatedCommand = $@" +if( -not (Test-Path -Path HKCR:) ) {{New-PSDrive -Name HKCR -PSProvider registry -Root Hkey_Classes_Root}};` +if(!(Test-Path -LiteralPath 'HKCR:\{key}\shell\{MenuKey}')) {{ New-Item -Path 'HKCR:\{key}\shell\{MenuKey}' }};` +Set-ItemProperty -LiteralPath 'HKCR:\{key}\shell\{MenuKey}' -Name '(Default)' -Value '{MenuLabel}';` +if(!(Test-Path -LiteralPath 'HKCR:\{key}\shell\{MenuKey}\command')) {{ New-Item -Path 'HKCR:\{key}\shell\{MenuKey}\command' }};` +Set-ItemProperty -LiteralPath 'HKCR:\{key}\shell\{MenuKey}\command' -Name '(Default)' -Value '{Command} \`""%1\`""';` +return 0;"; + + var helper = new StartChocolateyProcessHelper(this, PipelineStopToken); + helper.Start(workingDirectory: null, arguments: elevatedCommand, sensitiveStatements: null, elevated: true, minimized: true, noSleep: true); + + WriteHost($"'{MenuKey}' explorer menu item has been created"); + } + catch (Exception ex) + { + WriteWarning($"'{MenuKey}' explorer menu item was not created - {ex.Message}"); + } + } + } + + public enum ExplorerMenuItemType + { + File, + Directory, + } +} diff --git a/src/Chocolatey.PowerShell/Commands/InstallChocolateyFileAssociationCommand.cs b/src/Chocolatey.PowerShell/Commands/InstallChocolateyFileAssociationCommand.cs new file mode 100644 index 000000000..c7b4b515c --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/InstallChocolateyFileAssociationCommand.cs @@ -0,0 +1,88 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; + +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Install, "ChocolateyFileAssociation")] + public class InstallChocolateyFileAssociationCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +**NOTE:** Administrative Access Required. + +Creates an association between a file extension and a executable. + +.DESCRIPTION +Install-ChocolateyFileAssociation can associate a file extension +with a downloaded application. Once this command has created an +association, all invocations of files with the specified extension +will be opened via the executable specified. + +.NOTES +This command will assert UAC/Admin privileges on the machine. + +.INPUTS +None + +.OUTPUTS +None + +.PARAMETER Extension +The file extension to be associated. + +.PARAMETER Executable +The path to the application's executable to be associated. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +# This will create an association between Sublime Text 2 and all .txt +# files. Any .txt file opened will by default open with Sublime Text 2. +$sublimeDir = (Get-ChildItem $env:ALLUSERSPROFILE\chocolatey\lib\sublimetext* | select $_.last) +$sublimeExe = "$sublimeDir\tools\sublime_text.exe" +Install-ChocolateyFileAssociation ".txt" $sublimeExe +*/ + + [Parameter(Mandatory = true, Position = 0)] + public string Extension { get; set; } = string.Empty; + + [Parameter(Mandatory = true, Position = 1)] + public string Executable { get; set; } = string.Empty; + + protected override void End() + { + if (!ItemExists(this, Executable)) + { + // TODO: Replace RuntimeException with a proper exception + ThrowTerminatingError(new RuntimeException($"'{Executable}' does not exist, can't create file association").ErrorRecord); + } + + var extension = Extension.Trim(); + if (!extension.StartsWith(".")) + { + extension = $".{extension}"; + } + + var fileType = GetFileName(Executable).Replace(" ", "_"); + var elevatedCommand = $@" +cmd /c ""assoc {extension}={fileType}"" +cmd /c 'ftype {fileType}=""{Executable}"" ""%1"" ""%*""' +New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT +Set-ItemProperty -Path ""HKCR:\{fileType}"" -Name ""(Default)"" -Value ""{fileType} file"" -ErrorAction Stop +"; + + var helper = new StartChocolateyProcessHelper(this, PipelineStopToken); + helper.Start(workingDirectory: null, elevatedCommand, sensitiveStatements: null, elevated: true, minimized: true, noSleep: true); + + WriteHost($"'{extension}' has been associated with '{Executable}'"); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/InstallChocolateyInstallPackageCommand.cs b/src/Chocolatey.PowerShell/Commands/InstallChocolateyInstallPackageCommand.cs new file mode 100644 index 000000000..1487d1c83 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/InstallChocolateyInstallPackageCommand.cs @@ -0,0 +1,249 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Invoke, "PackageInstaller")] + public class InvokePackageInstallerCommand : ChocolateyCmdlet + { + /*.SYNOPSIS +**NOTE:** Administrative Access Required. + +Installs software into "Programs and Features". Use +Install-ChocolateyPackage when software must be downloaded first. + +.DESCRIPTION +This will run an installer (local file) on your machine. + +.NOTES +This command will assert UAC/Admin privileges on the machine. + +If you are embedding files into a package, ensure that you have the +rights to redistribute those files if you are sharing this package +publicly (like on the community feed). Otherwise, please use +Install-ChocolateyPackage to download those resources from their +official distribution points. + +This is a native installer wrapper function. A "true" package will +contain all the run time files and not an installer. That could come +pre-zipped and require unzipping in a PowerShell script. Chocolatey +works best when the packages contain the software it is managing. Most +software in the Windows world comes as installers and Chocolatey +understands how to work with that, hence this wrapper function. + +.INPUTS +None + +.OUTPUTS +None + +.PARAMETER PackageName +The name of the package - while this is an arbitrary value, it's +recommended that it matches the package id. + +.PARAMETER FileType +This is the extension of the file. This can be 'exe', 'msi', or 'msu'. +Licensed editions of Chocolatey use this to automatically determine +silent arguments. If this is not provided, Chocolatey will +automatically determine this using the downloaded file's extension. + +.PARAMETER SilentArgs +OPTIONAL - These are the parameters to pass to the native installer, +including any arguments to make the installer silent/unattended. +Pro/Business Editions of Chocolatey will automatically determine the +installer type and merge the arguments with what is provided here. + +Try any of the to get the silent installer - +`/s /S /q /Q /quiet /silent /SILENT /VERYSILENT`. With msi it is always +`/quiet`. Please pass it in still but it will be overridden by +Chocolatey to `/quiet`. If you don't pass anything it could invoke the +installer with out any arguments. That means a nonsilent installer. + +Please include the `notSilent` tag in your Chocolatey package if you +are not setting up a silent/unattended package. Please note that if you +are submitting to the community repository, it is nearly a requirement +for the package to be completely unattended. + +When you are using this with an MSI, it will set up the arguments as +follows: `"C:\Full\Path\To\msiexec.exe" /i "$fileFullPath" $silentArgs`, +where `$fileFullPath` is `$file` or `$file64`, depending on what has been +decided to be used. + +When you use this with MSU, it is similar to MSI above in that it finds +the right executable to run. + +When you use this with executable installers, the `$fileFullPath` will +be `$file` or `$file64` and expects to be a full +path to the file. If the file is in the package, see the parameters for +"File" and "File64" to determine how you can get that path at runtime in +a deterministic way. SilentArgs is everything you call against that +file, as in `"$fileFullPath" $silentArgs"`. An example would be +`"c:\path\setup.exe" /S`, where `$fileFullPath = "c:\path\setup.exe"` +and `$silentArgs = "/S"`. + +.PARAMETER File +Full file path to native installer to run. If embedding in the package, +you can get it to the path with +`"$(Split-Path -parent $MyInvocation.MyCommand.Definition)\\INSTALLER_FILE"` + +`FileFullPath` is an alias for File. + +This can be a 32-bit or 64-bit file. This is mandatory in earlier versions +of Chocolatey, but optional if File64 has been provided. + +.PARAMETER File64 +Full file path to a 64-bit native installer to run. +If embedding in the package, you can get it to the path with +`"$(Split-Path -parent $MyInvocation.MyCommand.Definition)\\INSTALLER_FILE"` + +Provide this when you want to provide both 32-bit and 64-bit +installers or explicitly only a 64-bit installer (which will cause a package +install failure on 32-bit systems). + +.PARAMETER ValidExitCodes +Array of exit codes indicating success. Defaults to `@(0)`. + +.PARAMETER UseOnlyPackageSilentArguments +Do not allow choco to provide/merge additional silent arguments and +only use the ones available with the package. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +$packageName= 'bob' +$toolsDir = "$(Split-Path -Parent $MyInvocation.MyCommand.Definition)" +$fileLocation = Join-Path $toolsDir 'INSTALLER_EMBEDDED_IN_PACKAGE' + +$packageArgs = @{ + packageName = $packageName + fileType = 'msi' + file = $fileLocation + silentArgs = "/qn /norestart" + validExitCodes= @(0, 3010, 1641) + softwareName = 'Bob*' +} + +Install-ChocolateyInstallPackage @packageArgs + +.EXAMPLE +> +$packageArgs = @{ + packageName = 'bob' + fileType = 'exe' + file = '\\SHARE_LOCATION\to\INSTALLER_FILE' + silentArgs = "/S" + validExitCodes= @(0) + softwareName = 'Bob*' +} + +Install-ChocolateyInstallPackage @packageArgs + + +.EXAMPLE +> +$packageName= 'bob' +$toolsDir = "$(Split-Path -Parent $MyInvocation.MyCommand.Definition)" +$fileLocation = Join-Path $toolsDir 'someinstaller.msi' + +$packageArgs = @{ + packageName = $packageName + fileType = 'msi' + file = $fileLocation + silentArgs = "/qn /norestart MSIPROPERTY=`"true`"" + validExitCodes= @(0, 3010, 1641) + softwareName = 'Bob*' +} + +Install-ChocolateyInstallPackage @packageArgs + +.EXAMPLE +> +$packageName= 'bob' +$toolsDir = "$(Split-Path -Parent $MyInvocation.MyCommand.Definition)" +$fileLocation = Join-Path $toolsDir 'someinstaller.msi' +$mstFileLocation = Join-Path $toolsDir 'transform.mst' + +$packageArgs = @{ + packageName = $packageName + fileType = 'msi' + file = $fileLocation + silentArgs = "/qn /norestart TRANSFORMS=`"$mstFileLocation`"" + validExitCodes= @(0, 3010, 1641) + softwareName = 'Bob*' +} + +Install-ChocolateyInstallPackage @packageArgs + + +.EXAMPLE +Install-ChocolateyInstallPackage 'bob' 'exe' '/S' "$(Split-Path -Parent $MyInvocation.MyCommand.Definition)\bob.exe" + +.EXAMPLE +> +Install-ChocolateyInstallPackage -PackageName 'bob' -FileType 'exe' ` + -SilentArgs '/S' ` + -File "$(Split-Path -Parent $MyInvocation.MyCommand.Definition)\bob.exe" ` + -ValidExitCodes @(0) + +.LINK +Install-ChocolateyPackage + +.LINK +Uninstall-ChocolateyPackage + +.LINK +Get-UninstallRegistryKey + +.LINK +Start-ChocolateyProcessAsAdmin +*/ + [Parameter(Mandatory = true, Position = 0)] + public string PackageName { get; set; } = string.Empty; + + [Parameter(Position = 1)] + [Alias("InstallerType", "InstallType")] + public string FileType { get; set; } = "exe"; + + [Parameter(Position = 2)] + [Alias("SilentArgs")] + public string[] SilentArguments { get; set; } = Array.Empty(); + + [Parameter(Position = 3)] + [Alias("FileFullPath")] + public string File { get; set; } + + [Parameter] + [Alias("FileFullPath64")] + public string File64 { get; set; } + + [Parameter] + public int[] ValidExitCodes { get; set; } = new[] { 0 }; + + [Parameter] + [Alias("UseOnlyPackageSilentArgs")] + public SwitchParameter UseOnlyPackageSilentArguments { get; set; } + + protected override void End() + { + WindowsInstallerHelper.Install( + this, + PackageName, + File, + File64, + FileType, + SilentArguments, + UseOnlyPackageSilentArguments.ToBool(), + ValidExitCodes, + PipelineStopToken); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/InstallChocolateyPackageCommand.cs b/src/Chocolatey.PowerShell/Commands/InstallChocolateyPackageCommand.cs new file mode 100644 index 000000000..f1b662437 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/InstallChocolateyPackageCommand.cs @@ -0,0 +1,123 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; + +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Install, "ChocolateyPackage")] + public class InstallChocolateyPackageCommand : ChocolateyCmdlet + { + [Parameter(Mandatory = true, Position = 0)] + public string PackageName { get; set; } = string.Empty; + + [Parameter(Position = 1)] + [Alias("InstallerType", "InstallType")] + public string FileType { get; set; } = "exe"; + + [Parameter(Position = 2)] + public string[] SilentArgs { get; set; } = new[] { string.Empty }; + + [Parameter(Position = 3)] + public string Url { get; set; } = string.Empty; + + [Parameter(Position = 4)] + [Alias("Url64")] + public string Url64 { get; set; } = string.Empty; + + [Parameter] + public int[] ValidExitCodes { get; set; } = new int[] { 0 }; + + [Parameter] + public string Checksum { get; set; } = string.Empty; + + [Parameter] + public ChecksumType ChecksumType { get; set; } + + [Parameter] + public string Checksum64 { get; set; } = string.Empty; + + [Parameter] + public ChecksumType ChecksumType64 { get; set; } + + [Parameter] + public Hashtable Options { get; set; } = new Hashtable + { + { "Headers", new Hashtable() } + }; + + [Parameter] + [Alias("FileFullPath", "File")] + public string Path { get; set; } = string.Empty; + + [Parameter] + [Alias("FileFullPath64", "File64")] + public string Path64 { get; set; } = string.Empty; + + [Parameter] + [Alias("UseOnlyPackageSilentArgs")] + public SwitchParameter UseOnlyPackageSilentArguments { get; set; } + + [Parameter] + public SwitchParameter UseOriginalLocation { get; set; } + + [Parameter] + public ScriptBlock BeforeInstall { get; set; } + + protected override void End() + { + var silentArgs = string.Join(" ", SilentArgs); + var chocoTempDir = EnvironmentVariable("TEMP"); + var chocoPackageName = EnvironmentVariable(EnvironmentVariables.ChocolateyPackageName); + + var tempDir = CombinePaths(this, chocoTempDir, chocoPackageName); + + var version = EnvironmentVariable(EnvironmentVariables.ChocolateyPackageVersion); + if (!string.IsNullOrEmpty(version)) + { + tempDir = CombinePaths(this, tempDir, version); + } + + tempDir = tempDir.Replace("\\chocolatey\\chocolatey\\", "\\chocolatey\\"); + if (!ItemExists(this, tempDir)) + { + NewDirectory(this, tempDir); + } + + var downloadFilePath = CombinePaths(this, tempDir, $"{PackageName}Install.{FileType}"); + + var url = string.IsNullOrEmpty(Url) ? Path : Url; + var url64 = string.IsNullOrEmpty(Url64) ? Path64 : Url64; + + var filePath = downloadFilePath; + if (UseOriginalLocation) + { + filePath = url; + if (ArchitectureWidth.Matches(64)) + { + var forceX86 = EnvironmentVariable(EnvironmentVariables.ChocolateyForceX86); + if (ConvertTo(forceX86)) + { + WriteDebug("User specified '-x86' so forcing 32-bit"); + } + else + { + if (!string.IsNullOrEmpty(url64)) + { + filePath = url64; + } + } + } + } + else + { + // TODO: finsih this code after web code is done + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/InstallChocolateyPowerShellCommandCommand.cs b/src/Chocolatey.PowerShell/Commands/InstallChocolateyPowerShellCommandCommand.cs new file mode 100644 index 000000000..b41dcd7b0 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/InstallChocolateyPowerShellCommandCommand.cs @@ -0,0 +1,235 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Text; + +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Install, "ChocolateyPowerShellCommand")] + public class InstallChocolateyPowerShellCommandCommand : ChocolateyCmdlet + { + /* + .SYNOPSIS + Installs a PowerShell Script as a command + + .DESCRIPTION + This will install a PowerShell script as a command on your system. Like + an executable can be run from a batch redirect, this will do the same, + calling PowerShell with this command and passing your arguments to it. + If you include a url, it will first download the PowerShell file. + + .NOTES + Chocolatey works best when the packages contain the software it is + managing and doesn't require downloads. However most software in the + Windows world requires redistribution rights and when sharing packages + publicly (like on the community feed), maintainers may not have those + aforementioned rights. Chocolatey understands how to work with that, + hence this function. You are not subject to this limitation with + internal packages. + + .INPUTS + None + + .OUTPUTS + None + + .PARAMETER PackageName + The name of the package - while this is an arbitrary value, it's + recommended that it matches the package id. + + .PARAMETER PsFileFullPath + Full file path to PowerShell file to turn into a command. If embedding + it in the package next to the install script, the path will be like + `"$(Split-Path -parent $MyInvocation.MyCommand.Definition)\\Script.ps1"` + + `File` and `FileFullPath` are aliases for PsFileFullPath. + + .PARAMETER Url + This is the 32 bit url to download the resource from. This resource can + be used on 64 bit systems when a package has both a Url and Url64bit + specified if a user passes `--forceX86`. If there is only a 64 bit url + available, please remove do not use this parameter (only use Url64bit). + Will fail on 32bit systems if missing or if a user attempts to force + a 32 bit installation on a 64 bit system. + + Prefer HTTPS when available. Can be HTTP, FTP, or File URIs. + + .PARAMETER Url64bit + OPTIONAL - If there is a 64 bit resource available, use this + parameter. Chocolatey will automatically determine if the user is + running a 64 bit OS or not and adjust accordingly. Please note that + the 32 bit url will be used in the absence of this. This parameter + should only be used for 64 bit native software. If the original Url + contains both (which is quite rare), set this to '$url' Otherwise remove + this parameter. + + Prefer HTTPS when available. Can be HTTP, FTP, or File URIs. + + .PARAMETER Checksum + The checksum hash value of the Url resource. This allows a checksum to + be validated for files that are not local. The checksum type is covered + by ChecksumType. + + **NOTE:** Checksums in packages are meant as a measure to validate the + originally intended file that was used in the creation of a package is + the same file that is received at a future date. Since this is used for + other steps in the process related to the community repository, it + ensures that the file a user receives is the same file a maintainer + and a moderator (if applicable), plus any moderation review has + intended for you to receive with this package. If you are looking at a + remote source that uses the same url for updates, you will need to + ensure the package also stays updated in line with those remote + resource updates. You should look into [automatic packaging](https://docs.chocolatey.org/en-us/create/automatic-packages) + to help provide that functionality. + + .PARAMETER ChecksumType + The type of checksum that the file is validated with - valid + values are 'md5', 'sha1', 'sha256' or 'sha512' - defaults to 'md5'. + + MD5 is not recommended as certain organizations need to use FIPS + compliant algorithms for hashing - see + https://support.microsoft.com/en-us/kb/811833 for more details. + + The recommendation is to use at least SHA256. + + .PARAMETER Checksum64 + OPTIONAL if no Url64bit - The checksum hash value of the Url64bit + resource. This allows a checksum to be validated for files that are not + local. The checksum type is covered by ChecksumType64. + + **NOTE:** Checksums in packages are meant as a measure to validate the + originally intended file that was used in the creation of a package is + the same file that is received at a future date. Since this is used for + other steps in the process related to the community repository, it + ensures that the file a user receives is the same file a maintainer + and a moderator (if applicable), plus any moderation review has + intended for you to receive with this package. If you are looking at a + remote source that uses the same url for updates, you will need to + ensure the package also stays updated in line with those remote + resource updates. You should look into [automatic packaging](https://docs.chocolatey.org/en-us/create/automatic-packages) + to help provide that functionality. + + **NOTE:** To determine checksums, you can get that from the original + site if provided. You can also use the [checksum tool available on + the community feed](https://community.chocolatey.org/packages/checksum) (`choco install checksum`) + and use it e.g. `checksum -t sha256 -f path\to\file`. Ensure you + provide checksums for all remote resources used. + + .PARAMETER ChecksumType64 + OPTIONAL - The type of checksum that the file is validated with - valid + values are 'md5', 'sha1', 'sha256' or 'sha512' - defaults to + ChecksumType parameter value. + + MD5 is not recommended as certain organizations need to use FIPS + compliant algorithms for hashing - see + https://support.microsoft.com/en-us/kb/811833 for more details. + + The recommendation is to use at least SHA256. + + .PARAMETER Options + OPTIONAL - Specify custom headers. + + .PARAMETER IgnoredArguments + Allows splatting with arguments that do not apply. Do not use directly. + + .EXAMPLE + > + $psFile = Join-Path $(Split-Path -Parent $MyInvocation.MyCommand.Definition) "Install-WindowsImage.ps1" + Install-ChocolateyPowershellCommand -PackageName 'installwindowsimage.powershell' -PSFileFullPath $psFile + + .EXAMPLE + > + $psFile = Join-Path $(Split-Path -Parent $MyInvocation.MyCommand.Definition) ` + "Install-WindowsImage.ps1" + Install-ChocolateyPowershellCommand ` + -PackageName 'installwindowsimage.powershell' ` + -PSFileFullPath $psFile ` + -PSFileFullPath $psFile ` + -Url 'http://somewhere.com/downloads/Install-WindowsImage.ps1' + + .EXAMPLE + > + $psFile = Join-Path $(Split-Path -Parent $MyInvocation.MyCommand.Definition) ` + "Install-WindowsImage.ps1" + Install-ChocolateyPowershellCommand ` + -PackageName 'installwindowsimage.powershell' ` + -PSFileFullPath $psFile ` + -Url 'http://somewhere.com/downloads/Install-WindowsImage.ps1' ` + -Url64 'http://somewhere.com/downloads/Install-WindowsImagex64.ps1' + + .LINK + Get-ChocolateyWebFile + + .LINK + Install-ChocolateyInstallPackage + + .LINK + Install-ChocolateyPackage + + .LINK + Install-ChocolateyZipPackage + */ + private const string BatchScript = @"@echo off +powershell -NoProfile -ExecutionPolicy unrestricted -Command ""& '{0}' %*"""; + + [Parameter(Position = 0)] + public string PackageName { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + [Alias("PSFileFullPath", "FileFullPath", "File")] + public string Path { get; set; } = string.Empty; + + [Parameter(Position = 2)] + public string Url { get; set; } = string.Empty; + + [Parameter(Position = 3)] + public string Url64Bit { get; set; } = string.Empty; + + [Parameter] + public string Checksum { get; set; } + + [Parameter] + public ChecksumType? ChecksumType { get; set; } + + [Parameter] + public string Checksum64 { get; set; } + + [Parameter] + public ChecksumType? ChecksumType64 { get; set; } + + [Parameter] + public Hashtable Options { get; set; } = new Hashtable + { + { "Headers", new Hashtable() }, + }; + + protected override void End() + { + if (!string.IsNullOrEmpty(Url)) + { + // TODO: Call into GetChocolateyWebFile + } + + var chocolateyPackageName = EnvironmentVariable(EnvironmentVariables.ChocolateyPackageName); + if (!string.IsNullOrEmpty(chocolateyPackageName) && chocolateyPackageName == EnvironmentVariable(EnvironmentVariables.ChocolateyInstallDirectoryPackage)) + { + WriteWarning("Install Directory override not available for PowerShell command packages."); + } + + var binPath = CombinePaths(this,ChocolateyInstallLocation, "bin"); + + var fullPath = GetFullPath(this,Path); + var commandName = System.IO.Path.GetFileNameWithoutExtension(fullPath); + var batchFileName = CombinePaths(this,binPath, $"{commandName}.bat"); + + WriteHost($"Adding {batchFileName} and pointing it to powershell command {fullPath}"); + SetContent(this, batchFileName, string.Format(BatchScript, fullPath), Encoding.ASCII); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/NewShimCommand.cs b/src/Chocolatey.PowerShell/Commands/NewShimCommand.cs new file mode 100644 index 000000000..d06cf266e --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/NewShimCommand.cs @@ -0,0 +1,231 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Management.Automation; +using System.Text; + +using static Chocolatey.PowerShell.Helpers.PSHelper; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsCommon.New, "Shim")] + public class NewShimCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +Creates a shim (or batch redirect) for a file that is on the PATH. + +.DESCRIPTION +Chocolatey installs have the folder `$($env:ChocolateyInstall)\bin` +included in the PATH environment variable. Chocolatey automatically +shims executables in package folders that are not explicitly ignored, +putting them into the bin folder (and subsequently onto the PATH). + +When you have other files you want to shim to add them to the PATH or +if you want to handle the shimming explicitly, use this function. + +If you do use this function, ensure you also add `Uninstall-BinFile` to +your `chocolateyUninstall.ps1` script as Chocolatey will not +automatically clean up shims created with this function. + +.NOTES +Not normally needed for exe files in the package folder, those are +automatically discovered and added as shims after the install script +completes. + +.INPUTS +None + +.OUTPUTS +None + +.PARAMETER Name +The name of the redirect file, will have .exe appended to it. + +.PARAMETER Path +The path to the original file. Can be relative from +`$($env:ChocolateyInstall)\bin` back to your file or a full path to the +file. + +.PARAMETER UseStart +This should be passed if the shim should not wait on the action to +complete. This is usually the case with GUI apps, you don't want the +command shell blocked waiting for the GUI app to be shut back down. + +.PARAMETER Command +OPTIONAL - This is any additional command arguments you want passed +every time to the command. This is not normally used, but may be +necessary if you are calling something and then your application. For +example if you are calling Java with your JAR, the command would be the +JAR file plus any other options to start Java appropriately. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.LINK +Uninstall-BinFile + +.LINK +Install-ChocolateyShortcut + +.LINK +Install-ChocolateyPath + */ + + [Parameter(Mandatory = true, Position = 0)] + public string Name { get; set; } = string.Empty; + + [Parameter(Mandatory = true, Position = 1)] + public string Path { get; set; } = string.Empty; + + [Parameter] + [Alias("IsGui")] + public SwitchParameter UseStart { get; set; } + + [Parameter] + public string Command { get; set; } = string.Empty; + + protected override void End() + { + var nugetPath = ChocolateyInstallLocation; + var nugetExePath = CombinePaths(this, nugetPath, "bin"); + + var packageBashFileName = CombinePaths(this, nugetExePath, Name); + var packageBatchFileName = packageBashFileName + ".bat"; + var packageShimFileName = packageBashFileName + ".exe"; + + if (ItemExists(this, packageBatchFileName)) + { + RemoveItem(this, packageBatchFileName); + } + + if (ItemExists(this, packageBashFileName)) + { + RemoveItem(this, packageBashFileName); + } + + var path = Path.ToLower().Replace(nugetPath.ToLower(), @"..\").Replace(@"\\", @"\"); + + var shimGenArgs = new StringBuilder().AppendFormat("-o \"{0}\" -p \"{1}\" -i \"{2}\"", packageShimFileName, path, Path); + + if (!string.IsNullOrEmpty(Command)) + { + shimGenArgs.AppendFormat(" -c {0}", Command); + } + + if (UseStart) + { + shimGenArgs.Append(" -gui"); + } + + if (Debug) + { + shimGenArgs.Append(" -debug"); + } + + var shimGenPath = CombinePaths(this, ChocolateyInstallLocation, @"tools\shimgen.exe"); + if (!ItemExists(this, shimGenPath)) + { + EnvironmentHelper.UpdateSession(this); + shimGenPath = CombinePaths(this, EnvironmentVariable(EnvironmentVariables.ChocolateyInstall), @"tools\shimgen.exe"); + } + + shimGenPath = GetFullPath(this, shimGenPath); + + var args = shimGenArgs.ToString(); + + WriteDebug($"ShimGen found at '{shimGenPath}'"); + WriteDebug($"Calling {shimGenPath} {args}"); + + if (ItemExists(this, shimGenPath)) + { + var process = new Process + { + StartInfo = new ProcessStartInfo(shimGenPath, args) + { + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + } + }; + + // TODO: use the helper class to do this bit tbh + process.Start(); + process.WaitForExit(); + } + + if (ItemExists(this, packageShimFileName)) + { + WriteHost($"Added {packageShimFileName} shim pointed to '{path}'."); + } + else + { + WriteWarning("An error occurred generating shim, using old method."); + CreateScriptShims(this, Name, path, packageBatchFileName, packageBashFileName, UseStart); + } + } + + private static void CreateScriptShims( + PSCmdlet cmdlet, + string name, + string exePath, + string batchFileName, + string bashFileName, + bool useStart) + { + var path = $"%DIR%{exePath}"; + var pathBash = path.Replace(@"%DIR%..\", @"$DIR/../").Replace(@"\", "/"); + + PSHelper.WriteHost(cmdlet, $"Adding {batchFileName} and pointing to '{path}'."); + PSHelper.WriteHost(cmdlet, $"Adding {bashFileName} and pointing to '{path}'."); + + if (useStart) + { + PSHelper.WriteHost(cmdlet, $"Setting up {name} as a non-command line application."); + SetContent( + cmdlet, + batchFileName, + string.Format(BatchFileContentWithStart, path), + Encoding.ASCII); + + SetContent( + cmdlet, + bashFileName, + string.Format(BashFileContentWithStart, pathBash)); + } + else + { + SetContent( + cmdlet, + batchFileName, + string.Format(BatchFileContent, path), + Encoding.ASCII); + + SetContent( + cmdlet, + bashFileName, + string.Format(BashFileContent, pathBash)); + } + } + + private const string BatchFileContentWithStart = @" +@echo off +SET DIR=%~dp0% +start """" ""{0}"" %* +"; + + private const string BashFileContentWithStart = @"#!/bin/sh`nDIR=`${{0%/*}}`n""{0}"" ""`$@"" &`n"; + + private const string BatchFileContent = @" +@echo off +SET DIR=%~dp0% +cmd /c """"{0}"" %*"" +exit /b %ERRORLEVEL% +"; + private const string BashFileContent = @"#!/bin/sh`nDIR=`${{0%/*}}`n""{0}"" ""`$@""`nexit `$?`n"; + } +} diff --git a/src/Chocolatey.PowerShell/Commands/Resources.txt b/src/Chocolatey.PowerShell/Commands/Resources.txt new file mode 100644 index 000000000..cba9d9d45 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/Resources.txt @@ -0,0 +1,34 @@ +Command Precedence +https://technet.microsoft.com/en-us/library/hh848304.aspx?f=255&MSPPError=-2147217396 +https://technet.microsoft.com/en-us/library/hh848304(v=wps.620).aspx + + Windows PowerShell uses the following + precedence order when it runs commands: + + 1. Alias + 2. Function + 3. Cmdlet + 4. Native Windows commands + +Building a Cmdlet +http://blogs.technet.com/b/heyscriptingguy/archive/tags/windows+powershell/guest+blogger/sean+kearney/build+your+own+cmdlet/ + +C# Cmdlets +https://msdn.microsoft.com/en-us/library/ff602031(v=vs.85).aspx + +http://www.powershellmagazine.com/2014/03/18/writing-a-powershell-module-in-c-part-1-the-basics/ +http://www.powershellmagazine.com/2014/04/08/basics-of-writing-a-powershell-module-with-c-part-2-debugging/ +http://infoworks.tv/howto-write-a-powershell-module-or-function-in-c-with-visual-studio-2012/ + +POSH v2 issues? +http://kungfukode.blogspot.it/2011/05/write-your-first-powershell-cmdlet-in-c.html + +Writing a PowerShell Module +https://msdn.microsoft.com/en-us/library/dd878310%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + +Aliases +https://msdn.microsoft.com/en-us/library/ms714640(v=vs.85).aspx +https://technet.microsoft.com/en-us/library/ms714640(v=vs.85).aspx + +Programmatically creating aliases +http://stackoverflow.com/a/13584769/18475 \ No newline at end of file diff --git a/src/Chocolatey.PowerShell/Commands/StartChocolateyProcessCommand.cs b/src/Chocolatey.PowerShell/Commands/StartChocolateyProcessCommand.cs new file mode 100644 index 000000000..42e184d40 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/StartChocolateyProcessCommand.cs @@ -0,0 +1,167 @@ +using Chocolatey.PowerShell; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.IO; +using System.Management.Automation; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsLifecycle.Start, "ChocolateyProcess")] + public class StartChocolateyProcessCommand : ChocolateyCmdlet + { + //.SYNOPSIS + //** NOTE:** Administrative Access Required. + // + //Runs a process with administrative privileges.If `-ExeToRun` is not + //specified, it is run with PowerShell. + // + //.NOTES + //This command will assert UAC/Admin privileges on the machine. + // + //Will automatically call Set-PowerShellExitCode to set the package exit + //code in the following ways: + // + //- 4 if the binary turns out to be a text file. + //- The same exit code returned from the process that is run.If a 3010 is returned, it will set 3010 for the package. + // + //Aliases `Start-ChocolateyProcess` and `Invoke-ChocolateyProcess`. + // + //.INPUTS + // None + // + //.OUTPUTS + //None + // + //.PARAMETER Statements + //Arguments to pass to `ExeToRun` or the PowerShell script block to be + //run. + // + //.PARAMETER ExeToRun + //The executable/application/installer to run.Defaults to `'powershell'`. + // + //.PARAMETER Elevated + //Indicate whether the process should run elevated. + // + //.PARAMETER Minimized + //Switch indicating if a Windows pops up (if not called with a silent + //argument) that it should be minimized. + // + //.PARAMETER NoSleep + //Used only when calling PowerShell - indicates the window that is opened + //should return instantly when it is complete. + // + //.PARAMETER ValidExitCodes + //Array of exit codes indicating success.Defaults to `@(0)`. + // + //.PARAMETER WorkingDirectory + //The working directory for the running process.Defaults to + //`Get-Location`. If current location is a UNC path, uses + //`$env:TEMP` for default. + // + //.PARAMETER SensitiveStatements + //Arguments to pass to `ExeToRun` that are not logged. + // + //Note that only licensed versions of Chocolatey provide a way to pass + //those values completely through without having them in the install + //script or on the system in some way. + // + //.PARAMETER IgnoredArguments + //Allows splatting with arguments that do not apply. Do not use directly. + // + //.EXAMPLE + //Start-ChocolateyProcessAsAdmin -Statements "$msiArgs" -ExeToRun 'msiexec' + // + //.EXAMPLE + //Start-ChocolateyProcessAsAdmin -Statements "$silentArgs" -ExeToRun $file + // + //.EXAMPLE + //Start-ChocolateyProcessAsAdmin -Statements "$silentArgs" -ExeToRun $file -ValidExitCodes @(0,21) + // + //.EXAMPLE + //> + //# Run PowerShell statements + //$psFile = Join-Path "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" 'someInstall.ps1' + //Start-ChocolateyProcessAsAdmin "& `'$psFile`'" + // + //.EXAMPLE + //# This also works for cmd and is required if you have any spaces in the paths within your command + //$appPath = "$env:ProgramFiles\myapp" + //$cmdBatch = "/c `"$appPath\bin\installmyappservice.bat`"" + //Start-ChocolateyProcessAsAdmin $cmdBatch cmd + //# or more explicitly + //Start-ChocolateyProcessAsAdmin -Statements $cmdBatch -ExeToRun "cmd.exe" + // + //.LINK + //Install-ChocolateyPackage + // + //.LINK + //Install-ChocolateyInstallPackage + + private StartChocolateyProcessHelper _helper; + + [Parameter] + [Alias("Statements")] + public string[] Arguments { get; set; } = Array.Empty(); + + [Parameter] + [Alias("exeToRun")] + public string ProcessName { get; set; } = "powershell"; + + [Parameter] + public SwitchParameter Elevated { get; set; } + + [Parameter] + public SwitchParameter Minimized { get; set; } + + [Parameter] + public SwitchParameter NoSleep { get; set; } + + [Parameter] + public int[] ValidExitCodes { get; set; } = new int[] { 0 }; + + [Parameter] + public string WorkingDirectory { get; set; } = string.Empty; + + [Parameter] + public string SensitiveStatements { get; set; } + + protected override void Begin() + { + if (MyInvocation.InvocationName == "Start-ChocolateyProcessAsAdmin") + { + Elevated = true; + } + } + + protected override void End() + { + var arguments = Arguments is null + ? null + : string.Join(" ", Arguments); + + try + { + _helper = new StartChocolateyProcessHelper(this, PipelineStopToken, ProcessName); + var exitCode = _helper.Start(WorkingDirectory, arguments, SensitiveStatements, Elevated.IsPresent, Minimized.IsPresent, NoSleep.IsPresent, ValidExitCodes); + WriteObject(exitCode); + } + catch (FileNotFoundException notFoundEx) + { + ThrowTerminatingError(new ErrorRecord(notFoundEx, ErrorId, ErrorCategory.ObjectNotFound, ProcessName)); + } + catch (Exception ex) + { + ThrowTerminatingError(new ErrorRecord(ex, ErrorId, ErrorCategory.NotSpecified, ProcessName)); + } + finally + { + } + } + + protected override void Stop() + { + _helper?.CancelWait(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/TestProcessRunningAsAdminCommand.cs b/src/Chocolatey.PowerShell/Commands/TestProcessRunningAsAdminCommand.cs new file mode 100644 index 000000000..dc49c5e11 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/TestProcessRunningAsAdminCommand.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System; +using System.Management.Automation; +using System.Security.Principal; +using Chocolatey.PowerShell; +using Chocolatey.PowerShell.Shared; +using Chocolatey.PowerShell.Helpers; + +namespace Chocolatey.PowerShell.Commands +{ + [Cmdlet(VerbsDiagnostic.Test, "ProcessRunningAsAdmin")] + [OutputType(typeof(bool))] + public class TestProcessRunningAsAdminCommand : ChocolateyCmdlet + { + // .SYNOPSIS + // Tests whether the current process is running with administrative rights. + // + // .DESCRIPTION + // This function checks whether the current process has administrative + // rights by checking if the current user identity is a member of the + // Administrators group. It returns `$true` if the current process is + // running with administrative rights, `$false` otherwise. + // + // On Windows Vista and later, with UAC enabled, the returned value + // represents the actual rights available to the process, e.g. if it + // returns `$true`, the process is running elevated. + // + // .INPUTS + // None + // + // .OUTPUTS + // System.Boolean + protected override void End() + { + var result = ProcessInformation.IsElevated(); + + WriteDebug($"Test-ProcessRunningAsAdmin: returning {result}"); + + WriteObject(result); + } + } +} diff --git a/src/Chocolatey.PowerShell/Extensions/DoubleExtensions.cs b/src/Chocolatey.PowerShell/Extensions/DoubleExtensions.cs new file mode 100644 index 000000000..aad01ef57 --- /dev/null +++ b/src/Chocolatey.PowerShell/Extensions/DoubleExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Chocolatey.PowerShell.Extensions +{ + internal static class DoubleExtensions + { + internal static string AsFileSizeString(this double size) + { + var units = new[] { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB" }; + foreach (var unit in units) + { + if (size < 1024) + { + return string.Format("{0:0.##} {1}", size, unit); + } + + size /= 1024; + } + + return string.Format("{0:0.##} YB", size); + } + } +} diff --git a/src/Chocolatey.PowerShell/Extensions/StringExtensions.cs b/src/Chocolatey.PowerShell/Extensions/StringExtensions.cs new file mode 100644 index 000000000..9008d60d5 --- /dev/null +++ b/src/Chocolatey.PowerShell/Extensions/StringExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Security; +using System.Text; + +namespace Chocolatey.PowerShell.Extensions +{ + public static class StringExtensions + { + /// + /// Takes a string and returns a secure string + /// + /// The input. + /// + public static SecureString ToSecureStringSafe(this string input) + { + var secureString = new SecureString(); + + if (string.IsNullOrWhiteSpace(input)) return secureString; + + foreach (char character in input) + { + secureString.AppendChar(character); + } + + return secureString; + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/ArchitectureWidth.cs b/src/Chocolatey.PowerShell/Helpers/ArchitectureWidth.cs new file mode 100644 index 000000000..2413a76f6 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/ArchitectureWidth.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Chocolatey.PowerShell.Helpers +{ + internal static class ArchitectureWidth + { + internal static int Get() + { + return Environment.Is64BitProcess ? 64 : 32; + } + + internal static bool Matches(int compareTo) + { + return Get() == compareTo; + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/CancellableSleepHelper.cs b/src/Chocolatey.PowerShell/Helpers/CancellableSleepHelper.cs new file mode 100644 index 000000000..f6687386f --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/CancellableSleepHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Chocolatey.PowerShell.Helpers +{ + internal class CancellableSleepHelper : IDisposable + { + private readonly object _lock = new object(); + + private bool _disposed; + private bool _stopping; + private ManualResetEvent _waitHandle; + + public void Dispose() + { + if (!_disposed) + { + _waitHandle.Dispose(); + _waitHandle = null; + } + + _disposed = true; + } + + // Call from Cmdlet.StopProcessing() + internal void Cancel() + { + _stopping = true; + _waitHandle?.Set(); + } + + internal void Sleep(int milliseconds) + { + lock (_lock) + { + if (!_stopping) + { + _waitHandle = new ManualResetEvent(false); + } + } + + _waitHandle?.WaitOne(milliseconds, true); + } + + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/EnvironmentHelper.cs b/src/Chocolatey.PowerShell/Helpers/EnvironmentHelper.cs index d57affe7d..60fee418a 100644 --- a/src/Chocolatey.PowerShell/Helpers/EnvironmentHelper.cs +++ b/src/Chocolatey.PowerShell/Helpers/EnvironmentHelper.cs @@ -51,6 +51,16 @@ public static string GetVariable(PSCmdlet cmdlet, string name, EnvironmentVariab return GetVariable(cmdlet, name, scope, preserveVariables: false); } + /// + /// Get an environment variable from the current process scope by name. + /// + /// The name of the variable to retrieve. + /// The value of the environment variable. + public static string GetVariable(string name) + { + return Environment.GetEnvironmentVariable(name); + } + /// /// Gets the value of the environment variable with the target . /// @@ -138,6 +148,17 @@ public static string[] GetVariableNames(EnvironmentVariableTarget scope) } } + + /// + /// Sets the value of an environment variable for the current process only. + /// + /// The name of the environment variable to set. + /// The value to set the environment variable to. + public static void SetVariable(string name, string value) + { + Environment.SetEnvironmentVariable(name, value); + } + /// /// Sets the value of an environment variable for the current process only. /// diff --git a/src/Chocolatey.PowerShell/Helpers/PSHelper.cs b/src/Chocolatey.PowerShell/Helpers/PSHelper.cs index 9463dfe94..14005bd1c 100644 --- a/src/Chocolatey.PowerShell/Helpers/PSHelper.cs +++ b/src/Chocolatey.PowerShell/Helpers/PSHelper.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; +using System.Linq; using System; using System.Management.Automation; using System.Reflection; @@ -117,6 +118,17 @@ public static T ConvertTo(object value) return (T)LanguagePrimitives.ConvertTo(value, typeof(T)); } + public static void CopyFile(PSCmdlet cmdlet, string source, string destination, bool overwriteExisting) + { + cmdlet.InvokeProvider.Item.Copy(path: new string[] { source }, destinationPath: destination, recurse: false, copyContainers: CopyContainers.CopyTargetContainer, force: overwriteExisting, literalPath: true); + } + + public static void DeleteFile(PSCmdlet cmdlet, string path) + { + cmdlet.InvokeProvider.Item.Remove(path, false); + } + + /// /// Checks for the existence of the target , creating it if it doesn't exist. /// @@ -130,6 +142,68 @@ public static void EnsureDirectoryExists(PSCmdlet cmdlet, string directory) } } + public static Collection GetChildItem(PSCmdlet cmdlet, string[] path, bool recurse, bool force, bool literalPath) + { + return cmdlet.InvokeProvider.ChildItem.Get(path, recurse, force, literalPath); + } + + public static Collection GetChildItem(PSCmdlet cmdlet, string path, bool recurse) + { + return cmdlet.InvokeProvider.ChildItem.Get(path, recurse); + } + + public static Collection GetChildItem(PSCmdlet cmdlet, string path) + { + return GetChildItem(cmdlet, path, recurse: false); + } + + public static string GetCurrentDirectory(PSCmdlet cmdlet) + { + return cmdlet.SessionState.Path.CurrentFileSystemLocation?.ToString(); + } + + public static string GetDirectoryName(PSCmdlet cmdlet, string path) + { + return cmdlet.SessionState.Path.ParseChildName(GetParentDirectory(cmdlet, path)); + } + + public static FileInfo GetFileInfoFor(PSCmdlet cmdlet, string path) + { + return (FileInfo)cmdlet.InvokeProvider.Item.Get(path).FirstOrDefault()?.BaseObject; + } + + public static string GetFullPath(PSCmdlet cmdlet, string path) + { + return string.IsNullOrWhiteSpace(path) + ? string.Empty + : CombinePaths(cmdlet, GetParentDirectory(cmdlet, path), GetFileName(path)); + } + + public static Collection GetItem(PSCmdlet cmdlet, string path, bool force, bool literalPath) + { + return cmdlet.InvokeProvider.Item.Get(new[] { path }, force, literalPath); + } + + public static Collection GetItem(PSCmdlet cmdlet, string path) + { + return GetItem(cmdlet, path, force: false, literalPath: false); + } + + public static Collection GetItem(PSCmdlet cmdlet, string path, bool literalPath) + { + return GetItem(cmdlet, path, force: false, literalPath); + } + + public static FileInfo GetFileInfo(PSCmdlet cmdlet, string path) + { + return ConvertTo(GetItem(cmdlet, path).FirstOrDefault()); + } + + public static FileInfo GetFileInfo(PSCmdlet cmdlet, string path, bool literalPath) + { + return ConvertTo(GetItem(cmdlet, path, literalPath).FirstOrDefault()); + } + /// /// Test the equality of two values, based on PowerShell's equality checks, case insensitive for string values. /// Equivalent to -eq in PowerShell. @@ -233,6 +307,101 @@ public static Version GetPSVersion() return result ?? new Version(2, 0); } + public static bool IsLike(string value, string pattern) + { + return new WildcardPattern(pattern, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant).IsMatch(value); + } + + public static string Replace(string input, string pattern, string replacement, bool caseSensitive) + { + return Regex.Replace(input, pattern, replacement, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase); + } + + public static string Replace(string input, string pattern, string replacement) + { + return Replace(input, pattern, replacement, caseSensitive: false); + } + + public static void RemoveItem(PSCmdlet cmdlet, string path) + { + cmdlet.InvokeProvider.Item.Remove(path, recurse: false); + } + + public static void RemoveItem(PSCmdlet cmdlet, string path, bool recurse) + { + cmdlet.InvokeProvider.Item.Remove(path, recurse); + } + + public static void RemoveItem(PSCmdlet cmdlet, string[] path, bool recurse, bool force, bool literalPath) + { + cmdlet.InvokeProvider.Item.Remove(path, recurse, force, literalPath); + } + + public static void SetContent(PSCmdlet cmdlet, string path, string content, Encoding encoding) + { + var fullPath = GetFullPath(cmdlet, path); + + using (var stream = File.OpenWrite(fullPath)) + using (var writer = new StreamWriter(stream, encoding)) + { + WriteContent(writer, content); + } + } + + public static void SetContent(PSCmdlet cmdlet, string path, string content) + { + var fullPath = GetFullPath(cmdlet, path); + + using (var writer = new StreamWriter(fullPath)) + { + WriteContent(writer, content); + } + } + + private static void WriteContent(StreamWriter writer, string content) + { + writer.Write(content); + writer.Flush(); + writer.Close(); + writer.Dispose(); + } + + public static void SetExitCode(PSCmdlet cmdlet, int exitCode) + { + Environment.SetEnvironmentVariable(EnvironmentVariables.ChocolateyPackageExitCode, exitCode.ToString()); + cmdlet.Host.SetShouldExit(exitCode); + } + + private static void WriteConsoleLine(string message, ConsoleColor color) + { + var oldColor = Console.ForegroundColor; + Console.ForegroundColor = color; + + try + { + Console.WriteLine(message); + } + finally + { + Console.ForegroundColor = oldColor; + } + } + + public static void WriteDebug(PSCmdlet cmdlet, string message) + { + cmdlet.WriteDebug(message); + } + + public static void WriteVerbose(PSCmdlet cmdlet, string message) + { + cmdlet.WriteVerbose(message); + } + + public static void WriteWarning(PSCmdlet cmdlet, string message) + { + cmdlet.WriteWarning(message); + } + /// /// Turns a relative path into a full path based on the current context the is running in, /// without ensuring that the path actually exists. diff --git a/src/Chocolatey.PowerShell/Helpers/Paths.cs b/src/Chocolatey.PowerShell/Helpers/Paths.cs index d1e58f02f..64348d70e 100644 --- a/src/Chocolatey.PowerShell/Helpers/Paths.cs +++ b/src/Chocolatey.PowerShell/Helpers/Paths.cs @@ -162,5 +162,41 @@ void updatePath() } } } + + /// + /// Gets the file path corresponding to the desired . + /// + /// The type of path to retrieve the value for. + /// The requested path as a string. + /// If the provided path type is not implemented. + public static string GetChocolateyPathType(PSCmdlet cmdlet, ChocolateyPathType pathType) + { + switch (pathType) + { + case ChocolateyPathType.PackagePath: + var path = EnvironmentHelper.GetVariable(EnvironmentVariables.ChocolateyPackageFolder); + if (!string.IsNullOrEmpty(path)) + { + return path; + } + + path = EnvironmentHelper.GetVariable(EnvironmentVariables.PackageFolder); + if (!string.IsNullOrEmpty(path)) + { + return path; + } + else + { + var installPath = GetChocolateyPathType(cmdlet, ChocolateyPathType.InstallPath); + var packageName = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPackageName); + + return PSHelper.CombinePaths(cmdlet, installPath, "lib", packageName); + } + case ChocolateyPathType.InstallPath: + return PSHelper.GetInstallLocation(cmdlet); + default: + throw new NotImplementedException($"The path value for type '{pathType}' is not known."); + }; + } } } diff --git a/src/Chocolatey.PowerShell/Helpers/SevenZipHelper.cs b/src/Chocolatey.PowerShell/Helpers/SevenZipHelper.cs new file mode 100644 index 000000000..464c3e139 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/SevenZipHelper.cs @@ -0,0 +1,179 @@ +using System; +using System.Diagnostics; +using System.Management.Automation; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Helpers +{ + public sealed class SevenZipHelper : ProcessHandler + { + private readonly StringBuilder _zipFileList = new StringBuilder(); + private string _destinationFolder = string.Empty; + + private const string ErrorMessageAddendum = "This is most likely an issue with the '$env:chocolateyPackageName' package and not with Chocolatey itself. Please follow up with the package maintainer(s) directly."; + + public SevenZipHelper(PSCmdlet cmdlet, CancellationToken pipelineStopToken) + : base(cmdlet, pipelineStopToken) + { + } + + public int Run7zip(string path, string path64, string packageName, string destination, string specificFolder, bool disableLogging) + { + if (path is null && path64 is null) + { + throw new ArgumentException("Parameters are incorrect; either -Path or -Path64 must be specified."); + } + + var bitnessMessage = string.Empty; + var zipFilePath = path; + packageName = string.IsNullOrEmpty(packageName) + ? Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPackageName) + : packageName; + var zipExtractionLogPath = string.Empty; + + var forceX86 = PSHelper.IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyForceX86), "true"); + if (ArchitectureWidth.Matches(32) || forceX86) + { + if (PSHelper.ConvertTo(path)) + { + Cmdlet.ThrowTerminatingError(new RuntimeException($"32-bit archive is not supported for {packageName}").ErrorRecord); + } + + if (PSHelper.ConvertTo(path64)) + { + bitnessMessage = "32-bit "; + } + } + else if (PSHelper.ConvertTo(path64)) + { + zipFilePath = path64; + bitnessMessage = "64 bit "; + } + + if (PSHelper.ConvertTo(packageName)) + { + var libPath = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPackageFolder); + if (!PSHelper.ContainerExists(Cmdlet, libPath)) + { + PSHelper.NewDirectory(Cmdlet, libPath); + } + + zipExtractionLogPath = PSHelper.CombinePaths(Cmdlet, libPath, $"{PSHelper.GetFileName(zipFilePath)}.txt"); + } + + var envChocolateyPackageName = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPackageName); + var envChocolateyInstallDirectoryPackage = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyInstallDirectoryPackage); + + if (PSHelper.ConvertTo(envChocolateyPackageName) && envChocolateyPackageName == envChocolateyInstallDirectoryPackage) + { + PSHelper.WriteWarning(Cmdlet, "Install Directory override not available for zip packages at this time.\n If this package also runs a native installer using Chocolatey\n functions, the directory will be honored."); + } + + PSHelper.WriteHost(Cmdlet, $"Extracting {bitnessMessage}{zipFilePath} to {destination}..."); + + PSHelper.EnsureDirectoryExists(Cmdlet, destination); + + var exePath = PSHelper.CombinePaths(Cmdlet, PSHelper.GetInstallLocation(Cmdlet), "tools", "7z.exe"); + + if (!PSHelper.ItemExists(Cmdlet, exePath)) + { + EnvironmentHelper.UpdateSession(Cmdlet); + exePath = PSHelper.CombinePaths(Cmdlet, EnvironmentHelper.GetVariable(EnvironmentVariables.ChocolateyInstall), @"tools\7zip.exe"); + } + + exePath = PSHelper.GetFullPath(Cmdlet, exePath); + + PSHelper.WriteDebug(Cmdlet, $"7zip found at '{exePath}'"); + + // 32-bit 7z would not find C:\Windows\System32\config\systemprofile\AppData\Local\Temp, + // because it gets translated to C:\Windows\SysWOW64\... by the WOW redirection layer. + // Replace System32 with sysnative, which does not get redirected. + // 32-bit 7z is required so it can see both architectures + if (ArchitectureWidth.Matches(64)) + { + var systemPath = Environment.GetFolderPath(Environment.SpecialFolder.System); + var sysNativePath = PSHelper.CombinePaths(Cmdlet, EnvironmentHelper.GetVariable("SystemRoot"), "SysNative"); + zipFilePath = PSHelper.Replace(zipFilePath, Regex.Escape(systemPath), sysNativePath); + destination = PSHelper.Replace(destination, Regex.Escape(systemPath), sysNativePath); + } + + var workingDirectory = PSHelper.GetCurrentDirectory(Cmdlet); + if (workingDirectory is null) + { + PSHelper.WriteDebug(Cmdlet, "Unable to use current location for Working Directory. Using Cache Location instead."); + workingDirectory = EnvironmentHelper.GetVariable("TEMP"); + } + + var loggingOption = disableLogging ? "-bb0" : "-bb1"; + + var options = $"x -aoa -bd {loggingOption} -o\"{destination}\" -y \"{zipFilePath}\""; + if (PSHelper.ConvertTo(specificFolder)) + { + options += $" \"{specificFolder}\""; + } + + PSHelper.WriteDebug(Cmdlet, $"Executing command ['{exePath}' {options}]"); + + _destinationFolder = destination; + + var exitCode = StartProcess(exePath, workingDirectory, options, sensitiveStatements: null, elevated: false, ProcessWindowStyle.Hidden, noNewWindow: true); + + PSHelper.SetExitCode(Cmdlet, exitCode); + + if (!(string.IsNullOrEmpty(zipExtractionLogPath) || disableLogging)) + { + PSHelper.SetContent(Cmdlet, zipExtractionLogPath, _zipFileList.ToString(), Encoding.UTF8); + } + + PSHelper.WriteDebug(Cmdlet, $"7z exit code: {exitCode}"); + + if (exitCode != 0) + { + var reason = GetExitCodeReason(exitCode); + // TODO: Replace RuntimeException with more specific exception + Cmdlet.ThrowTerminatingError(new RuntimeException($"{reason} {ErrorMessageAddendum}").ErrorRecord); + } + + EnvironmentHelper.SetVariable(EnvironmentVariables.ChocolateyPackageInstallLocation, destination); + + return exitCode; + } + + private string GetExitCodeReason(int exitCode) + { + switch (exitCode) + { + case 1: + return "Some files could not be extracted."; + case 2: + return "7-Zip encountered a fatal error while extracting the files."; + case 7: + return "7-Zip command line error."; + case 8: + return "7-Zip out of memory."; + case 255: + return "Extraction cancelled by the user."; + default: + return $"7-Zip signalled an unknown error (code {exitCode})"; + }; + } + + protected override void ProcessOutputHandler(object sender, DataReceivedEventArgs e) + { + if (!(e.Data is null)) + { + var line = e.Data; + + ProcessMessages?.Add((line, false)); + + if (line.StartsWith("- ")) + { + _zipFileList.AppendLine(_destinationFolder + '\\' + line.Substring(2)); + } + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/StartChocolateyProcessHelper.cs b/src/Chocolatey.PowerShell/Helpers/StartChocolateyProcessHelper.cs new file mode 100644 index 000000000..c97949185 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/StartChocolateyProcessHelper.cs @@ -0,0 +1,240 @@ +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Chocolatey.PowerShell.Helpers +{ + internal class StartChocolateyProcessHelper : ProcessHandler + { + const string ErrorId = "StartChocolateyProcessError"; + + private static readonly int[] _successExitCodes = { 0, 1605, 1614, 1641, 3010 }; + + private string _processName; + + internal StartChocolateyProcessHelper(PSCmdlet cmdlet, CancellationToken pipelineStopToken, string processName = "powershell") + : base(cmdlet, pipelineStopToken) + { + _processName = processName; + } + + internal int Start(string arguments) + { + return Start(arguments, validExitCodes: null); + } + + internal int Start(string arguments, int[] validExitCodes) + { + return Start(arguments, workingDirectory: null, validExitCodes); + } + + internal int Start(string arguments, string workingDirectory, int[] validExitCodes) + { + return Start(workingDirectory, arguments, sensitiveStatements: null, elevated: true, minimized: false, noSleep: false, validExitCodes); + } + + internal int Start(string workingDirectory, string arguments, string sensitiveStatements, bool elevated, bool minimized, bool noSleep, int[] validExitCodes = null) + { + if (validExitCodes is null) + { + validExitCodes = new[] { 0 }; + } + + if (string.IsNullOrWhiteSpace(workingDirectory)) + { + workingDirectory = PSHelper.GetCurrentDirectory(Cmdlet); + if (string.IsNullOrEmpty(workingDirectory)) + { + PSHelper.WriteDebug(Cmdlet, "Unable to use current location for Working Directory. Using Cache Location instead."); + workingDirectory = Environment.GetEnvironmentVariable("TEMP"); + } + } + + var alreadyElevated = ProcessInformation.IsElevated(); + + var debugMessagePrefix = elevated ? "Elevating permissions and running" : "Running"; + + var processName = NormalizeProcessName(_processName); + if (!string.IsNullOrWhiteSpace(arguments)) + { + arguments = arguments.Replace("\0", ""); + } + + if (PSHelper.IsEqual(processName, "powershell")) + { + processName = PSHelper.GetPowerShellLocation(); + var installerModulePath = PSHelper.CombinePaths( + Cmdlet, + PSHelper.GetInstallLocation(Cmdlet), + "helpers", + "chocolateyInstaller.psm1"); + var importChocolateyHelpers = $"Import-Module -Name '{installerModulePath}' -Verbose:$false | Out-Null"; + var statements = string.Format(PowerShellScriptWrapper, noSleep, importChocolateyHelpers, arguments); + + var encodedStatements = Convert.ToBase64String(Encoding.Unicode.GetBytes(statements)); + + arguments = string.Format("-NoLogo -NonInteractive -NoProfile -ExecutionPolicy Bypass -InputFormat Text -OutputFormat Text -EncodedCommand {0}", encodedStatements); + PSHelper.WriteDebug(Cmdlet, $@" +{debugMessagePrefix} powershell block: +{statements} +This may take a while, depending on the statements."); + } + else + { + PSHelper.WriteDebug(Cmdlet, $"{debugMessagePrefix} [\"$exeToRun\" {arguments}]. This may take a while, depending on the statements."); + } + + var exeIsTextFile = PSHelper.GetUnresolvedPath(Cmdlet, processName) + ".istext"; + if (PSHelper.ItemExists(Cmdlet, exeIsTextFile)) + { + PSHelper.SetExitCode(Cmdlet, 4); + Cmdlet.ThrowTerminatingError(new ErrorRecord( + new InvalidOperationException($"The file was a text file but is attempting to be run as an executable - '{processName}'"), + ErrorId, + ErrorCategory.InvalidOperation, + processName)); + } + + if (PSHelper.IsEqual(processName, "msiexec") || PSHelper.IsEqual(processName, "msiexec.exe")) + { + processName = PSHelper.CombinePaths(Cmdlet, Environment.GetEnvironmentVariable("SystemRoot"), "System32\\msiexec.exe"); + } + else if (!PSHelper.ItemExists(Cmdlet, processName)) + { + Cmdlet.WriteWarning($"May not be able to find '{processName}'. Please use full path for executables."); + } + + + var windowStyle = minimized ? ProcessWindowStyle.Minimized : ProcessWindowStyle.Normal; + var exitCode = StartProcess(processName, workingDirectory, arguments, sensitiveStatements, elevated, windowStyle, noNewWindow: false); + var reason = GetExitCodeReason(exitCode); + if (!string.IsNullOrWhiteSpace(reason)) + { + PSHelper.WriteWarning(Cmdlet, reason); + reason = $"Exit code indicates the following: {reason}"; + } + else + { + reason = "See log for possible error messages."; + } + + if (!validExitCodes.Contains(exitCode)) + { + PSHelper.SetExitCode(Cmdlet, exitCode); + // TODO: Replace RuntimeException with custom exception type + Cmdlet.ThrowTerminatingError(new RuntimeException($"Running [\"{processName}\" {arguments}] not successful. Exit code was {exitCode}. {reason}").ErrorRecord); + } + else if (!_successExitCodes.Contains(exitCode)) + { + PSHelper.WriteWarning(Cmdlet, $"Exit code '{exitCode}' was considered valid by script, but not as a normal Chocolatey success code. Returning '0'."); + exitCode = 0; + } + + return exitCode; + } + + private const string PowerShellScriptWrapper = @" +$noSleep = ${0} +{1} +try { + $progressPreference = ""SilentlyContinue"" + {2} + + if (-not $noSleep) { + Start-Sleep 6 + } +} +catch { + if (-not $noSleep) { + Start-Sleep 8 + } + + throw $_ +}"; + + private string GetExitCodeReason(int exitCode) + { + var errorMessageAddendum = $" This is most likely an issue with the '{Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPackageName)}' package and not with Chocolatey itself. Please follow up with the package maintainer(s) directly."; + + switch (exitCode) + { + case 0: + case 1: + case 3010: + return string.Empty; + // NSIS - http://nsis.sourceforge.net/Docs/AppendixD.html + // InnoSetup - http://www.jrsoftware.org/ishelp/index.php?topic=setupexitcodes + case 2: + return "Setup was cancelled."; + case 3: + return "A fatal error occurred when preparing or moving to next install phase. Check to be sure you have enough memory to perform an installation and try again."; + case 4: + return "A fatal error occurred during installation process." + errorMessageAddendum; + case 5: + return "User (you) cancelled the installation.'"; + case 6: + return "Setup process was forcefully terminated by the debugger."; + case 7: + return "While preparing to install, it was determined setup cannot proceed with the installation. Please be sure the software can be installed on your system."; + case 8: + return "While preparing to install, it was determined setup cannot proceed with the installation until you restart the system. Please reboot and try again.'"; + // MSI - https://msdn.microsoft.com/en-us/library/windows/desktop/aa376931.aspx + case 1602: + return "User (you) cancelled the installation."; + case 1603: + return "Generic MSI Error. This is a local environment error, not an issue with a package or the MSI itself - it could mean a pending reboot is necessary prior to install or something else (like the same version is already installed). Please see MSI log if available. If not, try again adding '--install-arguments=\"'/l*v c:\\$($env:chocolateyPackageName)_msi_install.log'\"'. Then search the MSI Log for \"Return Value 3\" and look above that for the error."; + case 1618: + return "Another installation currently in progress. Try again later."; + case 1619: + return "MSI could not be found - it is possibly corrupt or not an MSI at all. If it was downloaded and the MSI is less than 30K, try opening it in an editor like Notepad++ as it is likely HTML." + + errorMessageAddendum; + case 1620: + return "MSI could not be opened - it is possibly corrupt or not an MSI at all. If it was downloaded and the MSI is less than 30K, try opening it in an editor like Notepad++ as it is likely HTML." + + errorMessageAddendum; + case 1622: + return "Something is wrong with the install log location specified. Please fix this in the package silent arguments (or in install arguments you specified). The directory specified as part of the log file path must exist for an MSI to be able to log to that directory." + + errorMessageAddendum; + case 1623: + return "This MSI has a language that is not supported by your system. Contact package maintainer(s) if there is an install available in your language and you would like it added to the packaging."; + case 1625: + return "Installation of this MSI is forbidden by system policy. Please contact your system administrators."; + case 1632: + case 1633: + return "Installation of this MSI is not supported on this platform. Contact package maintainer(s) if you feel this is in error or if you need an architecture that is not available with the current packaging."; + case 1638: + return "This MSI requires uninstall prior to installing a different version. Please ask the package maintainer(s) to add a check in the chocolateyInstall.ps1 script and uninstall if the software is installed." + + errorMessageAddendum; + case 1639: + return "The command line arguments passed to the MSI are incorrect. If you passed in additional arguments, please adjust. Otherwise followup with the package maintainer(s) to get this fixed." + + errorMessageAddendum; + case 1640: + case 1645: + return "Cannot install MSI when running from remote desktop (terminal services). This should automatically be handled in licensed editions. For open source editions, you may need to run change.exe prior to running Chocolatey or not use terminal services."; + } + + return string.Empty; + } + + private string NormalizeProcessName(string processName) + { + if (!string.IsNullOrWhiteSpace(processName)) + { + processName = processName.Replace("\0", string.Empty); + + if (!string.IsNullOrWhiteSpace(processName)) + { + processName = processName.Trim().Trim('"', '\''); + } + } + + return processName; + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/WebHelper.cs b/src/Chocolatey.PowerShell/Helpers/WebHelper.cs new file mode 100644 index 000000000..6d1adb0f7 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/WebHelper.cs @@ -0,0 +1,734 @@ +using Chocolatey.PowerShell.Extensions; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +namespace Chocolatey.PowerShell.Helpers +{ + public class WebHelper + { + private readonly PSCmdlet _cmdlet; + + public WebHelper(PSCmdlet cmdlet) + { + _cmdlet = cmdlet; + } + + protected string GetChocolateyWebFile( + string packageName, + string fileFullPath, + string url, + string url64Bit, + string checksum, + ChecksumType? checksumType, + string checksum64, + ChecksumType? checksumType64, + Hashtable options, + bool getOriginalFileName, + bool forceDownload) + { + // user provided url overrides + var urlOverride = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyUrlOverride); + if (!string.IsNullOrWhiteSpace(urlOverride)) + { + url = urlOverride; + } + + var url64bitOverride = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyUrl64BitOverride); + if (!string.IsNullOrWhiteSpace(url64bitOverride)) + { + url64Bit = url64bitOverride; + } + + if (!string.IsNullOrWhiteSpace(url)) + { + url = url.Replace("//", "/").Replace(":/", "://"); + } + + if (!string.IsNullOrWhiteSpace(url64Bit)) + { + url64Bit = url64Bit.Replace("//", "/").Replace(":/", "://"); + } + + // user provided checksum values + var checksum32Override = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyChecksum32); + if (!string.IsNullOrWhiteSpace(checksum32Override)) + { + checksum = checksum32Override; + } + + var checksumType32Override = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyChecksumType32); + if (Enum.TryParse(checksumType32Override, out var type)) + { + checksumType = type; + } + + var checksum64Override = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyChecksum64); + if (!string.IsNullOrWhiteSpace(checksum64Override)) + { + checksum64 = checksum64Override; + } + + var checksumType64Override = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyChecksumType64); + if (Enum.TryParse(checksumType64Override, out var type64)) + { + checksumType64 = type64; + } + + var checksum32 = checksum; + var checksumType32 = checksumType; + + var is64BitProcess = IntPtr.Size == 8; + var bitWidth = is64BitProcess ? "64" : "32"; + PSHelper.WriteDebug(_cmdlet, $"CPU is {bitWidth} bit"); + + var url32Bit = url; + + // by default do not specify bit package + var bitPackage = string.Empty; + if (!PSHelper.IsEqual(url32Bit, url64Bit) && !string.IsNullOrWhiteSpace(url64Bit)) + { + bitPackage = "32 bit"; + } + + if (is64BitProcess && !string.IsNullOrWhiteSpace(url64Bit)) + { + PSHelper.WriteDebug(_cmdlet, $"Setting url to '{url64Bit}' and bitPackage to '64'."); + bitPackage = "64 bit"; + url = url64Bit; + // only set checksum/checksumType that will be used if the urls are different + if (!PSHelper.IsEqual(url32Bit, url64Bit)) + { + checksum = checksum64; + if (!(checksumType64 is null)) + { + checksumType = checksumType64; + } + } + } + + if (PSHelper.IsEqual(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyForceX86), "true")) + { + PSHelper.WriteDebug(_cmdlet, "User specified '-x86' so forcing 32-bit"); + if (!PSHelper.IsEqual(url32Bit, url64Bit)) + { + bitPackage = "32 bit"; + } + + url = url32Bit; + checksum = checksum32; + checksumType = checksumType32; + } + + // If we're on 32 bit or attempting to force 32 bit and there is no + // 32 bit url, we need to throw an error. + if (string.IsNullOrWhiteSpace(url)) + { + var architecture = string.IsNullOrWhiteSpace(bitPackage) ? "32 bit" : bitPackage; + _cmdlet.ThrowTerminatingError(new RuntimeException($"This package does not support {architecture} architecture.").ErrorRecord); + } + + // determine if url can be SSL/TLS + if (url?.ToLower().StartsWith("http://") == true) + { + try + { + var httpsUrl = url.Replace("http://", "https://"); + var sslHeaders = GetWebHeaders(httpsUrl); + if (sslHeaders.Count != 0) + { + url = httpsUrl; + PSHelper.WriteWarning(_cmdlet, "Url has SSL/TLS available, switching to HTTPS for download."); + } + } + catch (Exception ex) + { + PSHelper.WriteDebug(_cmdlet, $"Url does not have HTTPS available: {ex.Message}"); + } + } + + if (getOriginalFileName) + { + try + { + // remove \chocolatey\chocolatey\ + // Reason: https://github.com/chocolatey/choco/commit/ae2e8571ab9440e715effba9b34c25aceac34dac + fileFullPath = Regex.Replace(fileFullPath, @"\\chocolatey\\chocolatey\\", @"\chocolatey\", RegexOptions.IgnoreCase); + var fileDirectory = PSHelper.GetParentDirectory(_cmdlet, fileFullPath); + var originalFileName = PSHelper.GetFileName(fileFullPath); + fileFullPath = PSHelper.CombinePaths(_cmdlet, fileDirectory, GetWebFileName(url, originalFileName)); + } + catch (Exception ex) + { + PSHelper.WriteHost(_cmdlet, $"Attempt to use original download file name failed for '{url}'"); + PSHelper.WriteDebug(_cmdlet, $" Error was '{ex.Message}'."); + } + } + + try + { + PSHelper.EnsureDirectoryExists(_cmdlet, PSHelper.GetDirectoryName(_cmdlet, fileFullPath)); + } + catch (Exception ex) + { + PSHelper.WriteHost(_cmdlet, $"Attempt to create directory failed for '{url}'"); + PSHelper.WriteDebug(_cmdlet, $" Error was '{ex.Message}'."); + } + + var urlIsRemote = true; + var headers = new Hashtable(); + if (url?.ToLower().StartsWith("http") == true) + { + try + { + headers = GetWebHeaders(url); + } + catch (Exception ex) + { + PSHelper.WriteHost(_cmdlet, $"Attempt to get headers for '{url}' failed.\n {ex.Message}"); + } + + var needsDownload = true; + FileInfo fileInfoCached = null; + if (PSHelper.ItemExists(_cmdlet, fileFullPath)) + { + fileInfoCached = PSHelper.GetFileInfo(_cmdlet, fileFullPath); + } + + if (PSHelper.ItemExists(_cmdlet, fileFullPath) && !forceDownload) + { + if (!string.IsNullOrWhiteSpace(checksum)) + { + PSHelper.WriteHost(_cmdlet, "File appears to be downloaded already. Verifying with package checksum to determine if it needs to be re-downloaded."); + if (ChecksumValidator.IsValid(_cmdlet, fileFullPath, checksum, checksumType, url, out _)) + { + needsDownload = false; + } + else + { + PSHelper.WriteDebug(_cmdlet, "Existing file failed checksum. Will be re-downloaded from url."); + } + } + else if (headers.Count != 0 && headers.ContainsKey("Content-Length")) + { + long fileInfoCachedLength = fileInfoCached?.Length ?? 0; + if (PSHelper.IsEqual(PSHelper.ConvertTo(fileInfoCachedLength), PSHelper.ConvertTo(headers["Content-Length"]))) + { + needsDownload = false; + } + } + } + + if (needsDownload) + { + PSHelper.WriteHost( + _cmdlet, + string.Format( + "Downloading {0} {1}{2} from '{3}'.", + packageName, + bitPackage, + Environment.NewLine, + url)); + GetWebFile(url, fileFullPath, options); + } + else + { + PSHelper.WriteDebug( + _cmdlet, + string.Format( + "{0}'s requested file has already been downloaded. Using cached copy at{1} '{2}'.", + packageName, + Environment.NewLine, + fileFullPath)); + } + } + else if (url?.ToLower().StartsWith("ftp") == true) + { + // ftp the file + PSHelper.WriteHost(_cmdlet, string.Format("Ftp-ing {0}{1} from '{2}'.", packageName, Environment.NewLine, url)); + _cmdlet.InvokeCommand.InvokeScript($"Get-FtpFile -Url '{url}' -FileName '{fileFullPath}'"); + } + else if (!string.IsNullOrWhiteSpace(url)) + { + // copy the file + if (url?.ToLower().StartsWith("file:") == true) + { + var uri = new Uri(url); + url = uri.LocalPath; + } + + PSHelper.WriteHost(_cmdlet, string.Format("Copying {0}{1} from '{2}'", packageName, Environment.NewLine, url)); + PSHelper.CopyFile(_cmdlet, url, fileFullPath, overwriteExisting: true); + urlIsRemote = false; + } + + // give it a sec or two to finish up file operations on the file system + Thread.Sleep(2000); + + // validate the file now exists locally + // If the file exists we should be able to assume that `url` is not null by this point. + if (!PSHelper.ItemExists(_cmdlet, fileFullPath)) + { + throw new FileNotFoundException($"Chocolatey expected a file to be downloaded to '{fileFullPath}', but nothing exists at that location."); + } + + CheckVirusEngineResults(url, fileFullPath); + var fileInfo = PSHelper.GetFileInfo(_cmdlet, fileFullPath); + + if (headers.Count != 0 && string.IsNullOrWhiteSpace(checksum)) + { + long fileInfoLength = fileInfo.Length; + //validate content length since we don't have checksum to validate against + PSHelper.WriteDebug(_cmdlet, $"Checking that '{fileFullPath}' is the size we expect it to be."); + if (headers.ContainsKey("Content-Length") + && !PSHelper.IsEqual(fileInfoLength.ToString(), PSHelper.ConvertTo(headers["Content-Length"]))) + { + _cmdlet.ThrowTerminatingError(new RuntimeException( + string.Format( + "Chocolatey expected a file at '{0}' to be of length '{1}' but the length was '{2}'.", + fileFullPath, + PSHelper.ConvertTo(headers["Content-Length"]), + fileInfoLength.ToString())).ErrorRecord); + } + + if (headers.ContainsKey("X-Checksum-Sha1")) + { + var remoteChecksum = PSHelper.ConvertTo(headers["X-Checksum-Sha1"]); + PSHelper.WriteDebug(_cmdlet, $"Verifying remote checksum of '{remoteChecksum}' for '{fileFullPath}'."); + ChecksumValidator.AssertChecksumValid(_cmdlet, fileFullPath, checksum, ChecksumType.Sha1, url); + } + } + + // skip requirement for embedded files if checksum is not provided + // require checksum check if the url is remote + if (!string.IsNullOrWhiteSpace(checksum) || urlIsRemote) + { + PSHelper.WriteDebug(_cmdlet, $"Verifying package provided checksum of '{checksum}' for '{fileFullPath}'."); + ChecksumValidator.AssertChecksumValid(_cmdlet, fileFullPath, checksum, checksumType, url); + } + + return fileFullPath; + } + + public Hashtable GetWebHeaders(string url, string userAgent = WebResources.DefaultUserAgent) + { + PSHelper.WriteDebug(_cmdlet, $"Running licensed 'Get-WebHeaders' with url:'{url}', userAgent:'{userAgent}'"); + + if (string.IsNullOrEmpty(url)) + { + return new Hashtable(); + } + + //todo compare original url headers to new headers - report when different + + var downloadUrl = GetDownloadUrl(url); + var uri = new Uri(downloadUrl); + var request = (HttpWebRequest)WebRequest.Create(uri); + + // $request.Method = "HEAD" + var webClient = new WebClient(); + var defaultCredentials = CredentialCache.DefaultCredentials; + if (defaultCredentials != null) + { + request.Credentials = defaultCredentials; + webClient.Credentials = defaultCredentials; + } + + var proxy = ProxySettings.GetProxy(_cmdlet, uri); + if (proxy != null) + { + request.Proxy = proxy; + } + + request.Accept = "*/*"; + request.AllowAutoRedirect = true; + request.MaximumAutomaticRedirections = 20; + request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + request.Timeout = 30000; + var chocolateyRequestTimeout = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyRequestTimeout); + if (!string.IsNullOrWhiteSpace(chocolateyRequestTimeout)) + { + PSHelper.WriteDebug(_cmdlet, $"Setting request timeout to '{chocolateyRequestTimeout}'"); + int.TryParse(chocolateyRequestTimeout, out var requestTimeoutInt); + if (requestTimeoutInt <= 0) + { + requestTimeoutInt = 30000; + } + + request.Timeout = requestTimeoutInt; + } + + var chocolateyResponseTimeout = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyResponseTimeout); + if (!string.IsNullOrWhiteSpace(chocolateyResponseTimeout)) + { + PSHelper.WriteDebug(_cmdlet, $"Setting read/write timeout to '{chocolateyResponseTimeout}'"); + int.TryParse(chocolateyResponseTimeout, out var responseTimeoutInt); + if (responseTimeoutInt <= 0) + { + responseTimeoutInt = 300000; + } + + request.ReadWriteTimeout = responseTimeoutInt; + } + + // http://stackoverflow.com/questions/518181/too-many-automatic-redirections-were-attempted-error-message-when-using-a-httpw + request.CookieContainer = new CookieContainer(); + if (!string.IsNullOrWhiteSpace(userAgent)) + { + PSHelper.WriteDebug(_cmdlet, $"Setting the UserAgent to '{userAgent}'"); + request.UserAgent = userAgent; + } + + var requestHeadersDebugInfo = new StringBuilder().AppendLine("Request headers:"); + var requestHeaders = request.Headers.AllKeys; + foreach (var header in requestHeaders) + { + var value = request.Headers.Get(header)?.ToString(); + requestHeadersDebugInfo.AppendLine( + !string.IsNullOrWhiteSpace(value) + ? $" {header}={value}" + : $" {header}"); + } + + PSHelper.WriteDebug(_cmdlet, requestHeadersDebugInfo.ToString()); + + var headers = new Hashtable(); + + HttpWebResponse response = null; + try + { + response = (HttpWebResponse)request.GetResponse(); + if (response == null) + { + PSHelper.WriteWarning(_cmdlet, $"No response from server at '{uri}'."); + return headers; + } + + var responseHeadersDebugInfo = new StringBuilder().AppendLine("Response Headers:"); + var responseHeaders = response.Headers.AllKeys; + foreach (var header in responseHeaders) + { + var value = response.Headers.Get(header)?.ToString(); + responseHeadersDebugInfo.AppendLine( + !string.IsNullOrWhiteSpace(value) + ? $" {header}={value}" + : $" {header}"); + + if (!string.IsNullOrWhiteSpace(value)) + { + headers.Add(header, value); + } + } + + OnSuccessfulWebRequest(url, downloadUrl); + } + catch (Exception ex) + { + if (request != null) + { + request.ServicePoint.MaxIdleTime = 0; + request.Abort(); + GC.Collect(); + } + + throw new RuntimeException( + string.Format( + "The remote file either doesn't exist, is unauthorized, or is forbidden for url '{0}'. {1} {2}", + uri, + Environment.NewLine, + ex.Message), + ex); + } + finally + { + response?.Close(); + } + + return headers; + } + + protected virtual void OnSuccessfulWebRequest(string url, string downloadUrl) + { + } + + protected void GetWebFile(string url, string fileName, Hashtable options) + { + GetWebFile(url, fileName, userAgent: WebResources.DefaultUserAgent, options); + } + + protected void GetWebFile(string url, string fileName, string userAgent, Hashtable options) + { + if (string.IsNullOrEmpty(url)) return; + + fileName = PSHelper.GetFullPath(_cmdlet, fileName); + + var downloadUrl = GetDownloadUrl(url, writeWarning: true); + var uri = new Uri(downloadUrl); + + if (uri.IsFile) + { + PSHelper.WriteDebug(_cmdlet, "Url is local file, setting destination."); + if (!PSHelper.IsEqual(uri.LocalPath, fileName)) + { + PSHelper.CopyFile(_cmdlet, uri.LocalPath, fileName, true); + } + + return; + } + + var request = (HttpWebRequest)WebRequest.Create(uri); + var webClient = new WebClient(); + var defaultCredentials = CredentialCache.DefaultCredentials; + if (defaultCredentials != null) + { + request.Credentials = defaultCredentials; + webClient.Credentials = defaultCredentials; + } + + var proxy = ProxySettings.GetProxy(_cmdlet, uri); + if (proxy != null) + { + request.Proxy = proxy; + } + + request.Accept = "*/*"; + request.AllowAutoRedirect = true; + request.MaximumAutomaticRedirections = 20; + request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + request.Timeout = 30000; + var chocolateyRequestTimeout = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyRequestTimeout); + if (!(string.IsNullOrWhiteSpace(chocolateyRequestTimeout))) + { + PSHelper.WriteDebug(_cmdlet, $"Setting request timeout to '{chocolateyRequestTimeout}'"); + int.TryParse(chocolateyRequestTimeout, out var requestTimeoutInt); + if (requestTimeoutInt <= 0) + { + requestTimeoutInt = 30000; + } + + request.Timeout = requestTimeoutInt; + } + + var chocolateyResponseTimeout = Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyResponseTimeout); + if (!string.IsNullOrWhiteSpace(chocolateyResponseTimeout)) + { + PSHelper.WriteDebug(_cmdlet, $"Setting read/write timeout to '{chocolateyResponseTimeout}'"); + int.TryParse(chocolateyResponseTimeout, out var responseTimeoutInt); + if (responseTimeoutInt <= 0) + { + responseTimeoutInt = 300000; + } + + request.ReadWriteTimeout = responseTimeoutInt; + } + + // http://stackoverflow.com/questions/518181/too-many-automatic-redirections-were-attempted-error-message-when-using-a-httpw + request.CookieContainer = new CookieContainer(); + if (!string.IsNullOrWhiteSpace(userAgent)) + { + PSHelper.WriteDebug(_cmdlet, $"Setting the UserAgent to '{userAgent}'"); + request.UserAgent = userAgent; + } + + if (options != null && options.Count != 0 && options.ContainsKey("Headers")) + { + var headers = options["Headers"] as Hashtable; + if (headers?.Count > 0) + { + PSHelper.WriteDebug(_cmdlet, "Setting custom headers"); + SetRequestFields(_cmdlet, request, headers); + } + } + + HttpWebResponse response = null; + try + { + response = (HttpWebResponse)request.GetResponse(); + + if (response == null) + { + PSHelper.WriteWarning(_cmdlet, $"No response from server at '{uri}'."); + return; + } + + var binaryIsTextCheckFile = fileName + ".istext"; + + if (PSHelper.ItemExists(_cmdlet, binaryIsTextCheckFile)) + { + try + { + PSHelper.RemoveItem(_cmdlet, binaryIsTextCheckFile); + } + catch (Exception e) + { + PSHelper.WriteWarning(_cmdlet, $"Unable to remove .istext file: {e.Message}"); + } + } + + try + { + var contentType = response.Headers["Content-Type"] ?? string.Empty; + if (IsPlainTextOrHtml(contentType)) + { + var name = PSHelper.GetFileName(fileName); + var message = $"'{name}' has content type '{contentType}'"; + PSHelper.WriteWarning(_cmdlet, message); + PSHelper.SetContent(_cmdlet, binaryIsTextCheckFile, message, Encoding.UTF8); + } + } + catch (Exception ex) + { + PSHelper.WriteDebug(_cmdlet, $"Error getting content type - {ex.Message}"); + } + + if (response.StatusCode == HttpStatusCode.OK) + { + double goal = response.ContentLength; + var goalFormatted = goal.AsFileSizeString(); + + PSHelper.EnsureDirectoryExists(_cmdlet, PSHelper.GetParentDirectory(_cmdlet, fileName)); + + using (var reader = GetDownloadStream(response.GetResponseStream())) + using (var writer = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) + { + var buffer = new byte[ChunkSize]; + + double total = 0; + int count = 0; + int iterationLoop = 0; + + var progress = new ProgressRecord(0, $"Downloading '{uri}' to '{fileName}'", "Preparing to save the file.") + { + PercentComplete = 0 + }; + + do + { + iterationLoop++; + count = reader.Read(buffer, 0, buffer.Length); + writer.Write(buffer, 0, count); + total += count; + + if (goal > 0 && iterationLoop % 10 == 0) + { + var progressPercentage = total / goal * 100; + + progress.StatusDescription = $"Saving {total.AsFileSizeString()} of {goalFormatted}"; + progress.PercentComplete = (int)progressPercentage; + _cmdlet.WriteProgress(progress); + } + } + while (count > 0); + + progress.Activity = $"Completed download of '{uri}'."; + progress.StatusDescription = $"Completed download of '{PSHelper.GetFileName(fileName)}' ({goalFormatted})."; + progress.PercentComplete = 100; + + _cmdlet.WriteProgress(progress); + + writer.Flush(); + + PSHelper.WriteHost(_cmdlet, ""); + PSHelper.WriteHost(_cmdlet, $"Download of '{PSHelper.GetFileName(fileName)}' ({goalFormatted}) completed."); + } + } + } + catch (Exception ex) + { + if (request != null) + { + request.ServicePoint.MaxIdleTime = 0; + request.Abort(); + } + + PSHelper.SetExitCode(_cmdlet, 404); + throw new RuntimeException($"The remote file either doesn't exist, is unauthorized, or is forbidden for url '{uri}'. \n {ex.Message}", ex); + } + finally + { + response?.Close(); + } + } + + private void SetRequestFields(PSCmdlet cmdlet, HttpWebRequest request, Hashtable headers) + { + foreach (DictionaryEntry header in headers) + { + var key = PSHelper.ConvertTo(header.Key); + var value = PSHelper.ConvertTo(header.Value); + PSHelper.WriteDebug(cmdlet, $" * {key}={value}"); + + switch (key.ToLower()) + { + case "accept": + request.Accept = value; + break; + case "referer": + request.Referer = value; + break; + case "cookie": + if (request.CookieContainer is null) + { + request.CookieContainer = new CookieContainer(); + } + + request.CookieContainer.SetCookies(request.RequestUri, value); + break; + case "useragent": + request.UserAgent = value; + break; + default: + request.Headers.Add(key, value); + break; + } + } + } + + protected virtual void CheckVirusEngineResults(string url, string fileFullPath) + { + } + + /// + /// Extension point to allow rewrapping the request stream for downloads in downstream code. + /// + /// + /// + protected virtual Stream GetDownloadStream(Stream reader) + { + return reader; + } + + /// + /// Extension point to allow altering the chunk size used during downloads. + /// + protected virtual int ChunkSize { get; set; } = 1048576; // 1MB + + protected string GetDownloadUrl(string url) + { + return GetDownloadUrl(url, writeWarning: false); + } + + /// + /// Extension point to allow overriding the download URL in downstream code. + /// + /// + /// + protected virtual string GetDownloadUrl(string url, bool writeWarning) + { + return url; + } + + protected bool IsPlainTextOrHtml(string contentType) + { + return contentType.Contains("text/html") || contentType.Contains("text/plain"); + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/WindowsInstallerHelper.cs b/src/Chocolatey.PowerShell/Helpers/WindowsInstallerHelper.cs new file mode 100644 index 000000000..da53354b8 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/WindowsInstallerHelper.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using Chocolatey.PowerShell.Shared; + +namespace Chocolatey.PowerShell.Helpers +{ + public class WindowsInstallerHelper + { + public static void Install( + PSCmdlet cmdlet, + string packageName, + string file, + string file64, + string fileType, + string[] silentArguments, + bool useOnlySilentArguments, + int[] validExitCodes, + CancellationToken cancellationToken) + { + var silentArgs = string.Join(" ", silentArguments); + + string bitnessMessage = string.Empty; + + var filePath = file; + if (ArchitectureWidth.Matches(32) || EnvironmentHelper.GetVariable(EnvironmentVariables.ChocolateyForceX86).ToLower() == "true") + { + if (!PSHelper.ConvertTo(file)) + { + // TODO: Replace RuntimeException + cmdlet.ThrowTerminatingError(new RuntimeException($"32-bit installation is not supported for {packageName}").ErrorRecord); + } + + if (PSHelper.ConvertTo(file64)) + { + bitnessMessage = "32-bit "; + } + } + else if (PSHelper.ConvertTo(file64)) + { + filePath = file64; + bitnessMessage = "64-bit "; + } + + if (string.IsNullOrEmpty(filePath)) + { + // TODO: Replace RuntimeException + cmdlet.ThrowTerminatingError(new RuntimeException("Package parameters incorrect, either File or File64 must be specified.").ErrorRecord); + } + + PSHelper.WriteHost(cmdlet, $"Installing {bitnessMessage}{packageName}..."); + + if (string.IsNullOrEmpty(fileType)) + { + PSHelper.WriteDebug(cmdlet, "No FileType supplied. Using the file extension to determine FileType"); + fileType = Path.GetExtension(filePath).Replace(".", string.Empty); + } + + if (!IsKnownInstallerType(fileType)) + { + PSHelper.WriteWarning(cmdlet, $"FileType '{fileType}' is unrecognised, using 'exe' instead."); + fileType = "exe"; + } + + EnvironmentHelper.SetVariable(EnvironmentVariables.ChocolateyInstallerType, fileType); + + var additionalInstallArgs = EnvironmentHelper.GetVariable(EnvironmentVariables.ChocolateyInstallArguments); + if (additionalInstallArgs == null) + { + additionalInstallArgs = string.Empty; + } + else + { + if (_installDirectoryRegex.IsMatch(additionalInstallArgs)) + { + const string installOverrideWarning = @" +Pro / Business supports a single, ubiquitous install directory option. + Stop the hassle of determining how to pass install directory overrides + to install arguments for each package / installer type. + Check out Pro / Business - https://chocolatey.org/compare +"; + PSHelper.WriteWarning(cmdlet, installOverrideWarning); + } + } + + var overrideArguments = useOnlySilentArguments || PSHelper.ConvertTo(EnvironmentHelper.GetVariable(EnvironmentVariables.ChocolateyInstallOverride)); + + // remove \chocolatey\chocolatey\ + // might be a slight issue here if the download path is the older + silentArgs = silentArgs.Replace(@"\chocolatey\chocolatey\", @"\chocolatey\"); + additionalInstallArgs = additionalInstallArgs.Replace(@"\chocolatey\chocolatey\", @"\chocolatey\"); + + var updatedFilePath = filePath.Replace(@"\chocolatey\chocolatey\", @"\chocolatey\"); + if (PSHelper.ItemExists(cmdlet, updatedFilePath)) + { + filePath = updatedFilePath; + } + + var ignoreFile = filePath + ".ignore"; + var chocolateyInstall = EnvironmentHelper.GetVariable(EnvironmentVariables.ChocolateyInstall); + if (PSHelper.ConvertTo(chocolateyInstall) + && Regex.IsMatch(ignoreFile, Regex.Escape(chocolateyInstall))) + { + try + { + PSHelper.SetContent(cmdlet, ignoreFile, string.Empty); + } + catch + { + PSHelper.WriteWarning(cmdlet, $"Unable to generate '{ignoreFile}"); + } + } + + var workingDirectory = PSHelper.GetCurrentDirectory(cmdlet); + try + { + workingDirectory = PSHelper.GetParentDirectory(cmdlet, filePath); + } + catch + { + PSHelper.WriteWarning(cmdlet, $"Unable to set the working directory for installer to location of '{filePath}"); + workingDirectory = EnvironmentHelper.GetVariable("TEMP"); + } + + try + { + // make sure any logging folder exists + foreach (var argString in new[] { silentArgs, additionalInstallArgs }) + { + foreach (Match match in _pathRegex.Matches(argString)) + { + var directory = match.Groups[1]?.Value; + if (string.IsNullOrEmpty(directory)) + { + continue; + } + + directory = PSHelper.GetParentDirectory(cmdlet, PSHelper.GetFullPath(cmdlet, directory)); + PSHelper.WriteDebug(cmdlet, $"Ensuring {directory} exists"); + PSHelper.EnsureDirectoryExists(cmdlet, directory); + } + } + } + catch (Exception ex) + { + PSHelper.WriteDebug(cmdlet, $"Error ensuring directories exist - {ex.Message}"); + } + + string args; + if (PSHelper.ConvertTo(overrideArguments)) + { + PSHelper.WriteHost(cmdlet, $"Overriding package arguments with '{additionalInstallArgs}' (replacing '{silentArgs}')"); + args = additionalInstallArgs; + } + else + { + args = $"{silentArgs} {additionalInstallArgs}"; + } + + if (PSHelper.IsEqual(fileType, "msi")) + { + InstallMsi(cmdlet, filePath, workingDirectory, args, validExitCodes, cancellationToken); + } + else if (PSHelper.IsEqual(fileType, "msp")) + { + InstallMsp(cmdlet, filePath, workingDirectory, args, validExitCodes, cancellationToken); + } + else if (PSHelper.IsEqual(fileType, "msu")) + { + InstallMsu(cmdlet, filePath, workingDirectory, args, validExitCodes, cancellationToken); + } + else if (PSHelper.IsEqual(fileType, "exe")) + { + RunInstaller(cmdlet, filePath, workingDirectory, args, validExitCodes, cancellationToken); + } + + PSHelper.WriteHost(cmdlet, $"{packageName} has been installed"); + } + + private static void RunInstaller( + PSCmdlet cmdlet, + string filePath, + string workingDirectory, + string arguments, + int[] validExitCodes, + CancellationToken cancellationToken) + { + var helper = new StartChocolateyProcessHelper(cmdlet, cancellationToken, filePath); + var exitCode = helper.Start(arguments, workingDirectory, validExitCodes); + EnvironmentHelper.SetVariable(EnvironmentVariables.ChocolateyExitCode, exitCode.ToString()); + } + + private static void InstallMsu( + PSCmdlet cmdlet, + string filePath, + string workingDirectory, + string arguments, + int[] validExitCodes, + CancellationToken cancellationToken) + { + var msuArgs = $"\"{filePath}\" {arguments}"; + + var wusaExe = PSHelper.CombinePaths(cmdlet, EnvironmentHelper.GetVariable("SystemRoot"), @"System32\wusa.exe"); + RunInstaller(cmdlet, wusaExe, workingDirectory, msuArgs, validExitCodes, cancellationToken); + } + + private static void InstallMsi( + PSCmdlet cmdlet, + string filePath, + string workingDirectory, + string arguments, + int[] validExitCodes, + CancellationToken cancellationToken) + { + var msiArgs = $"/i \"{filePath}\" {arguments}"; + RunMicrosoftInstaller(cmdlet, msiArgs, workingDirectory, validExitCodes, cancellationToken); + } + + private static void InstallMsp( + PSCmdlet cmdlet, + string filePath, + string workingDirectory, + string arguments, + int[] validExitCodes, + CancellationToken cancellationToken) + { + var mspArgs = $"/update \"{filePath}\" {arguments}"; + RunMicrosoftInstaller(cmdlet, mspArgs, workingDirectory, validExitCodes, cancellationToken); + } + + private static void RunMicrosoftInstaller( + PSCmdlet cmdlet, + string args, + string workingDirectory, + int[] validExitCodes, + CancellationToken cancellationToken) + { + var msiExe = PSHelper.CombinePaths(cmdlet, EnvironmentHelper.GetVariable("SystemRoot"), @"System32\msiexec.exe"); + RunInstaller(cmdlet, msiExe, workingDirectory, args, validExitCodes, cancellationToken); + } + + private const string PathPattern = @"(?:['""])(([a-zA-Z]:|\.)\\[^'""]+)(?:[""'])|(([a-zA-Z]:|\.)\\[\S]+)"; + + private static readonly Regex _pathRegex = new Regex(PathPattern, RegexOptions.Compiled); + + private const string InstallDirectoryArgumentPattern = "INSTALLDIR|TARGETDIR|dir=|/D="; + + private static readonly Regex _installDirectoryRegex = new Regex(InstallDirectoryArgumentPattern, RegexOptions.Compiled); + + private static bool IsKnownInstallerType(string type) + { + switch (type.ToLower()) + { + case "msi": + case "msu": + case "exe": + case "msp": + return true; + default: + return false; + }; + } + } +} diff --git a/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs b/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs index 23d677cf2..9ed340cfb 100644 --- a/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs +++ b/src/Chocolatey.PowerShell/Shared/ChocolateyCmdlet.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.Management.Automation; using System.Text; +using System.Threading; using Chocolatey.PowerShell.Helpers; namespace Chocolatey.PowerShell.Shared @@ -39,6 +40,36 @@ public abstract class ChocolateyCmdlet : PSCmdlet // { "Deprecated-CommandName", "New-CommandName" }, }; + // These members are used to coordinate use of StopProcessing() + private readonly object _lock = new object(); + private readonly CancellationTokenSource _pipelineStopTokenSource = new CancellationTokenSource(); + + /// + /// A cancellation token that will be triggered when StopProcessing() is called. + /// Use this cancellation token for any .NET methods called that accept a cancellation token, + /// and prefer overloads that accept a cancellation token. + /// This will allow Ctrl+C / StopProcessing to be handled appropriately by commands. + /// + protected CancellationToken PipelineStopToken + { + get + { + return _pipelineStopTokenSource.Token; + } + } + + /// + /// Convenience property to access MyInvocation.BoundParameters, the bound parameters for the + /// cmdlet call. + /// + protected Dictionary BoundParameters + { + get + { + return MyInvocation.BoundParameters; + } + } + /// /// The canonical error ID for the command to assist with traceability. /// For more specific error IDs where needed, use "{ErrorId}.EventName". @@ -51,6 +82,27 @@ protected string ErrorId } } + /// + /// Gets the directory that Chocolatey is installed in. + /// + protected string ChocolateyInstallLocation + { + get + { + return PSHelper.GetInstallLocation(this); + } + } + + protected bool Debug + { + get + { + return MyInvocation.BoundParameters.ContainsKey("Debug") + ? PSHelper.ConvertTo(MyInvocation.BoundParameters["Debug"]).ToBool() + : PSHelper.ConvertTo(GetVariableValue(PreferenceVariables.Debug)) != ActionPreference.SilentlyContinue; + } + } + /// /// For compatibility reasons, we always add the -IgnoredArguments parameter, so that newly added parameters /// won't break things too much if a package is run with an older version of Chocolatey. @@ -66,6 +118,48 @@ protected string ErrorId /// protected virtual bool Logging { get; } = true; + private void WriteCmdletCallDebugMessage() + { + if (!Logging) + { + return; + } + + var logMessage = new StringBuilder() + .Append("Running ") + .Append(MyInvocation.InvocationName); + + foreach (var param in MyInvocation.BoundParameters) + { + var paramNameLower = param.Key.ToLower(); + + if (paramNameLower == "ignoredarguments") + { + continue; + } + + var paramValue = paramNameLower == "sensitivestatements" || paramNameLower == "password" + ? "[REDACTED]" + : param.Value is IList list + ? string.Join(" ", list) + : LanguagePrimitives.ConvertTo(param.Value, typeof(string)); + + logMessage.Append($" -{param.Key} '{paramValue}'"); + } + + WriteDebug(logMessage.ToString()); + } + + private void WriteCmdletCompletionDebugMessage() + { + if (!Logging) + { + return; + } + + WriteDebug($"Finishing '{MyInvocation.InvocationName}'"); + } + private void WriteWarningForDeprecatedCommands() { if (_deprecatedCommandNames.TryGetValue(MyInvocation.InvocationName, out var replacement)) @@ -122,56 +216,87 @@ protected virtual void End() { } + protected sealed override void StopProcessing() + { + lock (_lock) + { + _pipelineStopTokenSource.Cancel(); + Stop(); + } + } + + /// + /// Override this method to define the cmdlet's behaviour when being asked to stop/cancel processing. + /// Note that this method will be called by , after an exclusive lock is + /// obtained. Do not call this method manually. + /// + /// + /// The will be triggered before this method is called. This method + /// need be called only if the cmdlet overriding it has its own stop or dispose behaviour that also + /// needs to be managed that are not dependent on the . + /// + protected virtual void Stop() + { + } + + + /// + /// Write a message directly to the host console, bypassing any output streams. + /// + /// protected void WriteHost(string message) { PSHelper.WriteHost(this, message); } + /// + /// Write an object to the pipeline, enumerating its contents. + /// Use to disable enumerating collections. + /// + /// protected new void WriteObject(object value) { PSHelper.WriteObject(this, value); } - protected void WriteCmdletCallDebugMessage() + /// + /// Get an environment variable from the current process scope by name. + /// + /// The name of the variable to retrieve. + /// The value of the environment variable. + protected string EnvironmentVariable(string name) { - if (!Logging) - { - return; - } - - var logMessage = new StringBuilder() - .Append("Running ") - .Append(MyInvocation.InvocationName); - - foreach (var param in MyInvocation.BoundParameters) - { - var paramNameLower = param.Key.ToLower(); - - if (paramNameLower == "ignoredarguments") - { - continue; - } - - var paramValue = paramNameLower == "sensitivestatements" || paramNameLower == "password" - ? "[REDACTED]" - : param.Value is IList list - ? string.Join(" ", list) - : LanguagePrimitives.ConvertTo(param.Value, typeof(string)); - - logMessage.Append($" -{param.Key} '{paramValue}'"); - } + return EnvironmentHelper.GetVariable(name); + } - WriteDebug(logMessage.ToString()); + /// + /// Gets an environment variable from the target scope by name, expanding + /// environment variable tokens present in the value. + /// + /// The name of the variable to retrieve. + /// The scope to retrieve the variable from. + /// The value of the environment variable. + protected string EnvironmentVariable(string name, EnvironmentVariableTarget scope) + { + return EnvironmentVariable(name, scope, preserveVariables: false); } - protected void WriteCmdletCompletionDebugMessage() + /// + /// Gets an environment variable from the target scope by name, expanding + /// environment variable tokens present in the value only if specified. + /// + /// The name of the variable to retrieve. + /// The scope to retrieve the variable from. + /// True if variables should be preserved, False if variables should be expanded. + /// The value of the environment variable. + protected string EnvironmentVariable(string name, EnvironmentVariableTarget scope, bool preserveVariables) { - if (!Logging) - { - return; - } + return EnvironmentHelper.GetVariable(this, name, scope, preserveVariables); + } - WriteDebug($"Finishing '{MyInvocation.InvocationName}'"); + protected bool IsEqual(object first, object second) + { + return PSHelper.IsEqual(first, second); } } } diff --git a/src/Chocolatey.PowerShell/Shared/ChocolateyPathType.cs b/src/Chocolatey.PowerShell/Shared/ChocolateyPathType.cs new file mode 100644 index 000000000..89f5c590a --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ChocolateyPathType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Chocolatey.PowerShell.Shared +{ + public enum ChocolateyPathType + { + PackagePath, + InstallPath, + } +} diff --git a/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs b/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs index 624e029c1..f1cefdc44 100644 --- a/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs +++ b/src/Chocolatey.PowerShell/Shared/EnvironmentVariables.cs @@ -18,9 +18,16 @@ namespace Chocolatey.PowerShell.Shared { + /// + /// Names of available environment variables that will be created or used by provided + /// PowerShell commands as part of executing Chocolatey CLI. + /// + /// + /// DEV NOTICE: Mark anything that is not meant for public consumption as + /// internal constants and not browsable, even if used in other projects. + /// public static class EnvironmentVariables { - public const string ChocolateyLastPathUpdate = "ChocolateyLastPathUpdate"; public const string ComputerName = "COMPUTERNAME"; public const string Path = "PATH"; public const string ProcessorArchitecture = "PROCESSOR_ARCHITECTURE"; @@ -31,6 +38,137 @@ public static class EnvironmentVariables public const string ChocolateyInstall = nameof(ChocolateyInstall); + /// + /// The date and time that the system environment variables (for example, PATH) were last updated by Chocolatey + /// + /// + /// Will be set during package installations if the system environment variables are updated / refreshed. + /// Not otherwise used by anything in Chocolatey itself. + /// + public const string ChocolateyLastPathUpdate = "ChocolateyLastPathUpdate"; + + /// + /// The version of the package that is being handled as it is defined in the embedded + /// nuspec file. + /// + /// + /// Will be sets during package installs, upgrades and uninstalls. + /// Environment variable is only for internal uses. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageNuspecVersion = nameof(ChocolateyPackageNuspecVersion); + + /// + /// The version of the package that is being handled as it is defined in the embedded + /// nuspec file. + /// + /// + /// Will be sets during package installs, upgrades and uninstalls. + /// Environment variable is only for internal uses. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string PackageNuspecVersion = nameof(PackageNuspecVersion); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyRequestTimeout = nameof(ChocolateyRequestTimeout); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyResponseTimeout = nameof(ChocolateyResponseTimeout); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyUrlOverride = nameof(ChocolateyUrlOverride); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyUrl64BitOverride = nameof(ChocolateyUrl64BitOverride); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyForceX86 = nameof(ChocolateyForceX86); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyChecksum32 = nameof(ChocolateyChecksum32); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyChecksumType32 = nameof(ChocolateyChecksumType32); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyChecksum64 = nameof(ChocolateyChecksum64); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyChecksumType64 = nameof(ChocolateyChecksumType64); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageName = nameof(ChocolateyPackageName); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageFolder = nameof(ChocolateyPackageFolder); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string PackageFolder = nameof(PackageFolder); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyInstallDirectoryPackage = nameof(ChocolateyInstallDirectoryPackage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageExitCode = nameof(ChocolateyPackageExitCode); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageInstallLocation = nameof(ChocolateyPackageInstallLocation); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageParameters = nameof(ChocolateyPackageParameters); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageParametersSensitive = nameof(ChocolateyPackageParametersSensitive); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyToolsLocation = nameof(ChocolateyToolsLocation); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyBinRoot = nameof(ChocolateyBinRoot); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyInstallerType = nameof(ChocolateyInstallerType); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyInstallArguments = nameof(ChocolateyInstallArguments); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyInstallOverride = nameof(ChocolateyInstallOverride); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyExitCode = nameof(ChocolateyExitCode); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public const string ChocolateyPackageVersion = nameof(ChocolateyPackageVersion); + [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] public const string ChocolateyIgnoreChecksums = nameof(ChocolateyIgnoreChecksums); diff --git a/src/Chocolatey.PowerShell/Shared/JankySwitchTransformAttribute.cs b/src/Chocolatey.PowerShell/Shared/JankySwitchTransformAttribute.cs new file mode 100644 index 000000000..2e2149d3c --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/JankySwitchTransformAttribute.cs @@ -0,0 +1,22 @@ +using Chocolatey.PowerShell.Helpers; +using System.Management.Automation; + +namespace Chocolatey.PowerShell.Shared +{ + public class BoolStringSwitchTransform : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + switch (inputData) + { + case SwitchParameter s: + return s; + case bool b: + return new SwitchParameter(b); + default: + return new SwitchParameter( + !string.IsNullOrEmpty(PSHelper.ConvertTo(inputData))); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Shared/PreferenceVariables.cs b/src/Chocolatey.PowerShell/Shared/PreferenceVariables.cs new file mode 100644 index 000000000..d38ac4d83 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/PreferenceVariables.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Chocolatey.PowerShell.Shared +{ + public static class PreferenceVariables + { + public const string Debug = "DebugPreference"; + + public const string Verbose = "VerbosePreference"; + + public const string Warning = "WarningPreference"; + + public const string ErrorAction = "ErrorActionPreference"; + + public const string Information = "InformationPreference"; + + public const string Progress = "ProgressPreference"; + } +} diff --git a/src/Chocolatey.PowerShell/Shared/ProcessHandler.cs b/src/Chocolatey.PowerShell/Shared/ProcessHandler.cs new file mode 100644 index 000000000..6431ed1a5 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ProcessHandler.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using Chocolatey.PowerShell.Helpers; +using System.Diagnostics; +using System.Linq; + +namespace Chocolatey.PowerShell.Shared +{ + public abstract class ProcessHandler + { + private TaskCompletionSource _eventHandled; + private readonly CancellationToken _pipelineStopToken; + + protected BlockingCollection<(string message, bool isError)> ProcessMessages; + protected readonly PSCmdlet Cmdlet; + + internal ProcessHandler(PSCmdlet cmdlet, CancellationToken pipelineStopToken) + { + Cmdlet = cmdlet; + _pipelineStopToken = pipelineStopToken; + } + + internal void CancelWait() + { + _eventHandled?.SetCanceled(); + } + + protected int StartProcess(string processName, string workingDirectory, string arguments, string sensitiveStatements, bool elevated, ProcessWindowStyle windowStyle, bool noNewWindow) + { + var alreadyElevated = ProcessInformation.IsElevated(); + + var process = new Process + { + EnableRaisingEvents = true, + StartInfo = new ProcessStartInfo + { + FileName = processName, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WorkingDirectory = workingDirectory, + WindowStyle = windowStyle, + CreateNoWindow = noNewWindow, + }, + }; + + if (!string.IsNullOrWhiteSpace(arguments)) + { + process.StartInfo.Arguments = arguments; + } + + if (elevated && !alreadyElevated && Environment.OSVersion.Version > new Version(6, 0)) + { + // This currently doesn't work as we're not using ShellExecute + PSHelper.WriteDebug(Cmdlet, "Setting RunAs for elevation"); + process.StartInfo.Verb = "RunAs"; + } + + process.OutputDataReceived += ProcessOutputHandler; + process.ErrorDataReceived += ProcessErrorHandler; + + // process.WaitForExit() is a bit unreliable, we use the Exiting event handler to register when + // the process exits. + process.Exited += ProcessExitingHandler; + + + var exitCode = 0; + + try + { + _eventHandled = new TaskCompletionSource(); + ProcessMessages = new BlockingCollection<(string message, bool isError)>(); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + PSHelper.WriteDebug(Cmdlet, "Waiting for process to exit"); + + // This will handle dispatching output/error messages until either the process has exited or the pipeline + // has been cancelled. + HandleProcessMessages(); + } + finally + { + process.OutputDataReceived -= ProcessOutputHandler; + process.ErrorDataReceived -= ProcessErrorHandler; + process.Exited -= ProcessExitingHandler; + + exitCode = process.ExitCode; + process.Dispose(); + } + + PSHelper.WriteDebug(Cmdlet, $"Command [\"{process}\" {arguments}] exited with '{exitCode}'."); + + return exitCode; + } + + protected virtual void HandleProcessMessages() + { + if (ProcessMessages is null) + { + return; + } + + // Use of the _pipelineStopToken allows us to respect calls for StopProcessing() correctly. + foreach (var item in ProcessMessages.GetConsumingEnumerable(_pipelineStopToken)) + { + if (item.isError) + { + Cmdlet.WriteError(new RuntimeException(item.message).ErrorRecord); + } + else + { + PSHelper.WriteVerbose(Cmdlet, item.message); + } + } + } + + protected virtual void ProcessExitingHandler(object sender, EventArgs e) + { + _eventHandled?.TrySetResult(true); + ProcessMessages?.CompleteAdding(); + } + + protected virtual void ProcessOutputHandler(object sender, DataReceivedEventArgs e) + { + if (!(e.Data is null)) + { + ProcessMessages?.Add((e.Data, false)); + } + } + + protected virtual void ProcessErrorHandler(object sender, DataReceivedEventArgs e) + { + if (!(e.Data is null)) + { + ProcessMessages?.Add((e.Data, true)); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Shared/ProxySettings.cs b/src/Chocolatey.PowerShell/Shared/ProxySettings.cs new file mode 100644 index 000000000..a1975aa24 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ProxySettings.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation.Host; +using System.Management.Automation; +using System.Net; +using System.Text; +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Extensions; + +namespace Chocolatey.PowerShell.Shared +{ + public static class ProxySettings + { + public static IWebProxy GetProxy(PSCmdlet cmdlet, Uri uri) + { + var explicitProxy = Environment.GetEnvironmentVariable("chocolateyProxyLocation"); + var explicitProxyUser = Environment.GetEnvironmentVariable("chocolateyProxyUser"); + var explicitProxyPassword = Environment.GetEnvironmentVariable("chocolateyProxyPassword"); + var explicitProxyBypassList = Environment.GetEnvironmentVariable("chocolateyProxyBypassList"); + var explicitProxyBypassOnLocal = Environment.GetEnvironmentVariable("chocolateyProxyBypassOnLocal"); + var defaultCredentials = CredentialCache.DefaultCredentials; + + if (explicitProxy != null) + { + var proxy = new WebProxy(explicitProxy, BypassOnLocal: PSHelper.IsEqual("true", explicitProxyBypassOnLocal)); + if (!string.IsNullOrWhiteSpace(explicitProxyPassword)) + { + var securePassword = explicitProxyPassword.ToSecureStringSafe(); + proxy.Credentials = new NetworkCredential(explicitProxyUser, securePassword); + } + + if (!string.IsNullOrWhiteSpace(explicitProxyBypassList)) + { + proxy.BypassList = explicitProxyBypassList.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + PSHelper.WriteHost(cmdlet, $"Using explicit proxy server '{explicitProxy}'."); + + return proxy; + } + + var webClient = new WebClient(); + if (webClient.Proxy != null && !webClient.Proxy.IsBypassed(uri)) + { + var credentials = defaultCredentials; + if (credentials == null && cmdlet.Host != null) + { + PSHelper.WriteDebug(cmdlet, "Default credentials were null. Attempting backup method"); + PSCredential cred = cmdlet.Host.UI.PromptForCredential("Enter username/password", "", "", ""); + credentials = cred.GetNetworkCredential(); + } + + var proxyAddress = webClient.Proxy.GetProxy(uri).Authority; + var proxy = new WebProxy(proxyAddress, BypassOnLocal: true) + { + Credentials = credentials + }; + + PSHelper.WriteHost(cmdlet, $"Using system proxy server '{proxyAddress}'."); + return proxy; + } + + return null; + } + } +} diff --git a/src/Chocolatey.PowerShell/Shared/RemoteDownloader.cs b/src/Chocolatey.PowerShell/Shared/RemoteDownloader.cs new file mode 100644 index 000000000..4193dfaa0 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/RemoteDownloader.cs @@ -0,0 +1,390 @@ +using Chocolatey.PowerShell.Helpers; +using Chocolatey.PowerShell.Shared; +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using Environment = System.Environment; + +namespace chocolatey.licensed.infrastructure.app.commandresources +{ + public class RemoteDownloader + { + private readonly PSCmdlet _cmdlet; + + public RemoteDownloader(PSCmdlet cmdlet) + { + _cmdlet = cmdlet; + } + + public string GetRemoteFileName(string url, string userAgent, string defaultName, PSHost host) + { + var originalFileName = defaultName; + + if (string.IsNullOrWhiteSpace(url)) + { + PSHelper.WriteDebug(_cmdlet, "Url was null, using the default name"); + return defaultName; + } + + var uri = new Uri(url); + var fileName = string.Empty; + + if (uri.IsFile) + { + fileName = PSHelper.GetFileName(uri.LocalPath); + PSHelper.WriteDebug(_cmdlet, "Url is local file, returning fileName."); + return fileName; + } + + var request = (HttpWebRequest)WebRequest.Create(url); + + if (request == null) + { + PSHelper.WriteDebug(_cmdlet, "Request was null, using the default name."); + return defaultName; + } + + var webClient = new WebClient(); + var defaultCredentials = CredentialCache.DefaultCredentials; + if (defaultCredentials != null) + { + request.Credentials = defaultCredentials; + webClient.Credentials = defaultCredentials; + } + + var proxy = ProxySettings.GetProxy(_cmdlet, uri); + if (proxy != null) + { + request.Proxy = proxy; + } + + request.Accept = "*/*"; + request.AllowAutoRedirect = true; + request.MaximumAutomaticRedirections = 20; + request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + request.Timeout = 30000; + var chocolateyRequestTimeout = Environment.GetEnvironmentVariable("ChocolateyRequestTimeout"); + if (!(string.IsNullOrWhiteSpace(chocolateyRequestTimeout))) + { + PSHelper.WriteDebug(_cmdlet, $"Setting request timeout to '{chocolateyRequestTimeout}'"); + var requestTimeoutInt = -1; + int.TryParse(chocolateyRequestTimeout, out requestTimeoutInt); + if (requestTimeoutInt <= 0) + { + requestTimeoutInt = 30000; + } + + request.Timeout = requestTimeoutInt; + } + + var chocolateyResponseTimeout = Environment.GetEnvironmentVariable("ChocolateyResponseTimeout"); + if (!(string.IsNullOrWhiteSpace(chocolateyResponseTimeout))) + { + PSHelper.WriteDebug(_cmdlet, $"Setting read/write timeout to '{chocolateyResponseTimeout}'"); + var responseTimeoutInt = -1; + int.TryParse(chocolateyResponseTimeout, out responseTimeoutInt); + if (responseTimeoutInt <= 0) + { + responseTimeoutInt = 300000; + } + + request.ReadWriteTimeout = responseTimeoutInt; + } + + // http://stackoverflow.com/questions/518181/too-many-automatic-redirections-were-attempted-error-message-when-using-a-httpw + request.CookieContainer = new CookieContainer(); + if (!string.IsNullOrWhiteSpace(userAgent)) + { + PSHelper.WriteDebug(_cmdlet, $"Setting the UserAgent to '{userAgent}'"); + request.UserAgent = userAgent; + } + + var containsABadCharacter = new Regex("[" + Regex.Escape(string.Join("", Path.GetInvalidFileNameChars())) + "\\=\\;]"); + + //var manager = new BitsManager(); + //manager.EnumJobs(JobOwner.CurrentUser); + + //var newJob = manager.CreateJob("TestJob", JobType.Download); + + //string remoteFile = @"http://www.pdrrelaunch.com/img/New Text Document.txt"; + //string localFile = @"C:\temp\Test Folder\New Text Document.txt"; + //newJob.AddFile(remoteFile, localFile); + //newJob.Resume(); + + //var downloadManager = new SharpBits.Base.BitsManager(); + //var job = downloadManager.CreateJob("name", JobType.Download); + //job.AddCredentials(new BitsCredentials {}); + + HttpWebResponse response = null; + try + { + response = (HttpWebResponse)request.GetResponse(); + if (response is null) + { + PSHelper.WriteWarning(_cmdlet, "Response was null, using the default name."); + return defaultName; + } + + var header = response.Headers["Content-Disposition"]; + var headerLocation = response.Headers["Location"]; + + // start with content-disposition header + if (!string.IsNullOrWhiteSpace(header)) + { + var fileHeaderName = "filename="; + var index = header.LastIndexOf(fileHeaderName, StringComparison.OrdinalIgnoreCase); + if (index > -1) + { + PSHelper.WriteDebug(_cmdlet, $"Using header 'Content-Disposition' ({header}) to determine file name."); + fileName = header.Substring(index + fileHeaderName.Length).Replace("\"", string.Empty); + } + } + if (containsABadCharacter.IsMatch(fileName)) fileName = string.Empty; + + // If empty, check location header next + if (string.IsNullOrWhiteSpace(fileName)) + { + if (!string.IsNullOrWhiteSpace(headerLocation)) + { + PSHelper.WriteDebug(_cmdlet, $"Using header 'Location' ({headerLocation}) to determine file name."); + fileName = PSHelper.GetFileName(headerLocation); + } + } + if (containsABadCharacter.IsMatch(fileName)) fileName = string.Empty; + + // Next comes using the response url value + if (string.IsNullOrWhiteSpace(fileName)) + { + var responseUrl = response.ResponseUri?.ToString() ?? string.Empty; + if (!responseUrl.Contains("?")) + { + PSHelper.WriteDebug(_cmdlet, $"Using response url to determine file name ('{responseUrl}')."); + fileName = PSHelper.GetFileName(responseUrl); + } + } + + if (containsABadCharacter.IsMatch(fileName)) + { + fileName = string.Empty; + } + + // Next comes using the request url value + if (string.IsNullOrWhiteSpace(fileName)) + { + var requestUrl = url; + var extension = Path.GetExtension(requestUrl); + if (!requestUrl.Contains("?") && !string.IsNullOrWhiteSpace(extension)) + { + PSHelper.WriteDebug(_cmdlet, $"Using request url to determine file name ('{requestUrl}')."); + fileName = PSHelper.GetFileName(requestUrl); + } + } + + // when all else fails, default the name + if (string.IsNullOrWhiteSpace(fileName) || containsABadCharacter.IsMatch(fileName)) + { + PSHelper.WriteDebug(_cmdlet, $"File name is null or illegal. Using the default name '{originalFileName}' instead."); + fileName = defaultName; + } + + PSHelper.WriteDebug(_cmdlet, $"File name determined from url is '{fileName}'"); + + return fileName; + } + catch (Exception ex) + { + if (request != null) + { + request.ServicePoint.MaxIdleTime = 0; + request.Abort(); + GC.Collect(); + } + + PSHelper.WriteDebug( + _cmdlet, + string.Format( + "Url request/response failed - file name will be the default name '{0}'. {1} {2}", + originalFileName, + Environment.NewLine, + ex.Message)); + + return defaultName; + } + finally + { + response?.Close(); + } + } + + public void DownloadFile(string url, string filePath, string userAgent) + { + DownloadFile(url, filePath, userAgent, showProgress: true); + } + + // this is different than GetWebFileCmdlet - resolve differences before setting it up. + public void DownloadFile(string url, string filePath, string userAgent, bool showProgress) + { + var uri = new Uri(url); + var request = (HttpWebRequest)WebRequest.Create(url); + var webClient = new WebClient(); + var defaultCredentials = CredentialCache.DefaultCredentials; + if (defaultCredentials != null) + { + request.Credentials = defaultCredentials; + webClient.Credentials = defaultCredentials; + } + + var proxy = ProxySettings.GetProxy(_cmdlet, uri); + if (proxy != null) + { + request.Proxy = proxy; + } + + request.Accept = "*/*"; + request.AllowAutoRedirect = true; + request.MaximumAutomaticRedirections = 20; + // 30 seconds + request.Timeout = 30000; + // 45 minutes + request.ReadWriteTimeout = 2700000; + + // http://stackoverflow.com/questions/518181/too-many-automatic-redirections-were-attempted-error-message-when-using-a-httpw + request.CookieContainer = new CookieContainer(); + if (!string.IsNullOrWhiteSpace(userAgent)) + { + _cmdlet.WriteDebug("Setting the UserAgent to '{userAgent}'"); + request.UserAgent = userAgent; + } + + var fileSystem = new DotNetFileSystem(); + + try + { + var downloadDirectory = fileSystem.GetDirectoryName(filePath); + fileSystem.EnsureDirectoryExists(downloadDirectory); + } + catch (Exception ex) + { + this.Log().Debug("Error creating directory for '{0}': {1}".FormatWith(filePath, ex.Message)); + } + + HttpWebResponse response = null; + try + { + response = (HttpWebResponse)request.GetResponse(); + + if (response == null) + { + this.Log().Warn(() => "No response from server at '{0}'.".FormatWith(url)); + return; + } + + try + { + var contentType = response.Headers["Content-Type"]; + if (contentType.ContainsSafe("text/html") || contentType.ContainsSafe("text/plain")) + { + this.Log().Warn("'{0}' is of content type '{1}'".FormatWith(fileSystem.GetFileName(filePath), contentType.ToStringSafe())); + fileSystem.WriteFile(filePath + ".istext", "{0} has content type {1}".FormatWith(fileSystem.GetFileName(filePath), contentType.ToStringSafe()), Encoding.UTF8); + } + } + catch (Exception ex) + { + this.Log().Debug("Error getting content type - {0}".FormatWith(ex.Message)); + } + + if (response.StatusCode == HttpStatusCode.OK) + { + double goal = response.ContentLength; + var goalFormatted = FormatFileSize(goal); + + var reader = response.GetResponseStream(); + + fileSystem.EnsureDirectoryExists(fileSystem.GetDirectoryName(filePath)); + + var writer = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + var buffer = new byte[1048576]; // 1MB + + double total = 0; + int count = 0; + int iterationLoop = 0; + + //todo: clean up with http://stackoverflow.com/a/955947/18475 + do + { + iterationLoop++; + count = reader.Read(buffer, 0, buffer.Length); + writer.Write(buffer, 0, count); + total += count; + + if (!showProgress) + { + continue; + } + + if (total != goal && goal > 0 && iterationLoop % 10 == 0) + { + var progressPercentage = (total / goal * 100); + + // http://stackoverflow.com/a/888569/18475 + Console.Write("\rDownloading: {0}% - {1}".FormatWith(progressPercentage.ToString("n2"), "Saving {0} of {1}.".FormatWith(FormatFileSize(total), goalFormatted, total, goal)).PadRight(Console.WindowWidth)); + } + + if (total == goal) + { + Console.Write("\rDownloading: 100% - {0}".FormatWith(goalFormatted).PadRight(Console.WindowWidth)); + } + } + while (count > 0); + + reader.Close(); + writer.Flush(); + writer.Close(); + reader.Dispose(); + writer.Dispose(); + + this.Log().Info(""); + this.Log().Info(() => "Download of '{0}' completed.".FormatWith(fileSystem.GetFileName(filePath))); + } + } + catch (Exception ex) + { + if (request != null) + { + request.ServicePoint.MaxIdleTime = 0; + request.Abort(); + } + + throw new Exception("The remote file either doesn't exist, is unauthorized, or is forbidden for url '{0}'. {1} {2}".FormatWith(url, Environment.NewLine, ex.Message)); + } + finally + { + if (response != null) + { + response.Close(); + } + } + } + + private string FormatFileSize(double size) + { + IList units = new List(new[] { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB" }); + foreach (var unit in units) + { + if (size < 1024) + { + return string.Format("{0:0.##} {1}", size, unit); + } + + size /= 1024; + } + + return string.Format("{0:0.##} YB", size); + } + } +} diff --git a/src/Chocolatey.PowerShell/Shared/WebResources.cs b/src/Chocolatey.PowerShell/Shared/WebResources.cs new file mode 100644 index 000000000..92558acb3 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/WebResources.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Chocolatey.PowerShell.Shared +{ + public static class WebResources + { + internal const string DefaultUserAgent = "chocolatey command line"; + } +} diff --git a/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 b/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 index a350aedb9..b10e7a019 100644 --- a/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 +++ b/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 @@ -134,6 +134,17 @@ if (Test-Path $extensionsPath) { } } +Set-Alias -Name 'Start-ChocolateyProcessAsAdmin' -Value 'Start-ChocolateyProcess' +Set-Alias -Name 'Invoke-ChocolateyProcess' -Value 'Start-ChocolateyProcess' +Set-Alias -Name 'Get-ChocolateyUnzip' -Value 'Expand-ChocolateyArchive' +Set-Alias -Name 'Get-ProcessorBits' -Value 'Get-OSArchitectureWidth' +Set-Alias -Name 'Get-OSBitness' -Value 'Get-OSArchitectureWidth' +Set-Alias -Name 'refreshenv' -Value 'Update-SessionEnvironment' +Set-Alias -Name 'Get-EnvironmentVariableNames' -Value 'Get-EnvironmentVariableName' +Set-Alias -Name 'Generate-BinFile' -Value 'New-Shim' +Set-Alias -Name 'Add-BinFile' -Value 'New-Shim' +Set-Alias -Name 'Install-BinFile' -Value 'New-Shim' +Set-Alias -Name 'Install-ChocolateyInstallPackage' -Value 'Invoke-PackageInstaller' Set-Alias -Name 'Get-CheckSumValid' -Value 'Assert-ValidChecksum' # Exercise caution and test _thoroughly_ with AND without the licensed extension installed diff --git a/src/chocolatey/Properties/AssemblyInfo.cs b/src/chocolatey/Properties/AssemblyInfo.cs index b580b3906..e57ea65b4 100644 --- a/src/chocolatey/Properties/AssemblyInfo.cs +++ b/src/chocolatey/Properties/AssemblyInfo.cs @@ -14,6 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM diff --git a/src/chocolatey/StringExtensions.cs b/src/chocolatey/StringExtensions.cs index 1c36ee88c..8feb1489a 100644 --- a/src/chocolatey/StringExtensions.cs +++ b/src/chocolatey/StringExtensions.cs @@ -323,6 +323,22 @@ public static string QuoteIfContainsPipe(this string input) return input; } + internal static string AsFileSizeString(this double size) + { + IList units = new List(new[] { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB" }); + foreach (var unit in units) + { + if (size < 1024) + { + return string.Format("{0:0.##} {1}", size, unit); + } + + size /= 1024; + } + + return string.Format("{0:0.##} YB", size); + } + #pragma warning disable IDE0022, IDE1006 [Obsolete("This overload is deprecated and will be removed in v3.")] public static string format_with(this string input, params object[] formatting) diff --git a/src/chocolatey/StringResources.cs b/src/chocolatey/StringResources.cs index e28c11eb7..95a626bce 100644 --- a/src/chocolatey/StringResources.cs +++ b/src/chocolatey/StringResources.cs @@ -55,6 +55,46 @@ public static class EnvironmentVariables [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] internal const string PackageNuspecVersion = "packageNuspecVersion"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyRequestTimeout = "chocolateyRequestTimeout"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyResponseTimeout = "chocolateyResponseTimeout"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyUrlOverride = "chocolateyUrlOverride"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyUrl64BitOverride = "chocolateyUrl64BitOverride"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyForceX86 = "chocolateyForceX86"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksum32 = "chocolateyChecksum32"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksumType32 = "chocolateyChecksumType32"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksum64 = "chocolateyChecksum64"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksumType64 = "chocolateyChecksumType64"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal static string ChocolateyPackageName = "chocolateyPackageName"; } public static class ErrorMessages diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 44d3754eb..0a0226341 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -207,6 +207,7 @@ + diff --git a/src/chocolatey/infrastructure.app/common/ProxySettings.cs b/src/chocolatey/infrastructure.app/common/ProxySettings.cs new file mode 100644 index 000000000..2f5602379 --- /dev/null +++ b/src/chocolatey/infrastructure.app/common/ProxySettings.cs @@ -0,0 +1,97 @@ +// Copyright © 2017-2019 Chocolatey Software, Inc ("Chocolatey") +// Copyright © 2015-2017 RealDimensions Software, LLC +// +// Chocolatey Professional, Chocolatey for Business, and Chocolatey Architect are licensed software. +// +// ===================================================================== +// End-User License Agreement +// Chocolatey Professional, Chocolatey for Service Providers, Chocolatey for Business, +// and/or Chocolatey Architect +// ===================================================================== +// +// IMPORTANT- READ CAREFULLY: This Chocolatey Software ("Chocolatey") End-User License Agreement +// ("EULA") is a legal agreement between you ("END USER") and Chocolatey for all Chocolatey products, +// controls, source code, demos, intermediate files, media, printed materials, and "online" or electronic +// documentation (collectively "SOFTWARE PRODUCT(S)") contained with this distribution. +// +// Chocolatey grants to you as an individual or entity, a personal, nonexclusive license to install and use the +// SOFTWARE PRODUCT(S). By installing, copying, or otherwise using the SOFTWARE PRODUCT(S), you +// agree to be bound by the terms of this EULA. If you do not agree to any part of the terms of this EULA, DO +// NOT INSTALL, USE, OR EVALUATE, ANY PART, FILE OR PORTION OF THE SOFTWARE PRODUCT(S). +// +// In no event shall Chocolatey be liable to END USER for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a result of the use or inability to use the +// SOFTWARE PRODUCT(S) (including but not limited to damages for loss of goodwill, work stoppage, computer +// failure or malfunction, or any and all other commercial damages or losses). +// +// The liability of Chocolatey to END USER for any reason and upon any cause of action related to the +// performance of the work under this agreement whether in tort or in contract or otherwise shall be limited to the +// amount paid by the END USER to Chocolatey pursuant to this agreement. +// +// ALL SOFTWARE PRODUCT(S) are licensed not sold. If you are an individual, you must acquire an individual +// license for the SOFTWARE PRODUCT(S) from Chocolatey or its authorized resellers. If you are an entity, you +// must acquire an individual license for each machine running the SOFTWARE PRODUCT(S) within your +// organization from Chocolatey or its authorized resellers. Both virtual and physical machines running the +// SOFTWARE PRODUCT(S) or benefitting from licensed features such as Package Builder or Package +// Internalizer must be counted in the SOFTWARE PRODUCT(S) licenses quantity of the organization. +namespace chocolatey.infrastructure.app.common +{ + using System; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Net; + + public static class ProxySettings + { + public static IWebProxy GetProxy(Uri uri, PSHost host) + { + var explicitProxy = Environment.GetEnvironmentVariable("chocolateyProxyLocation"); + var explicitProxyUser = Environment.GetEnvironmentVariable("chocolateyProxyUser"); + var explicitProxyPassword = Environment.GetEnvironmentVariable("chocolateyProxyPassword"); + var explicitProxyBypassList = Environment.GetEnvironmentVariable("chocolateyProxyBypassList"); + var explicitProxyBypassOnLocal = Environment.GetEnvironmentVariable("chocolateyProxyBypassOnLocal"); + var defaultCredentials = CredentialCache.DefaultCredentials; + + if (explicitProxy != null) + { + var proxy = new WebProxy(explicitProxy, BypassOnLocal: explicitProxyBypassOnLocal.ToStringSafe().IsEqualTo("true")); + if (!string.IsNullOrWhiteSpace(explicitProxyPassword)) + { + var securePassword = explicitProxyPassword.ToSecureStringSafe(); + proxy.Credentials = new NetworkCredential(explicitProxyUser, securePassword); + } + if (!string.IsNullOrWhiteSpace(explicitProxyBypassList)) + { + proxy.BypassList = explicitProxyBypassList.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + "chocolatey".Log().Info(() => "Using explicit proxy server '{0}'.".FormatWith(explicitProxy)); + + return proxy; + } + + var webClient = new WebClient(); + if (webClient.Proxy != null && !webClient.Proxy.IsBypassed(uri)) + { + var credentials = defaultCredentials; + if (credentials == null && host != null) + { + "chocolatey".Log().Debug(() => "Default credentials were null. Attempting backup method"); + PSCredential cred = host.UI.PromptForCredential("Enter username/password", "", "", ""); + credentials = cred.GetNetworkCredential(); + } + + var proxyAddress = webClient.Proxy.GetProxy(uri).Authority; + var proxy = new WebProxy(proxyAddress, BypassOnLocal: true) + { + Credentials = credentials + }; + + "chocolatey".Log().Info(() => "Using system proxy server '{0}'.".FormatWith(proxyAddress)); + return proxy; + } + + return null; + } + } +}