I recently updated PSWindowsUpdate from version 1.6.1.1 to the latest version (2.1.0.1) and when I try to run the script:
Write-Host " Centralized Update"
Write-Host "================================"
ipmo activedirectory
$computers = Get-ADComputer -Filter {enabled -eq $true} -properties * -SearchBase "OU=Workstations, DC=contoso, DC=com" | select name
$Script = {ipmo PSWindowsUpdate; Get-WUInstall -AcceptAll -Install -Verbose}
foreach ($computer in $computers) {
Write-Host "Running update on:" $computer.name
Invoke-WUJob -ComputerName $computer.name -Script $Script -Confirm:$false -RunNow
}
Write-Host "================================"
pause
I get the following error:
Invoke-WUJob : The system cannot find the file specified. (Exception from HRESULT: 0x80070002)
At C:\Users\Administrator\Desktop\Update_Workstations.ps1:10
char:2
+ Invoke-WUJob -ComputerName $computer.name -Script $Script -Confir ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Invoke-WUJob], FileNotFoundException
+ FullyQualifiedErrorId : System.IO.FileNotFoundException,PSWindowsUpdate.InvokeWUJob
The only edit to the script was to change:
Invoke-WUInstall -ComputerName $computer.name -Script $Script -Confirm:$false
To:
Invoke-WUJob -ComputerName $computer.name -Script $Script -Confirm:$false -RunNow
More details:
The version of PowerShell is 5.1 (both client and server side)
The list of terminals is correctly extracted from the "Workstations" organizational unit
I can connect via "enter-pssession" to all the terminals without problems
Using "Invoke-Command" instead of "Invoke-WUJob" run but fails at the time of download with the error "UnauthorizedAccessException"
What's wrong with the code ? before updating to version 2.1.0.1 it works fine
I was also getting the same error. By scratching my head for a day finally a found one power shell command needs to run on the target server before proceeding invoke-wujob
the command is Enable-WURemoting
I have been working this for a couple days now, and no matter how I run this and work it, it seems to uninstall the program via PowerShell and returns the success code:
__GENUS : 2
__CLASS : __PARAMETERS
__SUPERCLASS :
__DYNASTY : __PARAMETERS
__RELPATH :
__PROPERTY_COUNT : 1
__DERIVATION : {}
__SERVER :
__NAMESPACE :
__PATH :
ReturnValue : 0
PSComputerName :
This happens with various notoriously difficult to remove software such as McAfee.
The command being used is:
Get-WmiObject -Class win32_product -Filter "Name like '%McAfee%'" | ForEach-Object {$_.Uninstall()}
I've tried various scripts, solutions here, and variations of these (such as below).
$uninstall32 = gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "Yahoo Messenger" } | select UninstallString
$uninstall64 = gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "Yahoo Messenger" } | select UninstallString
if ($uninstall64) {
$uninstall64 = $uninstall64.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall64 = $uninstall64.Trim()
Write "Uninstalling (x64)..."
start-process "msiexec.exe" -arg "/X $uninstall64 /qb" -Wait
}
if ($uninstall32) {
$uninstall32 = $uninstall32.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall32 = $uninstall32.Trim()
Write "Uninstalling (x32)..."
start-process "msiexec.exe" -arg "/X $uninstall32 /qb" -Wait}
Even something simple like Yahoo Messenger, the command fails to uninstall the application when run from a Powershell Window as Administrator yet returns a success code and/or is no longer present on the WMI application list.
You can check the MSIInstaller events to find a clue why the uninstall failed:
Get-WinEvent -computername <computername> -ProviderName MSIInstaller -Maxevents 30
You can also log the MSI activity with /le '<logfilepath>' added to your invocation of msiexec.exe and check the results.
I believe the msi install/uninstall operations are asynchronous. You may have to wait within your pssession until the install is finished.
McAfee Agent sometimes requires frminst.exe /forceuninsall to be removed.
I'm having issue with a script I've written and would love some help.
Please note I'm very new to powershell.
I've written a script that uses a txt file that contains remote computers on a domain, I appears to be working to some degree but in the event of a machine being offline I get errors which then loop the script.
$machines
$pcname
Name = 'Machine'
Expression = { $_.PsComputerName }
}
ForEach ($System in $Machines)
{
#Pings machine's found in text file
if (!(test-Connection -ComputerName $System -BufferSize 16 -Count 1 -ea 0 -Quiet))
{
Write-Output "$System Offline"
}
Else
{
#Providing the machine is reachable
#Checks installed programs for products that contain Kaspersky in the name
gwmi win32_product -Filter {Name like "%Kaspersky%"} -ComputerName $Machines | Select-Object -Property $pcname,Name,Version
}
}
At present this runs and output's like so:
Machine Name Version
UKTEST01 Kaspersky Security Center Network Agent 10.1.249
UKTEST02 Kaspersky Endpoint Security 10 for Windows 10.2.1.23
But in the event of a machine not being reachable the following error is given:
gwmi : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
At C:\Scripts\Powershell\Kaspersky Endpoint Security 10\Script\New folder\Kaspersky Checker working v2.ps1:15 char:9
+ gwmi win32_product -Filter {Name like "%Kaspersky%"} -ComputerName $Mach ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject], COMException
+ FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
And then moves to the next machine in the list, and then repeats from the beginning again.
I'd like for this to simply show as:
UKTEST03 Offline
And stop once the last machine in the txt file is done.
Any help or advise would be greatly appreciated.
This is the perfect time to use a Try/Catch/Finally block. The flow is this : Try the block of code here, if you encounter an error, suppress the message and do what is in the Catch block instead.
I've modified your code a bit, so simply copy this whole code block and drop it in, replacing your Else {scriptblock} in your original code.
Else
{
#Providing the machine is reachable
#Checks installed programs for products that contain Kaspersky in the name
Try {Get-WMIObject -Class win32_product -Filter {Name like "%Kaspersky%"} `
-ComputerName $Machines -ErrorAction STOP |
Select-Object -Property $pcname,Name,Version }
Catch {#If an error, do this instead
Write-Output "$system Offline }
}
}
Your completed answer
I've folded in the change you requested, to keep your script from running on every machine in $machines instead of $system, as you likely intended.
ForEach ($System in $Machines){
#Pings machine's found in text file
if (!(test-Connection -ComputerName $System -BufferSize 16 -Count 1 -ea 0 -Quiet))
{
Write-Output "$System Offline"
}
Else
{
#Providing the machine is reachable
#Checks installed programs for products that contain Kaspersky in the name
Try {Get-WMIObject -Class win32_product -Filter {Name like "%Kaspersky%"} `
-ComputerName $System -ErrorAction STOP |
Select-Object -Property $pcname,Name,Version }
Catch {#If an error, do this instead
Write-Output "$system Offline "}
#EndofElse
}
#EndofForEach
}
You could try this:
$machines=... # your machines' names
foreach ($machine in $machines)
{
trap{"$machine`: not reachable or not running WsMan";continue}
if(test-wsman -ComputerName $machine -ea stop){
gcim -Class CIM_Product -Filter 'Name like "%Kaspersky%"' |
select pscomputername,name,version
}
}
I'm using gcim because gwmi is deprecated.
Correction: the correct name is Kaspersky; I corrected it.
I'm very new to PowerShell (forgive my ignorance) and am trying to install a program remotely on multiple computers on my domain. Currently, I'm just trying to get it to work on one computer. The script below was found online and adapted for my needs. Yesterday it worked, but today it's complaining about the session parameter.
I don't fully understand "sessions", but I have ensured on the client machine that the winrm service is running and I have invoked Enable-PSRemoting -force.
Here's the script:
$computers = Get-Content "c:\tmpPS\computers.txt"
$rs = Get-PSSession
Get-PSSession | Get-Member
######
## Functions
################
foreach ($comp in $computers)
{
Write-Host "should work with $comp"
}
PushMSI
RemoteConnect
InstallMSI
Function PushMSI {
Write-Host "------------------------------------------------"
Write-Host "This will copy the MSI file from localhost c:\tmpPS\"
write-Host "------------------------------------------------"
Write-Host ""
Write-Host ""
foreach ($comp in $computers)
{
Copy-Item -path "c:\tmpPS\clientInstall.msi" -Destination \\$comp\c$\tmpPS
}
}
Function RemoteConnect
{
Write-Host "------------------------------------------------"
Write-Host "This will establish a PSSession with all computers in c:\temp\computers.txt"
write-Host "------------------------------------------------"
Write-Host ""
Write-Host ""
Get-Content C:\tmpPS\computers.txt | New-PSSession -ThrottleLimit 50
}
Function InstallMSI
{
Write-Host "------------------------------------------------"
Write-Host "This will Install UPS Update on all computers with an Established PSSession"
write-Host "------------------------------------------------"
Write-Host "After the Install PSSessions will be removed"
Write-Host ""
Invoke-Command -Session $rs -ScriptBlock {invoke-item "c:\tmpPS\ClientInstall.msi"}
}
Get-PSSession | Remove-PSSession
And here's the output:
PS C:\Users\Me> C:\tmpPS\remoteInstall.ps1
Get-Member : No object has been specified to the get-member cmdlet.
At C:\tmpPS\remoteInstall.ps1:3 char:17
+ Get-PSSession | Get-Member
+ ~~~~~~~~~~
+ CategoryInfo : CloseError: (:) [Get-Member], InvalidOperationException
+ FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
should work with eSignWin81.informa.local
------------------------------------------------
This will copy the MSI file from localhost c:\tmpPS\
------------------------------------------------
------------------------------------------------
This will establish a PSSession with all computers in c:\temp\computers.txt
------------------------------------------------
Id Name ComputerName State ConfigurationName Availability
-- ---- ------------ ----- ----------------- ------------
6 Session6 eSignWin81.i... Opened Microsoft.PowerShell Available
------------------------------------------------
This will Install UPS Update on all computers with an Established PSSession
------------------------------------------------
After the Install PSSessions will be removed
Invoke-Command : Cannot validate argument on parameter 'Session'. The argument is null or empty. Supply an argument that is not null or empty and then try
the command again.
At C:\tmpPS\remoteInstall.ps1:49 char:25
+ Invoke-Command -Session $rs -ScriptBlock {invoke-item "c:\tmpPS\ClientInstall.ms ...
+ ~~~
+ CategoryInfo : InvalidData: (:) [Invoke-Command], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeCommandCommand
Try not to use Write-Host as it brakes the pipeline and kills puppy's.
I've tried to improve your script a little bit, so you understand the logic behind the structure better. It hasn't been tested, but it should do the trick.
# First parameters
[CmdletBinding()]
Param(
[ValidateScript({Test-Path $_ -PathType leaf})]
$ComputerList = "c:\tmpPS\computers.txt",
[ValidateScript({Test-Path $_ -PathType leaf})]
$MSI = "c:\tmpPS\clientInstall.msi"
)
# Then functions
Begin {
Function Copy-MSI {
foreach ($Com in $Computers) {
Copy-Item -path $MSI -Destination "\\$Com\c$\tmpPS"
}
}
Function Install-MSI {
foreach ($Com in $Computers) {
Enter-PSSession -ComputerName $Com
invoke-item "c:\tmpPS\ClientInstall.msi"
Exit-PSSession
}
}
}
# Then the actions
Process {
$Computers = Get-Content $ComputerList
Copy-MSI
Install-MSI
}
If you run this, you'll find the information you're looking for:
Get-Help Enter-PSSession
Is there a simple way to hook into the standard 'Add or Remove Programs' functionality using PowerShell to uninstall an existing application? Or to check if the application is installed?
$app = Get-WmiObject -Class Win32_Product | Where-Object {
$_.Name -match "Software Name"
}
$app.Uninstall()
Edit: Rob found another way to do it with the Filter parameter:
$app = Get-WmiObject -Class Win32_Product `
-Filter "Name = 'Software Name'"
EDIT: Over the years this answer has gotten quite a few upvotes. I would like to add some comments. I have not used PowerShell since, but I remember observing some issues:
If there are more matches than 1 for the below script, it does not work and you must append the PowerShell filter that limits results to 1. I believe it's -First 1 but I'm not sure. Feel free to edit.
If the application is not installed by MSI it does not work. The reason it was written as below is because it modifies the MSI to uninstall without intervention, which is not always the default case when using the native uninstall string.
Using the WMI object takes forever. This is very fast if you just know the name of the program you want to uninstall.
$uninstall32 = gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "SOFTWARE NAME" } | select UninstallString
$uninstall64 = gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "SOFTWARE NAME" } | select UninstallString
if ($uninstall64) {
$uninstall64 = $uninstall64.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall64 = $uninstall64.Trim()
Write "Uninstalling..."
start-process "msiexec.exe" -arg "/X $uninstall64 /qb" -Wait}
if ($uninstall32) {
$uninstall32 = $uninstall32.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall32 = $uninstall32.Trim()
Write "Uninstalling..."
start-process "msiexec.exe" -arg "/X $uninstall32 /qb" -Wait}
To fix up the second method in Jeff Hillman's post, you could either do a:
$app = Get-WmiObject
-Query "SELECT * FROM Win32_Product WHERE Name = 'Software Name'"
Or
$app = Get-WmiObject -Class Win32_Product `
-Filter "Name = 'Software Name'"
One line of code:
get-package *notepad* |% { & $_.Meta.Attributes["UninstallString"]}
function Uninstall-App {
Write-Output "Uninstalling $($args[0])"
foreach($obj in Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall") {
$dname = $obj.GetValue("DisplayName")
if ($dname -contains $args[0]) {
$uninstString = $obj.GetValue("UninstallString")
foreach ($line in $uninstString) {
$found = $line -match '(\{.+\}).*'
If ($found) {
$appid = $matches[1]
Write-Output $appid
start-process "msiexec.exe" -arg "/X $appid /qb" -Wait
}
}
}
}
}
Call it this way:
Uninstall-App "Autodesk Revit DB Link 2019"
I found out that Win32_Product class is not recommended because it triggers repairs and is not query optimized. Source
I found this post from Sitaram Pamarthi with a script to uninstall if you know the app guid. He also supplies another script to search for apps really fast here.
Use like this: .\uninstall.ps1 -GUID
{C9E7751E-88ED-36CF-B610-71A1D262E906}
[cmdletbinding()]
param (
[parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$ComputerName = $env:computername,
[parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
[string]$AppGUID
)
try {
$returnval = ([WMICLASS]"\\$computerName\ROOT\CIMV2:win32_process").Create("msiexec `/x$AppGUID `/norestart `/qn")
} catch {
write-error "Failed to trigger the uninstallation. Review the error message"
$_
exit
}
switch ($($returnval.returnvalue)){
0 { "Uninstallation command triggered successfully" }
2 { "You don't have sufficient permissions to trigger the command on $Computer" }
3 { "You don't have sufficient permissions to trigger the command on $Computer" }
8 { "An unknown error has occurred" }
9 { "Path Not Found" }
9 { "Invalid Parameter"}
}
To add a little to this post, I needed to be able to remove software from multiple Servers. I used Jeff's answer to lead me to this:
First I got a list of servers, I used an AD query, but you can provide the array of computer names however you want:
$computers = #("computer1", "computer2", "computer3")
Then I looped through them, adding the -computer parameter to the gwmi query:
foreach($server in $computers){
$app = Get-WmiObject -Class Win32_Product -computer $server | Where-Object {
$_.IdentifyingNumber -match "5A5F312145AE-0252130-432C34-9D89-1"
}
$app.Uninstall()
}
I used the IdentifyingNumber property to match against instead of name, just to be sure I was uninstalling the correct application.
Here is the PowerShell script using msiexec:
echo "Getting product code"
$ProductCode = Get-WmiObject win32_product -Filter "Name='Name of my Software in Add Remove Program Window'" | Select-Object -Expand IdentifyingNumber
echo "removing Product"
# Out-Null argument is just for keeping the power shell command window waiting for msiexec command to finish else it moves to execute the next echo command
& msiexec /x $ProductCode | Out-Null
echo "uninstallation finished"
I will make my own little contribution. I needed to remove a list of packages from the same computer. This is the script I came up with.
$packages = #("package1", "package2", "package3")
foreach($package in $packages){
$app = Get-WmiObject -Class Win32_Product | Where-Object {
$_.Name -match "$package"
}
$app.Uninstall()
}
I hope this proves to be useful.
Note that I owe David Stetler the credit for this script since it is based on his.
Based on Jeff Hillman's answer:
Here's a function you can just add to your profile.ps1 or define in current PowerShell session:
# Uninstall a Windows program
function uninstall($programName)
{
$app = Get-WmiObject -Class Win32_Product -Filter ("Name = '" + $programName + "'")
if($app -ne $null)
{
$app.Uninstall()
}
else {
echo ("Could not find program '" + $programName + "'")
}
}
Let's say you wanted to uninstall Notepad++. Just type this into PowerShell:
> uninstall("notepad++")
Just be aware that Get-WmiObject can take some time, so be patient!
Use:
function remove-HSsoftware{
[cmdletbinding()]
param(
[parameter(Mandatory=$true,
ValuefromPipeline = $true,
HelpMessage="IdentifyingNumber can be retrieved with `"get-wmiobject -class win32_product`"")]
[ValidatePattern('{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}}')]
[string[]]$ids,
[parameter(Mandatory=$false,
ValuefromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
HelpMessage="Computer name or IP adress to query via WMI")]
[Alias('hostname,CN,computername')]
[string[]]$computers
)
begin {}
process{
if($computers -eq $null){
$computers = Get-ADComputer -Filter * | Select dnshostname |%{$_.dnshostname}
}
foreach($computer in $computers){
foreach($id in $ids){
write-host "Trying to uninstall sofware with ID ", "$id", "from computer ", "$computer"
$app = Get-WmiObject -class Win32_Product -Computername "$computer" -Filter "IdentifyingNumber = '$id'"
$app | Remove-WmiObject
}
}
}
end{}}
remove-hssoftware -ids "{8C299CF3-E529-414E-AKD8-68C23BA4CBE8}","{5A9C53A5-FF48-497D-AB86-1F6418B569B9}","{62092246-CFA2-4452-BEDB-62AC4BCE6C26}"
It's not fully tested, but it ran under PowerShell 4.
I've run the PS1 file as it is seen here. Letting it retrieve all the Systems from the AD and trying to uninstall multiple applications on all systems.
I've used the IdentifyingNumber to search for the Software cause of David Stetlers input.
Not tested:
Not adding ids to the call of the function in the script, instead starting the script with parameter IDs
Calling the script with more then 1 computer name not automatically retrieved from the function
Retrieving data from the pipe
Using IP addresses to connect to the system
What it does not:
It doesn't give any information if the software actually was found on any given system.
It does not give any information about failure or success of the deinstallation.
I wasn't able to use uninstall(). Trying that I got an error telling me that calling a method for an expression that has a value of NULL is not possible. Instead I used Remove-WmiObject, which seems to accomplish the same.
CAUTION: Without a computer name given it removes the software from ALL systems in the Active Directory.
For Most of my programs the scripts in this Post did the job.
But I had to face a legacy program that I couldn't remove using msiexec.exe or Win32_Product class. (from some reason I got exit 0 but the program was still there)
My solution was to use Win32_Process class:
with the help from nickdnk this command is to get the uninstall exe file path:
64bit:
[array]$unInstallPathReg= gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match $programName } | select UninstallString
32bit:
[array]$unInstallPathReg= gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match $programName } | select UninstallString
you will have to clean the the result string:
$uninstallPath = $unInstallPathReg[0].UninstallString
$uninstallPath = $uninstallPath -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstallPath = $uninstallPath .Trim()
now when you have the relevant program uninstall exe file path you can use this command:
$uninstallResult = (Get-WMIObject -List -Verbose | Where-Object {$_.Name -eq "Win32_Process"}).InvokeMethod("Create","$unInstallPath")
$uninstallResult - will have the exit code. 0 is success
the above commands can also run remotely - I did it using invoke command but I believe that adding the argument -computername can work
For msi installs, "uninstall-package whatever" works fine. For non-msi installs (Programs provider), it takes more string parsing. This should also take into account if the uninstall exe is in a path with spaces and is double quoted. Install-package works with msi's as well.
$uninstall = get-package whatever | % { $_.metadata['uninstallstring'] }
# split quoted and unquoted things on whitespace
$prog, $myargs = $uninstall | select-string '("[^"]*"|\S)+' -AllMatches |
% matches | % value
$prog = $prog -replace '"',$null # call & operator doesn't like quotes
$silentoption = '/S'
$myargs += $silentoption # add whatever silent uninstall option
& $prog $myargs # run uninstaller silently
Start-process doesn't mind the double quotes, if you need to wait anyway:
# "C:\Program Files (x86)\myapp\unins000.exe"
get-package myapp | foreach { start -wait $_.metadata['uninstallstring'] /SILENT }
On more recent windows systems, you can use the following to uninstall msi installed software. You can also check $pkg.ProviderName -EQ "msi" if you like.
$pkg = get-package *name*
$prodCode = "{" + $pkg.TagId + "}"
msiexec.exe /X $prodCode /passive