I am trying to scan computers in a specific OU in my AD to get the activation status of Windows.
I keep getting
Test-Connection : Testing connection to computer 'CN=PCNAME,OU=MY-OU' failed: No such host is known
although when i try to test the command against a single remote PC, it works fine getting the output
I tries getting all hosts using
$Hosts = Get-ADComputer -Filter \* -SearchBase "OU=MY-OU"
and then ran a for loop to test the connection of each host and using
foreach ($PC in $Hosts) {
if (Test-Connection $PC -Count 1) {
$License = Get-CimInstance SoftwareLicensingProduct -Filter "Name like 'Windows%'" -ComputerName $PC |where { $_.PartialProductKey } | select Description, LicenseStatus
$csv = [PSCustomObject]#{
License = $License
Computername = $PC
}
}
}
Write-Output $csv
Currently your $PC value is like this
$PC = "CN=server1,OU=OU1,OU=OU2,OU=OU3,DC=domain,DC=org"
in order to get computer name, split the string like below and use that value for test-connection
$CN = $PC.Split(',')[0].Split('=')[1]
$domainName = "CN=server1,OU=OU1,OU=OU2,OU=OU3,DC=domain,DC=org"
$CN = $domainName.Split(',')[0].Split('=')[1]
$CN
Edited: There are multiple properties in $Hosts, so instead of splitting distinguished name, use Select-object to get dns hostname.
I do not have an environment to test this code.. so please try yourself.
$Hosts = Get-ADComputer -Filter \* -SearchBase "OU=MY-OU" | Select-Object dnsHostName
$csv = foreach ($dnsHostName in $Hosts) {
Write-Output $dnsHostName
if (Test-Connection $dnsHostName -Count 1) {
$License = Get-CimInstance SoftwareLicensingProduct -Filter "Name like 'Windows%'" -ComputerName $dnsHostName | where { $_.PartialProductKey } | select Description, LicenseStatus
[PSCustomObject]#{
License = $License
Computername = $dnsHostName
}
}
}
$csv | Export-csv -Path C:\temp\output.csv -NoTypeInformation
I am trying to get the availability of the software on pc. My condition is that I need to fetch whether the application is installed or not on my laptop if it is installed is it in working condition?
# Setting Execution policy for the Current User
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
$currentExecutionPolicy = Get-ExecutionPolicy
Write-Output "The Execution Ploicy is set to $currentExecutionPolicy"
$programFilePath = #(
'Google Chrome-C:\Program Files\Google\Chrome\Application\chrome.exe'
'Brackets Text Editor-C:\Program Files (x86)\Brackets\Brackets.exe'
'Microsoft Edge-C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
'Microsoft Excel-C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE'
#'Microsoft Outlook-C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE'
)
$result = foreach($program in $programFilePath) {
$splitString = $program -split ('-')
$program_name = $splitString[0]
$program_path = $splitString[1]
foreach($program in $program_path) {
if (Test-Path -Path $program) {
# Write-Output "Program Path Exists"
$programProcess = Start-Process -FilePath $program -PassThru -ErrorAction SilentlyContinue
timeout 5
try{
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
timeout 1
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
}
catch{
$runningProcess = Get-Process -Name $programProcess.ProcessName
}
if($runningProcess -eq $true) {
[pscustomobject]#{
Application_Name = $program_name
Application_Availability = 'Installed'
Application_Status = 'Working'
}
}
else {
[pscustomobject]#{
Application_Name = $program_name
Application_Availability = 'Installed'
Application_Status = 'Not Working. Error Log is generated as Application_Error_Log text file.'
}
Get-EventLog -LogName Application | Where-Object {$_.InstanceID -eq '1000'} | Tee-Object -FilePath .\Application_Error_Log.txt
}
<# Action to perform if the condition is true #>
} else {
[pscustomobject]#{
Application_Name = $program_name
Application_Availability = 'Not Installed'
Application_Status = $null
}
}
}
}
" :: System Software Audit Report :: " | Out-File .\System_Software_Details.txt
$result | Tee-Object -FilePath ".\System_Software_Details.txt" -Append
timeout 60
Although I am getting the application active which are working and functional but in my output in Text file application status shows : Application_Status = 'Not Working. Error Log is generated as although my application is working fine
My second concern is I am unable to handle the application which is giving me an error
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
timeout 1
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
I think checking filesystem paths is an option but a bad one - you cannot ensure in any case that the expected path is used. Checking the filesystem is only necessary for portable applications.
A better approach is to check the following registry keys, by doing so you get the same result as it is displayed under add/remove programs:
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall
Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -Name DisplayName,DisplayVersion,InstallSource,Publisher,UninstallString
Another way is to query wmi/cim:
Get-CimInstance -query "select * from win32_product"
But by doing so you generate foreach discovered software product an event in the windows event log (Log: Application, Source: MSIInstaller).
To verify if you can start successfully an application by using start-process you need also to specify the parameter -wait and then check if the return code is 0.
$runningProcess -eq $true doesn't necessarily work as $runningProcess is not a boolean but an object. Alas it always returns false.
TL;DR
If you look at your code you see that to get to "...Not Working..." you have to evaluate ($runningProcess -eq $true). Ergo it returns false.
There's always get-package.
$list = '*chrome*','*firefox*','*notepad++*'
$list | % { get-package $_ }
Name Version Source ProviderName
---- ------- ------ ------------
Google Chrome 104.0.5112.102 msi
Mozilla Firefox (x64 en-US) 104.0.1 Programs
Notepad++ (64-bit x64) 7.8.9 msi
Faster as an argument list and no wildcards:
get-package 'google chrome','mozilla firefox (x64 en-us)',
'notepad++ (64-bit x64)'
Or with the threadjob module:
$list = '*chrome*','*firefox*','*notepad++*'
$list | % { start-threadjob { get-package $using:_ } } |
receive-job -wait -auto
I have a script that checks remote servers for tomcat and the associated java versions. It takes about 60 seconds to run against a list of about 16 servers. I'm just curious if the script is as efficient as realistically possible. I'm far from a PowerShell pro but I'm satisfied with the outcome. Just checking for where there is room for improvement.
$Servers = 'server1','server2','etc'
$Output = #()
foreach ($Server in $Servers)
{
$SName = gwmi -Class Win32_Service -ComputerName $Server -Filter {Name LIKE 'Tomcat%'}
IF ($SName -ne $null) {
$Output += [PSCustomObject]#{
Server_name = $SName.PSComputerName
Service_name = $SName.Name
Service_status = $SName.State
Tomcat_version = "$(Get-Content -Path ("\\"+$SName.PSComputerName+"\"+"$($SName.PathName.ToString())".Substring(0,$SName.Pathname.LastIndexOf("\")-3)+"\webapps\ROOT\RELEASE-NOTES.txt" -replace ":", "$") | Select-String -Pattern 'Apache Tomcat Version ')".TrimStart()
Java_Version = (Invoke-Command -ComputerName $Server -ScriptBlock {(GCI -Path "$((Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Apache Software Foundation\Procrun 2.0\Tomcat9\Parameters\Java').Jvm)").VersionInfo.ProductName})
}
}
Else {}
}
$Output | Select Server_name, Service_name,Service_status, Tomcat_Version, Java_Version | Format-Table -AutoSize
Can I simplify things anymore?
Is the time to completion decent for what is being performed?
Invoke-Command allows you to connect with multiple computers at the same time which should be more efficient.
+= is pretty bad, please read: Why should I avoid using the increase assignment operator (+=) to create a collection
You're querying WMI first and then if service is there you are using Invoke-Command, mind as well, connect once to the remote host and check everything.
I personally would do something like this
$Servers = 'server1','server2','etc'
$scriptBlock = {
# Since you're querying each server, and then if the service is there you Invoke-Command,
# mind as well Invoke-Command at first and if the service is there enter the If condition,
# else close the connection.
$tomcatServ = Get-CimInstance -Class Win32_Service -Filter "Name LIKE 'Tomcat%'"
if($tomcatServ)
{
##### This part is pretty confusing for someone reading your code, if you show us how does
##### RELEASE-NOTES.txt looks we may be able to improve it and simplified it a bit
$path = $tomcatServ.PathName.Substring(0,$tomcatServ.PathName.LastIndexOf("\")-3)
$path = Join-Path $path -ChildPath "webapps\ROOT\RELEASE-NOTES.txt"
$tomCatVer = ((Get-Content $path) -replace ":", "$" | Select-String -Pattern 'Apache Tomcat Version ').TrimStart()
##### This part is a bit confusing too
$key = 'HKLM:\SOFTWARE\WOW6432Node\Apache Software Foundation\Procrun 2.0\Tomcat9\Parameters\Java'
$javaVer = GetChild-Item -Path ((Get-ItemProperty -Path $key).Jvm).VersionInfo.ProductName
#####
[PSCustomObject]#{
Server_name = $env:ComputerName
Service_name = $tomcatServ.Name
Service_status = $tomcatServ.State
Tomcat_version = $tomCatVer
Java_Version = $javaVer
}
}
}
$Output = Invoke-Command -ComputerName $Servers -ScriptBlock $scriptBlock -HideComputerName
$Output | Select-Object * -ExcludeProperty RunspaceID | Format-Table -AutoSize
I am trying to get all the list of KB installed on multiple servers and get the last reboot time of the system. My requirement is to get the result in csv or text format with column name "Hostname" , "KB Name" , "installed by" , "installed on" and "Last reboot". I have to execute 2 script to get this done and then i have to format it and i dont want other columns which i am receiving from code 1 only limited column are required.. Can some please help me to get the same format which i reuired?
Output required in below format :
"Source" "Description" "HotFixID" "InstalledBy" "InstalledOn" "Last Reboot"
Please find below 2 code.
FYI : I am new to powershell.
Code 1: This will list all KB installed patch.
$computers = Get-Content -path "C:\Users\joy\Desktop\Machine_List.txt"
$patches = Get-Content -path "C:\Users\joy\Desktop\KB_List.txt"
foreach ($computer in $computers){
foreach ($patch in $patches){
Get-HotFix -id $patch -ComputerName $computer | -OutVariable results -ErrorAction SilentlyContinue
if ($results -ne $null) {
$results | Out-File C:\Users\joy\Desktop\report1.txt -Append -Force
}
else {
Add-content "$Patch is not Present in $computer" -path "C:\Users\joy\Desktop\report2.txt"
}
}
Code 2: This will get the last reboot of the system.
$machines = Get-Content C:\Users\joy\Desktop\Machine_List.txt
$report = #()
$object = #()
foreach($machine in $machines)
{
$machine
$object = gwmi win32_operatingsystem -ComputerName $machine | select csname, #{LABEL='LastBootUpTime';EXPRESSION={$_.ConverttoDateTime($_.lastbootuptime)}}
$report += $object
}
$report | Export-csv C:\Users\joy\Desktop\Reboot.csv
$computers = Get-Content C:\Users\XXXXXXXX\Desktop\Machine_List.txt
$patchlist = Get-Content C:\Users\XXXXXXXX\Desktop\KB_List.txt
foreach($computer in $computers)
{
Get-HotFix -ComputerName $computer -Id $patchlist | select
InstalledOn,InstalledBy,Description,HotFixID,__SERVER | Format-Table | Out-File
C:\Users\XXXXXXXX\Desktop\report1.txt
#Get-CimInstance -ClassName Win32_Operatingsystem | select csname, lastbootuptime |
Format-Table |Out-File C:\Users\XXXXXXXX\Desktop\report1.txt
gwmi win32_operatingsystem -ComputerName $computer | select csname,
#{LABEL='LastBootUpTime';EXPRESSION={$_.ConverttoDateTime($_.lastbootuptime)}} | Out-
File C:\Users\XXXXXXXX\Desktop\report1.txt -Append
}
Try this, This will allow you to get information what you are looking for. But this script only get installed patches information from the remote machines, If you want add one more loop to print the patches which are installed on another text file.
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