diff --git a/lib/PowerShell/System.Management.Automation.dll b/lib/PowerShell/System.Management.Automation.dll new file mode 100644 index 0000000000..d4be277488 Binary files /dev/null and b/lib/PowerShell/System.Management.Automation.dll differ diff --git a/src/chocolatey.console/chocolatey.console.csproj b/src/chocolatey.console/chocolatey.console.csproj index 4c663d1e50..932a406ac6 100644 --- a/src/chocolatey.console/chocolatey.console.csproj +++ b/src/chocolatey.console/chocolatey.console.csproj @@ -96,6 +96,10 @@ + + False + ..\..\lib\PowerShell\System.Management.Automation.dll + diff --git a/src/chocolatey.tests.integration/context/badpackage/1.0/tools/chocolateyInstall.ps1 b/src/chocolatey.tests.integration/context/badpackage/1.0/tools/chocolateyInstall.ps1 index cbb52a9316..c701a6d30b 100644 --- a/src/chocolatey.tests.integration/context/badpackage/1.0/tools/chocolateyInstall.ps1 +++ b/src/chocolatey.tests.integration/context/badpackage/1.0/tools/chocolateyInstall.ps1 @@ -1,9 +1,11 @@ -$packageName = 'badpackage' +try { -try { - - Write-Host "Ya!" - Write-Debug "A debug message" + Write-Output "This is $packageName v$packageVersion being installed to `n '$packageFolder'." + Write-Host "PowerShell Version is '$($PSVersionTable.PSVersion)' and CLR Version is '$($PSVersionTable.CLRVersion)'." + Write-Host "Execution Policy is '$(Get-ExecutionPolicy)'." + Write-Host "PSScriptRoot is '$PSScriptRoot'." + Write-Debug "A debug message." + Write-Verbose "Yo!" Write-Warning "A warning!" Write-Error "Oh no! An error" throw "We had an error captain!" diff --git a/src/chocolatey.tests.integration/context/badpackage/2.0/tools/chocolateyInstall.ps1 b/src/chocolatey.tests.integration/context/badpackage/2.0/tools/chocolateyInstall.ps1 index cbb52a9316..2e4b0d4244 100644 --- a/src/chocolatey.tests.integration/context/badpackage/2.0/tools/chocolateyInstall.ps1 +++ b/src/chocolatey.tests.integration/context/badpackage/2.0/tools/chocolateyInstall.ps1 @@ -1,9 +1,11 @@ -$packageName = 'badpackage' +try { -try { - - Write-Host "Ya!" - Write-Debug "A debug message" + Write-Output "This is $packageName v$packageVersion being installed to `n '$packageFolder'." + Write-Host "PowerShell Version is '$($PSVersionTable.PSVersion)' and CLR Version is '$($PSVersionTable.CLRVersion)'." + Write-Host "Execution Policy is '$(Get-ExecutionPolicy)'." + Write-Host "PSScriptRoot is '$PSScriptRoot'." + Write-Debug "A debug message." + Write-Verbose "Yo!" Write-Warning "A warning!" Write-Error "Oh no! An error" throw "We had an error captain!" diff --git a/src/chocolatey.tests.integration/context/installpackage/1.0.0/tools/chocolateyinstall.ps1 b/src/chocolatey.tests.integration/context/installpackage/1.0.0/tools/chocolateyinstall.ps1 index 2491151c21..53b064d5b7 100644 --- a/src/chocolatey.tests.integration/context/installpackage/1.0.0/tools/chocolateyinstall.ps1 +++ b/src/chocolatey.tests.integration/context/installpackage/1.0.0/tools/chocolateyinstall.ps1 @@ -1,5 +1,10 @@ $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" "simple file" | Out-File "$toolsDir\simplefile.txt" -force -Write-Output "$env:PackageName $env:PackageVersion Installed" +Write-Output "This is $packageName v$packageVersion being installed to `n $packageFolder" +Write-Host "Ya!" +Write-Debug "A debug message" +Write-Verbose "Yo!" +Write-Warning "A warning!" +Write-Output "$packageName v$packageVersion has been installed to `n $packageFolder" \ No newline at end of file diff --git a/src/chocolatey/StringExtensions.cs b/src/chocolatey/StringExtensions.cs index 62ab04ff7e..55c1f90528 100644 --- a/src/chocolatey/StringExtensions.cs +++ b/src/chocolatey/StringExtensions.cs @@ -17,6 +17,7 @@ namespace chocolatey { using System; using System.Globalization; + using System.Security; using System.Text.RegularExpressions; using infrastructure.app; using infrastructure.logging; @@ -83,6 +84,26 @@ public static string to_string(this string input) return input; } + /// + /// Takes a string and returns a secure string + /// + /// The input. + /// + public static SecureString to_secure_string(this string input) + { + var secureString = new SecureString(); + + if (string.IsNullOrWhiteSpace(input)) return secureString; + + foreach (char character in input) + { + secureString.AppendChar(character); + } + + return secureString; + } + + private static readonly Regex _spacePattern = new Regex(@"\s", RegexOptions.Compiled); /// diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 0cf1733747..59ce4fa0a4 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -58,6 +58,10 @@ + + False + ..\..\lib\PowerShell\System.Management.Automation.dll + ..\packages\Rx-Core.2.1.30214.0\lib\Net40\System.Reactive.Core.dll @@ -78,6 +82,8 @@ Properties\SolutionVersion.cs + + @@ -116,6 +122,9 @@ + + + @@ -203,6 +212,7 @@ + @@ -288,4 +298,4 @@ --> - + \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index f5e22c73de..2435440b31 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -101,6 +101,7 @@ public static class Features public static readonly string FailOnAutoUninstaller = "failOnAutoUninstaller"; public static readonly string AllowGlobalConfirmation = "allowGlobalConfirmation"; public static readonly string FailOnStandardError = "failOnStandardError"; + public static readonly string UsePowerShellHost = "powershellHost"; } public static class Messages diff --git a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs index 1bb651152c..8ea8a8fadc 100644 --- a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs +++ b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs @@ -188,6 +188,7 @@ private static void set_feature_flags(ChocolateyConfiguration config, ConfigFile config.Features.AutoUninstaller = set_feature_flag(ApplicationParameters.Features.AutoUninstaller, configFileSettings, defaultEnabled: true, description: "Uninstall from programs and features without requiring an explicit uninstall script."); config.Features.FailOnAutoUninstaller = set_feature_flag(ApplicationParameters.Features.FailOnAutoUninstaller, configFileSettings, defaultEnabled: false, description: "Fail if automatic uninstaller fails."); config.Features.FailOnStandardError = set_feature_flag(ApplicationParameters.Features.FailOnStandardError, configFileSettings, defaultEnabled: false, description: "Fail if install provider writes to stderr."); + config.Features.UsePowerShellHost = set_feature_flag(ApplicationParameters.Features.UsePowerShellHost, configFileSettings, defaultEnabled: true, description: "Use Chocolatey's built-in PowerShell host."); config.PromptForConfirmation = !set_feature_flag(ApplicationParameters.Features.AllowGlobalConfirmation, configFileSettings, defaultEnabled: false, description: "Prompt for confirmation in scripts or bypass."); } @@ -264,6 +265,9 @@ private static void set_global_options(IList args, ChocolateyConfigurati .Add("failstderr|failonstderr|fail-on-stderr|fail-on-standard-error|fail-on-error-output", "FailOnStandardError - Fail on standard error output (stderr), typically received when running external commands during install providers. This overrides the feature failOnStandardError.", option => config.Features.FailOnStandardError = option != null) + .Add("use-system-powershell", + "UseSystemPowerShell - Execute PowerShell using an external process instead of the built-in PowerShell host.", + option => config.Features.UsePowerShellHost = option == null) ; }, (unparsedArgs) => diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index b1cc843e9c..495c70ce95 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -336,6 +336,7 @@ public sealed class FeaturesConfiguration public bool CheckSumFiles { get; set; } public bool FailOnAutoUninstaller { get; set; } public bool FailOnStandardError { get; set; } + public bool UsePowerShellHost { get; set; } } //todo: retrofit other command configs this way diff --git a/src/chocolatey/infrastructure.app/services/PowershellService.cs b/src/chocolatey/infrastructure.app/services/PowershellService.cs index 90c66b93db..140756815d 100644 --- a/src/chocolatey/infrastructure.app/services/PowershellService.cs +++ b/src/chocolatey/infrastructure.app/services/PowershellService.cs @@ -15,8 +15,12 @@ namespace chocolatey.infrastructure.app.services { + using System; using System.IO; using System.Linq; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using System.Reflection; using adapters; using builders; using commandline; @@ -25,16 +29,16 @@ namespace chocolatey.infrastructure.app.services using filesystem; using infrastructure.commands; using logging; - using nuget; + using powershell; using results; + using Assembly = adapters.Assembly; + using Console = System.Console; using Environment = System.Environment; public class PowershellService : IPowershellService { private readonly IFileSystem _fileSystem; private readonly string _customImports; - private const string OPERATION_COMPLETED_SUCCESSFULLY = "The operation completed successfully."; - private const string INITIALIZE_DEFAULT_DRIVES = "Attempting to perform the InitializeDefaultDrives operation on the 'FileSystem' provider failed."; public PowershellService(IFileSystem fileSystem) : this(fileSystem, new CustomString(string.Empty)) @@ -234,63 +238,55 @@ public bool run_action(ChocolateyConfiguration configuration, PackageResult pack if (shouldRun) { installerRun = true; - var errorMessagesLogged = false; - var exitCode = PowershellExecutor.execute( - wrap_script_with_module(chocoPowerShellScript, configuration), - _fileSystem, - configuration.CommandExecutionTimeoutSeconds, - (s, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - //inspect for different streams - if (e.Data.StartsWith("DEBUG:")) - { - this.Log().Debug(() => " " + e.Data); - } - else if (e.Data.StartsWith("WARNING:")) - { - this.Log().Warn(() => " " + e.Data); - } - else if (e.Data.StartsWith("VERBOSE:")) - { - this.Log().Info(ChocolateyLoggers.Verbose, () => " " + e.Data); - } - else - { - this.Log().Info(() => " " + e.Data); - } - }, - (s, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - if (e.Data.is_equal_to(OPERATION_COMPLETED_SUCCESSFULLY) || e.Data.is_equal_to(INITIALIZE_DEFAULT_DRIVES)) - { - this.Log().Info(() => " " + e.Data); - } - else - { - errorMessagesLogged = true; - if (configuration.Features.FailOnStandardError) failure = true; - this.Log().Error(() => " " + e.Data); - } - }); - - if (exitCode != 0) + + if (configuration.Features.UsePowerShellHost) { - failure = true; + add_assembly_resolver(); + } + + var result = new PowerShellExecutionResults + { + ExitCode = -1 + }; + + try + { + result = configuration.Features.UsePowerShellHost + ? Execute.with_timeout(configuration.CommandExecutionTimeoutSeconds).command(() => run_host(configuration, chocoPowerShellScript), result) + : run_external_powershell(configuration, chocoPowerShellScript); + } + catch (Exception ex) + { + this.Log().Error(ex.Message); + result.ExitCode = -1; } - if (!configuration.Features.FailOnStandardError && errorMessagesLogged) + if (configuration.Features.UsePowerShellHost) { - this.Log().Warn(() => -@"Only an exit code of non-zero will fail the package by default. Set + remove_assembly_resolver(); + } + + if (result.StandardErrorWritten && configuration.Features.FailOnStandardError) + { + failure = true; + } + else if (result.StandardErrorWritten && result.ExitCode == 0) + { + this.Log().Warn( + () => + @"Only an exit code of non-zero will fail the package by default. Set `--failonstderr` if you want error messages to also fail a script. See `choco -h` for details."); } + if (result.ExitCode != 0) + { + failure = true; + } + if (failure) { - Environment.ExitCode = exitCode; + Environment.ExitCode = result.ExitCode; packageResult.Messages.Add(new ResultMessage(ResultType.Error, "Error while running '{0}'.{1} See log for details.".format_with(powershellScript.FirstOrDefault(), Environment.NewLine))); } packageResult.Messages.Add(new ResultMessage(ResultType.Note, "Ran '{0}'".format_with(chocoPowerShellScript))); @@ -299,5 +295,196 @@ public bool run_action(ChocolateyConfiguration configuration, PackageResult pack return installerRun; } + + private class PowerShellExecutionResults + { + public int ExitCode { get; set; } + public bool StandardErrorWritten { get; set; } + } + + private PowerShellExecutionResults run_external_powershell(ChocolateyConfiguration configuration, string chocoPowerShellScript) + { + var result = new PowerShellExecutionResults(); + result.ExitCode = PowershellExecutor.execute( + wrap_script_with_module(chocoPowerShellScript, configuration), + _fileSystem, + configuration.CommandExecutionTimeoutSeconds, + (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + //inspect for different streams + if (e.Data.StartsWith("DEBUG:")) + { + this.Log().Debug(() => " " + e.Data); + } + else if (e.Data.StartsWith("WARNING:")) + { + this.Log().Warn(() => " " + e.Data); + } + else if (e.Data.StartsWith("VERBOSE:")) + { + this.Log().Info(ChocolateyLoggers.Verbose, () => " " + e.Data); + } + else + { + this.Log().Info(() => " " + e.Data); + } + }, + (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + result.StandardErrorWritten = true; + this.Log().Error(() => " " + e.Data); + }); + + return result; + } + + private ResolveEventHandler _handler = null; + + private void add_assembly_resolver() + { + _handler = (sender, args) => + { + var requestedAssembly = new AssemblyName(args.Name); + + this.Log().Debug(ChocolateyLoggers.Verbose, "Redirecting {0}, requested by '{1}'".format_with(args.Name, args.RequestingAssembly == null ? string.Empty : args.RequestingAssembly.FullName)); + + AppDomain.CurrentDomain.AssemblyResolve -= _handler; + + // we build against v1 - everything should update in a kosher manner to the newest, but it may not. + var assembly = attempt_version_load(requestedAssembly, new Version(5, 0, 0, 0)) ?? attempt_version_load(requestedAssembly, new Version(4, 0, 0, 0)); + if (assembly == null) assembly = attempt_version_load(requestedAssembly, new Version(3, 0, 0, 0)); + if (assembly == null) assembly = attempt_version_load(requestedAssembly, new Version(1, 0, 0, 0)); + + return assembly; + }; + + AppDomain.CurrentDomain.AssemblyResolve += _handler; + } + + private System.Reflection.Assembly attempt_version_load(AssemblyName requestedAssembly, Version version) + { + if (requestedAssembly == null) return null; + + requestedAssembly.Version = version; + + try + { + return System.Reflection.Assembly.Load(requestedAssembly); + } + catch (Exception ex) + { + this.Log().Debug(ChocolateyLoggers.Verbose, "Attempting to load assembly {0} failed:{1} {2}".format_with(requestedAssembly.Name, Environment.NewLine, ex.Message)); + return null; + } + } + + private void remove_assembly_resolver() + { + if (_handler != null) + { + AppDomain.CurrentDomain.AssemblyResolve -= _handler; + } + } + + private PowerShellExecutionResults run_host(ChocolateyConfiguration config, string chocoPowerShellScript) + { + var result = new PowerShellExecutionResults(); + string commandToRun = wrap_script_with_module(chocoPowerShellScript, config); + var host = new PoshHost(config); + this.Log().Debug(() => "Calling built-in PowerShell host with ['{0}']".format_with(commandToRun.escape_curly_braces())); + + var initialSessionState = InitialSessionState.CreateDefault(); + // override system execution policy without accidentally setting it + initialSessionState.AuthorizationManager = new AuthorizationManager("choco"); + using (var runspace = RunspaceFactory.CreateRunspace(host, initialSessionState)) + { + runspace.Open(); + + // this will affect actual execution policy + //RunspaceInvoke invoker = new RunspaceInvoke(runspace); + //invoker.Invoke("Set-ExecutionPolicy ByPass"); + + using (var pipeline = runspace.CreatePipeline()) + { + // The powershell host itself handles the following items: + // * Write-Debug + // * Write-Host + // * Write-Verbose + // * Write-Warning + // + // the two methods below will pick up Write-Output and Write-Error + + // Write-Output + pipeline.Output.DataReady += (sender, args) => + { + PipelineReader reader = sender as PipelineReader; + + if (reader != null) + { + while (reader.Count > 0) + { + host.UI.WriteLine(reader.Read().to_string()); + } + } + }; + + // Write-Error + pipeline.Error.DataReady += (sender, args) => + { + PipelineReader reader = sender as PipelineReader; + + if (reader != null) + { + while (reader.Count > 0) + { + host.UI.WriteErrorLine(reader.Read().to_string()); + } + } + }; + + pipeline.Commands.Add(new Command(commandToRun, isScript: true, useLocalScope: false)); + + try + { + pipeline.Invoke(); + } + catch (Exception ex) + { + // Unfortunately this doesn't print line number and character. It might be nice to get back to those items unless it involves tons of work. + this.Log().Error("ERROR: {0}".format_with(ex.Message)); //, !config.Debug ? string.Empty : "{0} {1}".format_with(Environment.NewLine,ex.StackTrace))); + } + + if (pipeline.PipelineStateInfo != null) + { + switch (pipeline.PipelineStateInfo.State) + { + // disconnected is not available unless the assembly version is at least v3 + //case PipelineState.Disconnected: + case PipelineState.Running: + case PipelineState.NotStarted: + case PipelineState.Failed: + case PipelineState.Stopping: + case PipelineState.Stopped: + host.SetShouldExit(1); + host.HostException = pipeline.PipelineStateInfo.Reason; + break; + case PipelineState.Completed: + host.SetShouldExit(0); + break; + } + + } + } + } + + this.Log().Debug("Built-in PowerShell host called with ['{0}'] exited with '{1}'.".format_with(commandToRun.escape_curly_braces(), host.ExitCode)); + + result.ExitCode = host.ExitCode; + result.StandardErrorWritten = host.StandardErrorWritten; + + return result; + } } -} \ No newline at end of file +} diff --git a/src/chocolatey/infrastructure/adapters/Console.cs b/src/chocolatey/infrastructure/adapters/Console.cs index 4bff68365b..9e6a0b4721 100644 --- a/src/chocolatey/infrastructure/adapters/Console.cs +++ b/src/chocolatey/infrastructure/adapters/Console.cs @@ -16,6 +16,7 @@ namespace chocolatey.infrastructure.adapters { using System.IO; + using commandline; public sealed class Console : IConsole { @@ -24,9 +25,26 @@ public string ReadLine() return System.Console.ReadLine(); } - public TextWriter Error + public string ReadLine(int timeoutMilliseconds) { - get { return System.Console.Error; } + return ReadLineTimeout.read(timeoutMilliseconds); + } + + public System.ConsoleKeyInfo ReadKey(bool intercept) + { + return System.Console.ReadKey(intercept); + } + + public System.ConsoleKeyInfo ReadKey(int timeoutMilliseconds) + { + return ReadKeyTimeout.read_key(timeoutMilliseconds); + } + + public TextWriter Error { get { return System.Console.Error; } } + + public void Write(object value) + { + System.Console.Write(value.to_string()); } } -} \ No newline at end of file +} diff --git a/src/chocolatey/infrastructure/adapters/IConsole.cs b/src/chocolatey/infrastructure/adapters/IConsole.cs index 4649af2097..2e298645c6 100644 --- a/src/chocolatey/infrastructure/adapters/IConsole.cs +++ b/src/chocolatey/infrastructure/adapters/IConsole.cs @@ -39,6 +39,12 @@ public interface IConsole /// 1 string ReadLine(); + string ReadLine(int timeoutMilliseconds); + + System.ConsoleKeyInfo ReadKey(bool intercept); + + System.ConsoleKeyInfo ReadKey(int timeoutMilliseconds); + /// /// Gets the standard error output stream. /// @@ -47,7 +53,15 @@ public interface IConsole /// /// 1 TextWriter Error { get; } + + /// + /// Writes the specified string value to the standard output stream. + /// + /// The value to write. + /// An I/O error occurred. + /// 1 + void Write(object value); } // ReSharper restore InconsistentNaming -} \ No newline at end of file +} diff --git a/src/chocolatey/infrastructure/commandline/ReadKeyTimeout.cs b/src/chocolatey/infrastructure/commandline/ReadKeyTimeout.cs new file mode 100644 index 0000000000..18577626e6 --- /dev/null +++ b/src/chocolatey/infrastructure/commandline/ReadKeyTimeout.cs @@ -0,0 +1,67 @@ +namespace chocolatey.infrastructure.commandline +{ + using System; + using System.Threading; + + /// + /// Because sometimes you to timeout a readkey instead of blocking infinitely. + /// + /// + /// Based on http://stackoverflow.com/a/18342182/18475 + /// + public class ReadKeyTimeout : IDisposable + { + private readonly AutoResetEvent _backgroundResponseReset; + private readonly AutoResetEvent _foregroundResponseReset; + private ConsoleKeyInfo _input; + private readonly Thread _responseThread; + + private bool _isDisposing; + + private ReadKeyTimeout() + { + _backgroundResponseReset = new AutoResetEvent(false); + _foregroundResponseReset = new AutoResetEvent(false); + _responseThread = new Thread(console_read_key) + { + IsBackground = true + }; + _responseThread.Start(); + } + + private void console_read_key() + { + while (true) + { + _backgroundResponseReset.WaitOne(); + _input = Console.ReadKey(intercept:true); + _foregroundResponseReset.Set(); + } + } + + public static ConsoleKeyInfo read_key(int timeoutMilliseconds) + { + using (var readLine = new ReadKeyTimeout()) + { + readLine._backgroundResponseReset.Set(); + + return readLine._foregroundResponseReset.WaitOne(timeoutMilliseconds) ? + readLine._input + : new ConsoleKeyInfo('\0',ConsoleKey.Enter,false,false,false); + } + } + + public void Dispose() + { + if (_isDisposing) return; + + _isDisposing = true; + _responseThread.Abort(); + _backgroundResponseReset.Close(); + _backgroundResponseReset.Dispose(); + _foregroundResponseReset.Close(); + _foregroundResponseReset.Dispose(); + } + + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/commandline/ReadLineTimeout.cs b/src/chocolatey/infrastructure/commandline/ReadLineTimeout.cs new file mode 100644 index 0000000000..8ee1c610f1 --- /dev/null +++ b/src/chocolatey/infrastructure/commandline/ReadLineTimeout.cs @@ -0,0 +1,81 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.commandline +{ + using System; + using System.Threading; + + /// + /// Because sometimes you to timeout a readline instead of blocking infinitely. + /// + /// + /// Based on http://stackoverflow.com/a/18342182/18475 + /// + public class ReadLineTimeout : IDisposable + { + private readonly AutoResetEvent _backgroundResponseReset; + private readonly AutoResetEvent _foregroundResponseReset; + private string _input; + private readonly Thread _responseThread; + + private bool _isDisposing; + + private ReadLineTimeout() + { + _backgroundResponseReset = new AutoResetEvent(false); + _foregroundResponseReset = new AutoResetEvent(false); + _responseThread = new Thread(console_read) + { + IsBackground = true + }; + _responseThread.Start(); + } + + private void console_read() + { + while (true) + { + _backgroundResponseReset.WaitOne(); + _input = Console.ReadLine(); + _foregroundResponseReset.Set(); + } + } + + public static string read(int timeoutMilliseconds) + { + using (var readLine = new ReadLineTimeout()) + { + readLine._backgroundResponseReset.Set(); + + return readLine._foregroundResponseReset.WaitOne(timeoutMilliseconds) ? + readLine._input + : null; + } + } + + public void Dispose() + { + if (_isDisposing) return; + + _isDisposing = true; + _responseThread.Abort(); + _backgroundResponseReset.Close(); + _backgroundResponseReset.Dispose(); + _foregroundResponseReset.Close(); + _foregroundResponseReset.Dispose(); + } + } +} diff --git a/src/chocolatey/infrastructure/commands/Execute.cs b/src/chocolatey/infrastructure/commands/Execute.cs new file mode 100644 index 0000000000..a64640d15e --- /dev/null +++ b/src/chocolatey/infrastructure/commands/Execute.cs @@ -0,0 +1,95 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.commands +{ + using System; + using System.Threading.Tasks; + + /// + /// Execute a method or function + /// + public sealed class Execute + { + private readonly TimeSpan _timespan; + + /// + /// The number of seconds to wait for an operation to complete. + /// + /// The timeout in seconds. + /// + public static Execute with_timeout(int timeoutInSeconds) + { + return new Execute(TimeSpan.FromSeconds(timeoutInSeconds)); + } + + /// + /// The timespan to wait for an operation to complete. + /// + /// The timeout. + /// + public static Execute with_timeout(TimeSpan timeout) + { + return new Execute(timeout); + } + + private Execute(TimeSpan timespan) + { + _timespan = timespan; + } + + /// + /// Runs an operation with a timeout. + /// + /// The type to return + /// The function to execute. + /// The timeout default value. + /// The results of the function if completes within timespan, otherwise returns the default value. + public T command(Func function, T timeoutDefaultValue) + { + if (function == null) return timeoutDefaultValue; + + var task = Task.Factory.StartNew(function); + task.Wait(_timespan); + + if (task.IsCompleted) return task.Result; + return timeoutDefaultValue; + + //T result = timeoutDefaultValue; + //var thread = new Thread(() => result = function()); + //thread.Start(); + + //bool completed = thread.Join((int)TimeSpan.FromSeconds(timeoutInSeconds).TotalMilliseconds); + //if (!completed) thread.Abort(); + + //return result; + } + + /// + /// Calls a method with a timeout. + /// + /// The action to perform. + /// True if it finishes executing, false otherwise. + public bool command(Action action) + { + if (action == null) return false; + + var task = Task.Factory.StartNew(action); + task.Wait(_timespan); + + return task.IsCompleted; + } + } +} diff --git a/src/chocolatey/infrastructure/powershell/PoshHost.cs b/src/chocolatey/infrastructure/powershell/PoshHost.cs new file mode 100644 index 0000000000..302e1f5cdc --- /dev/null +++ b/src/chocolatey/infrastructure/powershell/PoshHost.cs @@ -0,0 +1,115 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.powershell +{ + using System; + using System.Globalization; + using System.Management.Automation.Host; + using app; + using app.configuration; + + public class PoshHost : PSHost + { + private readonly ChocolateyConfiguration _configuration; + private readonly Guid _hostId = Guid.NewGuid(); + private readonly PoshHostUserInterface _psUI; + + private bool _isClosing; + + // http://blogs.msdn.com/b/kebab/archive/2014/04/28/executing-powershell-scripts-from-c.aspx + // https://msdn.microsoft.com/en-us/library/ee706594(v=vs.85).aspx + // http://powershellstation.com/2009/11/10/writing-your-own-powershell-hosting-app-part-4/ + + // others + // http://stackoverflow.com/questions/16329448/hosting-powershell-powershell-vs-runspace-vs-runspacepool-vs-pipeline + // + + public int ExitCode { get; set; } + public Exception HostException { get; set; } + public bool StandardErrorWritten { get { return _psUI.StandardErrorWritten; } } + + public PoshHost(ChocolateyConfiguration configuration) + { + ExitCode = -1; + _configuration = configuration; + _psUI = new PoshHostUserInterface(configuration); + } + + public override void SetShouldExit(int exitCode) + { + if (!_isClosing) + { + _isClosing = true; + } + + ExitCode = exitCode; + } + + public override CultureInfo CurrentCulture + { + get { return CultureInfo.InvariantCulture; } + } + + public override CultureInfo CurrentUICulture + { + get { return CultureInfo.InvariantCulture; } + } + + public override Guid InstanceId + { + get { return _hostId; } + } + + public override string Name + { + get { return ApplicationParameters.Name + "_PSHost"; } + } + + public override PSHostUserInterface UI + { + get { return _psUI; } + } + + public override Version Version + { + get { return new Version(_configuration.Information.ChocolateyVersion); } + } + + #region Not Implemented / Empty + + public override void NotifyBeginApplication() + { + // no state to hold + } + + public override void NotifyEndApplication() + { + // no state to restore + } + + public override void EnterNestedPrompt() + { + throw new NotImplementedException("Nested prompt not implemented"); + } + + public override void ExitNestedPrompt() + { + throw new NotImplementedException("Nested prompt not implemented"); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/powershell/PoshHostRawUserInterface.cs b/src/chocolatey/infrastructure/powershell/PoshHostRawUserInterface.cs new file mode 100644 index 0000000000..d9a1445d27 --- /dev/null +++ b/src/chocolatey/infrastructure/powershell/PoshHostRawUserInterface.cs @@ -0,0 +1,115 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.powershell +{ + using System; + using System.Management.Automation.Host; + + public class PoshHostRawUserInterface : PSHostRawUserInterface + { + public override ConsoleColor BackgroundColor + { + get { return Console.BackgroundColor; } + set { Console.BackgroundColor = value; } + } + + public override Size BufferSize + { + get { return new Size(Console.BufferWidth, Console.BufferHeight); } + set { Console.SetBufferSize(value.Width, value.Height); } + } + + public override Coordinates CursorPosition { get; set; } + + public override int CursorSize + { + get { return Console.CursorSize; } + set { Console.CursorSize = value; } + } + + public override ConsoleColor ForegroundColor + { + get { return Console.ForegroundColor; } + set { Console.ForegroundColor = value; } + } + + public override bool KeyAvailable + { + get { return Console.KeyAvailable; } + } + + public override Size MaxPhysicalWindowSize + { + get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); } + } + + public override Size MaxWindowSize + { + get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); } + } + + public override Coordinates WindowPosition + { + get { return new Coordinates(Console.WindowLeft, Console.WindowTop); } + set { Console.SetWindowPosition(value.X, value.Y); } + } + + public override Size WindowSize + { + get { return new Size(Console.WindowWidth, Console.WindowHeight); } + set { Console.SetWindowSize(value.Width, value.Height); } + } + + public override string WindowTitle + { + get { return Console.Title; } + set { Console.Title = value; } + } + + #region Not Implemented / Empty + + public override void FlushInputBuffer() + { + } + + public override KeyInfo ReadKey(ReadKeyOptions options) + { + throw new NotImplementedException(); + } + + public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) + { + throw new NotImplementedException(); + } + + public override void SetBufferContents(Rectangle rectangle, BufferCell fill) + { + throw new NotImplementedException(); + } + + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + throw new NotImplementedException(); + } + + public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) + { + throw new NotImplementedException(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure/powershell/PoshHostUserInterface.cs b/src/chocolatey/infrastructure/powershell/PoshHostUserInterface.cs new file mode 100644 index 0000000000..38ce8c2d6e --- /dev/null +++ b/src/chocolatey/infrastructure/powershell/PoshHostUserInterface.cs @@ -0,0 +1,310 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.powershell +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Security; + using System.Text; + using app.configuration; + using logging; + using Console = adapters.Console; + + public class PoshHostUserInterface : PSHostUserInterface + { + private readonly ChocolateyConfiguration _configuration; + protected readonly Console Console = new Console(); + private readonly PoshHostRawUserInterface _rawUi = new PoshHostRawUserInterface(); + private const int TIMEOUT_IN_SECONDS = 30; + + public bool StandardErrorWritten { get; set; } + + public PoshHostUserInterface(ChocolateyConfiguration configuration) + { + _configuration = configuration; + } + + /// + /// Depending on whether we allow prompting or not, we will set Console.ReadLine(). + /// If the user has set confirm all prompts (-y), we still want to give them a + /// chance to make a selection, but it should ultimately time out and move on + /// so it doesn't break unattended operations. + /// + /// + public override string ReadLine() + { + if (!_configuration.PromptForConfirmation) + { + this.Log().Warn(ChocolateyLoggers.Important, @" Confirmation (`-y`) is set. + Respond within {0} seconds or the default selection will be chosen.".format_with(TIMEOUT_IN_SECONDS)); + + return Console.ReadLine(TIMEOUT_IN_SECONDS * 1000); + } + + return Console.ReadLine(); + } + + public override void Write(string value) + { + this.Log().Info(value); + //Console.Write(value); + } + + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + var originalForegroundColor = System.Console.ForegroundColor; + var originalBackgroundColor = System.Console.BackgroundColor; + System.Console.ForegroundColor = foregroundColor; + System.Console.BackgroundColor = backgroundColor; + + this.Log().Info(value); + + //Console.Write(value); + + System.Console.ForegroundColor = originalForegroundColor; + System.Console.BackgroundColor = originalBackgroundColor; + } + + public override void WriteLine() + { + base.WriteLine(); + } + + public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + var originalForegroundColor = System.Console.ForegroundColor; + var originalBackgroundColor = System.Console.BackgroundColor; + System.Console.ForegroundColor = foregroundColor; + System.Console.BackgroundColor = backgroundColor; + + this.Log().Info(value); + + //Console.Write(value); + + System.Console.ForegroundColor = originalForegroundColor; + System.Console.BackgroundColor = originalBackgroundColor; + } + + public override void WriteLine(string value) + { + this.Log().Info(value); + } + + public override void WriteErrorLine(string value) + { + StandardErrorWritten = true; + this.Log().Error(value); + } + + public override void WriteDebugLine(string message) + { + this.Log().Debug(message); + } + + public override void WriteProgress(long sourceId, ProgressRecord record) + { + if (record.PercentComplete == -1) return; + + if (record.PercentComplete == 100) this.Log().Debug(() => "Progress: 100%{0}".format_with(" ".PadRight(20))); + + // http://stackoverflow.com/a/888569/18475 + Console.Write("\rProgress: {0}%{1}".format_with(record.PercentComplete, " ".PadRight(20))); + } + + public override void WriteVerboseLine(string message) + { + this.Log().Info(ChocolateyLoggers.Verbose, "VERBOSE: " + message); + } + + public override void WriteWarningLine(string message) + { + this.Log().Warn("WARNING: " + message); + } + + public override Dictionary Prompt(string caption, string message, Collection descriptions) + { + this.Log().Info(ChocolateyLoggers.Important, caption); + var results = new Dictionary(); + foreach (FieldDescription field in descriptions) + { + if (string.IsNullOrWhiteSpace(field.Label)) this.Log().Warn(field.Name); + else + { + string[] label = get_hotkey_and_label(field.Label); + this.Log().Warn(label[1]); + } + + string selection = ReadLine(); + if (selection == null) return null; + + results[field.Name] = PSObject.AsPSObject(selection); + } + + return results; + } + + /// + /// Parse a string containing a hotkey character. + /// Take a string of the form + /// Yes to &all + /// and returns a two-dimensional array split out as + /// "A", "Yes to all". + /// + /// The string to process + /// + /// A two dimensional array containing the parsed components. + /// + private static string[] get_hotkey_and_label(string input) + { + var result = new[] { String.Empty, String.Empty }; + string[] fragments = input.Split('&'); + if (fragments.Length == 2) + { + if (fragments[1].Length > 0) result[0] = fragments[1][0].to_string().ToUpper(CultureInfo.CurrentCulture); + + result[1] = (fragments[0] + fragments[1]).Trim(); + } + else result[1] = input; + + return result; + } + + public override int PromptForChoice(string caption, string message, Collection choices, int defaultChoice) + { + this.Log().Warn(caption); + + string[,] promptData = build_hotkeys_and_plain_labels(choices); + + // Format the overall choice prompt string to display. + var choicePrompt = new StringBuilder(); + for (int element = 0; element < choices.Count; element++) + { + choicePrompt.Append(String.Format( + CultureInfo.CurrentCulture, + "|{0}> {1} ", + promptData[0, element], + promptData[1, element])); + } + + choicePrompt.Append(String.Format( + CultureInfo.CurrentCulture, + "[Default is ({0}]", + promptData[0, defaultChoice])); + + while (true) + { + this.Log().Warn(choicePrompt.ToString()); + string selection = ReadLine().trim_safe().ToUpper(CultureInfo.CurrentCulture); + + if (selection.Length == 0) return defaultChoice; + + for (int i = 0; i < choices.Count; i++) + { + if (promptData[0, i] == selection) return i; + } + + this.Log().Warn(ChocolateyLoggers.Important, "Invalid choice: " + selection); + } + } + + private static string[,] build_hotkeys_and_plain_labels(Collection choices) + { + var choiceSelections = new string[2, choices.Count]; + + for (int i = 0; i < choices.Count; ++i) + { + string[] hotkeyAndLabel = get_hotkey_and_label(choices[i].Label); + choiceSelections[0, i] = hotkeyAndLabel[0]; + choiceSelections[1, i] = hotkeyAndLabel[1]; + } + + return choiceSelections; + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) + { + return PromptForCredential(caption, message, userName, targetName, PSCredentialTypes.Default, PSCredentialUIOptions.Default); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) + { + this.Log().Warn(caption); + if (string.IsNullOrWhiteSpace(userName)) + { + this.Log().Warn("Please provide username:"); + string selection = ReadLine().trim_safe().ToUpper(CultureInfo.CurrentCulture); + + if (selection.Length == 0) selection = targetName; + + if (!string.IsNullOrWhiteSpace(selection)) userName = selection; + } + + var password = string.Empty; + this.Log().Warn("Please provide password:"); + var possibleNonInteractive = !_configuration.PromptForConfirmation; + ConsoleKeyInfo info = possibleNonInteractive ? Console.ReadKey(TIMEOUT_IN_SECONDS * 1000) : Console.ReadKey(true); + while (info.Key != ConsoleKey.Enter) + { + if (info.Key != ConsoleKey.Backspace) + { + Console.Write("*"); + password += info.KeyChar; + info = possibleNonInteractive ? Console.ReadKey(TIMEOUT_IN_SECONDS * 1000) : Console.ReadKey(true); + } + else if (info.Key == ConsoleKey.Backspace) + { + if (!string.IsNullOrEmpty(password)) + { + password = password.Substring(0, password.Length - 1); + // get the location of the cursor + int pos = System.Console.CursorLeft; + // move the cursor to the left by one character + System.Console.SetCursorPosition(pos - 1, System.Console.CursorTop); + // replace it with space + Console.Write(" "); + // move the cursor to the left by one character again + System.Console.SetCursorPosition(pos - 1, System.Console.CursorTop); + } + info = possibleNonInteractive ? Console.ReadKey(TIMEOUT_IN_SECONDS * 1000) : Console.ReadKey(true); + } + } + for (int i = 0; i < password.Length; i++) Console.Write("*"); + System.Console.WriteLine(""); + + if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password)) + { + this.Log().Warn(ChocolateyLoggers.Important, "A userName or password was not entered. This may result in future failures."); + } + + return new PSCredential(userName, password.to_secure_string()); + } + + public override PSHostRawUserInterface RawUI { get { return _rawUi; } } + + #region Not Implemented / Empty + + public override SecureString ReadLineAsSecureString() + { + throw new NotImplementedException("Reading secure strings is not implemented."); + } + + #endregion + } +}