Win SDK: find max version of java.exe

When we find a java.exe file, run java -version and
parse its version number. Only accept java 1.5 or
better.

There's a moderate effort to find the highest
version available in each category: for starter
scripts, it doesn't matter if we actually have
the highest. However within a given category
(env path, program files, registry), picking
up the highest available make sense. In normal
cases users won't have many concurrent versions.

Change-Id: I4f2504642a1712b62aa303562578572066d82d3b
diff --git a/find_java/find_java_lib.cpp b/find_java/find_java_lib.cpp
index 5a1c3f8..4c04e7f 100755
--- a/find_java/find_java_lib.cpp
+++ b/find_java/find_java_lib.cpp
@@ -35,18 +35,57 @@
 typedef LONG LSTATUS;

 #endif

 

-// Check whether we can find $PATH/java.exe

-static bool checkPath(CPath *inOutPath) {

+

+// Extract the first thing that looks like (digit.digit+).

+// Note: this will break when java reports a version with major > 9.

+// However it will reasonably cope with "1.10", if that ever happens.

+static bool extractJavaVersion(const char *start,

+                               int length,

+                               CString *outVersionStr,

+                               int *outVersionInt) {

+    const char *end = start + length;

+    for (const char *c = start; c < end - 2; c++) {

+        if (isdigit(c[0]) &&

+                c[1] == '.' &&

+                isdigit(c[2])) {

+            const char *e = c+2;

+            while (isdigit(e[1])) {

+                e++;

+            }

+            if (outVersionStr != NULL) {

+                outVersionStr->set(c, e - c + 1);

+            }

+            if (outVersionInt != NULL) {

+                // add major * 1000, currently only 1 digit

+                int value = (*c - '0') * 1000;

+                // add minor

+                for (int m = 1; *e != '.'; e--, m *= 10) {

+                    value += (*e - '0') * m;

+                }

+                *outVersionInt = value;

+            }

+            return true;

+        }

+    }

+    return false;

+}

+

+// Check whether we can find $PATH/java.exe.

+// inOutPath should be the directory where we're looking at.

+// In output, it will be the java path we tested.

+// Returns the java version integer found (e.g. 1006 for 1.6).

+// Return 0 in case of error.

+static int checkPath(CPath *inOutPath) {

     inOutPath->addPath("java.exe");

 

-    bool result = false;

+    int result = 0;

     PVOID oldWow64Value = disableWow64FsRedirection();

     if (inOutPath->fileExists()) {

-        // Make sure we can actually run "java -version".

-        CString cmd;

-        cmd.setf("\"%s\" -version", inOutPath->cstr());

-        int code = execWait(cmd.cstr());

-        result = (code == 0);

+        // Run java -version

+        // Reject the version if it's not at least our current minimum.

+        if (!getJavaVersion(*inOutPath, NULL /*versionStr*/, &result)) {

+            result = 0;

+        }

     }

 

     revertWow64FsRedirection(oldWow64Value);

@@ -54,46 +93,64 @@
 }

 

 // Check whether we can find $PATH/bin/java.exe

-static bool checkBinPath(CPath *inOutPath) {

+// Returns the Java version found (e.g. 1006 for 1.6) or 0 in case of error.

+static int checkBinPath(CPath *inOutPath) {

     inOutPath->addPath("bin");

     return checkPath(inOutPath);

 }

 

 // Search java.exe in the environment

-bool findJavaInEnvPath(CPath *outJavaPath) {

+int findJavaInEnvPath(CPath *outJavaPath) {

     SetLastError(0);

 

+    int currVersion = 0;

+

     const char* envPath = getenv("JAVA_HOME");

     if (envPath != NULL) {

         CPath p(envPath);

-        if (checkBinPath(&p)) {

-            if (gIsDebug) msgBox("Java found via JAVA_HOME: %s", p.cstr());

+        currVersion = checkBinPath(&p);

+        if (currVersion > 0) {

+            if (gIsDebug) {

+                fprintf(stderr, "Java %d found via JAVA_HOME: %s\n", currVersion, p.cstr());

+            }

             *outJavaPath = p;

-            return true;

+        }

+        if (currVersion >= MIN_JAVA_VERSION) {

+            // As an optimization for runtime, if we find a suitable java

+            // version in JAVA_HOME we won't waste time looking at the PATH.

+            return currVersion;

         }

     }

 

     envPath = getenv("PATH");

-    if (!envPath) return false;

+    if (!envPath) return currVersion;

+

+    // Otherwise look at the entries in the current path.

+    // If we find more than one, keep the one with the highest version.

 

     CArray<CString> *paths = CString(envPath).split(';');

     for(int i = 0; i < paths->size(); i++) {

         CPath p((*paths)[i].cstr());

-        if (checkPath(&p)) {

-            if (gIsDebug) msgBox("Java found via env PATH: %s", p.cstr());

+        int v = checkPath(&p);

+        if (v > currVersion) {

+            if (gIsDebug) {

+                fprintf(stderr, "Java %d found via env PATH: %s\n", v, p.cstr());

+            }

+            currVersion = v;

             *outJavaPath = p;

-            delete paths;

-            return true;

         }

     }

 

     delete paths;

-    return false;

+    return currVersion;

 }

 

 // --------------

 

-bool getRegValue(const char *keyPath, const char *keyName, REGSAM access, CString *outValue) {

+static bool getRegValue(const char *keyPath,

+                        const char *keyName,

+                        REGSAM access,

+                        CString *outValue) {

     HKEY key;

     LSTATUS status = RegOpenKeyExA(

         HKEY_LOCAL_MACHINE,         // hKey

@@ -135,67 +192,149 @@
     return false;

 }

 

-bool exploreJavaRegistry(const char *entry, REGSAM access, CPath *outJavaPath) {

+// Explore the registry to find a suitable version of Java.

+// Returns an int which is the version of Java found (e.g. 1006 for 1.6) and the

+// matching path in outJavaPath.

+// Returns 0 if nothing suitable was found.

+static int exploreJavaRegistry(const char *entry, REGSAM access, CPath *outJavaPath) {

 

     // Let's visit HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment [CurrentVersion]

-    CPath subKey("SOFTWARE\\JavaSoft\\");

-    subKey.addPath(entry);

+    CPath rootKey("SOFTWARE\\JavaSoft\\");

+    rootKey.addPath(entry);

 

-    CString currVersion;

-    if (getRegValue(subKey.cstr(), "CurrentVersion", access, &currVersion)) {

+    int versionInt = 0;

+    CString currentVersion;

+    CPath subKey(rootKey);

+    if (getRegValue(subKey.cstr(), "CurrentVersion", access, &currentVersion)) {

         // CurrentVersion should be something like "1.7".

         // We want to read HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\1.7 [JavaHome]

-        subKey.addPath(currVersion);

+        subKey.addPath(currentVersion);

         CPath javaHome;

         if (getRegValue(subKey.cstr(), "JavaHome", access, &javaHome)) {

-            if (checkBinPath(&javaHome)) {

+            versionInt = checkBinPath(&javaHome);

+            if (versionInt >= 0) {

+                if (gIsDebug) {

+                    fprintf(stderr,

+                            "Java %d found via registry: %s\n",

+                            versionInt, javaHome.cstr());

+                }

                 *outJavaPath = javaHome;

-                return true;

+            }

+            if (versionInt >= MIN_JAVA_VERSION) {

+                // Heuristic: if the current version is good enough, stop here

+                return versionInt;

             }

         }

     }

 

+    // Try again, but this time look at all the versions available

+    HKEY javaHomeKey;

+    LSTATUS status = RegOpenKeyExA(

+        HKEY_LOCAL_MACHINE,         // hKey

+        "SOFTWARE\\JavaSoft",       // lpSubKey

+        0,                          // ulOptions

+        KEY_READ | access,          // samDesired

+        &javaHomeKey);              // phkResult

+    if (status == ERROR_SUCCESS) {

+        char name[256];

+        DWORD index = 0;

+        CPath javaHome;

+        for (LONG result = ERROR_SUCCESS; result == ERROR_SUCCESS; index++) {

+            DWORD nameLen = 255;

+            name[nameLen] = 0;

+            result = RegEnumKeyExA(

+                            javaHomeKey,  // hKey

+                            index,        // dwIndex

+                            name,         // lpName

+                            &nameLen,     // lpcName

+                            NULL,         // lpReserved

+                            NULL,         // lpClass

+                            NULL,         // lpcClass,

+                            NULL);        // lpftLastWriteTime

+            if (result == ERROR_SUCCESS && nameLen < 256) {

+                name[nameLen] = 0;

+                CPath subKey(rootKey);

+                subKey.addPath(name);

+

+                if (getRegValue(subKey.cstr(), "JavaHome", access, &javaHome)) {

+                    int v = checkBinPath(&javaHome);

+                    if (v > versionInt) {

+                        if (gIsDebug) {

+                            fprintf(stderr,

+                                    "Java %d found via registry: %s\n",

+                                    versionInt, javaHome.cstr());

+                        }

+                        *outJavaPath = javaHome;

+                        versionInt = v;

+                    }

+                }

+            }

+        }

+

+        RegCloseKey(javaHomeKey);

+    }

+

+    return 0;

+}

+

+static bool getMaxJavaInRegistry(const char *entry, REGSAM access, CPath *outJavaPath, int *inOutVersion) {

+    CPath path;

+    int version = exploreJavaRegistry(entry, access, &path);

+    if (version > *inOutVersion) {

+        *outJavaPath  = path;

+        *inOutVersion = version;

+        return true;

+    }

     return false;

 }

 

-bool findJavaInRegistry(CPath *outJavaPath) {

+int findJavaInRegistry(CPath *outJavaPath) {

     // We'll do the registry test 3 times: first using the default mode,

     // then forcing the use of the 32-bit registry then forcing the use of

     // 64-bit registry. On Windows 2k, the 2 latter will fail since the

     // flags are not supported. On a 32-bit OS the 64-bit is obviously

-    // useless and the 2 first test should be equivalent so we just

+    // useless and the 2 first tests should be equivalent so we just

     // need the first case.

 

     // Check the JRE first, then the JDK.

-    if (exploreJavaRegistry("Java Runtime Environment", 0, outJavaPath) ||

-            exploreJavaRegistry("Java Development Kit", 0, outJavaPath)) {

-        return true;

-    }

+    int version = MIN_JAVA_VERSION - 1;

+    bool result = false;

+    result |= getMaxJavaInRegistry("Java Runtime Environment", 0, outJavaPath, &version);

+    result |= getMaxJavaInRegistry("Java Development Kit",     0, outJavaPath, &version);

 

-    // Check the real sysinfo state (not the one hidden by WOW64) for x86

+    // Get the app sysinfo state (the one hidden by WOW64)

     SYSTEM_INFO sysInfo;

+    GetSystemInfo(&sysInfo);

+    WORD programArch = sysInfo.wProcessorArchitecture;

+    // Check the real sysinfo state (not the one hidden by WOW64) for x86

     GetNativeSystemInfo(&sysInfo);

+    WORD actualArch = sysInfo.wProcessorArchitecture;

 

     // Only try to access the WOW64-32 redirected keys on a 64-bit system.

-    // There's no point in doing that on a 32-bit system.

-    if (sysInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) {

-        if (exploreJavaRegistry("Java Runtime Environment", KEY_WOW64_32KEY, outJavaPath) ||

-                exploreJavaRegistry("Java Development Kit", KEY_WOW64_32KEY, outJavaPath)) {

-            return true;

-        }

+    // There's no point in doing this on a 32-bit system.

+    if (actualArch == PROCESSOR_ARCHITECTURE_AMD64) {

+        if (programArch != PROCESSOR_ARCHITECTURE_INTEL) {

+            // If we did the 32-bit case earlier, don't do it twice.

+            result |= getMaxJavaInRegistry(

+                "Java Runtime Environment", KEY_WOW64_32KEY, outJavaPath, &version);

+            result |= getMaxJavaInRegistry(

+                "Java Development Kit",     KEY_WOW64_32KEY, outJavaPath, &version);

 

-        if (exploreJavaRegistry("Java Runtime Environment", KEY_WOW64_64KEY, outJavaPath) ||

-                exploreJavaRegistry("Java Development Kit", KEY_WOW64_64KEY, outJavaPath)) {

-            return true;

+        } else if (programArch != PROCESSOR_ARCHITECTURE_AMD64) {

+            // If we did the 64-bit case earlier, don't do it twice.

+            result |= getMaxJavaInRegistry(

+                "Java Runtime Environment", KEY_WOW64_64KEY, outJavaPath, &version);

+            result |= getMaxJavaInRegistry(

+                "Java Development Kit",     KEY_WOW64_64KEY, outJavaPath, &version);

         }

     }

 

-    return false;

+    return result ? version : 0;

 }

 

 // --------------

 

-static bool checkProgramFiles(CPath *outJavaPath) {

+static bool checkProgramFiles(CPath *outJavaPath, int *inOutVersion) {

 

     char programFilesPath[MAX_PATH + 1];

     HRESULT result = SHGetFolderPathA(

@@ -223,9 +362,11 @@
         if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {

             CPath temp(path);

             temp.addPath(findData.cFileName);

-            // Check C:\\Program Files[x86]\\Java\\{jdk,jre}*\\bin\\java.exe

-            if (checkBinPath(&temp)) {

+            // Check C:\\Program Files[x86]\\Java\\j*\\bin\\java.exe

+            int v = checkBinPath(&temp);

+            if (v > *inOutVersion) {

                 found = true;

+                *inOutVersion = v;

                 *outJavaPath = temp;

             }

         }

@@ -235,11 +376,14 @@
     return found;

 }

 

-bool findJavaInProgramFiles(CPath *outJavaPath) {

+int findJavaInProgramFiles(CPath *outJavaPath) {

+

     // Check the C:\\Program Files (x86) directory

     // With WOW64 fs redirection in place by default, we should get the x86

     // version on a 64-bit OS since this app is a 32-bit itself.

-    if (checkProgramFiles(outJavaPath)) return true;

+    bool result = false;

+    int version = MIN_JAVA_VERSION - 1;

+    result |= checkProgramFiles(outJavaPath, &version);

 

     // Check the real sysinfo state (not the one hidden by WOW64) for x86

     SYSTEM_INFO sysInfo;

@@ -249,17 +393,21 @@
         // On a 64-bit OS, try again by disabling the fs redirection so

         // that we can try the real C:\\Program Files directory.

         PVOID oldWow64Value = disableWow64FsRedirection();

-        bool found = checkProgramFiles(outJavaPath);

+        result |= checkProgramFiles(outJavaPath, &version);

         revertWow64FsRedirection(oldWow64Value);

-        return found;

     }

 

-    return false;

+    return result ? version : 0;

 }

 

 // --------------

 

-bool getJavaVersion(CPath &javaPath, CString *version) {

+

+// Tries to invoke the java.exe at the given path and extract it's

+// version number.

+// - outVersionStr: if not null, will capture version as a string (e.g. "1.6")

+// - outVersionInt: if not null, will capture version as an int (major * 1000 + minor, e.g. 1006).

+bool getJavaVersion(CPath &javaPath, CString *outVersionStr, int *outVersionInt) {

     bool result = false;

 

     // Run "java -version", which outputs something like to *STDERR*:

@@ -291,11 +439,11 @@
             &stdoutPipeWt,      // hWritePipe,

             &saAttr,            // lpPipeAttributes,

             0)) {               // nSize (0=default buffer size)

-        displayLastError("CreatePipe failed: ");

+        if (gIsConsole || gIsDebug) displayLastError("CreatePipe failed: ");

         return false;

     }

     if (!SetHandleInformation(stdoutPipeRd, HANDLE_FLAG_INHERIT, 0)) {

-        displayLastError("SetHandleInformation failed: ");

+        if (gIsConsole || gIsDebug) displayLastError("SetHandleInformation failed: ");

         return false;

     }

 

@@ -322,7 +470,7 @@
             &startup,               // startup info, i.e. std handles

             &pinfo);

 

-    if (gIsConsole && !ok) displayLastError("CreateProcess failed: ");

+    if ((gIsConsole || gIsDebug) && !ok) displayLastError("CreateProcess failed: ");

 

     // Close the write-end of the output pipe (we're only reading from it)

     CloseHandle(stdoutPipeWt);

@@ -376,26 +524,17 @@
     }

     CloseHandle(stdoutPipeRd);

 

-    if (index > 0) {

+    if (result && index > 0) {

         // Look for a few keywords in the output however we don't

         // care about specific ordering or case-senstiviness.

         // We only captures roughtly the first line in lower case.

         char *j = strstr(first32, "java");

         char *v = strstr(first32, "version");

-        if (gIsDebug && gIsConsole && (!j || !v)) {

+        if ((gIsConsole || gIsDebug) && (!j || !v)) {

             fprintf(stderr, "Error: keywords 'java version' not found in '%s'\n", first32);

         }

         if (j != NULL && v != NULL) {

-            // Now extract the first thing that looks like digit.digit

-            for (int i = 0; i < index - 2; i++) {

-                if (isdigit(first32[i]) &&

-                        first32[i+1] == '.' &&

-                        isdigit(first32[i+2])) {

-                    version->set(first32 + i, 3);

-                    result = true;

-                    break;

-                }

-            }

+            result = extractJavaVersion(first32, index, outVersionStr, outVersionInt);

         }

     }