Based on the previous post handling the removal of the Ask software (the beloved add-on that everyone joyfully installs along with Java) a more developed script took form to handle any type of software.
Its based on the following borrowed pieces of code,
Get-LHSInstInstalledApp has been extended to also output the installationdate. Apart from that everything is as is from the original function
Convert-DateString has been used to convert the InstallationDate string to a date that can be used for calculations
ExitWithCode is a function that is simply used to end the script with an accumulated Exit Code from all uninstallations.
The script will accept best canada online casinos the following parameters;
ApplicationName – a wild card search for the applications we want to remove.
PublisherName – we can validate that the right publisher have installed the application
InstallDateOlder – amount of days since the application was installed for us to remove it. Standard is 30
IgnoreInstallDate – True / False – we can choose to completely ignore when the application was installed
If the application is something other than an MSI – it will just report that a productcode is missing and not attempt the installation.
A log-file will be created in %WINDIR%\TEMP\APP_(yourappname)_Removal.LOG
Each uninstall will have a log-file written in %WINDIR%\TEMP with AP_UNINSTALL as prefix.
Running the script requires admin permissions
#========================================================================
# Created with: PowerShell ISE
# Created on: 2015-02-21 23:32
# Created by: Nicke Källén
# Organization: Applepie.se
# Filename: SCCM_Uninstall_Unused_Application
# Comment: Uninstalls an application (msi support only) based
# on Display Name in ARP, Publisher and how long ago
# it was installed
# Convert-DateString function
# http://www.powershellmagazine.com/2013/07/08/pstip-
# converting-a-string-to-a-system-datetime-object/
# Get-LHSInstalledApp - appended InstallDate to output
# https://gallery.technet.microsoft.com/scriptcenter/
# Get-Installed-Application-615fa73a
# Exit function
# http://weblogs.asp.net/soever/returning-an-exit-
# code-from-a-powershell-script
#========================================================================
param (
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$True)]
[string]$ApplicationName = "",
[Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$false)]
[string]$PublisherName = "",
[int]$InstallDateOlder = "30",
[Parameter(mandatory=$false)]
[bool]$IgnoreInstallDate=$false
)
function Convert-DateString ([String]$Date, [String[]]$Format)
{
$result = New-Object DateTime
$convertible = [DateTime]::TryParseExact(
$Date,
$Format,
[System.Globalization.CultureInfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::None,
[ref]$result)
if ($convertible) { $result }
}
Function Get-LHSInstalledApp {
<#
.SYNOPSIS
List installed applications for local or remote computers.
.DESCRIPTION
List installed applications for local or remote computers.
List both 32-bit and 64-bit applications. Note that
dotNet 4.0 Support for Powershell 2.0 needed.
Output looks like this:
-------------------------
ComputerName : N104100
AppID : {90120000-001A-0407-0000-0000000FF1CE}
AppName : Microsoft Office Outlook MUI (German) 2007
Publisher : Microsoft Corporation
Version : 12.0.6612.1000
Architecture : 32bit
UninstallString : MsiExec.exe /X{90120000-001A-0407-0000-0000000FF1CE}
.PARAMETER ComputerName
Outputs applications for the named computer(s).
If you omit this parameter, the local computer is assumed.
.PARAMETER AppID
Outputs applications with the specified application ID.
An application's appID is equivalent to its subkey name underneath the Uninstall registry key.
For Windows Installer-based applications, this is the application's product code GUID
(e.g. {3248F0A8-6813-11D6-A77B-00B0D0160060}). Wildcards are permitted.
.PARAMETER AppName
Outputs applications with the specified application name.
The AppName is the application's name as it appears in the
Add/Remove Programs list. Wildcards are permitted.
.PARAMETER Publisher
Outputs applications with the specified publisher name.
Wildcards are permitted
.PARAMETER Version
Outputs applications with the specified version.
Wildcards are permitted.
.EXAMPLE
PS C:\> Get-LHSInstalledApp
This command outputs installed applications on the current computer.
.EXAMPLE
PS C:\> Get-LHSInstalledApp | Select-Object AppName,Version | Sort-Object AppName
This command outputs a sorted list of applications on the current computer.
.EXAMPLE
PS C:\> Get-LHSInstalledApp wks1,wks2 -Publisher "*microsoft*"
This command outputs all installed Microsoft applications on the named computers.
* regular expression to match any characters.
.EXAMPLE
PS C:\> Get-LHSInstalledApp wks1,wks2 -AppName "*Office 97*"
This command outputs any Application Name that match "Office 97" on the named computers.
* regular expression to match any characters.
.EXAMPLE
PS C:\> Get-Content ComputerList.txt | Get-LHSInstalledApp -AppID "{1A97CF67-FEBB-436E-BD64-431FFEF72EB8}" | Select-Object ComputerName
This command outputs the computer names named in ComputerList.txt that have the specified application installed.
.EXAMPLE
Get-LHSInstalledApp | Where-Object {-not ( $_.AppID -like "KB*") } |
ConvertTo-CSV -Delimiter ';' -NoTypeInformation | Out-File -FilePath C:\temp\AppsInfo.csv
Invoke-Item C:\temp\AppsInfo.csv
Outputs all installed application except KB fixes to an CSV file and opens in Excel
.INPUTS
System.String, you can pipe ComputerNames to this Function
.OUTPUTS
PSObjects containing the following properties:
ComputerName - computer where the application is installed
AppID - the application's AppID
AppName - the application's name
Publisher - the application's publisher
Version - the application's version
Architecture - the application's architecture (32-bit or 64-bit)
UninstallString - the application uninstall String
.NOTES
More Info:
==========
Why not using Get-WmiObject
---------------------------
* Win32_Product
At first glance, Win32_Product would appear to be one of those best solutions.
The Win32_product class is not query optimized.
Queries such as “select * from Win32_Product where (name like 'Sniffer%')”
require WMI to use the MSI provider to enumerate all of the installed
products and then parse the full list sequentially to handle the “where” clause:,
* This process initiates a consistency check of packages installed,
and then verifying and repairing the installations.
* If you have an application that makes use of the Win32_Product class,
you should contact the vendor to get an updated version that does not use this class.
On Windows Server 2003, Windows Vista, and newer operating systems, querying Win32_Product
will trigger Windows Installer to perform a consistency check to verify the health of the
application. This consistency check could cause a repair installation to occur. You can
confirm this by checking the Windows Application Event log. You will see the following
events each time the class is queried and for each product installed:
Event ID: 1035
Description: Windows Installer reconfigured the product. Product Name: <ProductName>.
Product Version: <VersionNumber>. Product Language: <languageID>.
Reconfiguration success or error status: 0.
Event ID: 7035/7036
Description: The Windows Installer service entered the running state.
I would not recommend querying Win32_Product in your production environment unless you are in a maintenance window.
* Win32Reg_AddRemovePrograms
Win32Reg_AddRemovePrograms is not a standard Windows class.
This WMI class is only loaded during the installation of an SMS/SCCM client.
What is great about Win32Reg_AddRemovePrograms is that it contains similar properties and
returns results noticeably quicker than Win32_Product.
Using Registry:
----------------
By default, if your process is running as a 32 bit process you will end up accessing the 32 bit "reflection" of
the remote system. Therefore, registry keys like HKLM\Software will actually be mapped to HKLM\Software\Wow6432Node
which gets very frustrating! You can access the 64 bit "reflection" via WMI, but personally I find that quite painful.
Fortunately, in .NET 4, the registry class had some extra features added to it which allowed for a new
overload "RegistryView". Therefore, you can now specify exactly which "reflection" of the registry
you want to access and manipulate! No more headaches!
In order to use this function, the Powershell instance must support .Net 4.0 or greater, which is fairly straightforward if you follow these instructions.
1. Open notepad and copy the below text exactly as shown into the document.
<?xml version="1.0"?>
<configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0.30319"/> <supportedRuntime version="v2.0.50727"/> </startup>
</configuration>
2. Save this document as c:\windows\System32\WindowsPowerhsell\v1.0\Powershell.exe.config
(and/or c:\windows\System32\WindowsPowerhsell\v1.0\Powershell_ise.exe.config)
(in addition for the 32bit Powershell on a 64bit Windows C:\Windows\SysWOW64\WindowsPowerShell\v1.0\*.config)
3. Reload powershell and type the following command: $PsVersionTable.clrVersion (It should show Major version 4 if .Net 4 is supported.)
NAME: Get-LHSInatalledApp.ps1
AUTHOR: u104018
LASTEDIT: 02/06/2012 16:01:40
KEYWORDS: Registry Redirection, Installed software, Registry64, WOW6432Node,Accessing Remote x64 Registry From an x86/x32 OS Computer
.LINK
http://poshcode.org/3186
http://blogs.technet.com/b/heyscriptingguy/archive/2011/11/13/use-powershell-to-quickly-find-installed-software.aspx
http://msdn.microsoft.com/en-us/library/aa393067%28VS.85%29.aspx
#Requires -Version 2.0
#>
[cmdletbinding(DefaultParameterSetName = 'Default', ConfirmImpact = 'low')]
Param(
[Parameter(ParameterSetName='AppID', Position=0,Mandatory=$False,ValueFromPipeline=$True)]
[Parameter(ParameterSetName='Default', Position=0,Mandatory=$False,ValueFromPipeline=$True)]
[string[]] $ComputerName=$ENV:COMPUTERNAME,
[Parameter(ParameterSetName='AppID', Position=1)]
[String] $AppID = "*",
[Parameter(ParameterSetName='Default', Position=1)]
[String] $AppName = "*",
[Parameter(ParameterSetName='Default', Position=2)]
[String] $Publisher = "*",
[Parameter(ParameterSetName='Default', Position=3)]
[String] $Version = "*"
)
BEGIN {
${CmdletName} = $Pscmdlet.MyInvocation.MyCommand.Name
If (!($PsVersionTable.clrVersion.Major -ge 4)) {Write-Error "Requires .Net 4.0 support for Powershell 2.0"; Return}
} # end BEGIN
PROCESS {
#Write-Verbose -Message "${CmdletName}: Starting Process Block"
ForEach ($Computer in $ComputerName) {
Write-Verbose "`$Computer contains $Computer"
IF (Test-Connection -ComputerName $Computer -Count 2 -Quiet) {
try {
Write-Verbose "Get Architechture Type of the system"
$OSArch = (Get-WMIObject -ComputerName $Computer win32_operatingSystem -ErrorAction Stop).OSArchitecture
if ($OSArch -like "*64*") {$Architectures = @("32bit","64bit")}
else {$Architectures = @("32bit")}
#Create an array to capture program objects.
$arApplications = @()
foreach ($Architecture in $Architectures){
#We have a 64bit machine, get the 32 bit software.
if ($Architecture -like "*64*"){
#Define the entry point to the registry.
$strSubKey = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
$SoftArchitecture = "32bit"
$RegViewEnum = [Microsoft.Win32.RegistryView]::Registry64
}
#We have a 32bit machine, use the 32bit registry provider.
elseif ($Architectures -notcontains "64bit"){
#Define the entry point to the registry.
$strSubKey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
$SoftArchitecture = "32bit"
$RegViewEnum = [Microsoft.Win32.RegistryView]::Registry32
}
#We have "64bit" in our array, capture the 64bit software.
else{
#Define the entry point to the registry.
$strSubKey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
$SoftArchitecture = "64bit"
$RegViewEnum = [Microsoft.Win32.RegistryView]::Registry64
}
Write-Verbose "Create a remote registry connection to the Computer."
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Computer, $RegViewEnum)
$RegKey = $Reg.OpenSubKey($strSubKey)
Write-Verbose "Get all subkeys that exist in the entry point."
$RegSubKeys = $RegKey.GetSubKeyNames()
Write-Debug "Architecture : $Architecture"
Write-Debug "SoftArchitecture : $SoftArchitecture"
Write-Verbose "Enumerate the subkeys."
foreach ($SubKey in $RegSubKeys)
{
Write-Debug "`$SubKey : $SubKey"
$Program = $Reg.OpenSubKey("$strSubKey\\$SubKey")
$strDisplayName = $Program.GetValue("DisplayName")
if ($strDisplayName -eq $NULL) { continue } # skip entry if empty display name
switch ($PsCmdlet.ParameterSetName)
{
"AppID" { if ((split-path $SubKey -leaf) -like $AppID)
{
$RegKey = ("HKLM\$strSubKey\$SubKey").replace("\\","\")
$output = new-object PSObject
$output | add-member NoteProperty "ComputerName" -value $computer
$output | add-member NoteProperty "RegKey" -value ($RegKey) # useful when debugging
$output | add-member NoteProperty "AppID" -value (split-path $SubKey -leaf)
$output | add-member NoteProperty "AppName" -value $strDisplayName
$output | add-member NoteProperty "Publisher" -value $Program.GetValue("Publisher")
$output | add-member NoteProperty "Version" -value $Program.GetValue("DisplayVersion")
$output | add-member NoteProperty "Architecture" -value $SoftArchitecture
$output | add-member NoteProperty "UninstallString" -value $Program.GetValue("UninstallString")
$output | add-member NoteProperty "InstallDate" -value $Program.GetValue("InstallDate")
$output
} #end if
} #end "AppID"
"Default" { If (( $strDisplayName -like $AppName ) -and (
$Program.GetValue("Publisher") -like $Publisher ) -and (
$Program.GetValue("DisplayVersion") -like $Version ))
{
$RegKey = ("HKLM\$strSubKey\$SubKey").replace("\\","\")
$output = new-object PSObject
$output | add-member NoteProperty "ComputerName" -value $computer
$output | add-member NoteProperty "RegKey" -value ($RegKey) # useful when debugging
$output | add-member NoteProperty "AppID" -value (split-path $SubKey -leaf)
$output | add-member NoteProperty "AppName" -value $strDisplayName
$output | add-member NoteProperty "Publisher" -value $Program.GetValue("Publisher")
$output | add-member NoteProperty "Version" -value $Program.GetValue("DisplayVersion")
$output | add-member NoteProperty "Architecture" -value $SoftArchitecture
$output | add-member NoteProperty "UninstallString" -value $Program.GetValue("UninstallString")
$output | add-member NoteProperty "InstallDate" -value $Program.GetValue("InstallDate")
$output
} #end if
} #end "Default"
} #end switch
} # end foreach ($SubKey in $RegSubKeys)
} # end foreach ($Architecture in $Architectures)
} Catch {
write-error $_
}
} Else {
Write-Warning "\\$Computer DO NOT reply to ping"
} # end IF (Test-Connection -ComputerName $Computer -count 2 -quiet)
} # end ForEach ($Computer in $computerName)
} # end PROCESS
END { Write-Verbose "Function ${CmdletName} finished." }
} # end Function Get-LHSInatalledApp
function Log
{
[cmdletbinding()]
param (
[Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
[string]$text,
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$False)]
[string]$filename
)
begin
{
Write-Debug 'Logging starting'
Write-Debug "Filename: $($filename)"
}
process
{
foreach ($txt in $text)
{
Out-File $filename -append -noclobber -inputobject $txt -encoding ASCII
Write-Verbose $txt
}
}
end
{
Write-Debug 'Logging ending'
}
}
function ExitWithCode
{
param
($exitcode)
Write-Verbose "Ending with $($ExitCode)"
$host.SetShouldExit($exitcode)
exit
}
function Remove-InstalledMSI
{
[cmdletbinding()]
param (
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$True,ValueFromPipelineByPropertyName)]
[ValidatePattern('\{.+\}')]
[Alias('AppID')]
[string]$ProductCode = $null,
[Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$False)]
[string]$LogFilePath,
[Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$False)]
[string]$Property
)
begin
{
Write-Verbose 'Start of Remove-InstallMSI'
$exitcode = $null
}
process
{
foreach ($pc in $ProductCode)
{
Write-Verbose "Uninstall ProductCode: $($pc)"
$AppName = $((Get-LHSInstalledApp -AppID $pc).AppName)
Write-Verbose "AppName: $($AppName)"
if ($LogFilePath -ne $null)
{
$LogFilePath = "c:\windows\TEMP\AP_UNINSTALL_$($AppName).log"
}
else
{
Write-verbose "LogFilePath: $LogFilePath"
}
$argumentlist = "/x $pc /qn REBOOT=ReallySuppress /lv `"$($LogFilePath)`" "
$argumentlist += $Property
Write-Verbose "Argument List: $($argumentlist)"
$exitcode = (Start-Process -filepath "msiexec.exe" -ArgumentList $argumentlist -Wait -PassThru).ExitCode
Write-Verbose "Exit Code: $($exitcode)"
$output = new-object PSObject
$output | add-member NoteProperty "ProductCode" -value $pc
$output | add-member NoteProperty "AppName" -value $AppName
$output | add-member NoteProperty "ExitCode" -value $($ExitCode)
$output | add-member NoteProperty "LogFilePath" -value $LogFilePath
$output
}
}
end
{
Write-Verbose 'End of Remove-InstallMSI'
}
}
$logfile = "$env:windir\temp\AP_$($ApplicationName)_Removal.log"
Write-Verbose "Logfile: $($logfile)"
try { Remove-Item $logfile -force -ErrorAction SilentlyContinue }
catch { Write-Warning $_ }
log $logfile '--------------------------------------------'
log $logfile "$(get-date) - $($ApplicationName) - Removal Started"
log $logfile "$(get-date) - $($ApplicationName) - Searching for $($PublisherName) $($ApplicationName)"
if ($IgnoreInstallDate -eq $true)
{
Write-Verbose "IgnoreInstallDate set $($IgnoreInstallDate)"
log $logfile "$(get-date) - $($ApplicationName) - Ignoring installation date"
}
else
{
Write-Verbose "IgnoreInstallDate set $($IgnoreInstallDate)"
log $logfile "$(get-date) - $($ApplicationName) - Removal only if install date is more than $($InstallDateOlder) days ago"
}
if ($PublisherName)
{
$applist = Get-LHSInstalledApp -AppName "*$($ApplicationName)*" -Publisher "*$($PublisherName)*"
}
else
{
$applist = Get-LHSInstalledApp -AppName "*$($ApplicationName)*"
}
log $logfile "$(get-date) - $($ApplicationName) - Found $($applist.Count) $($ApplicationName) installations"
log $logfile '--------------------------------------------'
$applist | foreach { write-verbose "AppName: $($_.AppName)"}
$ReturnValue = 0
Write-Verbose "Current Exit Code: $($ReturnValue)"
$applist | where {$_.appid -notmatch ('\{.+\}') } | foreach { log $logfile "$(get-date) - $($_.AppName) has no productcode" }
$applist = $applist | where {$_.appid -match ('\{.+\}') }
if ($IgnoreInstallDate -eq $true)
{
$uninstall = $applist | Remove-InstalledMSI
}
else
{
$uninstall = $applist | where-object { ($_.InstallDate -notin ($null,'')) -and`
( ((get-date) - (Convert-DateString -Date $($_.InstallDate) -Format 'yyyyMMdd')).days -gt $InstallDateOlder ) }`
| Remove-InstalledMSI
}
$uninstall | foreach { log $logfile "$(get-date) - $($_.AppName) - Exit Code: $($_.ExitCode)" ; $returnvalue += $_.exitcode }
log $logfile '--------------------------------------------'
log $logfile "$(get-date) - $($ApplicationName) - Removal Finished"
ExitWithCode($ReturnValue)