PowerShell: Detect and Uninstall Java FAST

I’ve been building out a remote, silent Java Updater for awhile now. Making sure it works perfectly in my environment is not an easy task. I’m 90% there, I just have to get over the last few hurdles.
One of the major problems was uninstalling Java efficiently. You can Google scripted Java uninstalls, and you’ll probably find the same recycled code over and over again. (I’ve provided it below, but don’t touch it) The problem is, this code is terrible.

Let me explain. The code works as intended. That’s not why it’s terrible. Why am I complaining then? It uses a very, very bad WMI class. What’s WMI you ask? Not the problem. The class is. It’s a huge waste of time. How bad is it?

Win32Product_Evil

When even Bing thinks it’s bad. It’s bad.

 

Get-WmiObject -Class win32_product would have been a very useful command, however, it’s by far the slowest and most inconsistent thing I have ever used.  Worse, you can’t fix it. Use a WHERE clause, use a FILTER, use wildcards, use explicit values. It doesn’t matter. It’s still slower than molasses. If you plan to use this command, you might as well just go to lunch after you hit execute.

If you didn’t check out those links above, let me summarize:

Invoking win32_product causes a computer to enumerate through the entire list of installed programs and validates each install, causing potentially extremely long wait times before returning results. Regardless of where clauses or filters, every program is always checked in this way.

In testing, the command takes 20-40 minutes to return results on a production server. Even on my computer it takes at least 5 minutes, normally much longer. I have ran this command and gotten results as fast as 11 seconds…after I ran it three times in quick succession. Then running it again five minutes later already took three minutes again. And that was the best case scenario test I had. That’s just unacceptable. Here’s that terrible code with a timer included. Take my word for it and just skip past this.

Win32_Product (Very Slow – Just Skip It!)

#### GET INSTALLED JAVA VERSION - SLOW!
$Start = Get-Date
$Java = Get-WmiObject -Class win32_product | WHERE { $_.Name -LIKE "Java [0-9]*" }
$Current = (Get-Date) - $Start
"Execution Time: $($Current.Hours):$($Current.Minutes):$($Current.Seconds)"
$Java

If you ran that, I’m sorry you wasted away half your life waiting for it to complete, but I did warn you.

My Method (Fast but Wordy)

I came up with a method to uninstall through registry guids. Detecting the java installation guid goes from 30 minutes to <1 second in this manner. The entire installation process was cut by at least 98%, over half an hour down to roughly 25 seconds! I can work with that! The code is a lot longer though.


########## GET INSTALLED JAVA VERSION - FAST!
$Start = Get-Date
Function Get-Uninstaller
{
$ErrorActionPreference = "SilentlyContinue"
$Guids = Get-ChildItem -Path 'Registry::HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall' | ForEach {$_.Name}
$Products = ForEach ($Guid in $Guids)
{
$Results = "" | SELECT DisplayName, IdentifyingNumber
$Results.DisplayName = Get-ItemProperty -Path $($Guid.Replace("HKEY_LOCAL_MACHINE","Registry::HKLM")) | ForEach {$_.DisplayName}
$Results.IdentifyingNumber = Get-ItemProperty -Path $($Guid.Replace("HKEY_LOCAL_MACHINE","Registry::HKLM")) | ForEach {$_.UninstallString.Split("X")[1]}
$Results
}
$Products
}#EndFunction Get-Uninstaller

$Java = Get-Uninstaller | WHERE { $_.DisplayName -LIKE "Java [0-9]*" }
$Current = (Get-Date) - $Start
"Execution Time: $($Current.Hours):$($Current.Minutes):$($Current.Seconds)"
$Java

Win32Reg_AddRemovePrograms (Fast, but last minute discovery)

But wait, there’s more! The documentation on how broken win32_products is provided me with an even easier method. I didn’t find it until researching some links for this blog though. I was very skeptical about this code until I ran it, because I never saw anyone suggesting its use online while searching for a PowerShell uninstaller. It’s actually just as instantaneous as my registry read method!


########## GET INSTALLED JAVA VERSION - Alternative
$Start = Get-Date
$Java = Get-WmiObject -Class win32reg_addremoveprograms | WHERE { $_.DisplayName -LIKE "Java [0-9]*" }
$Current = (Get-Date) - $Start
"Execution Time: $($Current.Hours):$($Current.Minutes):$($Current.Seconds)"
$Java.DisplayName
$Java.ProdID

Uninstall Script

Whichever method you choose, you’ll need to then run the uninstaller still. Just change the Guid Identifier appropriately for whichever script you choose. That is, $App.IdentifyingNumber or $App.ProdID. I’m logging to a Temp Java folder, so make sure that folder exists or just remove the /l and everything after it if you don’t care about the logging. The uninstall runs silently with no user interface and suppresses reboots.


########## UNINSTALL JAVA
ForEach ($App in $Java)
{
$ArgumentList = "/x $($App.IdentifyingNumber) /qn /norestart /l*vx C:\Temp\Java\UninstallLog.log"
Start-Process -FilePath "msiexec.exe" -ArgumentList $ArgumentList -Wait -PassThru
}

 

PowerShell Recursive Registry Update

A friend asked me earlier this week if I could provide a PowerShell solution for him.  The task called for searching all sub folders under a root in the registry and updating a specific key name to a new value.
This sounded like fun and something easily handled, but it turned out to be rather difficult to provide a solution.  I floundered for a while trying to understand his requirements.  He finally thought to send me a reg file so I could test with the keys he needed.
Suddenly my eyes were opened and things went much smoother. I had a list of keys now, but a lot of long code that didn’t work well for the update task.  Eventually I had a concise, simple solution, but I’m a bit embarrassed to say that it took me much longer than I had planned.

Background

This problem was specifically for Putty, which I had never heard of before this week.  I still know basically nothing about it, except that it was made by this guy because his name is in the registry path.  (clever way to make sure everyone knows who created your software)

You can use my final solution at the bottom to get and update Putty’s registry properties. I assume that’s useful, my friend seemed to be happy with it. For non-Putty users, you may have some need to search the registry for a key that you know exists but are not sure exactly where it is located. The Get statement can help you out there, since searching in regedit can be quite slow. I haven’t fathomed a scenario where I would need to update all like-named keys to a new identical value outside of Putty though.

The Script

Originally, my plan was to just use the following code for a fast solution.

Get-ItemProperty -Path $Path -Recurse

That’s not actually valid code though.

Turns out -Recurse isn't a valid parameter in every cmdlet

Turns out -Recurse isn’t a valid parameter in every cmdlet

So I started working on a horrific work-around involving ForEach statements and lots of piping. I eventually had a solution to get all the keys, but then updating them from that list did not work, so I went back to the basics, and realized it was a very easy query.

You can search the registry using Get-ItemProperty or Get-ChildItem. The latter has pros and cons though. While you can use -Recurse with it, the results are a bit uglier to read and why I was originally avoiding it.  I also assumed that I could not Set values that were piped from it. However, in this case, it is the superior method. I have included a before and after Get statement in the working script so that you can see the keys you are about to update before, and then verify the new values afterwards.

Disclaimer: I apologize if this code has dreadful formatting, WordPress was ignoring my line breaks while I used an outdated browser.

## Set Variables
$Path = "HKCU:\Software\SimonTatham\Putty\Sessions"
$Key = "LogFileName"
$NewValue = "C:\text.txt"

### Check Existing Values
Get-ChildItem $Path | Get-ItemProperty -Name $Key | 
    Select PsChildName,LogFileName

### Update Values
Get-ChildItem $Path | Set-ItemProperty -Name $Key -Value $NewValue

### Verify New Values
Get-ChildItem $Path | Get-ItemProperty -Name $Key | 
    Select PsChildName,LogFileName

Remotely Get & Set Delayed Start for a Service in PowerShell

This week I was asked to check all the SQL Servers to verify that a specific service was running and that it was set to Delayed Start. Requests like this are great fun for me, I get to play with PowerShell and sometimes even get to reuse scripts I’ve written.

The first step was something I’ve done before, setting a service through PowerShell, so I was confident that I had a script for it and the task would be complete in minutes.

Setup

I had around a hundred servers to update. My advice is to use a notepad file with a server list if you have to update more than even half a dozen computers. It’s just a lot easier than formatting them all for a variable. The below declaration will pull the details of your notepad file using the Get-Content cmdlet. I keep a computer list on my desktop and the variable $Env:UserName will pull the currently logged in user, so anytime I share a script, the next person just has to make a similar list. Remember to set one computer per notepad line.

$Computers = ( Get-Content C:\Users\$Env:UserName\Desktop\Computers.txt )

I’m going to work with the Windows Update service in the following examples since every computer should have this.

$Service = "wuauserv"

What doesn’t work

It turns out that PowerShell does not natively support setting Auto Delayed Start with the Set-Service cmdlet. Why was it overlooked? There are a few connect threads out there for the issue. Go ahead and vote them up, we might get lucky and it will make a difference.

SetStartUpType

Like the Connect threads mention, there is no member for Startup Type, you can verify that it’s missing with the following query to Get-Service.

Get-Service | Get-Member -MemberType Property
Notice no Startup
Notice Startup Type is missing

This means we will have to set the service to running first, then set the startup type via a workaround afterwards.

There are a number of ways to set a service to running in PowerShell, but I’ll share two short ways. They are basically the same method; the first one just pipes the command. The benefit of the first method is that you can run the command before the pipe to verify what you are about to affect. It’s a bit like doing a select statement before you run an update in SQL. It’s all about being safe.

Method 1: Piping

Run the code before the pipe (the | character) first to verify what is going to be set. Then run the whole part once you are satisfied with the results.

 Get-Service $Service -ComputerName $Computers |
Set-Service -Status Running 

Method 2: Single Cmdlet

This method is more straightforward. Nothing is wrong with Method 2; it’s just for those who are a bit more confident in their PowerShell skills, or for the ones who like to throw caution to the wind.

Set-Service -Name $Service -ComputerName $Computers -Status Running

Verify Service is Running

It’s a good idea to verify that the services are now running afterwards. There’s almost always a computer that fails to connect and you’ll have the peace of mind that some random code you found online did run successfully.This query returns any computers with a service that is still not running.

Get-Service $Service -ComputerName $Computers |
Where-Object { $_.Status -ne 'Running' } |
Format-Table MachineName,Status,Name -AutoSize

Set Delayed Start

This step gets problematic. We have to step outside PowerShell a bit to use an obscure command line script, at least it’s obscure to me, Sc Config. Throwing a ForEach loop around the command will allow it to run against all your computers, just remember the \\$Computer parameter and that Start= Delayed-Auto has to be written exactly like I have it (including the space after the equal sign) unless you like getting errors. I added an optional Write-Host to insert whitespace and the computer name. You’ll want that if any connections fail so that you know which computer is the issue.

ForEach ( $Computer In $Computers )
{
Write-Host "`n $Computer"
SC.EXE \\$Computer Config $Service Start= Delayed-Auto
}

Verifying Delayed Start

Now we have to use yet another method to verify that the service is set to Delayed Start. We know PowerShell doesn’t work already. Using WMI doesn’t help either, it returns misleading results. Everything is Auto! It doesn’t tell you if it’s Delayed Start or regular Automatic. For example:

ForEach ( $Computer In $Computers )
{
Get-WmiObject -Query "Select StartMode From Win32_Service Where Name='$Service'" |
Select-Object @{l='MachineName';e={$Computer}},StartMode
}

The only viable choice is to search for a registry key that is created for Delayed Start services. The following script will return your desired service if it has a Delayed Auto Start value. Since it’s such a weird query, I wanted to see everything this time. I’m using Invoke-Command to demonstrate a different cmdlet. Notice that I defined $Service inside the ScriptBlock. If you fail to do this, the variable will not be declared. It’s all compartmentalized inside the Invoke-Command per computer.

Invoke-Command -ComputerName $Computers -ScriptBlock {
$Service = "wuauserv"
Write-Host "`n $Env:ComputerName"
Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\Services |
Where-Object {$_.Property -Contains "DelayedAutoStart" -And $_.PsChildName -Like "$Service*" } |
Select-Object -ExpandProperty PSChildName
}

Summary

It was a lot more work than I anticipated to set Delayed Auto Start on a service, but I learned some neat tricks in the meantime. If you just have to set a service to running or one of the three supported Startup methods, you’ve gotten off easy, and you won’t need this work around.

PowerShell Solution to Java Version

I have to verify many patch statuses each month. A few days ago, I needed to get the current version of Java installed on all of my servers. Obviously I wasn’t going to log into every box, so I needed a remote method. Of course PowerShell could come to my rescue, but which method would be best?

After a little bit of research online, I discovered that there are numerous of ways to find the version of Java installed, but these are not all created equally, I’ve mentioned a few below, along with my final script at the bottom.

WMI Performing a WMI query was by far the worst method to get Java. I can’t actually tell you if it works, because the query did not complete. I even tried running it again the entire time I was writing this post. I didn’t get to the point of trying to filter or refine the result to fit my needs, let alone make it work remotely. Just pass over this code and continue to the useful ones below.

Get-WmiObject -Class Win32_Product | Where { $_.Name -like "Java" }

Registry Key Reading the registry though Get-ItemProperty is quick and easy if you want to pull the local server. It gets much tougher if you want to read remotely though. My remote registry read will work here as well, but the registry key was not showing the version level detail I needed. Java 1.8.0_xx isn’t enough, I need to know each computer has Java 8.0.xxx.xx installed; I need the build number.

Local Only Query

$Key ='HKLM:\Software\JavaSoft\Java Runtime Environment'
(Get-ItemProperty -Path $Key -Name Java7FamilyVersion).Java7FamilyVersion

Remote Query

$Computer = "$Env:ComputerName"
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$Computer)
$RegKey= $Reg.OpenSubKey("Software\JavaSoft\Java Runtime Environment")
$RegKey.GetValue("Java7FamilyVersion")

Get-ChildItem The idea of this method seemed strange to me, but it actually worked the best, so it’s what I used for my final solution. In its purest form, you just get the details of the java executable and then display the version info.

(Get-ChildItem "C:\Program Files\Java\jre*\bin\java.exe").VersionInfo.ProductVersion

Below is my full version of the script, designed with remote querying in mind and error handling.

Final Solution

# Update $Computers to reflect your server list
$Computers = (Get-Content C:\Users\$Env:UserName\Desktop\AllServers.txt)
ForEach ( $Computer in $Computers )
{
$PingStatus = Gwmi Win32_PingStatus -Filter "Address = '$Computer'" | Select-Object StatusCode
IF ($PingStatus.StatusCode -eq 0)
{
IF (Test-Path ("\\$Computer\C$\Program Files\Java\jre*\bin\java.exe"))
{
$Java = gci "\\$Computer\C$\Program Files\Java\jre*\bin\java.exe"
$Java | Select @{n='Computer';e={$Computer}},@{n='JavaVersion';e={$java.VersionInfo.ProductVersion}}

}
ELSEIF (Test-Path ("\\$Computer\C$\Program Files (x86)\Java\jre*\bin\java.exe"))
{
$Java = gci "\\$Computer\C$\Program Files (x86)\Java\jre*\bin\java.exe"
$Java | Select @{n='Computer';e={$Computer}},@{n='JavaVersion';e={$java.VersionInfo.ProductVersion}}

}
ELSE
{
$Computer | Select @{n='Computer';e={$Computer}},@{n='JavaVersion';e={'-'}}
}
}#EndIf
ELSE
{
$Computer | Select @{n='Computer';e={$Computer}},@{n='JavaVersion';e={'ConnectFailed'}}
}
}#EndForEach

Most of the final script is just error handling. My first If statement tests if the server is pinging, then If statement nested within that tests for Java on a 64 or 32 bit system. This way, I know what version of Java is installed, if no Java is installed, or if the connection failed entirely.

Remote Registry Read/Write

Eventually you will need to verify registry keys on a list of computers, whether for security reasons, or some project. I find navigating the registry editor very tedious because of the sheer number of files that must be waded through. The endless collapsing and expanding…it’s just dreadful. Getting lost in a huge subfolder is one of my biggest annoyances. Registry read issues are further compounded when you have to deal with multiple computers. I find it much easier, and faster, to read and set values across many computers all at once using TSQL, or better yet, PowerShell.

SQL Server has a way to query and set values in the registry, although you may not realize it. This isn’t something everyone has to every day, but when you do, you want an easy way to handle it. The TSQL commands are actually relatively simple. XP_REGREAD and XP_REGWRITE can solve your read/write woes, but with limitations.

For all examples, I’ll be reading and writing values for UAC, which can be located in the registry under HKEY_LOCAL_MACHINE at Software\Microsoft\Windows\CurrentVersion\Policies\System controlled by the value EnableLUA. A value of 1 means UAC is On, while a value of 0 means UAC is Off.

To read a registry value using TSQL, run the following command:

EXECUTE MASTER.dbo.xp_regread
'HKEY_LOCAL_MACHINE' --HKEY directory
,'Software\Microsoft\Windows\CurrentVersion\Policies\System' --Registry Path
,'EnableLUA' --Registry Key

To set UAC On using TSQL, run the following command: (Set the last value to ‘0’ to turn UAC Off)

EXECUTE MASTER.dbo.xp_regwrite
'HKEY_LOCAL_MACHINE'
,'Software\Microsoft\Windows\CurrentVersion\Policies\System'
,'EnableLUA'
,'reg_sz' --Other options include reg_binary and reg_dword
,'1' --Key Value to set, in this case it would be 0 or 1

You may notice that the Results Pane indicates that 0 rows are affected. It made me angry when I tried to build these examples, but SQL Server is a liar here. Read the value of the registry key once again, and you will see that the value did update to your desired value. Flip the value on and off if you really don’t believe me. The problem with using TSQL lies in the limitations of SQL Server. Obviously you cannot read/write registry values of computers that do not have SQL running on them, and this includes passive nodes. How about PowerShell as a better solution?

PowerShell is a skill that is becoming more necessary as environments grow. I would argue that anytime you need to touch more than one computer, you should consider a remote solution using PowerShell. Plus, reading and writing to the registry just feels right when working through PowerShell.

The simple way of querying a registry value in PowerShell is as follows:

$Path = 'HKLM:Software\Microsoft\Windows\CurrentVersion\Policies\System'
$Key = 'EnableLUA'
Get-ItemProperty -Path $Path -Name $Key

However, unlike most Cmdlets, this one does not accept a ComputerName Property. Probably because Microsoft hates us. You can use a slightly more complicated method to query a remote registry though.

$Computer = "YourComputerHere"
$Path = 'Software\Microsoft\Windows\CurrentVersion\Policies\System'
$Key = 'EnableLUA'
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$Computer)
$RegKey = $Reg.OpenSubKey($Path)
$Value = $RegKey.GetValue($Key)
Write-Host "$Computer $Value"

Please note that I did not include HKLM: in the $Path variable this time. If you leave that in, you’ll receive an error stating that “You cannot call a method on a null-valued expression.” Using this script does not require the HK as it only deals with Registry Paths. The original Cmdlet we used, Get-ItemProperty, can get the properties of files, so it needs the extra distinction.

Next, we will expand the script slightly to include a ForEach statement in order to query multiple computers at once.

$ComputerList = "Computer1","Computer2"
ForEach ($Computer in $ComputerList)
{
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$Computer)
$RegKey = $Reg.OpenSubKey($Path)
$Value = $RegKey.GetValue($Key)
Write-Host $Computer $Value
}

PowerShell includes an equally easy way to set the registry values, can you guess what the Cmdlet name is? Luckily that was a rhetorical question, and I will not make you answer in the comments. You can use Set-ItemProperty to update registry values. However, you are still limited to the local computer with this method. Let’s skip straight to the multi-server solution, as the baby steps are the same as those we took above with the Get statement.

$Newvalue = 1
ForEach ($Computer in $ComputerList)
{
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$Computer)
$RegKey= $Reg.OpenSubKey($Path,$True)
$RegKey.SetValue($Key,$NewValue,[Microsoft.Win32.RegistryValueKind]::String)
$Value = $RegKey.GetValue($Key)
Write-Host $Computer $Value
}

I keep these scripts in a repository so I can quickly verify Registry Keys across my environment. PowerShell is a perfect solution for reading and writing settings remotely. There’s no limit to the number of bells and whistles you can add to PowerShell scripts,(try adding a Try-Catch for unreachable servers) but the important thing is getting the work done without having to touch every computer, one at a time. Don’t work hard; work smart, so you can read more blogs.