daisy/pipeline-assembly

Windows NSIS installer's JRE version checking logic doesn't work with the newest JRE

Closed this issue ยท 14 comments

The newest version of Java, 1.8.0_91, is not recognized by the installer, which leads the installer to install an older version.
On Windows Server 2008 R2, at least, Java 1.8.0_91 installer does not add the keys (HKLM "SOFTWARE\JavaSoft\Java Runtime Environment\$2" "MicroVersion" and HKLM "SOFTWARE\JavaSoft\Java Runtime Environment\$2" "UpdateVersion") expected in https://github.com/daisy/pipeline-assembly/blob/master/src/main/nsis/GetJavaVersion.nsh#L32 to the registry. This causes the install to fail. This can (but should not!) be worked around by removing the whole JRE check block starting from https://github.com/daisy/pipeline-assembly/blob/master/src/main/nsis/installer.nsi#L173 . Two possible options would be to just check for Java 1.8 (if the minor versions don't have any necessary updates, or adding the possibility to bypass the check (with warnings, naturally).
Of course, it's possible that for some reason the Java installer just misbehaves on this particular server; unfortunately, I have no easy way of checking if it works on other boxes myself.

thanks for the issue @jukkae. Unfortunately we need to check the minor version (something related to javafx), I'll try to see if we can send some kind of argument to the installer so we can bypass the version check.
daisy/pipeline#479

Another temporary workaround for this is to accept the JRE installation and cancel it -- the installer then continues as normal.

@bertfrees and I have been discussing this, and we have some ideas:

It seems the problem (or at least one of them) we found was between 64-bit and 32-bit JRE/JDK versions.

Bert has a 32-bit JDK 1.8 installed on Windows Server 2012 (along with a few 64-bit JDKs), and I have a 64-bit JDK 1.8 installed on Windows 10. He does not get a JRE install prompt during installation, but I do.

In GetJavaVersion.nsh, ReadRegStr HKLM "SOFTWARE\JavaSoft" gets redirected to the 32-bit key HKLM\WOW6432Node\SOFTWARE\JavaSoft as NSIS defaults to 32-bit registry mode. Because I have a 64-bit JRE installed, there are no Java version keys in WOW6432Node for me, so the installer can't find the version and therefore I don't pass the JRE/JDK version check.

To solve this, we can add another set of registry checks (before our current check), and temporarily SetRegView 64. As described here, on 64-bit Windows it won't get redirected and should therefore be able to find the Java version, and on 32-bit the SetRegView 64 will simply be ignored.
(we will also likely want to add the ${If} ${RunningX64} check mentioned in that thread)

A few more considerations

An alternative is to just not look in the registry at all and require the user to set their JAVA_HOME, and link users to instructions on our website for this. This would make the startup script (pipeline2.bat) much simpler, and in GetJavaVersion we could simply parse the output of java -version (this is what our linux startup script pipeline2 does). Though this could be error-prone if with other java versions java -version has different output.

It would be nice if we could check all of the installed JRE/JDK versions, and pass the check if any of them meet the required version. This behavior would also have to be replicated in pipeline2.bat to ensure we use the correct JRE/JDK at runtime. It seems regedit.exe starts in 32-bit or 64-bit mode according to the OS (https://serverfault.com/a/593529), but we would have to manually look in each registry area separately and check which JRE/JDK meets the requirements again. Note also that we might eventually move most of our startup code to a Java launcher (keeping a minimal startup script), which will likely make this a bit easier.

We also need to support Java 9, which has its registry entries set up differently and will require a bit of modification to JRE/JDK check.
@bertfrees has confirmed as of JDK 9,

  • "the registry entry is HKLM\SOFTWARE\JavaSoft\JDK instead of HKLM\SOFTWARE\JavaSoft\Java Development Kit"
  • the versioning has changed from 1.8.X to 9.x.x
  • the "MicroVersion" is removed and now included in the version string (e.g. "9.0.4")

In fact, we plan to eventually move to Java 9 as the minimum requirement, so we could either integrate this into our current version check or wait until we do move to Java 9.

The key point I think is that the installer and the launch script should be compatible. If the launch script first looks in JAVA_HOME, the installer should do the same. If the installer checks for a specific version of Java, the launch script should make sure that that version is actually used. The launch script does not necessarily have to be as advanced as the installer though. For example, the installer could check if any of the installed Java versions meet the requirements, while the launch script could simply fail if the first version it finds does not meet the requirements, and then explain how the user can set up his/her system in order to make it work.

Note also that we might eventually move most of our startup code to a Java launcher (keeping a minimal startup script), which will likely make this a bit easier.

The Java launcher (daisy/pipeline-tasks#120) would simplify quite a bit of the launch script, but the only thing that it can not replace is exactly what we are talking about here: locating the right java executable. It could be a possibility to make a little Java program that if needed launches another Java process, but that wasn't the original intent.

the only thing that [the Java launcher] can replace is... locating the right java executable.

Ah true, maybe we could create a minimal batch script that locates the java executable, and then call that from our main startup script. That would make it easier to transition when we do make the Java launcher.

It could be a possibility to make a little Java program that if needed launches another Java process

Interesting idea, it would be nice to have this but ATM I'm not sure if we'll need it. It sure would eliminate having to deal with batch in all its glory, but it might end up being be easier to just keep a minimal script.

P.S. @bertfrees thanks for summing up my main point nicely ๐Ÿ‘ .

It seems the problem (or at least one of them) we found was between 64-bit and 32-bit JRE/JDK versions.

This 64-bit detection has been implemented in 4bbfa6. We first check for 64-bit versions, then 32-bit versions. The key point is that SetRegView 64 will just be ignored by the OS on 32-bit machines (source), and so it will look in the correct place.


After some extensive testing with various versions of Java (1.8.172, 1.8.145, 1.8.40, 1.7.80, 1.6.45; 32-bit and 64-bit versions), we've realized a couple of things:

  • GetJavaVersion.nsh looks at the most current java version the user has installed, regardless of the order they were installed in.

This means that we shouldn't have to iterate through and check each version.

  • Java installation updates the most current java executable version in "C:\ProgramData\Oracle\Java\javapath", and adds this to the %Path% environment variable (e.g. java -version gives the most current version of Java, not the most recently installed).

This means we can trust that our launcher script will also use the most current version of Java.

This simplifies everything a lot, as realistically all we would ever need to check is the most current java version the user has installed, and my testing has shown this is already the version check's current behavior.

However, I can think of two reasons we might not want to use the most recent version of Java:

  1. Our current approach forces us to modify the Java version check every time the registry setup changes for a new version of Java, otherwise the version check fails
    • This is currently the case with Java 9
  2. Newer versions of Java could deprecate APIs, causing NoSuchMethod exceptions if we currently use them
    • But this is rare

The solution to both of these problems would be to look at each java version installed, and use the least current version that meets our requirement. We would also have to have a maximum version requirement for the case where the user, for example, only has Java 9 installed. However this would require a considerable amount of work, so is this worth implementing?

What do you think @bertfrees? Can you think of any other reasons we might not want to use the most current version? I mean, if the installer fails it's not a huge deal for the user, just an inconvenience to have to install an older version of Java.

Good job on the first part!

Only checking the most current version of Java makes sense, yes. It would indeed simplify things a lot.

Newer versions of Java could indeed deprecate APIs. Java has had excellent backwards compatibility in the past but they changed their updating strategy and AFAIU backwards incompatibilities may happen more often in the future (in theory). However I think we should try to keep up with the most recent version of Java anyway. Meaning, if we get reports of incompatibilities with newer versions we should try to fix it immediately. If these updates result in incompatibilities with older versions we should increase of minimal requirements.

Also, to make sure that people are able to use a version of Java that is older than their newest installed version, we can keep supporting the %JAVA% and/or %JAVA_HOME% variables.

Another thing I noticed in my testing is that the MicroVersion registry key's value is always included in the JavaHome key's path, and there's never been an UpdateVersion registry key as far as I could tell (testing with Java 6, 7, 8, 9, and 10). Also, if you look in the comments at the top of GetJavaVersion.nsh, you can see it was written around the time of Java 2. This code is outdated, and @bertfrees and I agreed that we really don't need to use the MicroVersion or UpdateVersion keys anymore.

So I've rewritten the old code to only get the version from the JavaHome key's path, but I've run into a problem: Java 9 and 10 no longer follow the versioning scheme of 1.X.X_XXX, they follow X(X).X.X. This has presented a few challenges within the code I've rewritten and would require more work to get it to conform to our version check. @bertfrees, when are we planning on moving to Java 9 as the minimum requirement? Conforming to the new versioning scheme requires a lot of trickery, and I think we will want to remove that "trickery" for readability when we do move to Java 9 anyway, so we should wait for that.

We want to move to Java 9 ASAP. There have been some important bug fixes in JavaFX related to accessibility.

Okay so we have moved to Java 9 (daisy/pipeline@37346a0), and decided to move all of our JRECheck code to a common batch script (daisy/pipeline@96a07c0). This allows for maintainability and consistency across our installer check and launcher check.

It finds the Java version via various methods, respecting the JAVA and JAVA_HOME environment variables first, and falls back to the other methods in order of reliability. Mostly it parses the java -version output, but there is an instance where we get the version straight from the CurrentVersion registry key (in Java 9+ this seems to always contain the full version). The versions found are also validated in that they contain at least 3 numbers. The script sets the JAVA environment variable (if it isn't used itself) to the path to the java executable that passed the version check. It returns whether the version check has passed via the exitCode: 0 if the check passes, 1 if it didn't, and >1 if there was an error.

In the installer, it calls the script with nsExec to get the output and write it to the installer's DetailView. I also moved the JRECheck section to after the main installer section -- mostly to allow us to use checkJavaVersion.bat after it has been extracted, but I also like the flow of it a bit better (I feel when you click "install" it should install right away). This is still up for debate though, and if we do keep the JRECheck after the main installer section then we will likely want to improve it at some point by having it output to it's own page (I wasn't sure how to do this).

In the launcher, it calls the script and checks the exitCode, and just exits if the version check didn't pass.

P.S. Looks like Oracle has updated their Migration Guide, there are now some details on the Windows Registry changes in Java 9+: https://docs.oracle.com/javase/9/migrate/toc.htm#JSMIG-GUID-EEED398E-AE37-4D12-AB10-49F82F720027.

Great work Sean! I'll test it a bit now.

The key point is that SetRegView 64 will just be ignored by the OS on 32-bit machines (source), and so it will look in the correct place.

SetRegView 64 will cause registry operations to fail on some 32-bit versions of Windows. Newer versions of NSIS v3 are stricter and will fail most registry operations, not just those that fail inside advapi32.dll because of the unsupported WOW64 flag.

Wrapping it in a 64-bit OS check like you do for your Java 9 code is correct.

@sredna Thanks for the heads up. So you mean that the old CheckJavaVersion.nsh code might not always have worked, but the new checkJavaVersion.bat code should according to you?