Skip to content

Commit

Permalink
(extension) Added Uninstall Extension Package
Browse files Browse the repository at this point in the history
  • Loading branch information
dtgm committed May 18, 2016
1 parent f558e6f commit f7fe281
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>chocolatey-uninstall.extension</id>
<version>0.1.0</version>
<packageSourceUrl>https://github.com/chocolatey/chocolatey-coreteampackages</packageSourceUrl>
<owners>chocolatey</owners>
<title>chocolatey-uninstall.extension (Install)</title>
<authors>dtgm ferventcoder</authors>
<copyright>© 2016-Present Chocolatey Core Team Package Contributors</copyright>
<projectUrl>https://github.com/chocolatey/chocolatey-coreteampackages</projectUrl>
<licenseUrl>https://github.com/chocolatey/choco/blob/master/LICENSE</licenseUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/chocolatey/chocolatey-coreteampackages</projectSourceUrl>
<docsUrl>https://github.com/chocolatey/chocolatey-coreteampackages/wiki</docsUrl>
<mailingListUrl>https://groups.google.com/forum/#!forum/chocolatey</mailingListUrl>
<bugTrackerUrl>https://github.com/chocolatey/chocolatey-coreteampackages/issues</bugTrackerUrl>
<tags>uninstall extension admin</tags>
<summary>Helper Function for retrieving registry keys during uninstall of packages</summary>
<description>
Provides a helper function to help with retrieving registry keys during uninstall of Chocolatey packages.
</description>
<releaseNotes>
* 0.1.0 - Initial release of extension package
</releaseNotes>
</metadata>
<files>
<file src="extensions\**" target="extensions" />
</files>
</package>
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
function Get-UninstallRegistryKey {
<#
.SYNOPSIS
Retrieve registry uninstall key(s)
.DESCRIPTION
This function will attempt to retrieve a matching registry key to be used
within a chocolateyUninstall.ps1 script.
.PARAMETER SoftwareName
Part or all of the Display Name as you see it in Programs and Features.
It should be enough to be unique.
.EXAMPLE
[array]$key = Get-UninstallRegistryKey -SoftwareName "Gpg4win (2.3.0)"
[array]$key = Get-UninstallRegistryKey -SoftwareName "Launchy 2.5"
[array]$key = Get-UninstallRegistryKey -SoftwareName "Mozilla Firefox*"
$key.UninstallString
.INPUTS
Accepts [string]
.OUTPUTS
This script searches registry objects and returns PSCustomObject of the
matched key's properties.
Retrieve properties with dot notation, for example: $key.UninstallString
.NOTES
This helper reduces the number of lines one would have to write to
retrieve registry keys to 1 line. It also prevents Get-ItemProperty from
failing when handling wrongly encoded registry keys.
Using this function in a package requires adding the extension as a
dependency. Add the following the nuspec:
<dependencies>
<dependency id="chocolatey-uninstall.extension" />
</dependencies>
.LINK
Uninstall-ChocolateyPackage
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True)]
[ValidateNotNullOrEmpty()]
[string] $softwareName
)
Write-Debug "Running 'Get-UninstallRegistryKey' for `'$env:ChocolateyPackageName`' with SoftwareName:`'$softwareName`'";

$ErrorActionPreference = 'Stop'
$local_key = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machine_key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machine_key6432 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'

Write-Verbose "Retrieving all uninstall registry keys"
[array]$keys = Get-ChildItem -Path @($machine_key6432, $machine_key, $local_key) `
-ErrorAction SilentlyContinue

Write-Debug "Error handling check: Get-ItemProperty will fail if a registry key is written incorrectly."
Write-Debug "If such a key is found, loop to try to bypass all badKeys"
[int]$maxAttempts = 10
for ([int]$attempt = 1; $attempt -le $maxAttempts; $attempt++) {
[bool]$success = $FALSE

try {
[array]$foundKey = Get-ItemProperty -Path $keys.PsPath `
-ErrorAction SilentlyContinue `
| Where-Object {$_.DisplayName -like $softwareName}
$success = $TRUE
} catch {
Write-Debug "Found bad key."
foreach ($key in $keys){try{Get-ItemProperty $key.PsPath > $null}catch{$badKey = $key.PsPath}}
Write-Verbose "Skipping bad key: $($key.PsPath)"
[array]$keys = Get-ChildItem -Path @($machine_key6432, $machine_key, $local_key) `
-ErrorAction SilentlyContinue `
| Where-Object {$badKey -NotContains $_.PsPath}
}

if ($success) {break;}
}

return $foundKey
}

31 comments on commit f7fe281

@ferventcoder
Copy link
Contributor

@ferventcoder ferventcoder commented on f7fe281 May 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtgm The file name - path too long - extensions/chocolatey-uninstall.extension/extensions/Get-UninstallRegistryKey.psm1 just needs to be extensions/Get-UninstallRegistryKey.psm1

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 May 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ferventcoder I think you mistook the filename for github path.

first part extensions/chocolatey-uninstall.extension is GH path
second part extensions/Get-UninstallRegistryKey.psm1 is package path

@ferventcoder
Copy link
Contributor

@ferventcoder ferventcoder commented on f7fe281 May 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OH, yeah. Whooopsy. Nothing to see here, carry on. :)

👍

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 May 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) so you want to proceed with this as the fix as is? Any changes?

@ferventcoder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to say the PowerShell is super clean. Good job!

@DarwinJS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtgm - from having written something very similar in the past, I am fairly certain this code will miss 64bit Uninstall key when run under a 32-bit process on a 64-bit system. (management system like SCCM, Altiris sometimes stick with a 32-bit agent for compat. Or do really weird stuff like SCCM 2012 "packages" run 32-bit and "application objects" run 64-bit).

I have code that works around this if you are interested.

@ferventcoder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly certain you are right. Do you think it is an edge case that we should support?

@ferventcoder
Copy link
Contributor

@ferventcoder ferventcoder commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only time this would come into play is when a 32-bit process, like when you are running a config manager in 32 bit mode (Chef/Puppet) - less so for Puppet since it has had a 64-bit agent for 2 years now. Chef is just getting theirs out cc @mwrock.

So likely it would be a good idea to address this.

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DarwinJS Of course I'm interested :) I know the code could be optimised to work more directly with registry objects than relying on Get-ItemProperty, but not sure that is related to the issue you are describing. Can you clarify further why this code will miss 64-bit uninstall keys under 32-bit processes? It is looking at posh registry objects, that as far as I am aware, are not differentiated with bit-depth. I'm not seeing where the issue could be; why would a 32-bit process not be able to enumerate the WOW path and find the key?

I could see that maybe handling an uninstall binary found by the key with this script that is 64-bit under a 32-bit process failing, but that wouldn't have anything to do with this code.

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it looks like @ferventcoder clarified in #222 why it would fail

doesn't use the flag to override Registry Redirection

@DarwinJS
Copy link

@DarwinJS DarwinJS commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ferventcoder - I feel strongly it is not edge case. For instance, many sysinternals tools like psexec are 32-bit - and many other "callers" could be 32-bit.

@dtgm - when you are in a 32-bit process 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall*' is mapped to the real location 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall*' - it is a part of the implementation of WOW64. So you can never build a reference to the 64-bit location.

@ferventcoder - this can be done in powershell without the possibly unpredictable results of playing with the process level redirection with the below code.

The below code also handles:

  • HKCU location does not exist
  • Filtering out blank records (that I never figured out why they existed - but found them when testing on over 10,000 machines)
  • parses MSI GUID, KB GUID and KBID into fields when they exist
  • offers three optional filters which can make the output look exactly the same as Programs and Features if desired.

I would interested in some type of attribution for contributing this code (unfortunately I don't have time to work it into the Chocolatey codebase or I would do that instead of this brain dump)

#In a 64-bit process Sysnative does not exist - only create alias if applicable
#FYI - alias works great as part of a global library for doing 64-bit calls from 32-bit powershell
if ((Test-path "$env:windir\sysnative\WindowsPowerShell\v1.0\powershell.exe") -and -not(test-path alias:\Invoke-64bitExpressionFrom32bitProc)){New-Alias -Name Invoke-64bitExpressionFrom32bitProc -Scope Global -Option AllScope -Value "$env:windir\sysnative\WindowsPowerShell\v1.0\powershell.exe"}

#Grab Native Process Bitness Key
$allsoftware = @()
$allsoftware = Get-ItemProperty hklm:\software\microsoft\windows\currentversion\uninstall\* -ErrorAction SilentlyContinue

#Grab Per-User ONLY if it exists
if (Test-Path hkcu:\software\microsoft\windows\currentversion\uninstall) {
  $allsoftware += Get-ItemProperty hkcu:\software\microsoft\windows\currentversion\uninstall\* -ErrorAction SilentlyContinue
  }

#Grab Opposite Bitness of one we are running on (only when on 64-bit Windows)
if (test-path env:ProgramFiles`(x86`)) {
    #We are on 64-bit
    if ($env:processor_architecture -eq "x86") {
      # but running in a 32-bit process, so get 64-bit keys
      $allsoftware += Invoke-64bitExpressionFrom32bitProc -noprofile { Get-ItemProperty hklm:\software\microsoft\windows\currentversion\uninstall\* -ErrorAction SilentlyContinue}
    } else {
      # running in a 64-bit process, so get 32-bit keys
      $allsoftware += Get-ItemProperty hklm:\software\wow6432node\microsoft\windows\currentversion\uninstall\* -ErrorAction SilentlyContinue
      #$keylist = get-item "registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Systems Internals\*" -ea continue
      #$keylist = get-item "registry::HKEY_LOCAL_MACHINE\software\wow6432node\microsoft\windows\currentversion\uninstall\*" -ea continue
      #$keylist | %{$allsoftware += get-itemproperty "registry::$($_.Name)"}
    }
  }

  #removes any objects that are null:
  $allsoftware = $allsoftware | ? {$_}

  $allsoftware | % { 

    if ($_.UninstallString -match '(?<GUIDFromUninstallString>[0-9a-fA-F{}-]{38})') {
      Add-Member -InputObject $_ -MemberType NoteProperty -Name GUIDFromUninstallString -Value ([regex]::match($_.UninstallString,'(?<GUIDFromUninstallString>[0-9a-fA-F{}-]{38})') | foreach {$_.groups["GUIDFromUninstallString"].value})
        }
        if ($_.PSChildName -match '(?<KBGUIDFromKeyName>[0-9a-fA-F{}-]{38})\.KB' ) {
          Add-Member -InputObject $_ -MemberType NoteProperty -Name KBGUIDFromKeyName -Value ([regex]::match($_.PSChildName,'(?<KBGUIDFromKeyName>[0-9a-fA-F{}-]{38})\.KB') | foreach {$_.groups["KBGUIDFromKeyName"].value})
        }
    if ($_.PSChildName -match '\.(?<KBIDFromKeyName>KB[0-9]{2,8})' ) {
      Add-Member -InputObject $_ -MemberType NoteProperty -Name KBIDFromKeyName -Value ([regex]::match($_.PSChildName,'\.(?<KBIDFromKeyName>KB[0-9]{2,8})') | foreach {$_.groups["KBIDFromKeyName"].value})
    }
  }

  $softwarefilter = $null
  if (-not $IncludeKBUpdates) {$softwarefilter = "`$_.DisplayName -ne `$Null"}
  if (-not $IncludeChildUpdates) {if($softwarefilter) {$softwarefilter += " -and "}; $softwarefilter += "!`$_.ParentKeyName"}
  if (-not $IncludeHiddenItems) {if($softwarefilter) {$softwarefilter += " -and "}; $softwarefilter += "`$_.SystemComponent -ne 1"}

  $commandstring = "`$allsoftware | "
  if ($softwarefilter) { $commandstring += "where-object {$softwarefilter} | "}
  $commandstring += "sort-object DisplayName, DisplayVersion, InstallDate, GUIDFromUninstallString, KBGUIDFromKeyName, KBIDFromKeyName -unique"

  $softwarefilteredarps = Invoke-Expression $commandstring

  return $softwarefilteredarps

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DarwinJS As I noted in #222 (comment) the keys aren't listed as affected to redirection. Not sure if the technet article is accurate or not though.

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DarwinJS Also I edited your comment so the code would show correctly. (use 3 backticks to surround a multi-line codeblock)

@DarwinJS
Copy link

@DarwinJS DarwinJS commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtgm - that page is trying to say that it is affected:

image

Learned it inside out while writing this: https://www.amazon.com/Deploying-Supporting-Applications-64-bit-Windows-ebook/dp/B0098P9Z22

@DarwinJS
Copy link

@DarwinJS DarwinJS commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, any key that you find under HKLM\Software\WOW6432node you know is redirected for sure - wow6432node is a 32-bit processes view of the keyname hierarchically above it. [except when you see it twice in a row, that is someone hardcoding it and then their code runs under 32-bit and creates a doubling of the key]

And when it get's to COM it's even more fun. ProdIDs aren't redirected, but CLSID (which progids point to) is.

D.

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DarwinJS Correct, my bad. I saw that but missed that subkeys inherit from that as your screenshot also shows.

Subkeys of the keys in this table inherit the parent key's behavior unless otherwise specified.

@ferventcoder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you just call another powershell natively? That's an interesting way around the problem. Not sure if I like it though. At least not yet

@ferventcoder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be the least worst bad way of accessing the registry.

@DarwinJS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the disabling of WOW64 redirection could be pretty bad - I am thinking it also disables the file system redirection at the same time - so your 32-bit proc will be looking at 64-bit binaries in it's "system32" folder.

The code ran on over 10,000 machines without incident and the piece that dynamically grabs object output was a pattern I stole from somewhere reputable - can't remember where though!

Very cool that PowerShell can pass the objects from proc to proc.

Starting a new 64-bit proc is the cleanest way to bridge the two (if avoiding the wow64 redirection disablement).

You can also instruct the WMI registry provider on which registry bitness view to connect to - I believe even when operating locally - but then you'd be looking at a whole different type of data coming back.

@DarwinJS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh - here's the original - you can even see some of my 3 year old comments against the article.

http://powershell.com/cs/blogs/tips/archive/2013/05/30/running-portions-of-code-in-32-bit-or-64-bit.aspx

Aleksandar Nikolić was telling me at a conference that he puts powershell.com snippets through some pretty rigorous testing before publishing.

@ferventcoder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: disabling

It does not have to disable both. Since I already have quite a bit of experience doing this in Ruby, I'm positive that you can do it with a flag being sent to registry in a 32 bit process and not disable SysWOW64 entirely, which we both agree is bad.

@ferventcoder
Copy link
Contributor

@ferventcoder ferventcoder commented on f7fe281 Jun 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I'm not fully sure about .NET yet (and back to v2, since we may need to support if running from Posh), which is why I said maybe this is not that bad of an option. :)

@DarwinJS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that the code I submitted above (and most code on the net) assumes that if you are on 64-bit Windows, that WOW64 is installed and available.

Given that it is optional on Server and ServerCore and non-existent on Nano - this assumption should not be made.

Testing for "%systemroot%\syswow64" (from a 64-bit process) and "%systemroot%\sysnative" (from a 32-bit on 64-bit OS process) would confirm both [1] I am on a 64-bit os, [2] WOW64 is installed.

@gep13
Copy link
Member

@gep13 gep13 commented on f7fe281 Jun 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DarwinJS are you in a position to submit a PR to correct this? Thanks!

@DarwinJS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gep13 - I was reporting it for the code in this thread which is not implemented yet.

Do you mean that you'd like a PR for the existing code for the same problem?

@gep13
Copy link
Member

@gep13 gep13 commented on f7fe281 Jun 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DarwinJS sorry, you lost me. I thought you were saying that there was an issue with the new uninstall extension which was added with this commit. Is this not the case?

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gep13 the code provided by DarwinJS is an example of how he tackles the issue. Most of the code appears to be outside the scope for this function since we are only interested in retrieving the key. I think the only pertinent portion is we would want to add something like:

if (([IntPtr]::size -eq 4) -and (Test-Path env:\PROCESSOR_ARCHITEW6432)) {
  Write-Debug "Detected 32-bit process running on WoW"
  # launch instance of 64-bit powershell.exe to get correct keys
  $startPosh64 = "$env:windir\sysnative\WindowsPowerShell\v1.0\powershell.exe"
  & $startPosh64 -Command {#existing code to enumerate WoW6432Node keys}
}

A possibly better, but more difficult solution, would require rewriting the cmdlets we are using in this function (Get-Item/Property) that are handling the Registry Objects so we can directly specify redirection in a powershell 32-bit process running in a WoW environment so the keys are read properly.

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function has also been migrated to an internal helper, so any fixes here should also be applied over there https://github.com/chocolatey/choco/commits/master/src/chocolatey.resources/helpers/functions/Get-UninstallRegistryKey.ps1

@DarwinJS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtgm - I am not sure what code substitutes for "#existing code to enumerate Wow6432Node".

Is the -ErrorAction SilentlyContinue in this code:

 [array]$keys = Get-ChildItem -Path @($machine_key6432, $machine_key, $local_key) `
                                -ErrorAction SilentlyContinue

Intentionally covering the following cases:

Non-existence of 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' in two cases - [1] you are running on 32-bit or [2] on 64-bit with no 32-bit subsystem installed?

[3] Non-existence of 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall' ?

@dtgm
Copy link
Contributor Author

@dtgm dtgm commented on f7fe281 Jun 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. yes
  2. is that possible?
  3. yes

I only anticipated the location may not exist which I can't imagine the user caring about. But testing for locations would allow to only retrieve keys from locations that exist so ErrorAction could be set to Continue.

When I say #existing code to enumerate WoW6432Node keys I am meaning the code already present in the script that tries to get redirected keys that would fail otherwise. -Command may have been misleading but it accepts a script-block.

@DarwinJS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtgm:
#2 - yes - that is why I chimed into this thread. On server Core (2012, 2016) WOW64 is optional. On Nano Server it is not supported.

It might be more robust if your code for skipping bad keys was looking for a specific known set of exceptions - to my eye it seems to assume all errors will be of the type that the code is intended to filter out. (BTW - great piece of code to effectively handle bad registry keys)

If you were to be selective about which keys you checked, then you'd need a little more logic to not include the WOW6432NODE when running pure 32-bit or on 64-bit where WOW64 is not installed. If you decide to be more selective, keep in mind that it is not reliable to test for "WOW6432NODE" registry key because many bad software programs and installers accidentally create one while in 32-bit mode - so it is quite typical to see it present on pure 32-bit and to see HKLM\Software\wow6432node\wow6432node" on 64-bit machines.

Please sign in to comment.