-
-
Save tresf/9bf55330421860e923fcbad8f606f53a to your computer and use it in GitHub Desktop.
# 2019 Tres Finocchiaro, Ed Resleff - CC0/Public Domain | |
# - Searches the system for Java | |
# - Looks for specified files or features (e.g. Commercial Features) | |
# Whether or not to show script debbuging information | |
$SHOW_DEBUG = $True; | |
# List of possible Java Runtime locations (including JDK) search locations | |
# Also traverses through 32-bit registry hive (WOW6432Node) | |
$JRE_SEARCH = @{ | |
# Windows-only | |
"HKLM:\SOFTWARE\JavaSoft\Java Runtime Environment" = $null; | |
"HKLM:\SOFTWARE\JavaSoft\Java Development Kit" = $null; | |
"HKLM:\SOFTWARE\JavaSoft\JDK" = $null; | |
# Mac-only | |
"/usr/libexec/java_home" = $null; | |
"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home" = $null; | |
# Universal | |
"PATH" = $null; | |
"JAVA_HOME" = $null; | |
}; | |
# Detect system-specific variables | |
If([System.Environment]::OSVersion.Platform -eq "Unix") { | |
$SLASH = "/"; | |
$UNIX = $True; | |
$NEWLINE = "`n"; | |
$SUFFIX = ""; | |
} Else { | |
$SLASH = "\"; | |
$UNIX = $False; | |
$NEWLINE = "`r`n"; | |
$SUFFIX = ".exe"; | |
} | |
# Wildcard for matching files in JavaHome\bin\, useful for inferring certain commercial features | |
$JAVA_MATCH = @("*.exe"); | |
$jre_found = @(); | |
# Make a human readable title | |
Function Titlize($title) { | |
Write-Host "" | |
Write-Host "".PadRight($title.Length + 4, "="); | |
Write-Host " $title"; | |
Write-Host "".PadRight($title.Length + 4, "="); | |
} | |
Function Debug($item, $color) { | |
If($SHOW_DEBUG) { | |
If($color -ne "" -and $color -ne $null) { | |
Write-Host $item -ForegroundColor $color; | |
} Else { | |
Write-Host $item; | |
} | |
} | |
} | |
# Explodes a string array into a human-readble format | |
Function Explode($arr) { | |
$exploded = " [$NEWLINE"; | |
$arr | % { $exploded += " =>'$_'$NEWLINE"}; | |
$exploded += " ]$NEWLINE"; | |
$exploded; | |
} | |
# Gets the Java directory based on a registry key or environmental variable | |
Function GetJavaHome($key) { | |
Debug "Looking for Java in $key..."; | |
# Continue on errors to avoid early script termination. Change to "Stop" for debugging. | |
$ErrorAction = "SilentlyContinue" | |
If($UNIX -and $key -like "*/java_home" -and (Test-Path $key)) { | |
# Use java_home utility on MacOS | |
Debug " - $key was provided, calling it directly to find Java..."; | |
$javaHome = &$key; | |
} ElseIf($UNIX -and $key -like "*/Internet Plug-Ins/*") { | |
# Passthru for common MacOS directory | |
Debug " - $key was provided, looking there..."; | |
$javaHome = $key; | |
} ElseIf($key -notlike "HK*:\*") { | |
# Treat as environmental variable | |
Debug " - $key doesn't match known pattern; Assuming env var..."; | |
If ($key -eq "PATH") { | |
$javaHome = (Get-Command java -ErrorAction $ErrorAction).Source; | |
Debug " - PATH provided; Using absolute path: $javaHome"; | |
If($javaHome -ne "") { | |
$javaHome = Split-Path -parent (Split-Path -parent $javaHome); | |
Debug " - Calculated parent directory: $javaHome"; | |
} | |
} Else { | |
$javaHome = (Get-Item "env:$key" -ErrorAction $ErrorAction).Value; | |
Debug " - $key provided; Using underlying value: $javaHome"; | |
} | |
} Else { | |
# Treat as registry key | |
Debug " - $key is probably a registry key, traversing..."; | |
$ver = (Get-ItemProperty -Path $key -Name CurrentVersion -ErrorAction $ErrorAction).CurrentVersion; | |
Debug " - Registry shows CurrentVersion=$ver"; | |
If($ver -ne "") { | |
Debug " - Looking at $key\$ver JavaHome..."; | |
$javaHome = (Get-ItemProperty -Path "$key\$ver" -Name JavaHome -ErrorAction $ErrorAction).JavaHome; | |
Debug " - Registry shows JavaHome=$javaHome"; | |
} Else { | |
Debug " - CurrentVersion is blank; Cannot use."; | |
} | |
# Set repeast flag for 32-bit nodes | |
# PowerShell sucks with recursion, so we have to recurse last | |
$try32bit = ($key -notlike "*WOW6432Node*"); | |
} | |
# Sanitize before adding it to the list | |
# TODO: "/usr" may cause issues with macOS by propting to install JRE | |
if ($javaHome -ne "" -and $javaHome -ne $null -and (Test-Path $javaHome)) { | |
Debug " - Appending $javaHome to list of valid locations"; | |
$obj = New-Object -TypeName PSObject; | |
$obj | Add-Member -MemberType NoteProperty -Name Path -Value $javaHome; | |
$obj | Add-Member -MemberType NoteProperty -Name Version -Value (GetJavaVersion $javaHome); | |
$obj; | |
} Else { | |
Debug " - JavaHome is empty or points to a missing location. Skipping."; | |
} | |
If($try32bit) { | |
Debug " - Done but checking the 32-bit registry, just incase..."; | |
GetJavaHome $key.Replace("HKLM:\SOFTWARE\","HKLM:\SOFTWARE\WOW6432Node\"); | |
} | |
} | |
# Tries to parse the Java version from a path, returns as a System.Version object | |
Function GetJavaVersion($value) { | |
# Parse java -version on Unix (can probably be used on Windows, needs testing | |
# stderr needs to be redirected to stdout | |
Debug "Calling java -version to parse version info..."; | |
$info = &$value$($SLASH)bin$($SLASH)java -version 2>&1; | |
# assume it's first value in quotes, e.g. java version "11.0.0" | |
Debug " - Found the following:"; | |
Debug "$info" yellow; | |
Debug " - Assuming version is in double quotes, splitting:"; | |
$parts = "$info" -Split '"'; | |
Debug (Explode $parts) green; | |
$part = $parts[1]; | |
Debug " - Grabbing item at index 1:"; | |
Debug " $part" cyan; | |
Debug " - Splitting $part on any non-digits to separate parts:"; | |
$digits = "$part" -Split "\D+" | Where({ $_ -ne "" }); | |
Debug (Explode $digits) green; | |
If($digits[0] -eq "1") { | |
Debug " - Stripping first digit because it's in old 1.x format..."; | |
# User newer Java formatting (8.0 instead of 1.8) | |
$unused,$digits=$digits; # drop first element | |
Debug (Explode $digits) green; | |
} | |
Debug " - Joining parts with periods and casting to [System.Version]:"; | |
$ver = [System.Version]($digits -Join "."); | |
Debug " $ver" magenta; | |
$ver; | |
} | |
# Calls jcmd and returns a custom property | |
Function CheckCustomProperty($jcmd, $proc, $property) { | |
If((&$jcmd $proc help) -contains "$property") { | |
&$jcmd $proc $property; | |
} Else { | |
Write-Warning "`"$property`" was not found calling `"$jcmd`" $proc help" | |
} | |
} | |
$JRE_SEARCH.Keys | % { $jre_found += (GetJavaHome $_) } | |
Titlize "Java found in the following locations" | |
# Remove duplicates | |
$jre_found = ($jre_found |Sort-Object -Property Path -Unique) | |
# Sort by version | |
$jre_found = ($jre_found |Sort-Object -Property Version -Descending) | |
$jre_found|Format-Table; | |
Debug "Looking for highest JDK version..."; | |
$best = "NOT_FOUND"; | |
$jre_found | % { | |
$jcmdPath = "$($_.Path)$($SLASH)bin$($SLASH)jcmd$($SUFFIX)"; | |
If(Test-Path $jcmdPath) { | |
Debug "- JDK found $_" yellow; | |
$best = $_; | |
Return; | |
} Else { | |
Debug "- This does not appear to be a JDK $_"; | |
} | |
}; | |
Write-Host "`nUsing `"$($best.Path)`"" | |
# List all files in \bin with "java" in the name | |
$jre_found | % { | |
$dir = $_.Path; | |
$files = @(); | |
$JAVA_MATCH | % { | |
Titlize "Matching `"$_`" in `"$dir`""; | |
$files += (Get-ChildItem -Path $dir$($SLASH)bin -Filter $_ -Recurse | %{ $_.BaseName }); | |
Write-Host $files; | |
} | |
}; | |
# Call jcmd from the highest JDK directory found | |
$jcmd = "$($best.Path)$($SLASH)bin$($SLASH)jcmd"; | |
Titlize "Running `"$jcmd`" to get processes found" | |
# Take all PIDs that aren't spawned from jcmd itself :) | |
$pids = (& $jcmd -l).Split("`n") -notmatch "JCmd" | |
$procs = @(); | |
$pids | % { | |
# Get the PID of the Java process | |
$proc = "$_".Split()[0]; | |
# Counter for displaying additional features | |
$feat = 0; | |
# Build an object containing the process info | |
$obj = New-Object -TypeName PSObject | |
# Get the PID | |
$obj | Add-Member -MemberType NoteProperty -Name PID -Value $proc; | |
# Get the path to the running JAR | |
$obj | Add-Member -MemberType NoteProperty -Name Process -Value $_.Substring($proc.Length); | |
# Get the path to the running Java executable | |
$obj | Add-Member -MemberType NoteProperty -Name Path -Value (Get-Process -id $proc).Path; | |
# Any additional features to check for | |
$obj | Add-Member -MemberType NoteProperty -Name "Feature$($feat + 1)" -Value (CheckCustomProperty $jcmd $proc "VM.check_commercial_features"); | |
# Add object to list | |
$procs += $obj; | |
}; | |
$procs |Format-Table # echo everything to the screen |
tresf
commented
May 16, 2019
•
I'm curious.
I can see the value in showing the Java found in the following locations
directories, that's cool and helpful. But what is the purpose of showing the locations of files with the word java
in them? Isn't that a simple dir operation to exhaustively search the file system?
Then, when printing out the Sorted Java versions
, wouldn't it be more useful to show the install locations with the versions detected? In you example above I can see that you have a version of Java (JRE/JDK?) 8.0.202, but I don't know where. It also has five lines of Java version, but only four Java locations found?
Sorry if I sound negative :-) I know people do want some tool to search for installations of Java, so this is a useful task.
what is the purpose of showing the locations of files with the word java in them?
I don't know for sure... @elresleff is the original author and he had it in his example. Inferring from our Slack messages, the purpose is to gather information about potential "Commercial Features". For example, here's an excerpt from our Slack messages...
[...] J Usage Tracker doesn't have an .exe, but if
JAVA_HOME\lib\management\usagetracker.properties
exists... 99% chance that JUT was enabled and is in use. JUT [Java Usage Tracker] is used by JMC [Java (Advanced) Management Console].
With the end-goal to obtain a list of commercial features...
[The] list of Java CF's (Commercial Features) is:
• Java SE Enterprise (MSI) Installer
• Java Flight Recorder
• Java Mission Control
• Java Advanced Management Console
• JRE Usage Tracking
• JRocket Flight Recorder
• JRocket Mission Control
• JRocket Real Time Deterministic GC
So my educated guess is that certain binaries are only present when commercial features are enabled. @elresleff would need to confirm that. I expect the script to start being better at spotting commercial features when he's put his business logic into it.
when printing out the Sorted Java versions, wouldn't it be more useful to show the install locations with the versions detected?
Yes, it should be converted to a sortable object array. String arrays are quicker to write in PowerShell (versus objects) so that's just what was initially chosen. Would be a nice enhancement to the code.
In you example above I can see that you have a version of Java (JRE/JDK?) 8.0.202, but I don't know where
Well, you can figure that out by scrolling up, but yes it would be more useful in context.
has five lines of Java version, but only four Java locations found?
I believe you've found a bug. 11.0.3_7 is listed twice for some reason.
11 0 3 7
@tellison You make a great point. Finding all files with the word java in them was really only a starting point - it's not THE most important thing... it only proves that the files are there. What IS more important is finding all java installations for both JRE and JDK - including version numbers/update levels - if they exist and whether commercial features (CF's) are enabled and in use. the tricky part is that it's very hard to tell if CFs are actually in use and I believe it was designed that way (less intrusion from the M$ overseers).
I actually think listing the files found while tedious and taxing to the system provides proof that the binaries are there. I would agree that they printout may not be necessary and just the install paths themselves be listed. The real magic that Tres was able to pull in was at the bottom of the output where he lists all java versions found and the results of issuing "jcmd VM.check_commercial_features" ...! This is golden!
So @tresf - your "educated guess" is almost spot on. Their mere presence doesn't indicate they are "enabled". So to confirm, let me explain (although you may already know this - and if you do you may opt out) as briefly as possible. When only JRE is installed, Java Usage Tracker (JUT) is the only CF that can be configured and JUT is disabled by default. Enable and configure it by creating a properties file named usagetracker.properties which is usually placed in JAVA_HOME. I think the actual location is wherever the person configuring it decides, but the presence of this file indicates that at one point in time this CF was either attempted to be enabled, or was enabled and is in use, or may not be in use anymore. There is no binary.
The other CF binaries are installed via JDK, but are disabled by default. The CF that utilizes JUT is Java Advanced Management Console (AMC). This is a big ball of wax because it relies on the Console being installed on a server to support i.e. WebLogic but, the AMCAgent gets installed on the server/desktop/laptop, and usually a service is created on a Windows box to run the amcagent.exe binary. The two CF's that are probably used the most are Java Mission Control (JMC) and Java Flight Recorder (JFR), There is no .exe for JFR per se... but you enable JFR by issuing the cmd via the java binary (this cmd includes starting a recording):
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=myrecording.jfr MyApp
JMC (jmc.exe) is a binary that we could search for but it too is disbled by default. However, this one is used by launching it or just double-clicking on a particular .jfr recording file. When JMC is activated though, the "jcmd" is available to retrieve the info from "VM.check_commercial_features" among other cmds. I suspect this same behavior would be available if the amcagent.exe were active, but I haven't been able to test yet. Same goes for the MSI installer. I do not know yet what binary that would relate to and I choose not to hazard a guess, but the binary would be on the server where SCCM is being utilized. One bit I do know, if the MSI is being used in SCCM... then ALL workstations require a license regardless of CF's being enabled or used because the SCCM uses the MSI to push out Java SE Advanced - requiring a license. Which also might be a good first check...(?)
I've driveled on long enough... but I hope this at least gives an idea from my research on commercial features. And of course if I wasn't clear or it prompts more questions... by all means please ask.
@elresleff I think that explanation helps. I think @tellison's asking more broadly in regards to the usefulness of those lines. If the script is useful, I encourage others to fork and edit it. It may be a good candidate for a small CmdLet where we can do...
DetectJava.ps1 # lists JREs and JDKs and their locations
DetectJava.ps1 -CF # same as above also listing commercial features
This detection logic can get fun too... Some JDKs are provided by 3rd parties (WebSphere, SAP) and we can add more search locations easily. If it gets to that point, we can move it to GitHub so that we have a centralized location for the code, and I can even throw some unit tests in the mix if it gets to that point.
@tresf I'm glad it helped. And with the idea of expanding it to include other third parties... I can see this helping others immensely. @tellison may be more right than not... the usefulness of those lines might be considered extra fluff and not necessary. However, in my case they may hold more weight. I'd love to help collaborate further on this, especially after I've gotten more testing done.
I'd like to first take a stab at introducing Get-content machinelist.txt and export-csv before asking you to do it.
Updated. Details below:
what is the purpose of showing the locations of files with the word java in them?
@tellison addressing this and per @elresleff's request, I've switched this to *.exe
(this won't match anything on non-Windows OSs) and condensed the format to make it fit on a terminal screen. Sample updated.
more useful to show the install locations with the versions detected?
@tellison Done.
PS U:\> .\FindJava.ps1
=========================================
Java found in the following locations
=========================================
Path Version
---- -------
C:\Program Files\AdoptOpenJDK\jdk-11.0.3.7-hotspot 11.0.3
c:\Program Files\Java\jdk-11.0.2\ 11.0.2
C:\Program Files\Java\jre1.8.0_211 8.0.211
C:\Program Files\Java\jdk1.8.0_202 8.0.202
has five lines of Java version, but only four Java locations found?
11.0.3_7 is listed twice for some reason.
@tellison Fixed.
I'd like to first take a stab at introducing Get-content machinelist.txt and export-csv before asking you to do it.
Sure, I haven't added this yet. Knock yourself out. ;)
Hi Tres,
I'm wondering if the jcmd cmd was altered in the script...(?) I just ran the script against my machine, and got basically nothing at the point the jcmd was executed. This, although I'd like to have a statement like "No Commercial Features running" or what I had seen before "Java commercial features are locked" is understandable since I didn't have a CF running. I then launched JMC and reran the script and now it returns: (Please pardon the irregular formatting...)
================================================================================
Running "C:\Program Files\Java\jdk1.8.0_201\bin\jcmd" to get processes found
================================================================================
Error parsing arguments: No command specified
WARNING: "VM.check_commercial_features" was not found calling "C:\Program Files\Java\jdk1.8.0_201\bin\jcmd 12340"
PID Process Path Feature1
--- ------- ---- --------
12340 C:\Program Files\Java\jdk1.8.0_201\bin\jmc.exe
However, scratching my head, I just ran jcmd separate from the script and got this:
PS > jcmd 12340 VM.check_commercial_features
12340:
Commercial Features are unlocked.
Status of individual features:
Java Flight Recorder has been used.
Resource Management is disabled.
Current Memory Restriction: None (0)
PS >
So, I actually would expect if any CF's are running to have the script return at least: "Commercial Features are unlocked." but would like to have the whole "Status" message.
So did something change?
Ed
Probably a bug in my VM.check_commercial_features
sanitization logic.
@elresleff I've tested the script on various JDKs and I cannot reproduce the error. It's probably a bug, but I'm not sure where. It should return the same value as if you call it on command line.
I'm wondering if JAVA_HOME has anything to do with it. I'm getting the following errors (I apologize for the length AND the formatting... the bullets are supposed to be "+" chars):
PS C:\temp> .\er_Find_java_CFs_20190518-1030.ps1
& : The term 'C:\Program Files\Java\bin\java' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\temp\er_Find_java_CFs_20190518-1030.ps1:88 char:14
-
$info = &$value$($SLASH)bin$($SLASH)java -version 2>&1;
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- CategoryInfo : ObjectNotFound: (C:\Program Files\Java\bin\java:String) [], CommandNotFoundException
- FullyQualifiedErrorId : CommandNotFoundException
Cannot index into a null array.
At C:\temp\er_Find_java_CFs_20190518-1030.ps1:92 char:8
-
If($digits[0] -eq "1") {
-
~~~~~~~~~~~~~~~~~~
- CategoryInfo : InvalidOperation: (:) [], RuntimeException
- FullyQualifiedErrorId : NullArray
Cannot convert value "" to type "System.Version". Error: "Version string portion was too short or too long."
At C:\temp\er_Find_java_CFs_20190518-1030.ps1:96 char:5
-
[System.Version]($digits -Join ".");
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- CategoryInfo : InvalidArgument: (:) [], RuntimeException
- FullyQualifiedErrorId : InvalidCastParseTargetInvocation
& : The term 'C:\Program Files (x86)\Common Files\Oracle\Java\bin\java' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\temp\er_Find_java_CFs_20190518-1030.ps1:88 char:14
-
$info = &$value$($SLASH)bin$($SLASH)java -version 2>&1;
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- CategoryInfo : ObjectNotFound: (C:\Program File...e\Java\bin\java:String) [], CommandNotFoundException
- FullyQualifiedErrorId : CommandNotFoundException
Cannot index into a null array.
At C:\temp\er_Find_java_CFs_20190518-1030.ps1:92 char:8
-
If($digits[0] -eq "1") {
-
~~~~~~~~~~~~~~~~~~
- CategoryInfo : InvalidOperation: (:) [], RuntimeException
- FullyQualifiedErrorId : NullArray
Cannot convert value "" to type "System.Version". Error: "Version string portion was too short or too long."
At C:\temp\er_Find_java_CFs_20190518-1030.ps1:96 char:5
-
[System.Version]($digits -Join ".");
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- CategoryInfo : InvalidArgument: (:) [], RuntimeException
- FullyQualifiedErrorId : InvalidCastParseTargetInvocation
=========================================
Java found in the following locations
Path Version
C:\Program Files\Java\jdk1.8.0_201 8.0.201
C:\Program Files\Java\jre1.8.0_201 8.0.201
C:\Program Files (x86)\Common Files\Oracle\Java
C:\Program Files\Java
Using "C:\Program Files\Java\jdk1.8.0_201"
============================================================
Matching "*.exe" in "C:\Program Files\Java\jdk1.8.0_201"
appletviewer extcheck idlj jabswitch jar jarsigner java-rmi java javac javadoc javafxpackager javah javap javapackager javaw javaws jcmd jconsole jdb jdeps jhat jinfo jjs jmap jmc jps jrunscript jsadebugd jstack jstat jstatd jvisualvm keytool kinit klist ktab native2ascii orbd pack200 policytool rmic rmid rmiregistry schemagen serialver servertool tnameserv unpack200 wsgen wsimport xjc
============================================================
Matching "*.exe" in "C:\Program Files\Java\jre1.8.0_201"
jabswitch java-rmi java javacpl javaw javaws jjs jp2launcher keytool kinit klist ktab orbd pack200 policytool rmid rmiregistry servertool ssvagent tnameserv unpack200
=========================================================================
Matching "*.exe" in "C:\Program Files (x86)\Common Files\Oracle\Java"
===============================================
Matching "*.exe" in "C:\Program Files\Java"
================================================================================
Running "C:\Program Files\Java\jdk1.8.0_201\bin\jcmd" to get processes found
Error parsing arguments: No command specified
WARNING: "VM.check_commercial_features" was not found calling "C:\Program Files\Java\jdk1.8.0_201\bin\jcmd 12340"
PID Process Path Feature1
12340 C:\Program Files\Java\jdk1.8.0_201\bin\jmc.exe
PS C:\temp>
My thinking is GetJavaVersion function is trying to run even though I'm running it in Windows. "C:\Program Files\Java\bin\java" has something to do with it... because my PATH is listed as "C:\Program Files\Java\jdk1.8.0_201\bin..." and I don't know where "C:\Program Files\Java\bin\java" is coming from.