From 72934ba41bfe4d383416707e9bb2ac2f85d6f3b9 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 24 Apr 2016 17:53:57 -0500 Subject: [PATCH 1/9] (maint) better error handling Add better error handling to some of the PowerShell files. --- .../functions/Get-ChocolateyWebFile.ps1 | 8 +- .../helpers/functions/Get-FileName.ps1 | 21 +- .../helpers/functions/Get-WebFile.ps1 | 207 ++++++++++-------- .../helpers/functions/Get-WebHeaders.ps1 | 25 ++- 4 files changed, 149 insertions(+), 112 deletions(-) diff --git a/src/chocolatey.resources/helpers/functions/Get-ChocolateyWebFile.ps1 b/src/chocolatey.resources/helpers/functions/Get-ChocolateyWebFile.ps1 index 76201956a3..5a95852a89 100644 --- a/src/chocolatey.resources/helpers/functions/Get-ChocolateyWebFile.ps1 +++ b/src/chocolatey.resources/helpers/functions/Get-ChocolateyWebFile.ps1 @@ -149,13 +149,17 @@ param( $headers = @{} if ($url.StartsWith('http')) { try { - $headers = Get-WebHeaders $url + $headers = Get-WebHeaders $url -ErrorAction "Stop" } catch { if ($host.Version -lt (new-object 'Version' 3,0)) { Write-Debug "Converting Security Protocol to SSL3 only for Powershell v2" # this should last for the entire duration [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Ssl3 - $headers = Get-WebHeaders $url + try { + $headers = Get-WebHeaders $url -ErrorAction "Stop" + } catch { + Write-Host "Attempt to get headers for $url failed.`n $($_.Exception.Message)" + } } else { Write-Host "Attempt to get headers for $url failed.`n $($_.Exception.Message)" } diff --git a/src/chocolatey.resources/helpers/functions/Get-FileName.ps1 b/src/chocolatey.resources/helpers/functions/Get-FileName.ps1 index 43109d5947..1228585d62 100644 --- a/src/chocolatey.resources/helpers/functions/Get-FileName.ps1 +++ b/src/chocolatey.resources/helpers/functions/Get-FileName.ps1 @@ -126,9 +126,6 @@ param( $containsEquals = [System.IO.Path]::GetFileName($url).Contains('=') $fileName = [System.IO.Path]::GetFileName($response.ResponseUri.ToString()) } - - $response.Close() - $response.Dispose() [System.Text.RegularExpressions.Regex]$containsABadCharacter = New-Object Regex("[" + [System.Text.RegularExpressions.Regex]::Escape([System.IO.Path]::GetInvalidFileNameChars()) + "]", [System.Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace); @@ -141,12 +138,22 @@ param( Write-Debug "File name determined from url is '$fileName'" return $fileName - } catch - { - $request.ServicePoint.MaxIdleTime = 0 - $request.Abort(); + } catch { + if ($request -ne $null) { + $request.ServicePoint.MaxIdleTime = 0 + $request.Abort(); + # ruthlessly remove $request to ensure it isn't reused + Remove-Variable request + Start-Sleep 1 + [GC]::Collect() + } + Write-Debug "Url request/response failed - file name will be '$originalFileName': $($_)" return $originalFileName + } finally { + if ($response -ne $null) { + $response.Close(); + } } } \ No newline at end of file diff --git a/src/chocolatey.resources/helpers/functions/Get-WebFile.ps1 b/src/chocolatey.resources/helpers/functions/Get-WebFile.ps1 index 743c9b7fdb..fc2727fddd 100644 --- a/src/chocolatey.resources/helpers/functions/Get-WebFile.ps1 +++ b/src/chocolatey.resources/helpers/functions/Get-WebFile.ps1 @@ -94,119 +94,140 @@ param( } } - $res = $req.GetResponse(); + try { + [System.Net.HttpWebResponse]$res = $req.GetResponse(); - try { - $headers = @{} - foreach ($key in $res.Headers) { - $value = $res.Headers[$key]; - if ($value) { - $headers.Add("$key","$value") + try { + $headers = @{} + foreach ($key in $res.Headers) { + $value = $res.Headers[$key]; + if ($value) { + $headers.Add("$key","$value") + } } - } - if ($headers.ContainsKey("Content-Type")) { - $contentType = $headers['Content-Type'] - if ($contentType -ne $null) { - if ($contentType.ToLower().Contains("text/html") -or $contentType.ToLower().Contains("text/plain")) { - Write-Warning "$fileName is of content type $contentType" - Set-Content -Path "$fileName.istext" -Value "$fileName has content type $contentType" -Encoding UTF8 -Force + if ($headers.ContainsKey("Content-Type")) { + $contentType = $headers['Content-Type'] + if ($contentType -ne $null) { + if ($contentType.ToLower().Contains("text/html") -or $contentType.ToLower().Contains("text/plain")) { + Write-Warning "$fileName is of content type $contentType" + Set-Content -Path "$fileName.istext" -Value "$fileName has content type $contentType" -Encoding UTF8 -Force + } } + } + } catch { + # not able to get content-type header + Write-Debug "Error getting content type - $($_.Exception.Message)" + } + + if($fileName -and !(Split-Path $fileName)) { + $fileName = Join-Path (Get-Location -PSProvider "FileSystem") $fileName + } + elseif((!$Passthru -and ($fileName -eq $null)) -or (($fileName -ne $null) -and (Test-Path -PathType "Container" $fileName))) + { + [string]$fileName = ([regex]'(?i)filename=(.*)$').Match( $res.Headers["Content-Disposition"] ).Groups[1].Value + $fileName = $fileName.trim("\/""'") + if(!$fileName) { + $fileName = $res.ResponseUri.Segments[-1] + $fileName = $fileName.trim("\/") + if(!$fileName) { + $fileName = Read-Host "Please provide a file name" + } + $fileName = $fileName.trim("\/") + if(!([IO.FileInfo]$fileName).Extension) { + $fileName = $fileName + "." + $res.ContentType.Split(";")[0].Split("/")[1] + } } - } - } catch { - # not able to get content-type header - Write-Debug "Error getting content type - $($_.Exception.Message)" - } + $fileName = Join-Path (Get-Location -PSProvider "FileSystem") $fileName + } + if($Passthru) { + $encoding = [System.Text.Encoding]::GetEncoding( $res.CharacterSet ) + [string]$output = "" + } - if($fileName -and !(Split-Path $fileName)) { - $fileName = Join-Path (Get-Location -PSProvider "FileSystem") $fileName - } - elseif((!$Passthru -and ($fileName -eq $null)) -or (($fileName -ne $null) -and (Test-Path -PathType "Container" $fileName))) - { - [string]$fileName = ([regex]'(?i)filename=(.*)$').Match( $res.Headers["Content-Disposition"] ).Groups[1].Value - $fileName = $fileName.trim("\/""'") - if(!$fileName) { - $fileName = $res.ResponseUri.Segments[-1] - $fileName = $fileName.trim("\/") - if(!$fileName) { - $fileName = Read-Host "Please provide a file name" - } - $fileName = $fileName.trim("\/") - if(!([IO.FileInfo]$fileName).Extension) { - $fileName = $fileName + "." + $res.ContentType.Split(";")[0].Split("/")[1] - } + if($res.StatusCode -eq 401 -or $res.StatusCode -eq 403 -or $res.StatusCode -eq 404) { + $env:ChocolateyExitCode = $res.StatusCode + throw "Remote file either doesn't exist, is unauthorized, or is forbidden for '$url'." } - $fileName = Join-Path (Get-Location -PSProvider "FileSystem") $fileName - } - if($Passthru) { - $encoding = [System.Text.Encoding]::GetEncoding( $res.CharacterSet ) - [string]$output = "" - } - if($res.StatusCode -eq 200) { - [long]$goal = $res.ContentLength - $goalFormatted = Format-FileSize $goal - $reader = $res.GetResponseStream() + if($res.StatusCode -eq 200) { + [long]$goal = $res.ContentLength + $goalFormatted = Format-FileSize $goal + $reader = $res.GetResponseStream() - if ($fileName) { - $fileDirectory = $([System.IO.Path]::GetDirectoryName($fileName)) - if (!(Test-Path($fileDirectory))) { - [System.IO.Directory]::CreateDirectory($fileDirectory) | Out-Null - } + if ($fileName) { + $fileDirectory = $([System.IO.Path]::GetDirectoryName($fileName)) + if (!(Test-Path($fileDirectory))) { + [System.IO.Directory]::CreateDirectory($fileDirectory) | Out-Null + } - try { - $writer = new-object System.IO.FileStream $fileName, "Create" - } catch { - throw $_.Exception + try { + $writer = new-object System.IO.FileStream $fileName, "Create" + } catch { + throw $_.Exception + } } - } - [byte[]]$buffer = new-object byte[] 1048576 - [long]$total = [long]$count = [long]$iterLoop =0 + [byte[]]$buffer = new-object byte[] 1048576 + [long]$total = [long]$count = [long]$iterLoop =0 - $originalEAP = $ErrorActionPreference - $ErrorActionPreference = 'Stop' - try { - do - { - $count = $reader.Read($buffer, 0, $buffer.Length); - if($fileName) { - $writer.Write($buffer, 0, $count); - } - - if($Passthru){ - $output += $encoding.GetString($buffer,0,$count) - } elseif(!$quiet) { - $total += $count - $totalFormatted = Format-FileSize $total - if($goal -gt 0 -and ++$iterLoop%10 -eq 0) { - Write-Progress "Downloading $url to $fileName" "Saving $totalFormatted of $goalFormatted ($total/$goal)" -id 0 -percentComplete (($total/$goal)*100) + $originalEAP = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + try { + do + { + $count = $reader.Read($buffer, 0, $buffer.Length); + if($fileName) { + $writer.Write($buffer, 0, $count); } + + if($Passthru){ + $output += $encoding.GetString($buffer,0,$count) + } elseif(!$quiet) { + $total += $count + $totalFormatted = Format-FileSize $total + if($goal -gt 0 -and ++$iterLoop%10 -eq 0) { + Write-Progress "Downloading $url to $fileName" "Saving $totalFormatted of $goalFormatted ($total/$goal)" -id 0 -percentComplete (($total/$goal)*100) + } - if ($total -eq $goal) { - Write-Progress "Completed download of $url." "Completed download of $fileName ($goalFormatted)." -id 0 -Completed + if ($total -eq $goal) { + Write-Progress "Completed download of $url." "Completed download of $fileName ($goalFormatted)." -id 0 -Completed + } } - } - } while ($count -gt 0) - Write-Host "" - Write-Host "Download of $([System.IO.Path]::GetFileName($fileName)) ($goalFormatted) completed." - } catch { - throw $_.Exception - } finally { - $ErrorActionPreference = $originalEAP - } + } while ($count -gt 0) + Write-Host "" + Write-Host "Download of $([System.IO.Path]::GetFileName($fileName)) ($goalFormatted) completed." + } catch { + throw $_.Exception + } finally { + $ErrorActionPreference = $originalEAP + } - $reader.Close() - if($fileName) { - $writer.Flush() - $writer.Close() + $reader.Close() + if($fileName) { + $writer.Flush() + $writer.Close() + } + if($Passthru){ + $output + } + } + } catch { + if ($req -ne $null) { + $req.ServicePoint.MaxIdleTime = 0 + $req.Abort(); + # ruthlessly remove $req to ensure it isn't reused + Remove-Variable req + Start-Sleep 1 + [GC]::Collect() } - if($Passthru){ - $output + + throw "The remote file either doesn't exist, is unauthorized, or is forbidden for url '$url'. $($_.Exception.Message)" + } finally { + if ($res -ne $null) { + $res.Close() } } - $res.Close(); } # this could be cleaned up with http://learn-powershell.net/2013/02/08/powershell-and-events-object-events/ diff --git a/src/chocolatey.resources/helpers/functions/Get-WebHeaders.ps1 b/src/chocolatey.resources/helpers/functions/Get-WebHeaders.ps1 index b76bfdd173..b3f7d4ff31 100644 --- a/src/chocolatey.resources/helpers/functions/Get-WebHeaders.ps1 +++ b/src/chocolatey.resources/helpers/functions/Get-WebHeaders.ps1 @@ -98,16 +98,21 @@ param( Write-Debug " `'$key`':`'$value`'" } } - $response.Close(); - } - catch { - $request.ServicePoint.MaxIdleTime = 0 - $request.Abort(); - # ruthlessly remove $request to ensure it isn't reused - Remove-Variable request - Start-Sleep 1 - [GC]::Collect() - throw + } catch { + if ($request -ne $null) { + $request.ServicePoint.MaxIdleTime = 0 + $request.Abort(); + # ruthlessly remove $request to ensure it isn't reused + Remove-Variable request + Start-Sleep 1 + [GC]::Collect() + } + + throw "The remote file either doesn't exist, is unauthorized, or is forbidden for url '$url'. $($_.Exception.Message)" + } finally { + if ($response -ne $null) { + $response.Close(); + } } $headers From 0e9b775fe48878794beba6787d496a4d1a41936d Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 24 Apr 2016 17:57:44 -0500 Subject: [PATCH 2/9] (GH-710) prompt with timeout on some commands - When running as non-admin, the prompt should be presented with a timeout. - When running auto uninstaller and it can not find silent commands, it should prompt with a timeout. --- .../runners/GenericRunner.cs | 7 ++--- .../services/AutomaticUninstallerService.cs | 26 ++++++++++--------- .../commandline/InteractivePrompt.cs | 4 +-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/chocolatey/infrastructure.app/runners/GenericRunner.cs b/src/chocolatey/infrastructure.app/runners/GenericRunner.cs index a02c419856..d6853a53df 100644 --- a/src/chocolatey/infrastructure.app/runners/GenericRunner.cs +++ b/src/chocolatey/infrastructure.app/runners/GenericRunner.cs @@ -192,7 +192,7 @@ public void warn_when_admin_needs_elevation(ChocolateyConfiguration config) } // NOTE: blended options may not have been fully initialized yet - if (!config.PromptForConfirmation) return; + var timeoutInSeconds = config.PromptForConfirmation ? 0 : 20; if (shouldWarn) { @@ -205,9 +205,10 @@ require admin rights. Only advanced users should run choco w/out an var selection = InteractivePrompt.prompt_for_confirmation(@" Do you want to continue?", new[] { "yes", "no" }, defaultChoice: null, - requireAnswer: true, + requireAnswer: false, allowShortAnswer: true, - shortPrompt: true + shortPrompt: true, + timeoutInSeconds: timeoutInSeconds ); if (selection.is_equal_to("no")) diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs index 81f3691415..40b6122f79 100644 --- a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -141,18 +141,20 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) if (!key.HasQuietUninstall && installer.GetType() == typeof(CustomInstaller)) { var skipUninstaller = true; - if (config.PromptForConfirmation) - { - var selection = InteractivePrompt.prompt_for_confirmation( - "Uninstall may not be silent (could not detect). Proceed?", - new[] { "yes", "no" }, - defaultChoice: null, - requireAnswer: true, - allowShortAnswer: true, - shortPrompt: true - ); - if (selection.is_equal_to("yes")) skipUninstaller = false; - } + + var timeout = config.PromptForConfirmation ? 0 : 30; + + var selection = InteractivePrompt.prompt_for_confirmation( + "Uninstall may not be silent (could not detect). Proceed?", + new[] { "yes", "no" }, + defaultChoice: "no", + requireAnswer: true, + allowShortAnswer: true, + shortPrompt: true, + timeoutInSeconds: timeout + ); + if (selection.is_equal_to("yes")) skipUninstaller = false; + if (skipUninstaller) { diff --git a/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs b/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs index 9bd83c9ced..f245caa5f9 100644 --- a/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs +++ b/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs @@ -40,7 +40,7 @@ private static IConsole Console get { return _console.Value; } } - public static string prompt_for_confirmation(string prompt, IEnumerable choices, string defaultChoice, bool requireAnswer, bool allowShortAnswer = true, bool shortPrompt = false, int repeat = 10) + public static string prompt_for_confirmation(string prompt, IEnumerable choices, string defaultChoice, bool requireAnswer, bool allowShortAnswer = true, bool shortPrompt = false, int repeat = 10, int timeoutInSeconds = 0) { if (repeat < 0) throw new ApplicationException("Too many bad attempts. Stopping before application crash."); Ensure.that(() => prompt).is_not_null(); @@ -109,7 +109,7 @@ public static string prompt_for_confirmation(string prompt, IEnumerable Console.Write(shortPrompt ? "): " : "> "); - var selection = Console.ReadLine(); + var selection = timeoutInSeconds == 0 ? Console.ReadLine() : Console.ReadLine(timeoutInSeconds * 1000); if (shortPrompt) Console.WriteLine(); if (string.IsNullOrWhiteSpace(selection) && defaultChoice != null) From 2272358a558679d0b40b05d8f356a95090ff0f56 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 24 Apr 2016 18:06:18 -0500 Subject: [PATCH 3/9] (GH-512) Display exit code for packages If a package is a warning or an error, display that exit code result next to the package name. --- .../services/ChocolateyPackageService.cs | 10 +++++----- src/chocolatey/infrastructure/results/PackageResult.cs | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index 72a97692d5..5bebc34afc 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -372,7 +372,7 @@ public ConcurrentDictionary install_run(ChocolateyConfigu this.Log().Warn(ChocolateyLoggers.Important, "Warnings:"); foreach (var warning in packageInstalls.Where(p => p.Value.Warning).or_empty_list_if_null()) { - this.Log().Warn(ChocolateyLoggers.Important, " - {0}".format_with(warning.Value.Name)); + this.Log().Warn(ChocolateyLoggers.Important, " - {0}{1}".format_with(warning.Value.Name, warning.Value.ExitCode != 0 ? " (exit code {0})".format_with(warning.Value.ExitCode) : string.Empty)); } } @@ -381,7 +381,7 @@ public ConcurrentDictionary install_run(ChocolateyConfigu this.Log().Error("Failures:"); foreach (var failure in packageInstalls.Where(p => !p.Value.Success).or_empty_list_if_null()) { - this.Log().Error(" - {0}".format_with(failure.Value.Name)); + this.Log().Error(" - {0}{1}".format_with(failure.Value.Name, failure.Value.ExitCode != 0 ? " (exit code {0})".format_with(failure.Value.ExitCode) : string.Empty)); } } @@ -574,7 +574,7 @@ public ConcurrentDictionary upgrade_run(ChocolateyConfigu this.Log().Warn(ChocolateyLoggers.Important, "Warnings:"); foreach (var warning in packageUpgrades.Where(p => p.Value.Warning).or_empty_list_if_null()) { - this.Log().Warn(ChocolateyLoggers.Important, " - {0}".format_with(warning.Value.Name)); + this.Log().Warn(ChocolateyLoggers.Important, " - {0}{1}".format_with(warning.Value.Name, warning.Value.ExitCode != 0 ? " (exit code {0})".format_with(warning.Value.ExitCode) : string.Empty)); } } @@ -583,7 +583,7 @@ public ConcurrentDictionary upgrade_run(ChocolateyConfigu this.Log().Error("Failures:"); foreach (var failure in packageUpgrades.Where(p => !p.Value.Success).or_empty_list_if_null()) { - this.Log().Error(" - {0}".format_with(failure.Value.Name)); + this.Log().Error(" - {0}{1}".format_with(failure.Value.Name, failure.Value.ExitCode != 0 ? " (exit code {0})".format_with(failure.Value.ExitCode) : string.Empty)); } } @@ -654,7 +654,7 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi this.Log().Error("Failures"); foreach (var failure in packageUninstalls.Where(p => !p.Value.Success).or_empty_list_if_null()) { - this.Log().Error(" - {0}".format_with(failure.Value.Name)); + this.Log().Error(" - {0}{1}".format_with(failure.Value.Name, failure.Value.ExitCode != 0 ? " (exit code {0})".format_with(failure.Value.ExitCode) : string.Empty)); } } diff --git a/src/chocolatey/infrastructure/results/PackageResult.cs b/src/chocolatey/infrastructure/results/PackageResult.cs index aadd6322b7..6f3d44dddd 100644 --- a/src/chocolatey/infrastructure/results/PackageResult.cs +++ b/src/chocolatey/infrastructure/results/PackageResult.cs @@ -41,6 +41,7 @@ public bool Warning public string InstallLocation { get; set; } public string Source { get; set; } public string SourceUri { get; set; } + public int ExitCode { get; set; } public PackageResult(IPackage package, string installLocation, string source = null) : this(package.Id.to_lower(), package.Version.to_string(), installLocation) { From 07277ac4af24a9386c25b19f55ac8068c00707e2 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 24 Apr 2016 18:09:03 -0500 Subject: [PATCH 4/9] (GH-709) Get Exit code from PowerShell Host When interacting with a PowerShell host, ensure it returns the proper exit code by calling the right method that will set it for both system powershell and the internal host. --- .../chocolatey.resources.csproj | 3 +++ .../functions/Set-PowerShellExitCode.ps1 | 23 +++++++++++++++++++ .../services/PowershellService.cs | 4 ++-- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/chocolatey.resources/helpers/functions/Set-PowerShellExitCode.ps1 diff --git a/src/chocolatey.resources/chocolatey.resources.csproj b/src/chocolatey.resources/chocolatey.resources.csproj index bc2ef12896..226eecc882 100644 --- a/src/chocolatey.resources/chocolatey.resources.csproj +++ b/src/chocolatey.resources/chocolatey.resources.csproj @@ -137,6 +137,9 @@ + + +