2

I have written the following script to check against an application name and echo back some properties if it's installed:

$input = "Microsoft Office Professional"
$appName = "*" + $input + "*"

$responseX64 = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* |  Where-Object {$_.DisplayName -like $appName} | Select-Object @{Expression={$_.DisplayName + "|" + $_.DisplayVersion +"|x64"}} | Sort-Object -Unique | ft -HideTableHeaders
$responseX86 = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |  Where-Object {$_.DisplayName -like $appName} | Select-Object @{Expression={$_.DisplayName + "|" + $_.DisplayVersion +"|x86"}} | Sort-Object -Unique | ft -HideTableHeaders

If (([string]::IsNullOrEmpty($responseX64)) -and ([string]::IsNullOrEmpty($responseX86)))
{
    Write-Output "No matches found."
    exit
}
else 
{
    Write-Output $responseX64
    Write-Output $responseX86
}

For completeness I am checking both the x86 and x64 Uninstall registry keys. This works as expected when I run it in an x64 PowerShell session and correctly returns that I have a single x86 install of Microsoft Office:

Microsoft Office Professional Plus 2016|16.0.4266.1001|x86

However, when I run it on the same machine in an x86 session of PowerShell (as it will be run by my x86 management agent), I get the following return:

Microsoft Office Professional Plus 2016|16.0.4266.1001|x64

Microsoft Office Professional Plus 2016|16.0.4266.1001|x86

I have double-checked my registry entries under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall just to be sure there isn't some x64 artifact of Office in there, but there is nothing. How can I amend this script so that it returns accurate results when run in an x86 context?

5
  • 4
    As an aside: $input is a reserved automatic variable name in PowerShell. Better not use that here. Commented Dec 8, 2018 at 21:34
  • You append the x64 hardcoded in your $responseX64 so that's why it outputs x64 but the registry key that's being read is the x86 key for sure. You can use procmon from sysinternals to verify. Commented Dec 8, 2018 at 22:02
  • You do too much in your query lines. Don't do more than get and where... Commented Dec 8, 2018 at 22:17
  • 1
    Possible duplicate of How to access the 64-bit registry from a 32-bit Powershell instance? Commented Dec 8, 2018 at 22:21
  • get-package '*Microsoft Office Professional*' -provider programs Commented Dec 25, 2022 at 15:37

3 Answers 3

3

Your problem is that to a 32-bit process, the following key paths refer to the same location, namely the registry view for 32-bit applications:

# To both 64- and 32-bit processes: The registry view for 32-bit applications.
HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall

# To 32-bit processes: same as above
# To 64-bit processes: the 64-bit view of the registry.
HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*

In other words: your code, when run in a 32-bit process, does the same thing twice.


Using just a registry key path won't allow a 32-bit process to see the 64-bit registry.

However, there are workarounds:

  • Call the 64-bit instance of PowerShell as an external process, via directory $env:WinDir\SysNative - see below.

  • Use .NET types directly, as shown in this answer.

  • Call the standard reg.exe utility, which has /reg:32 and /reg:64 parameters for targeting the bitness-specific hives. However, you'll lose PowerShell's rich data-type support with this approach and you'll have to parse the results as text.


Calling 64-bit PowerShell from (32-bit) PowerShell with a script block { ... } automatically serializes the 64-bit instance's output objects in CLIXML format, which makes the calling instance automatically deserialize them with reasonable fidelity[1], so that your original filtering commands should work:

# Works only in a 32-bit process.
& $env:WINDIR\SysNative\WindowsPowerShell\v1.0\powershell.exe {
  Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*
} | 
  Where-Object {$_.DisplayName -like $appName} | 
    Select-Object @{Expression={$_.DisplayName + "|" + $_.DisplayVersion +"|x64"}} |
      Sort-Object -Unique

To determine whether your script is being run in a 32-bit process, use this:

# PowerShell v3 and above.
$is32Bit = [Environment]::Is64BitProcess

# PowerShell version 2 alternative
$is32Bit = ${env:ProgramFiles(x86)} -eq ${env:ProgramFiles} -or 
             -not ${env:ProgramFiles(x86)}

The -or -not ${env:ProgramFiles(x86)} part detects the case of running on a pure 32-bit Windows version, but note that, obviously, no 64-bit definitions exist there.

In PowerShell v3 and above you can easily test whether the OS (Windows installation) is 64-bit or not with [Environment]::Is64BitOperatingSystem

Theo's helpful answer provides a helper function that determines both the process and the OS bitness in a backward-compatible fashion.


[1] Except for a few well-known types, input objects are deserialized as [PSCustomObject] instances with static properties reflecting the original object's property values - see this answer for details.

Sign up to request clarification or add additional context in comments.

Comments

2

To add to @mklement0's correct answer, maybe a small test function can help to determine the Windows bitness as opposed to the current PowerShell process bitness:

function Get-Architecture {
    # What architecture does Windows use
    $windowsBitness = switch ([Environment]::Is64BitOperatingSystem) {   # needs .NET 4
        $true  { 64; break }
        $false { 32; break }   
        default { 
            (Get-WmiObject -Class Win32_OperatingSystem).OSArchitecture -replace '\D+', '' 
            # Or do any of these:
            # ((Get-WmiObject -Class Win32_ComputerSystem).SystemType -replace '\D+', '') -replace '86', '32'
            # (Get-WmiObject -Class Win32_Processor).AddressWidth   # slow...
        }
    }

    # What architecture does this PowerShell process use
    $processBitness = [IntPtr]::Size * 8
    # Or do any of these:
    # $processBitness = ($env:PROCESSOR_ARCHITECTURE -replace '\D+]', '') -replace '86', '32'
    # $processBitness = if ([Environment]::Is64BitProcess) { 64 } else { 32 }

    # Return the info as object
    return New-Object -TypeName PSObject -Property @{
        'ProcessArchitecture' = "{0}-bit" -f $processBitness
        'WindowsArchitecture' = "{0}-bit" -f $windowsBitness
    }
}

Possible returns are:

64-bit PowerShell on 64-bit Windows

ProcessArchitecture WindowsArchitecture
------------------- -------------------
64-bit              64-bit

32-bit PowerShell on 64-bit Windows

ProcessArchitecture WindowsArchitecture
------------------- -------------------
32-bit              64-bit

32-bit PowerShell on 32-bit Windows

ProcessArchitecture WindowsArchitecture
------------------- -------------------
32-bit              32-bit

5 Comments

@mklement0 Thanks for the edit, although (Get-WmiObject -Class Win32_OperatingSystem).OSArchitecture actually also returns 64-bits (plural) ;)
Intriguing; that must be a localized representation, because on my US-English systems I always get 64-bit (W7, W10).
@mklement0 Really? I'm on a Dutch machine. Never would have thought this to be different on other system locales.
Yes, it is surprising - and worrisome. learn.microsoft.com/en-us/windows/desktop/wmisdk/… talks about support for localized classes, but says that the .Locale property would reflect that, but I don't see it. Note that in Dutch it is 64 bits (plural, but no -); in German it is 64-Bits (capitalized B).
@mklement0 It gets even stranger.. My Dutch Win7 Pro shows 64-bits (with the dash), but my other machine (Windows 10 Pro, also Dutch) shows 64 bits (no dash, but a space).. Good thing then to replace all non-digit characters in whatever it returns.
0

There's always get-package (5.1 only). It seems to work as both 64 & 32 bit (hello Kace client). There's both programs and msi provider listings, so I'm limiting it to programs. Only one entry appears in add/remove programs. Piping to format-table -autosize to get the full name to display. Pipe to format-list to see all the other properties, including $_.metadata['uninstallstring']. The msi version could be piped to uninstall-package.

get-package '*Microsoft Office Professional*' -provider programs | ft -a

Name                                    Version        Source ProviderName
----                                    -------        ------ ------------
Microsoft Office Professional Plus 2016 16.0.4266.1001        Programs
get-package '*Microsoft Office Professional*' -provider programs | 
  % {$_.metadata['uninstallstring']}

"C:\Program Files\Common Files\Microsoft Shared\OFFICE16\Office Setup Controller\setup.exe" /uninstall PROPLUS /dll OSETUP.DLL

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.