From 1e5402ea82b09b18eb1439f7118910f63684e829 Mon Sep 17 00:00:00 2001 From: Norberto Lopez <106690103+nobes888@users.noreply.github.com> Date: Sat, 7 May 2016 03:57:18 +0000 Subject: [PATCH] Updated logic to support - Code cleanup / refactor - Improved Java versioning related logic - Added support for Java version range (rather than just minimum version) - Added support for JRE updating - Improved update revert logic - Added support to untar jvm tar.gz archives --- src/distMaker/DistMakerEngine.java | 624 ++++++++++++++++------ src/distMaker/DistUtils.java | 70 ++- src/distMaker/ErrorDM.java | 37 ++ src/distMaker/MiscUtils.java | 182 ++++++- src/distMaker/gui/MemoryConfigPanel.java | 25 +- src/distMaker/jre/JreUtils.java | 35 +- src/distMaker/jre/JreVersion.java | 8 +- src/distMaker/node/AppCatalog.java | 91 +++- src/distMaker/node/AppRelease.java | 4 +- src/distMaker/platform/AppleUtils.java | 278 ++++++---- src/distMaker/platform/LinuxUtils.java | 194 ++++--- src/distMaker/platform/PlatformUtils.java | 161 ++++-- src/distMaker/platform/WindowsUtils.java | 175 +++--- template/appLauncher.jar | Bin 17850 -> 22751 bytes 14 files changed, 1337 insertions(+), 547 deletions(-) create mode 100644 src/distMaker/ErrorDM.java diff --git a/src/distMaker/DistMakerEngine.java b/src/distMaker/DistMakerEngine.java index 1163313..0bc5eff 100644 --- a/src/distMaker/DistMakerEngine.java +++ b/src/distMaker/DistMakerEngine.java @@ -10,10 +10,12 @@ import glum.task.*; import glum.unit.DateUnit; import glum.util.ThreadUtil; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.io.*; import java.net.MalformedURLException; import java.net.URL; -import java.nio.file.Files; +import java.nio.file.*; import java.security.MessageDigest; import java.util.*; @@ -21,13 +23,13 @@ import javax.swing.JFrame; import javax.swing.SwingUtilities; import com.google.common.base.Joiner; +import com.google.common.base.Strings; import distMaker.digest.Digest; import distMaker.digest.DigestUtils; import distMaker.gui.PickReleasePanel; import distMaker.jre.*; import distMaker.node.*; -import distMaker.platform.AppleUtils; import distMaker.platform.PlatformUtils; public class DistMakerEngine @@ -110,6 +112,14 @@ public class DistMakerEngine return currRelease; } + /** + * Returns the URL where software updates for this application are retrieved from. + */ + public URL getUpdateSite() + { + return updateSiteUrl; + } + /** * returns * @@ -154,7 +164,6 @@ public class DistMakerEngine private void initialize() { File appPath, cfgFile; - BufferedReader br; DateUnit dateUnit; String currInstr, strLine; String appName, verName, buildStr; @@ -180,11 +189,8 @@ public class DistMakerEngine } // Read in the configuration file - br = null; - try + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(cfgFile)))) { - br = new BufferedReader(new InputStreamReader(new FileInputStream(cfgFile))); - // Read the lines currInstr = "None"; while (true) @@ -215,10 +221,6 @@ public class DistMakerEngine { aExp.printStackTrace(); } - finally - { - IoUtil.forceClose(br); - } if (appName == null || verName == null) { @@ -343,7 +345,7 @@ public class DistMakerEngine } // Download the release - isPass = downloadRelease(aTask, chosenItem, deltaPath); + isPass = downloadAppRelease(aTask, chosenItem, deltaPath); if (isPass == false || aTask.isActive() == false) { IoUtil.deleteDirectory(deltaPath); @@ -370,6 +372,29 @@ public class DistMakerEngine aMsg += "Please check installation configuration."; } + // If the parentFrame is not visible then delay the showing until it is visible + if (parentFrame.isVisible() == false) + { + final String tmpMsg = aMsg; + parentFrame.addComponentListener(new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent aEvent) + { + msgPanel.setTitle("Application Updater"); + msgPanel.setInfo(tmpMsg); + msgPanel.setVisible(true); + + // Deregister for events after the parentFrame is made visible + parentFrame.removeComponentListener(this); + } + }); + + return; + } + + // Transform tabs to 3 spaces + aMsg = aMsg.replace("\t", " "); + // Display the message msgPanel.setTitle("Application Updater"); msgPanel.setInfo(aMsg); @@ -381,7 +406,7 @@ public class DistMakerEngine *

* Returns true if the release was downloaded properly. */ - private boolean downloadRelease(Task aTask, AppRelease aRelease, File destPath) + private boolean downloadAppRelease(Task aTask, AppRelease aRelease, File destPath) { AppCatalog staleCat, updateCat; Node staleNode, updateNode; @@ -390,7 +415,6 @@ public class DistMakerEngine Task mainTask, tmpTask; double progressVal; long tmpFileLen; - boolean isPass; try { @@ -404,20 +428,21 @@ public class DistMakerEngine return false; } - // Download the update catalog to the (local) delta location (Progress -> [0% - 1%]) - catUrl = IoUtil.createURL(updateUrl.toString() + "/catalog.txt"); - catalogFile = new File(destPath, "catalog.txt"); - if (DistUtils.downloadFile(new PartialTask(aTask, 0.00, 0.01), catUrl, catalogFile, refCredential, -1L, null) == false) - return false; - // Load the stale catalog catalogFile = new File(DistUtils.getAppPath(), "catalog.txt"); staleCat = DistUtils.readAppCatalog(aTask, catalogFile, staleUrl); if (staleCat == null) return false; + // Download the update app catalog to the (local) delta location (Progress -> [0% - 1%]) + File appNewPath = new File(destPath, "app"); + appNewPath.mkdirs(); + catUrl = IoUtil.createURL(updateUrl.toString() + "/catalog.txt"); + catalogFile = new File(appNewPath, "catalog.txt"); + if (DistUtils.downloadFile(new PartialTask(aTask, 0.00, 0.01), catUrl, catalogFile, refCredential, -1L, null) == false) + return false; + // Load the update catalog - catalogFile = new File(destPath, "catalog.txt"); updateCat = DistUtils.readAppCatalog(aTask, catalogFile, updateUrl); if (updateCat == null) return false; @@ -433,136 +458,29 @@ public class DistMakerEngine // Set up the mainTask for downloading of remote content (Progress -> [1% - 95%]) mainTask = new PartialTask(aTask, 0.01, 0.94); - // Ensure our JRE version is sufficient for this release + // Ensure our JRE version is compatible for this release + JreRelease targJre = null; JreVersion currJreVer = DistUtils.getJreVersion(); - JreVersion targJreVer = updateCat.getJreVersion(); - if (targJreVer != null && JreVersion.getBetterVersion(targJreVer, currJreVer) != currJreVer) + if (updateCat.isJreVersionCompatible(currJreVer) == false) { - List jreList; - - aTask.infoAppendln("Your current JRE is too old. It will need to be updated!"); - aTask.infoAppendln("\tCurrent JRE: " + currJreVer.getLabel()); - aTask.infoAppendln("\tMinimun JRE: " + targJreVer.getLabel() + "\n"); - - // Get list of all available JREs - jreList = JreUtils.getAvailableJreReleases(aTask, updateSiteUrl, refCredential); - if (jreList == null) - { - aTask.infoAppendln("The update site has not had any JREs deployed."); - aTask.infoAppendln("Please contact the update site adminstartor."); + // Bail if we failed to download a compatible JRE + targJre = downloadJreUpdate(mainTask, updateCat, destPath, releaseSizeFull); + if (targJre == null) return false; - } - if (jreList.size() == 0) - { - aTask.infoAppendln("No JRE releases found!"); - aTask.infoAppendln("Please contact the update site adminstartor."); - return false; - } - // Retrieve the latest appropriate JreRelease - String platform = DistUtils.getPlatform(); - jreList = JreUtils.getMatchingPlatforms(jreList, platform); - if (jreList.size() == 0) - { - aTask.infoAppendln("There are no JRE releases available for the platform: " + platform + "!"); - return false; - } - - JreRelease pickJre = jreList.get(0); - JreVersion pickJreVer = pickJre.getVersion(); - - if (JreVersion.getBetterVersion(pickJreVer, targJreVer) == currJreVer) - { - aTask.infoAppendln("The latest available JRE on the update site is not recent enought!"); - aTask.infoAppendln("Minimun required JRE: " + targJreVer.getLabel()); - aTask.infoAppendln("Latest available JRE: " + pickJreVer.getLabel()); - return false; - } - - // Update the number of bytes to be retrieved to take into account the JRE which we will be downloading - // and form the appropriate tmpTask - tmpFileLen = pickJre.getFileLen(); - releaseSizeFull += tmpFileLen; - tmpTask = new PartialTask(mainTask, mainTask.getProgress(), tmpFileLen / (releaseSizeFull + 0.00)); - - // Download the JRE - Digest targDigest, testDigest; - targDigest = pickJre.getDigest(); - MessageDigest msgDigest; - msgDigest = DigestUtils.getDigest(targDigest.getType()); - mainTask.infoAppendln("Downloading JRE... Version: " + pickJre.getVersion().getLabel()); - URL srcUrl = IoUtil.createURL(updateSiteUrl.toString() + "/jre/" + pickJre.getFileName()); - File dstFile = new File(new File(destPath, "jre"), pickJre.getFileName()); - DistUtils.downloadFile(tmpTask, srcUrl, dstFile, refCredential, tmpFileLen, msgDigest); - - // Validate that the JRE was downloaded successfully - testDigest = new Digest(targDigest.getType(), msgDigest.digest()); - if (targDigest.equals(testDigest) == false) - { - aTask.infoAppendln("The download of the JRE appears to be corrupted."); - aTask.infoAppendln("\tFile: " + dstFile); - aTask.infoAppendln("\t\tExpected " + targDigest.getDescr()); - aTask.infoAppendln("\t\tRecieved " + testDigest.getDescr() + "\n"); - return false; - } - releaseSizeCurr += tmpFileLen; + // Update the progress to reflect the downloaded / updated JRE + releaseSizeCurr += targJre.getFileLen(); + releaseSizeFull += targJre.getFileLen(); progressVal = releaseSizeCurr / (releaseSizeFull + 0.00); mainTask.setProgress(progressVal); - - // TODO: Unpack the JRE at the proper location and then delete it - File jrePath = PlatformUtils.getJreLocation(pickJre); - try - { - int finish_me_and_clean_me; -// Files.createTempDirectory(dstFile, prefix, attrs) -// Files.createTempDir(); - - - File unpackPath; - unpackPath = new File(destPath, "jre/unpack"); - unpackPath.mkdirs(); - MiscUtils.unTar(dstFile, unpackPath); - - File[] fileArr; - fileArr = unpackPath.listFiles(); - if (fileArr.length != 1 && fileArr[0].isDirectory() == false) - throw new Exception("Expected only one (top level) folder to be unpacked. Items extracted: " + fileArr.length + " Path: " + unpackPath); - - File jreUnpackedPath; - jreUnpackedPath = fileArr[0]; - - // Moved the unpacked file to the "standardized" jrePath - jreUnpackedPath.renameTo(jrePath); -// MiscUtils.unTar(dstFile, jrePath); - - // TODO: Remove the unpacked folder... - } - catch(Exception aExp) - { - aTask.infoAppendln("Failed to untar archive. The update has been aborted."); - aTask.infoAppendln("\tTar File: " + dstFile); - aTask.infoAppendln("\tDestination: " + jrePath); - return false; - } - - // TODO: Update the launch script or launch config file to point to the proper JRE - if (PlatformUtils.setJreLocation(jrePath) == false) - { - aTask.infoAppendln("Failed to update the configuration to point to the updated JRE!"); - aTask.infoAppendln("\tCurrent JRE: " + currJreVer.getLabel()); - aTask.infoAppendln("\t Chosen JRE: " + pickJreVer.getLabel()); - return false; - } - - return true; - - // TODO: Eventually remove the old JRE - perhaps from the app launcher } // Download the individual application files mainTask.infoAppendln("Downloading release: " + aRelease.getVersion() + " Nodes: " + updateCat.getAllNodesList().size()); for (Node aNode : updateCat.getAllNodesList()) { + boolean isPass; + // Bail if we have been aborted if (mainTask.isActive() == false) return false; @@ -580,9 +498,9 @@ public class DistMakerEngine { // Note we pass the SilentTask since // - This should be fairly fast since this should result in a local disk copy - // - This may fail, (but the failuer is recoverable and this serves just as an optimization) + // - This may fail, (but the failure is recoverable and this serves just as an optimization) // isPass = staleNode.transferContentTo(tmpTask, refCredential, destPath); - isPass = staleNode.transferContentTo(new SilentTask(), refCredential, destPath); + isPass = staleNode.transferContentTo(new SilentTask(), refCredential, appNewPath); if (isPass == true) mainTask.infoAppendln("\t(L) " + staleNode.getFileName()); } @@ -590,7 +508,7 @@ public class DistMakerEngine // Use the remote update copy, if we were not able to use a local stale copy if (isPass == false && mainTask.isActive() == true) { - isPass = updateNode.transferContentTo(tmpTask, refCredential, destPath); + isPass = updateNode.transferContentTo(tmpTask, refCredential, appNewPath); if (isPass == true) mainTask.infoAppendln("\t(R) " + updateNode.getFileName()); } @@ -601,7 +519,7 @@ public class DistMakerEngine mainTask.infoAppendln("Failed to download from update site."); mainTask.infoAppendln("\tSite: " + updateUrl); mainTask.infoAppendln("\tFile: " + updateNode.getFileName()); - mainTask.infoAppendln("\tDest: " + destPath); + mainTask.infoAppendln("\tDest: " + appNewPath); return false; } @@ -610,11 +528,246 @@ public class DistMakerEngine progressVal = releaseSizeCurr / (releaseSizeFull + 0.00); mainTask.setProgress(progressVal); } + mainTask.infoAppendln("Finished downloading release.\n"); // Update the platform configuration files - isPass = updatePlatformConfigFiles(aTask, aRelease); + try + { + PlatformUtils.updateAppRelease(aRelease); + } + catch(ErrorDM aExp) + { + aTask.infoAppendln("Failed updating application configuration."); + printErrorDM(aTask, aExp, 1); + return false; + } - return isPass; + // Retrieve the reference to the appCfgFile + File appCfgFile = PlatformUtils.getConfigurationFile(); + + // Create the delta.cmd file which provides the Updater with the clean activities to perform (based on fail / pass conditions) + File deltaCmdFile = new File(destPath, "delta.cmd"); + try (FileWriter tmpFW = new FileWriter(deltaCmdFile)) + { + if (targJre != null) + { + File rootPath = DistUtils.getAppPath().getParentFile(); + + JreVersion targJreVer = targJre.getVersion(); + tmpFW.write("# Define the fail section (clean up for failure)\n"); + tmpFW.write("sect,fail\n"); + tmpFW.write("copy," + "delta/" + appCfgFile.getName() + ".old," + MiscUtils.getRelativePath(rootPath, appCfgFile) + "\n"); + tmpFW.write("move,jre" + targJreVer.getLabel() + ",delta" + "\n"); + tmpFW.write("reboot\n\n"); + + tmpFW.write("# Define the pass section (clean up for success)\n"); + tmpFW.write("sect,pass\n"); + tmpFW.write("trash,jre" + currJreVer.getLabel() + "\n"); + tmpFW.write("exit\n\n"); + } + else + { + tmpFW.write("# Define the fail section (clean up for failure)\n"); + tmpFW.write("sect,fail\n"); + tmpFW.write("exit\n\n"); + + tmpFW.write("# Define the pass section (clean up for success)\n"); + tmpFW.write("sect,pass\n"); + tmpFW.write("exit\n\n"); + } + + tmpFW.write("# Define the reboot section\n"); + tmpFW.write("sect,reboot\n"); + tmpFW.write("exit\n\n"); + + tmpFW.write("# Define the test section\n"); + tmpFW.write("sect,test\n"); + tmpFW.write("exit\n\n"); + } + catch(IOException aExp) + { + aTask.infoAppendln("Failed to generate the delta.cfg file."); + aTask.infoAppendln(ThreadUtil.getStackTrace(aExp)); + return false; + } + + // We are done if there was no updated JRE + if (targJre == null) + return true; + + // Since an updated JRE was needed... + // Moved the JRE (unpacked folder) from its drop path to the proper location + File jreDropPath = new File(destPath, "jre" + targJre.getVersion().getLabel()); + File jreTargPath = PlatformUtils.getJreLocation(targJre); + jreTargPath.getParentFile().setWritable(true); + if (jreDropPath.renameTo(jreTargPath) == false) + { + aTask.infoAppendln("Failed to move the updated JRE to its target location!"); + aTask.infoAppendln("\t Current path: " + jreDropPath); + aTask.infoAppendln("\tOfficial path: " + jreTargPath); + return false; + } + + // Backup the application configuration + try + { + Files.copy(appCfgFile.toPath(), new File(destPath, appCfgFile.getName() + ".old").toPath(), StandardCopyOption.COPY_ATTRIBUTES); + } + catch(IOException aExp) + { + aTask.infoAppendln("Failed to backup application configuration file: " + deltaCmdFile); + aTask.infoAppendln(ThreadUtil.getStackTrace(aExp)); + return false; + } + + // Update the application configuration to reflect the proper JRE + try + { + PlatformUtils.setJreVersion(targJre.getVersion()); + } + catch(ErrorDM aExp) + { + aTask.infoAppendln("Failed to update the configuration to point to the updated JRE!"); + aTask.infoAppendln("\tCurrent JRE: " + currJreVer.getLabel()); + aTask.infoAppendln("\t Chosen JRE: " + targJre.getVersion().getLabel()); + printErrorDM(aTask, aExp, 1); + + // Remove the just installed JRE + IoUtil.deleteDirectory(jreTargPath); + + return false; + } + + return true; + } + + /** + * Helper method to download a compatible JreRelease for the AppCatalog to the specified destPath. + *

+ * On success the JreVersion that was downloaded is returned. + */ + private JreRelease downloadJreUpdate(Task aTask, AppCatalog aUpdateCat, File aDestPath, long releaseSizeFull) + { + List jreList; + + // Ensure our JRE version is compatible for this release + JreVersion currJreVer = DistUtils.getJreVersion(); + + // Let the user know why their version is not compatible + String updnStr = "downgraded"; + if (aUpdateCat.isJreVersionTooOld(currJreVer) == true) + updnStr = "upgraded"; + aTask.infoAppendln("Your current JRE is not compatible with this release. It will need to be " + updnStr + "!"); + aTask.infoAppendln("\tCurrent JRE: " + currJreVer.getLabel()); + aTask.infoAppendln("\tMinimun JRE: " + aUpdateCat.getMinJreVersion().getLabel()); + JreVersion tmpJreVer = aUpdateCat.getMaxJreVersion(); + if (tmpJreVer != null) + aTask.infoAppendln("\tMaximun JRE: " + tmpJreVer.getLabel()); + aTask.infoAppendln(""); + + // Bail if we are running a bundled JRE + if (DistUtils.isJreBundled() == false) + { + aTask.infoAppend("This is the non bundled JRE version of the application. You are running the system JRE. "); + aTask.infoAppendln("Please update the JRE (or path) to reflect a compatible JRE version.\n"); + return null; + } + + // Get list of all available JREs + jreList = JreUtils.getAvailableJreReleases(aTask, updateSiteUrl, refCredential); + if (jreList == null) + { + aTask.infoAppendln("The update site has not had any JREs deployed."); + aTask.infoAppendln("Please contact the update site adminstartor."); + return null; + } + if (jreList.size() == 0) + { + aTask.infoAppendln("No JRE releases found!"); + aTask.infoAppendln("Please contact the update site adminstartor."); + return null; + } + + // Retrieve the latest appropriate JreRelease + String platform = PlatformUtils.getPlatform(); + jreList = JreUtils.getMatchingPlatforms(jreList, platform); + if (jreList.size() == 0) + { + aTask.infoAppendln("There are no JRE releases available for the platform: " + platform + "!"); + return null; + } + + // Retrieve the JRE that is compatible from the list + JreRelease pickJre = aUpdateCat.getCompatibleJre(jreList); + if (pickJre == null) + { + aTask.infoAppendln("There are no compatible JREs found on the deploy site. Available JREs: " + jreList.size()); + for (JreRelease aJreRelease : jreList) + aTask.infoAppendln("\t" + aJreRelease.getFileName() + " ---> (JRE: " + aJreRelease.getVersion().getLabel() + ")"); + aTask.infoAppendln("\nPlease contact the update site adminstartor."); + return null; + } + JreVersion pickJreVer = pickJre.getVersion(); + + // Update the number of bytes to be retrieved to take into account the JRE which we will be downloading + long tmpFileLen = pickJre.getFileLen(); + releaseSizeFull += tmpFileLen; + + // Download the JRE + Digest targDigest, testDigest; + targDigest = pickJre.getDigest(); + MessageDigest msgDigest; + msgDigest = DigestUtils.getDigest(targDigest.getType()); + aTask.infoAppendln("Downloading JRE... Version: " + pickJreVer.getLabel()); + URL srcUrl = IoUtil.createURL(updateSiteUrl.toString() + "/jre/" + pickJreVer.getLabel() + "/" + pickJre.getFileName()); + File dstFile = new File(aDestPath, pickJre.getFileName()); + Task tmpTask = new PartialTask(aTask, aTask.getProgress(), (tmpFileLen * 0.75) / (releaseSizeFull + 0.00)); + if (DistUtils.downloadFile(tmpTask, srcUrl, dstFile, refCredential, tmpFileLen, msgDigest) == false) + return null; + + // Validate that the JRE was downloaded successfully + testDigest = new Digest(targDigest.getType(), msgDigest.digest()); + if (targDigest.equals(testDigest) == false) + { + aTask.infoAppendln("The download of the JRE appears to be corrupted."); + aTask.infoAppendln("\tFile: " + dstFile); + aTask.infoAppendln("\t\tExpected " + targDigest.getDescr()); + aTask.infoAppendln("\t\tRecieved " + testDigest.getDescr() + "\n"); + return null; + } + + // Unpack the JRE at the unpack location + aTask.infoAppendln("Finshed downloading JRE. Unpacking JRE..."); + File jreRootPath = null; + File jreTargPath = new File(aDestPath, "jre" + pickJreVer.getLabel()); + try + { + // Create the working unpack folder where the JRE will be initially unpacked to. + File unpackPath = new File(aDestPath, "unpack"); + unpackPath.mkdirs(); + + // Unpack the JRE to the working unpack folder and ensure that the unpacked JRE results in a 1 top level root folder. + tmpTask = new PartialTask(aTask, aTask.getProgress(), (tmpFileLen * 0.25) / (releaseSizeFull + 0.00)); + MiscUtils.unTar(tmpTask, dstFile, unpackPath); + File[] fileArr = unpackPath.listFiles(); + if (fileArr.length != 1 && fileArr[0].isDirectory() == false) + throw new Exception("Expected only one (top level) folder to be unpacked. Items extracted: " + fileArr.length + " Path: " + unpackPath); + jreRootPath = fileArr[0]; + + // Moved the unpacked JRE to aDestPath/jre/ folder and remove the working unpack folder and the tar.gz file + jreRootPath.renameTo(jreTargPath); + unpackPath.delete(); + dstFile.delete(); + } + catch(Exception aExp) + { + aTask.infoAppendln("Failed to properly untar archive. The update has been aborted."); + aTask.infoAppendln("\tTar File: " + dstFile); + aTask.infoAppendln("\tDestination: " + jreTargPath); + return null; + } + + return pickJre; } /** @@ -652,6 +805,39 @@ public class DistMakerEngine displayNotice(msg); } + /** + * Helper method that prints the exception of ErrorDM in an intelligent fashion to the specified task. + *

+ * All ErrorDM exceptions (and their causes) will be printed. If the cause is not of type ErrorDM then the stack trace will be printed as well. + */ + private void printErrorDM(Task aTask, ErrorDM aErrorDM, int numTabs) + { + Throwable cause; + String tabStr; + + tabStr = Strings.repeat("\t", numTabs); + + aTask.infoAppendln(tabStr + "Reason: " + aErrorDM.getMessage()); + cause = aErrorDM.getCause(); + while (cause != null) + { + if (cause instanceof ErrorDM) + { + aTask.infoAppendln(tabStr + "Reason: " + cause.getMessage()); + } + else + { + aTask.infoAppendln(tabStr + "StackTrace: "); + aTask.infoAppendln(ThreadUtil.getStackTrace(cause)); + break; + } + + cause = aErrorDM.getCause(); + } + + aTask.infoAppendln(""); + } + /** * Helper method that prompts the user for forms of input depending on the state of the App *

@@ -675,11 +861,8 @@ public class DistMakerEngine return; } - // Remove the retrieved update, and restore the platform configuration files to this (running) release - // It is necessary to do this, since the user may later cancel the update request and it is important to - // leave the program and configuration files in a stable state. - IoUtil.deleteDirectory(deltaPath); - updatePlatformConfigFiles(aTask, currRelease); + // Revert the update + revertUpdate(aTask); } // Query the user of the version to update to @@ -695,37 +878,126 @@ public class DistMakerEngine } /** - * Helper method to update platform specific configuration files + * Helper method that "reverts" an update. After this method is called the DistMaker application's configuration should be in the same state as before an + * update was applied. Reverting consists of the following: + *

+ *

+ * There should not be any issues with this roll back process. However if there are a best effort will be made to continue rolling back the updates - note + * that the application might be in an unstable state - and may not be able to be restarted. */ - private boolean updatePlatformConfigFiles(Task aTask, AppRelease aRelease) + private void revertUpdate(Task aTask) { - File installPath, pFile; - String errMsg; - - // Get the top level install path - installPath = DistUtils.getAppPath().getParentFile(); - - // Apple specific platform files - pFile = new File(installPath, "Info.plist"); - if (pFile.isFile() == false) - pFile = new File(installPath.getParentFile(), "Info.plist"); - - if (pFile.isFile() == true) + // Revert our application's configuration (which will be loaded when it is restarted) to reflect the proper JRE + try { - errMsg = null; - if (pFile.setWritable(true) == false) - errMsg = "Failure. No writable permmisions for file: " + pFile; - else - errMsg = AppleUtils.updateVersion(pFile, aRelease.getVersion()); - - if (errMsg != null) - { - aTask.infoAppendln(errMsg); - return false; - } + JreVersion currJreVer = DistUtils.getJreVersion(); + PlatformUtils.setJreVersion(currJreVer); + } + catch(ErrorDM aExp) + { + aTask.infoAppendln("Failed to revert application's JRE!"); + aTask.infoAppendln("\tApplication may be in an unstable state."); + printErrorDM(aTask, aExp, 1); } - return true; + // Revert any platform specific config files + try + { + PlatformUtils.updateAppRelease(currRelease); + } + catch(ErrorDM aExp) + { + aTask.infoAppendln("Failed to revert application configuration!"); + aTask.infoAppendln("\tApplication may be in an unstable state."); + printErrorDM(aTask, aExp, 1); + } + + // Determine the path to the delta (update) folder + File rootPath = DistUtils.getAppPath().getParentFile(); + File deltaPath = new File(rootPath, "delta"); + + // It is necessary to do this, since the user may later cancel the update request and it is important to + // leave the program and configuration files in a stable state. + + // Execute any trash commands from the "fail" section of the delta.cmd file + File deltaCmdFile = new File(deltaPath, "delta.cmd"); + try (BufferedReader br = MiscUtils.openFileAsBufferedReader(deltaCmdFile)) + { + String currSect = null; + while (true) + { + String inputStr = br.readLine(); + + // Delta command files should always have a proper exit (or reboot) and thus never arrive here + if (inputStr == null) + throw new ErrorDM("Command file (" + deltaCmdFile + ") is incomplete."); + + // Ignore empty lines and comments + if (inputStr.isEmpty() == true || inputStr.startsWith("#") == true) + continue; + + // Tokenize the input and retrieve the command + String[] strArr = inputStr.split(","); + String cmdStr = strArr[0]; + + // Skip to next line when we read a new section + if (strArr.length == 2 && cmdStr.equals("sect") == true) + { + currSect = strArr[1]; + continue; + } + + // Skip to the next line if we are not in the "fail" section + if (currSect != null && currSect.equals("fail") == false) + continue; + else if (currSect == null) + throw new ErrorDM("Command specified outside of section. Command: " + inputStr); + + // Exit if we reach the exit or reboot command + if (strArr.length == 1 && cmdStr.equals("exit") == true) + break; + else if (strArr.length == 1 && cmdStr.equals("reboot") == true) + break; + + // Execute the individual (trash) commands + if (strArr.length == 2 && cmdStr.equals("trash") == true) + { + // Resolve the argument to the corresponding file and ensure it is relative to our rootPath + File tmpFile = new File(rootPath, strArr[1]).getCanonicalFile(); + if (MiscUtils.getRelativePath(rootPath, tmpFile) == null) + throw new ErrorDM("File (" + tmpFile + ") is not relative to folder: " + rootPath); + + if (tmpFile.isFile() == true) + { + if (tmpFile.delete() == false) + throw new ErrorDM("Failed to delete file: " + tmpFile); + } + else if (tmpFile.isDirectory() == true) + { + if (IoUtil.deleteDirectory(tmpFile) == false) + throw new ErrorDM("Failed to delete folder: " + tmpFile); + } + else + { + throw new ErrorDM("File type is not recognized: " + tmpFile); + } + } + } + } + catch(IOException aExp) + { + aTask.infoAppendln("Failed to revert application configuration!"); + aTask.infoAppendln("\tApplication may be in an unstable state."); + aTask.infoAppendln(ThreadUtil.getStackTrace(aExp)); + } + + // Remove the entire delta folder + if (IoUtil.deleteDirectory(deltaPath) == false) + throw new ErrorDM("Failed to delete folder: " + deltaPath); } } diff --git a/src/distMaker/DistUtils.java b/src/distMaker/DistUtils.java index f10e31c..a3f6c7b 100644 --- a/src/distMaker/DistUtils.java +++ b/src/distMaker/DistUtils.java @@ -12,6 +12,8 @@ import glum.util.ThreadUtil; import java.io.*; import java.net.URL; import java.net.URLConnection; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.DigestInputStream; import java.security.MessageDigest; import java.util.*; @@ -75,24 +77,6 @@ public class DistUtils return new JreVersion(jreVer); } - /** - * Returns the platform (Apple, Linux, or Windows) on which the current JRE is running on. - */ - public static String getPlatform() - { - String osName; - - osName = System.getProperty("os.name").toUpperCase(); - if (osName.startsWith("LINUX") == true) - return "Linux"; - if (osName.startsWith("MAC OS X") == true) - return "Apple"; - if (osName.startsWith("WINDOWS") == true) - return "Windows"; - - return System.getProperty("os.name"); - } - /** * Returns the code associated with the update. */ @@ -117,6 +101,26 @@ public class DistUtils return isDevelopersEnvironment; } + /** + * Utility method to determine if the JRE is embedded with this application. + */ + public static boolean isJreBundled() + { + Path rootPath, jrePath; + + // Get the top level folder of our installation + rootPath = getAppPath().toPath().getParent(); + + // Get the path to the JRE + jrePath = Paths.get(System.getProperty("java.home")); + + // Determine if the java.home JRE is a subdirectory of the top level app folder + if (jrePath.startsWith(rootPath) == true) + return true; + + return false; + } + /** * Downloads the specified file from srcUrl to destFile. Returns true on success *

@@ -383,28 +387,23 @@ public class DistUtils public static AppCatalog readAppCatalog(Task aTask, File aCatalogFile, URL aUpdateUrl) { List nodeList; - JreVersion jreVersion; - InputStream inStream; - BufferedReader bufReader; + JreVersion minJreVersion, maxJreVersion; DigestType digestType; String errMsg, strLine; errMsg = null; nodeList = new ArrayList<>(); - jreVersion = null; + minJreVersion = null; + maxJreVersion = null; // Default to DigestType of MD5 digestType = DigestType.MD5; - inStream = null; - bufReader = null; - try + try (BufferedReader bufReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(new FileInputStream(aCatalogFile))));) { String[] tokens; // Read the contents of the file - inStream = new FileInputStream(aCatalogFile); - bufReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(inStream))); while (true) { strLine = bufReader.readLine(); @@ -447,16 +446,18 @@ public class DistUtils else digestType = tmpDigestType; } - else if (tokens.length == 2 && tokens[0].equals("jre") == true) + else if ((tokens.length == 2 || tokens.length == 3) && tokens[0].equals("jre") == true) { - if (jreVersion != null) + if (minJreVersion != null) { - aTask.infoAppendln("JRE version has already been specified. Current ver: " + jreVersion.getLabel() + " Requested ver: " + tokens[1] + aTask.infoAppendln("JRE version has already been specified. Current ver: " + minJreVersion.getLabel() + " Requested ver: " + tokens[1] + ". Skipping..."); continue; } - jreVersion = new JreVersion(tokens[1]); + minJreVersion = new JreVersion(tokens[1]); + if (tokens.length == 3) + maxJreVersion = new JreVersion(tokens[2]); } else { @@ -468,11 +469,6 @@ public class DistUtils { errMsg = ThreadUtil.getStackTrace(aExp); } - finally - { - IoUtil.forceClose(inStream); - IoUtil.forceClose(bufReader); - } // See if we are in a valid state if (errMsg != null) @@ -487,7 +483,7 @@ public class DistUtils return null; } - return new AppCatalog(jreVersion, nodeList); + return new AppCatalog(nodeList, minJreVersion, maxJreVersion); } /** diff --git a/src/distMaker/ErrorDM.java b/src/distMaker/ErrorDM.java new file mode 100644 index 0000000..5f5e45a --- /dev/null +++ b/src/distMaker/ErrorDM.java @@ -0,0 +1,37 @@ +package distMaker; + +/** + * Generic runtime exception thrown by various DistMaker modules/routines. + */ +public class ErrorDM extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + // State vars + private String subject; + + public ErrorDM(Throwable aCause, String aMessage, String aSubject) + { + super(aMessage, aCause); + subject = aSubject; + } + + public ErrorDM(Throwable aCause, String aMessage) + { + this(aCause, aMessage, null); + } + + public ErrorDM(String aMessage) + { + this(null, aMessage, null); + } + + /** + * Returns the subject specific to this error. May be null if this is just a generic error. + */ + public String getSubject() + { + return subject; + } + +} diff --git a/src/distMaker/MiscUtils.java b/src/distMaker/MiscUtils.java index bef57df..ddaa81a 100644 --- a/src/distMaker/MiscUtils.java +++ b/src/distMaker/MiscUtils.java @@ -1,47 +1,54 @@ package distMaker; +import glum.task.Task; + import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.zip.GZIPInputStream; + +import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.archivers.ArchiveStreamFactory; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; + +import com.google.common.io.CountingInputStream; /** - * Collection of generic utility methods that should be migrated to another library. + * Collection of generic utility methods that should be migrated to another library / class. */ public class MiscUtils { - public static void unTar(final File inputFile, final File aDestPath) throws Exception - { - throw new Exception("Incomplete..."); - } - /** - * Helper method to convert a Unix base-10 mode into the equivalent string. + * Utility method to convert a Unix base-10 mode into the equivalent string. *

* Example: 493 -> 'rwxr-xr-x' *

- * The returned string will always be of length 9 + * The returned string will always be of length 9 */ public static String convertUnixModeToStr(int aMode) { - char permArr[] = {'r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x'}; - + char permArr[] = { 'r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x' }; + for (int c1 = 8; c1 >= 0; c1--) { if (((aMode >> c1) & 0x01) != 1) permArr[8 - c1] = '-'; } - + return new String(permArr); } /** - * Helper method to convert a Unix base-10 mode into the Set. + * Utility method to convert a Unix base-10 mode into the Set. *

* Example: 493 -> 'rwxr-xr-x' *

- * The returned string will always be of length 9 + * The returned string will always be of length 9 */ public static Set convertUnixModeToPFP(int aMode) { @@ -49,9 +56,143 @@ public class MiscUtils } /** - * Helper method to output the specified strings to aFile + * Returns the relative path component of aAbsolutePath relative to aBasePath. + *

+ * Returns null if aAbsolutePath does not start with aBasePath. */ - public static boolean writeDoc(File aFile, List strList) + public static String getRelativePath(File aBasePath, File aAbsolutePath) + { + Path relPath; + + // Bail if aAbsolutePath does not start with aBasePath + if (aAbsolutePath.toPath().startsWith(aBasePath.toPath()) == false) + return null; + + relPath = aBasePath.toPath().relativize(aAbsolutePath.toPath()); + return relPath.toString(); + } + + /** + * Utility method that returns a BufferedReader corresponding to the specified file. + *

+ * This helps reduce boiler plate code. + */ + public static BufferedReader openFileAsBufferedReader(File aFile) throws IOException + { + return new BufferedReader(new InputStreamReader(new FileInputStream(aFile))); + } + + /** + * Untar an input file into an output file. + *

+ * Source based off of:
+ * http://stackoverflow.com/questions/315618/how-do-i-extract-a-tar-file-in-java/7556307#7556307 + *

+ * + * The output file is created in the output folder, having the same name as the input file, minus the '.tar' extension. + * + * @param inputFile + * the input .tar file + * @param aDestPath + * The destination folder where the content will be dumped. + * @throws IOException + * @throws FileNotFoundException + * + * @return The {@link List} of {@link File}s with the untared content. + * @throws ArchiveException + */ + public static List unTar(Task aTask, final File inputFile, final File aDestPath) throws FileNotFoundException, IOException, ArchiveException + { + Map pathMap; + InputStream iStream; + + final List untaredFiles = new ArrayList<>(); + long fullLen = inputFile.length(); + + // Open up the stream to the tar file (set up a counting stream to allow for progress updates) + CountingInputStream cntStream = new CountingInputStream(new FileInputStream(inputFile)); + iStream = cntStream; + if (inputFile.getName().toUpperCase().endsWith(".GZ") == true) + iStream = new GZIPInputStream(iStream); + + pathMap = new LinkedHashMap<>(); + final TarArchiveInputStream debInputStream = (TarArchiveInputStream)new ArchiveStreamFactory().createArchiveInputStream("tar", iStream); + TarArchiveEntry entry = null; + while ((entry = (TarArchiveEntry)debInputStream.getNextEntry()) != null) + { + final File outputFile = new File(aDestPath, entry.getName()); + if (entry.isDirectory()) + { + if (!outputFile.exists()) + { + if (!outputFile.mkdirs()) + { + throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile.getAbsolutePath())); + } + } + + long tmpUtc = entry.getModTime().getTime(); + outputFile.setLastModified(tmpUtc); + pathMap.put(outputFile, tmpUtc); + } + else if (entry.isSymbolicLink() == true) + { + File tmpFile = new File(entry.getLinkName()); + Path outLink = Files.createSymbolicLink(outputFile.toPath(), tmpFile.toPath()); + + long tmpUtc = entry.getModTime().getTime(); + outputFile.setLastModified(tmpUtc); + } + else if (entry.isFile() == true) + { + final OutputStream outputFileStream = new FileOutputStream(outputFile); + IOUtils.copy(debInputStream, outputFileStream); + outputFileStream.close(); + + // Update the modified time of the file + long tmpUtc = entry.getModTime().getTime(); + outputFile.setLastModified(tmpUtc); + } + else + { + System.err.println(String.format("Unrecognized entry: %s", entry.getName())); + } + + // Update the mode on all (non symbolic) files / paths + int mode = entry.getMode(); + if (entry.isSymbolicLink() == false) + { + String permStr; + + permStr = convertUnixModeToStr(mode); + Files.setPosixFilePermissions(outputFile.toPath(), PosixFilePermissions.fromString(permStr)); +// System.out.println(String.format("\tMode: %d %x %s %s name: %s", mode, mode, Integer.toOctalString(mode), permStr, entry.getName())); + } + + untaredFiles.add(outputFile); + + // Update the progress bar + aTask.infoUpdate("\tUnpacked: " + entry.getName()); + long currLen = cntStream.getCount(); + aTask.setProgress(currLen / (fullLen + 0.0)); + } + debInputStream.close(); + + // Update all of the times on the folders last + for (File aDir : pathMap.keySet()) + aDir.setLastModified(pathMap.get(aDir)); + + aTask.infoAppendln("\tUnpacked: " + untaredFiles.size() + " files\n"); + + return untaredFiles; + } + + /** + * Helper method to output the specified strings to aFile + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void writeDoc(File aFile, List strList) { // Output the strList try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(aFile)));) @@ -60,13 +201,10 @@ public class MiscUtils for (String aStr : strList) bw.write(aStr + '\n'); } - catch(Exception aExp) + catch(IOException aExp) { - aExp.printStackTrace(); - return false; + throw new ErrorDM(aExp, "Failed to write the file: " + aFile); } - - return true; } } diff --git a/src/distMaker/gui/MemoryConfigPanel.java b/src/distMaker/gui/MemoryConfigPanel.java index 0d4c8a4..e6402bc 100644 --- a/src/distMaker/gui/MemoryConfigPanel.java +++ b/src/distMaker/gui/MemoryConfigPanel.java @@ -8,6 +8,7 @@ import glum.gui.component.GSlider; import glum.gui.panel.GlassPanel; import glum.gui.panel.generic.MessagePanel; import glum.unit.ByteUnit; +import glum.util.ThreadUtil; import glum.zio.raw.ZioRaw; import java.awt.*; @@ -22,6 +23,7 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import net.miginfocom.swing.MigLayout; +import distMaker.ErrorDM; import distMaker.platform.MemUtils; import distMaker.platform.PlatformUtils; import static distMaker.platform.MemUtils.KB_SIZE; @@ -123,9 +125,26 @@ public class MemoryConfigPanel extends GlassPanel implements ActionListener, Zio targMemSize = (long)targMemS.getModelValue(); targMemSize = roundToMB(targMemSize); - // Bail if we are not able to set the DistMaker max heap memory - if (PlatformUtils.setMaxHeapMem(warnPanel, targMemSize) == false) - return; + try + { + // Delegate the updating of the memory + PlatformUtils.setMaxHeapMem(targMemSize); + } + catch(ErrorDM aExp) + { + String subjectStr = aExp.getSubject(); + if (subjectStr == null) + subjectStr = "Application Configuration Error"; + + String messageStr = aExp.getMessage(); + if (aExp.getCause() != null) + messageStr += "\n\n" + ThreadUtil.getStackTrace(aExp.getCause()); + + // Show the user the details of the failure + warnPanel.setTitle(subjectStr); + warnPanel.setInfo(messageStr); + warnPanel.setVisible(true); + } // Update our state vars instMemSize = targMemSize; diff --git a/src/distMaker/jre/JreUtils.java b/src/distMaker/jre/JreUtils.java index 99cd99e..07ee1b3 100644 --- a/src/distMaker/jre/JreUtils.java +++ b/src/distMaker/jre/JreUtils.java @@ -14,6 +14,7 @@ import java.util.*; import distMaker.digest.Digest; import distMaker.digest.DigestType; +import distMaker.platform.PlatformUtils; public class JreUtils { @@ -29,6 +30,7 @@ public class JreUtils BufferedReader bufReader; DigestType digestType; String errMsg, strLine; + String version; errMsg = null; retList = new ArrayList<>(); @@ -36,6 +38,7 @@ public class JreUtils // Default to DigestType of MD5 digestType = DigestType.MD5; + version = null; inStream = null; bufReader = null; @@ -62,6 +65,8 @@ public class JreUtils ; // Nothing to do else if (tokens.length >= 1 && tokens[0].equals("exit") == true) break; // Bail once we get the 'exit' command + else if (tokens.length == 2 && tokens[0].equals("name") == true && tokens[1].equals("JRE") == true) + ; // Nothing to do - we just entered the "JRE" section else if (tokens.length == 2 && tokens[0].equals("digest") == true) { DigestType tmpDigestType; @@ -72,17 +77,35 @@ public class JreUtils else digestType = tmpDigestType; } - else if (tokens.length == 6 && tokens[0].equals("jre") == true) + else if (tokens.length == 2 && tokens[0].equals("jre") == true) { - String platform, version, filename, digestStr; + version = tokens[1]; + } + else if (tokens.length == 4 && tokens[0].equals("F") == true) + { + String platform, filename, digestStr; long fileLen; + if (version == null) + { + aTask.infoAppendln("Skipping input: " + strLine); + aTask.infoAppendln("\tJRE version has not been specifed. Missing input line: jre,"); + continue; + } + // Form the JreRelease digestStr = tokens[1]; - platform = tokens[2]; - version = tokens[3]; - fileLen = GuiUtil.readLong(tokens[4], -1); - filename = tokens[5]; + fileLen = GuiUtil.readLong(tokens[2], -1); + filename = tokens[3]; + + platform = PlatformUtils.getPlatformOfJreTarGz(filename); + if (platform == null) + { + aTask.infoAppendln("Skipping input: " + strLine); + aTask.infoAppendln("\tFailed to determine the target platform of the JRE."); + continue; + } + retList.add(new JreRelease(platform, version, filename, new Digest(digestType, digestStr), fileLen)); } else diff --git a/src/distMaker/jre/JreVersion.java b/src/distMaker/jre/JreVersion.java index af2c641..a443425 100644 --- a/src/distMaker/jre/JreVersion.java +++ b/src/distMaker/jre/JreVersion.java @@ -24,7 +24,7 @@ public class JreVersion implements Comparable *

* The better version is defined as the later version (and the more specific version). *

- * Returns null if the better version can not be determined + * Returns null if the better version can not be determined or if the versions are equal. */ public static JreVersion getBetterVersion(JreVersion verA, JreVersion verB) { @@ -35,10 +35,12 @@ public class JreVersion implements Comparable tokenA = verA.getLabel().split("[._]"); tokenB = verB.getLabel().split("[._]"); - // Default return JreVersion is verA or the version that is more specific - defaultVer = verA; + // Default JreVersion is the version that is more specific + defaultVer = null; if (tokenA.length < tokenB.length) defaultVer = verB; + else if (tokenB.length < tokenA.length) + defaultVer = verA; // Set the idxCnt to the less specific JreVersion idxCnt = tokenA.length; diff --git a/src/distMaker/node/AppCatalog.java b/src/distMaker/node/AppCatalog.java index c2b2932..b0c7f77 100644 --- a/src/distMaker/node/AppCatalog.java +++ b/src/distMaker/node/AppCatalog.java @@ -5,7 +5,7 @@ import java.util.*; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import distMaker.jre.JreVersion; +import distMaker.jre.*; /** * Object that describes the structure (files, folders, and JRE version) of a Java application. @@ -13,23 +13,98 @@ import distMaker.jre.JreVersion; public class AppCatalog { /** The minimum JRE version required. */ - private JreVersion jreVersion; + private JreVersion minJreVer; + + /** The maximum JRE version allowed. This will be null if there is no maximum. */ + private JreVersion maxJreVer; /** A mapping of filename to to corresponding Node */ private ImmutableMap nodeMap; - public AppCatalog(JreVersion aJreVersion, List aNodeList) + public AppCatalog(List aNodeList, JreVersion aMinJreVer, JreVersion aMaxJreVer) { - jreVersion = aJreVersion; + minJreVer = aMinJreVer; + maxJreVer = aMaxJreVer; nodeMap = ImmutableMap.copyOf(formNameMap(aNodeList)); } /** - * Returns the minimum JreVersion required. + * Returns the most recent JRE from the specified release that is compatible with this Appcatalog. + *

+ * Returns null if there are no JREs that are compatible. */ - public JreVersion getJreVersion() + public JreRelease getCompatibleJre(List aJreList) { - return jreVersion; + // Sort the platforms, but reverse the order so that the newest version is first + Collections.sort(aJreList); + Collections.reverse(aJreList); + + for (JreRelease aRelease : aJreList) + { + if (isJreVersionTooNew(aRelease.getVersion()) == true) + continue; + + if (isJreVersionTooOld(aRelease.getVersion()) == true) + continue; + + return aRelease; + } + + return null; + } + + /** + * Returns the minimum JreVersion that is compatible. + */ + public JreVersion getMinJreVersion() + { + return minJreVer; + } + + /** + * Returns the maximum JreVersion that is compatible. + */ + public JreVersion getMaxJreVersion() + { + return maxJreVer; + } + + public boolean isJreVersionCompatible(JreVersion aJreVer) + { + // Check to make sure aJreVer is not too old + if (isJreVersionTooOld(aJreVer) == true) + return false; + + // Check to make sure aJreVer is not too new + if (isJreVersionTooNew(aJreVer) == true) + return false; + + // The version aJreVer must be compatible + return true; + } + + /** + * Returns true if the specified version is not compatible (too new) with this AppCatalog. + */ + public boolean isJreVersionTooNew(JreVersion aJreVer) + { + // Check to make sure aJreVer is not too old + if (maxJreVer != null && JreVersion.getBetterVersion(maxJreVer, aJreVer) == aJreVer) + return true; + + return false; + } + + /** + * Returns true if the specified version is not compatible (too old) with this AppCatalog. + */ + public boolean isJreVersionTooOld(JreVersion aJreVer) + { + // Check to make sure aJreVer is not too old + if (minJreVer != null && JreVersion.getBetterVersion(minJreVer, aJreVer) == minJreVer) + return true; + + return false; } /** @@ -54,7 +129,7 @@ public class AppCatalog * TODO: This should be renamed formNameMap to formDigestMap
* TODO: This should probably be a mapping of Digest to Node rather than filename to Node */ - public Map formNameMap(List aNodeList) + private Map formNameMap(List aNodeList) { Map retMap; diff --git a/src/distMaker/node/AppRelease.java b/src/distMaker/node/AppRelease.java index 3b23bae..30aa535 100644 --- a/src/distMaker/node/AppRelease.java +++ b/src/distMaker/node/AppRelease.java @@ -71,9 +71,9 @@ public class AppRelease implements Comparable, QueryItem } @Override - public void setValue(@SuppressWarnings("unused") LookUp aEnum, @SuppressWarnings("unused") Object aObj) + public void setValue(LookUp aEnum, Object aObj) { - throw new RuntimeException("Unsupported operation"); + throw new RuntimeException("Unsupported operation setValue(aEnum: " + aEnum + " aObj: " + aObj + ")"); } @Override diff --git a/src/distMaker/platform/AppleUtils.java b/src/distMaker/platform/AppleUtils.java index a3fc716..fc49dd9 100644 --- a/src/distMaker/platform/AppleUtils.java +++ b/src/distMaker/platform/AppleUtils.java @@ -1,7 +1,8 @@ package distMaker.platform; -import java.io.File; -import java.io.FileOutputStream; +import java.io.*; +import java.util.ArrayList; +import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -11,30 +12,22 @@ import javax.xml.transform.stream.StreamResult; import org.w3c.dom.*; -import distMaker.DistUtils; +import distMaker.*; +import distMaker.jre.JreVersion; /** * Utility class which contains a set of methods to interact with an Apple Info.plist file. */ public class AppleUtils { - /** - * Utility method to update the JRE to reflect the specified path. - *

- * TODO: Complete this comment and method. - */ - public static boolean updateJrePath(File aPath) - { - int zios_finish; - return false; - } - /** * Returns the plist file used to configure apple applications. *

* Two locations will be searched.... TODO: Add more details of those locations. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - private static File getPlistFile() + public static File getPlistFile() { File installPath; File pFile; @@ -42,58 +35,54 @@ public class AppleUtils // Get the top level install path installPath = DistUtils.getAppPath().getParentFile(); - // Attempt to locate the pList file + // Attempt to locate the pList file (from one of the possible locations) pFile = new File(installPath, "Info.plist"); if (pFile.isFile() == false) pFile = new File(installPath.getParentFile(), "Info.plist"); + + // Bail if we failed to locate the plist file + if (pFile.exists() == false) + throw new ErrorDM("The plist file could not be located."); + + // Bail if the plist file is not a regular file. if (pFile.isFile() == false) - pFile = null; + throw new ErrorDM("The plist file does not appear to be a regular file: " + pFile); return pFile; } /** - * Utility method to update the specified max memory (-Xmx) value in the plist file (aFile) to the specified maxMemVal. + * Utility method to update the specified version in the plist file (pFile) to the new version. *

- * In order for this method to succeed there must be a valid JVMOptions section followed by an array of string elements of JVM arguments. The array element - * may be empty but must be specified. - * - * @return Returns null on success or an error message describing the issue. + * Note this method is very brittle, and assumes that the version will occur in the sibling node which immediately follows the node with a value of + * CFBundleVersion. TODO: Consider reducing brittleness. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - public static String updateMaxMem(long numBytes) + public static void updateAppVersion(String aNewVersion) { // Utilize the system pList file and delegate. - return updateMaxMem(numBytes, getPlistFile()); + updateAppVersion(aNewVersion, getPlistFile()); } /** - * Utility method to update the specified max memory (-Xmx) value in the plist file (aFile) to the specified maxMemVal. + * Utility method to update the specified version in the plist file (pFile) to the new version. *

- * In order for this method to succeed there must be a valid JVMOptions section followed by an array of string elements of JVM arguments. The array element - * may be empty but must be specified. - * - * @return Returns null on success or an error message describing the issue. + * Note this method is very brittle, and assumes that the version will occur in the sibling node which immediately follows the node with a value of + * CFBundleVersion. TODO: Consider reducing brittleness. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - public static String updateMaxMem(long numBytes, File pFile) + public static void updateAppVersion(String aNewVersin, File pFile) { Document doc; Element docElement; - String evalStr, updateStr; - NodeList dictList, childList; - Node childNode, targNode; - Element evalE, arrE, memE; - String tagStr, valStr, currKeyVal; - int zios_Clean; + NodeList nodeList; + Node keyNode, strNode; - // Bail if we failed to locate the pList file. - if (pFile == null) - return "The plist file could not be located."; - // Bail if the plist file is not a regular file. - if (pFile.isFile() == false) - return "The plist file does not appear to be a regular file: " + pFile; - // Bail if the plist file is not writeable. + // Bail if the pFile is not writable. if (pFile.setWritable(true) == false) - return "The plist file is not writeable: " + pFile; + throw new ErrorDM("The plist file is not writeable: " + pFile); // Load the XML document via the javax.xml.parsers.* package try @@ -103,14 +92,148 @@ public class AppleUtils } catch(Exception aExp) { - aExp.printStackTrace(); - return "Failed to parse XML document. File: " + pFile; + throw new ErrorDM(aExp, "Failed to parse XML document. File: " + pFile); + } + + nodeList = docElement.getElementsByTagName("*"); + for (int c1 = 0; c1 < nodeList.getLength(); c1++) + { + keyNode = nodeList.item(c1).getFirstChild(); + if (keyNode != null && keyNode.getNodeValue().equals("CFBundleVersion") == true) + { + System.out.println("Updating contents of file: " + pFile); + + strNode = nodeList.item(c1 + 1).getFirstChild(); + System.out.println(" Old Version: " + strNode.getNodeValue()); + + strNode.setNodeValue(aNewVersin); + System.out.println(" New Version: " + strNode.getNodeValue()); + } + } + + // Update the file with the changed document + saveDoc(pFile, doc); + } + + /** + * Utility method to update the JRE to point to the specified path in the system plist file. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateJreVersion(JreVersion aJreVersion) + { + // Utilize the system pList file and delegate. + updateJreVersion(aJreVersion, getPlistFile()); + } + + /** + * Utility method to update the JRE to point to the specified path in the plist file (pFile). + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateJreVersion(JreVersion aJreVersion, File pFile) + { + List inputList; + String evalStr, tmpStr; + int currLineNum, targLineNum; + + // Bail if the pFile is not writable + if (pFile.setWritable(true) == false) + throw new ErrorDM("The pFile is not writeable: " + pFile); + + // Process our input + inputList = new ArrayList<>(); + try (BufferedReader br = MiscUtils.openFileAsBufferedReader(pFile)) + { + // Read the lines + currLineNum = 0; + targLineNum = -1; + while (true) + { + evalStr = br.readLine(); + if (evalStr == null) + break; + + // Record the start of the JVMRunitme section. Note the JRE should be specified on the next line + tmpStr = evalStr.trim(); + if (tmpStr.equals("JVMRuntime") == true) + targLineNum = currLineNum + 1; + + inputList.add(evalStr); + currLineNum++; + } + } + catch(IOException aExp) + { + throw new ErrorDM(aExp, "Failed while processing the pFile: " + pFile); + } + + // Update the pFile + String regex = "(.*?)"; + String repStr = "jre" + aJreVersion.getLabel() + ""; + if (targLineNum == -1) + throw new ErrorDM("[" + pFile + "] The pFile does not specify a 'JVMRuntime' section."); + else if (targLineNum >= inputList.size()) + throw new ErrorDM("[" + pFile + "] The pFile appears to be incomplete!"); + else + inputList.set(targLineNum, inputList.get(targLineNum).replaceFirst(regex, repStr)); + + // Write the pFile + MiscUtils.writeDoc(pFile, inputList); + } + + /** + * Utility method to update the specified max memory (-Xmx) value in the system plist file to the specified maxMemVal. + *

+ * In order for this method to succeed there must be a valid JVMOptions section followed by an array of string elements of JVM arguments. The array element + * may be empty but must be specified. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateMaxMem(long numBytes) + { + // Utilize the system pList file and delegate. + updateMaxMem(numBytes, getPlistFile()); + } + + /** + * Utility method to update the specified max memory (-Xmx) value in the plist file (pFile) to the specified maxMemVal. + *

+ * In order for this method to succeed there must be a valid JVMOptions section followed by an array of string elements of JVM arguments. The array element + * may be empty but must be specified. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateMaxMem(long numBytes, File pFile) + { + Document doc; + Element docElement; + String evalStr, updateStr; + NodeList dictList, childList; + Node childNode, targNode; + Element evalE, arrE, memE; + String tagStr, valStr, currKeyVal; + int zzz_Clean_this; + + // Bail if the pFile is not writable. + if (pFile.setWritable(true) == false) + throw new ErrorDM("The plist file is not writeable: " + pFile); + + // Load the XML document via the javax.xml.parsers.* package + try + { + doc = loadDoc(pFile); + docElement = doc.getDocumentElement(); + } + catch(Exception aExp) + { + throw new ErrorDM(aExp, "Failed to parse XML document. File: " + pFile); } // Locate the element dictList = docElement.getElementsByTagName("dict"); if (dictList.getLength() == 0) - return "No element found! File: " + pFile; + throw new ErrorDM("No element found! File: " + pFile); arrE = null; currKeyVal = null; @@ -148,7 +271,7 @@ public class AppleUtils // Bail if we failed to locate the array element if (arrE == null) - return "Failed to locate the element following the element: JVMOptions\nFile: " + pFile; + throw new ErrorDM("Failed to locate the element following the element: JVMOptions\nFile: " + pFile); memE = null; childList = arrE.getChildNodes(); @@ -192,57 +315,11 @@ public class AppleUtils evalStr = targNode.getNodeValue(); updateStr = MemUtils.transformMaxMemHeapString(evalStr, numBytes); if (updateStr == null) - return "Failed to transform the memory spec value. Original value: " + evalStr + "\nFile: " + pFile; + throw new ErrorDM("Failed to transform the memory spec value. Original value: " + evalStr + "\nFile: " + pFile); targNode.setNodeValue(updateStr); // Update the file with the changed document - System.out.println("Updating contents of file: " + pFile); - return saveDoc(pFile, doc); - } - - /** - * Utility method to update the specified version in the plist file (aFile) to the new version. - *

- * Note this method is very brittle, and assumes that the version will occur in the sibling node which immediately follows the node with a value of - * CFBundleVersion. - */ - public static String updateVersion(File aFile, String aNewVersin) - { - Document doc; - Element docElement; - NodeList nodeList; - Node keyNode, strNode; - - // Load the XML document via the javax.xml.parsers.* package - try - { - doc = loadDoc(aFile); - docElement = doc.getDocumentElement(); - } - catch(Exception aExp) - { - aExp.printStackTrace(); - return "Failed to parse XML document. File: " + aFile; - } - - nodeList = docElement.getElementsByTagName("*"); - for (int c1 = 0; c1 < nodeList.getLength(); c1++) - { - keyNode = nodeList.item(c1).getFirstChild(); - if (keyNode != null && keyNode.getNodeValue().equals("CFBundleVersion") == true) - { - System.out.println("Updating contents of file: " + aFile); - - strNode = nodeList.item(c1 + 1).getFirstChild(); - System.out.println(" Old Version: " + strNode.getNodeValue()); - - strNode.setNodeValue(aNewVersin); - System.out.println(" New Version: " + strNode.getNodeValue()); - } - } - - // Update the file with the changed document - return saveDoc(aFile, doc); + saveDoc(pFile, doc); } /** @@ -264,8 +341,10 @@ public class AppleUtils /** * Helper method to output aDoc to the specified file. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - private static String saveDoc(File aFile, Document aDoc) + private static void saveDoc(File aFile, Document aDoc) { try (FileOutputStream oStream = new FileOutputStream(aFile);) { @@ -282,11 +361,8 @@ public class AppleUtils } catch(Exception aExp) { - aExp.printStackTrace(); - return "Failed to write the file: " + aFile; + throw new ErrorDM(aExp, "Failed to write the file: " + aFile); } - - return null; } } diff --git a/src/distMaker/platform/LinuxUtils.java b/src/distMaker/platform/LinuxUtils.java index 025c66c..ab0dcf8 100644 --- a/src/distMaker/platform/LinuxUtils.java +++ b/src/distMaker/platform/LinuxUtils.java @@ -5,20 +5,114 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.List; -import distMaker.DistUtils; -import distMaker.MiscUtils; +import distMaker.*; +import distMaker.jre.JreVersion; public class LinuxUtils { /** - * Utility method to update the JRE to reflect the specified path. + * Returns the executable script used to launch the JVM. *

- * TODO: Complete this comment and method. + * If there are multiple launch scripts then this method may grab the wrong file and fail. + *

+ * TODO: In the future the launch script should pass itself as an argument to the JVM and DistMaker should keep track of that. If the script is significantly + * manipulated from the original the launch file may be improperly detected. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - public static boolean updateJrePath(File aPath) + public static File getScriptFile() { - int zios_finish; - return false; + File[] fileArr; + File installPath; + File retFile; + + installPath = DistUtils.getAppPath().getParentFile(); + fileArr = installPath.listFiles(); + + // Attempt to locate the path that matches run* file + retFile = null; + for (File aFile : fileArr) + { + if (aFile.getName().startsWith("run") == true) + retFile = aFile; + } + + // Bail if we failed to locate a regular file + if (retFile == null) + throw new ErrorDM("The script file could not be located."); + + // Ensure the file is a regular fie + if (retFile.isFile() == false) + throw new ErrorDM("The script file is NOT a regular file."); + + // Ensure the file is executable. If this is really the script file used to launch us then it should be executable! + if (Files.isExecutable(retFile.toPath()) == false) + throw new ErrorDM("The script file is NOT executable."); + + return retFile; + } + + /** + * Utility method to update the configuration to reflect the specified JRE version. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateJreVersion(JreVersion aJreVersion) + { + // Utilize the system scriptFile and delegate. + updateJreVersion(aJreVersion, getScriptFile()); + } + + /** + * Utility method to update the configuration to reflect the specified JRE version. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateJreVersion(JreVersion aJreVersion, File aScriptFile) + { + List inputList; + String evalStr, tmpStr; + int currLineNum, targLineNum; + + // Bail if the scritpFile is not writable + if (aScriptFile.setWritable(true) == false) + throw new ErrorDM("The script file is not writeable: " + aScriptFile); + + // Process our input + inputList = new ArrayList<>(); + try (BufferedReader br = MiscUtils.openFileAsBufferedReader(aScriptFile)) + { + // Read the lines + currLineNum = 0; + targLineNum = -1; + while (true) + { + evalStr = br.readLine(); + if (evalStr == null) + break; + + // Locate where the java executable is specified + tmpStr = evalStr.trim(); + if (tmpStr.startsWith("javaExe=") == true) + targLineNum = currLineNum; + + inputList.add(evalStr); + currLineNum++; + } + } + catch(IOException aExp) + { + throw new ErrorDM(aExp, "Failed while processing the script file: " + aScriptFile); + } + + // Update the script + if (targLineNum != -1) + inputList.set(targLineNum, "javaExe=../jre" + aJreVersion.getLabel() + "/bin/java"); + else + throw new ErrorDM("[" + aScriptFile + "] The script does not specify 'javaExe'."); + + // Write the scriptFile + MiscUtils.writeDoc(aScriptFile, inputList); } /** @@ -29,28 +123,39 @@ public class LinuxUtils *

* If the maxMem var definition is moved in the script file to after the launch of the application then this method will (silently) fail to configure the * value needed to launch the JVM. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - public static String updateMaxMem(long numBytes) + public static void updateMaxMem(long numBytes) + { + // Utilize the system scriptFile and delegate. + updateMaxMem(numBytes, getScriptFile()); + } + + /** + * Utility method to update the specified maxMem var in the script (aFile) to the requested number of bytes. + *

+ * Note this method assumes the specified file is a shell script built by DistMaker where the var maxMem holds the proper (right side) specification for the + * JVM's -Xmx value. + *

+ * If the maxMem var definition is moved in the script file to after the launch of the application then this method will (silently) fail to configure the + * value needed to launch the JVM. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateMaxMem(long numBytes, File aScriptFile) { List inputList; - File scriptFile; String evalStr, memStr, tmpStr; int currLineNum, injectLineNum, targLineNum; - // Bail if we fail to locate the scriptFile. - scriptFile = getScriptFile(); - if (scriptFile == null) - return "The script file could not be located."; - // Bail if the script file is not a regular file. - if (scriptFile.isFile() == false) - return "The script file does not appear to be a regular file: " + scriptFile; - // Bail if the script file is not writeable. - if (scriptFile.setWritable(true) == false) - return "The script file is not writeable: " + scriptFile; + // Bail if the scriptFile is not writable + if (aScriptFile.setWritable(true) == false) + throw new ErrorDM("The script file is not writeable: " + aScriptFile); // Process our input inputList = new ArrayList<>(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(scriptFile)));) + try (BufferedReader br = MiscUtils.openFileAsBufferedReader(aScriptFile)) { // Read the lines currLineNum = 0; @@ -73,10 +178,9 @@ public class LinuxUtils currLineNum++; } } - catch(Exception aExp) + catch(IOException aExp) { - aExp.printStackTrace(); - return "Failed while processing the script file: " + scriptFile; + throw new ErrorDM(aExp, "Failed while processing the script file: " + aScriptFile); } // Determine the memStr to use @@ -96,48 +200,8 @@ public class LinuxUtils inputList.add(1, "maxMem=" + memStr + "\n"); } - // Update the script - System.out.println("Updating contents of file: " + scriptFile); - if (MiscUtils.writeDoc(scriptFile, inputList) == false) - return "Failed to write the script file: " + scriptFile; - - // On success return null - return null; - } - - /** - * Returns the executable script used to launch the JVM. If one can not be determined then this method will return null. - *

- * If there are multiple launch scripts then this method may grab the wrong file and fail. - *

- * TODO: In the future the launch script should pass itself as an argument to the JVM and DistMaker should keep track of that. If the script is significantly - * manipulated from the original the launch file may be improperly detected. - */ - private static File getScriptFile() - { - File[] fileArr; - File installPath; - File retFile; - - installPath = DistUtils.getAppPath().getParentFile(); - fileArr = installPath.listFiles(); - - // Attempt to locate the path that matches run* file - retFile = null; - for (File aFile : fileArr) - { - if (aFile.getName().startsWith("run") == true) - retFile = aFile; - } - - if (retFile == null) - return null; - - if (retFile.isFile() == false && Files.isExecutable(retFile.toPath()) == false) - return null; - - System.out.println("Linux launch file: " + retFile); - return retFile; + // Write the scriptFile + MiscUtils.writeDoc(aScriptFile, inputList); } } diff --git a/src/distMaker/platform/PlatformUtils.java b/src/distMaker/platform/PlatformUtils.java index 550581b..ac9e6ab 100644 --- a/src/distMaker/platform/PlatformUtils.java +++ b/src/distMaker/platform/PlatformUtils.java @@ -1,104 +1,171 @@ package distMaker.platform; -import glum.gui.panel.generic.MessagePanel; - import java.io.File; import distMaker.DistUtils; +import distMaker.ErrorDM; import distMaker.jre.JreRelease; +import distMaker.jre.JreVersion; +import distMaker.node.AppRelease; public class PlatformUtils { + /** + * Utility method that returns the platform specific configuration file for the java application. + */ + public static File getConfigurationFile() + { + File cfgFile; + String platform; + + platform = PlatformUtils.getPlatform().toUpperCase(); + if (platform.equals("APPLE") == true) + cfgFile = AppleUtils.getPlistFile(); + else if (platform.equals("LINUX") == true) + cfgFile = LinuxUtils.getScriptFile(); + else if (platform.equals("WINDOWS") == true) + cfgFile = WindowsUtils.getConfigFile(); + else + throw new ErrorDM("Unsupported platform: " + platform); + + return cfgFile; + } + /** * Utility method that returns the path where the specified JRE should be unpacked to. */ public static File getJreLocation(JreRelease aJreRelease) + { + // Delegate to actual worker method + return getJreLocation(aJreRelease.getVersion()); + } + + /** + * Utility method that returns the path where the specified JRE should be unpacked to. + */ + public static File getJreLocation(JreVersion aJreVersion) { String platform, versionStr; File jrePath, installPath; jrePath = null; installPath = DistUtils.getAppPath(); - versionStr = aJreRelease.getVersion().getLabel(); + versionStr = aJreVersion.getLabel(); - platform = DistUtils.getPlatform().toUpperCase(); + platform = PlatformUtils.getPlatform().toUpperCase(); if (platform.equals("APPLE") == true) jrePath = new File(installPath.getParentFile(), "PlugIns/jre" + versionStr); else if (platform.equals("LINUX") == true) jrePath = new File(installPath.getParentFile(), "jre" + versionStr); - else if (platform.equals("Windows") == true) + else if (platform.equals("WINDOWS") == true) jrePath = new File(installPath.getParentFile(), "jre" + versionStr); return jrePath; } /** - * Utility method to configure the JRE location used by the active DistMaker distribution. - *

- * Note this will only take effect after the application has been relaunched. + * Returns the platform (Apple, Linux, or Windows) on which the current JRE is running on. */ - public static boolean setJreLocation(File aPath) + public static String getPlatform() + { + String osName; + + osName = System.getProperty("os.name").toUpperCase(); + if (osName.startsWith("LINUX") == true) + return "Linux"; + if (osName.startsWith("MAC OS X") == true) + return "Apple"; + if (osName.startsWith("WINDOWS") == true) + return "Windows"; + + return System.getProperty("os.name"); + } + + /** + * Returns the platform of the JRE file. + *

+ * This only examines the filename to determine the platform. + */ + public static String getPlatformOfJreTarGz(String aFileName) + { + aFileName = aFileName.toUpperCase(); + if (aFileName.contains("LINUX") == true) + return "Linux"; + if (aFileName.contains("MACOSX") == true) + return "Apple"; + if (aFileName.contains("WINDOWS") == true) + return "Windows"; + + return null; + } + + /** + * Utility method to configure the JRE version used by the (active) DistMaker distribution. + *

+ * Note this will only take effect after the application has been restarted. + *

+ * On failure this method will throw an exception of type ErrorDM. + * + * @param aJrePath + * Path to top of the JRE. + */ + public static void setJreVersion(JreVersion aJreVersion) { String platform; - boolean isPass; - isPass = false; - platform = DistUtils.getPlatform().toUpperCase(); + // Delegate to the proper platform code + platform = PlatformUtils.getPlatform().toUpperCase(); if (platform.equals("APPLE") == true) - isPass = AppleUtils.updateJrePath(aPath); + AppleUtils.updateJreVersion(aJreVersion); else if (platform.equals("LINUX") == true) - isPass = LinuxUtils.updateJrePath(aPath); - else if (platform.equals("Windows") == true) - isPass = WindowsUtils.updateJrePath(aPath); - - return isPass; + LinuxUtils.updateJreVersion(aJreVersion); + else if (platform.equals("WINDOWS") == true) + WindowsUtils.updateJreVersion(aJreVersion); + else + throw new ErrorDM("Unrecognized platform: " + platform); } /** * Utility method to configure the (active) DistMaker distribution to use the specified maxMem. *

- * Method will return false on failure. + * Note this will only take effect after the application has been restarted. + *

+ * On failure this method will throw an exception of type ErrorDM. * - * @param warnPanel - * GUI message panel to route error messages. * @param maxMemSize * Maximum heap memory in bytes. */ - public static boolean setMaxHeapMem(MessagePanel warnPanel, long maxMemSize) + public static void setMaxHeapMem(long maxMemSize) { String platform; - String errMsg; // Delegate to the proper platform code - errMsg = null; - platform = DistUtils.getPlatform().toUpperCase(); + platform = PlatformUtils.getPlatform().toUpperCase(); if (platform.equals("APPLE") == true) - errMsg = AppleUtils.updateMaxMem(maxMemSize); + AppleUtils.updateMaxMem(maxMemSize); else if (platform.equals("LINUX") == true) - errMsg = LinuxUtils.updateMaxMem(maxMemSize); - else if (platform.equals("Windows") == true) - errMsg = WindowsUtils.updateMaxMem(maxMemSize); + LinuxUtils.updateMaxMem(maxMemSize); + else if (platform.equals("WINDOWS") == true) + WindowsUtils.updateMaxMem(maxMemSize); else - errMsg = "Unrecognized platform: " + platform; + throw new ErrorDM(null, "Unrecognized platform: " + platform, "Unsupported Platform"); + } - // Display the warnPanel with the the error condition - if (errMsg != null) - { - if (platform.equals("APPLE") == true) - warnPanel.setTitle("Failed setting Apple properties."); - else if (platform.equals("LINUX") == true) - warnPanel.setTitle("Failed setting Linux configuration."); - else if (platform.equals("Windows") == true) - warnPanel.setTitle("Failed setting Windows configuration."); - else - warnPanel.setTitle("Platform is not supported."); + /** + * Utility method to update the (active) DistMaker distribution to reflect the specified AppRelease. + *

+ * Note this will only take effect after the application has been restarted. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateAppRelease(AppRelease aRelease) + { + String platform; - warnPanel.setInfo(errMsg); - warnPanel.setVisible(true); - return false; - } - - return true; + // Delegate to the proper platform code + platform = getPlatform().toUpperCase(); + if (platform.equals("APPLE") == true) + AppleUtils.updateAppVersion(aRelease.getVersion()); } } diff --git a/src/distMaker/platform/WindowsUtils.java b/src/distMaker/platform/WindowsUtils.java index b4db704..7bc52dc 100644 --- a/src/distMaker/platform/WindowsUtils.java +++ b/src/distMaker/platform/WindowsUtils.java @@ -4,20 +4,87 @@ import java.io.*; import java.util.ArrayList; import java.util.List; -import distMaker.DistUtils; -import distMaker.MiscUtils; +import distMaker.*; +import distMaker.jre.JreVersion; public class WindowsUtils { /** - * Utility method to update the JRE to reflect the specified path. + * Returns the l4j runtime configuration file. If one can not be determined then this method will return null. *

- * TODO: Complete this comment and method. + * If the configuration file is determined but does not exist, then an empty configuration file will be created. + *

+ * Note this method looks for a file that ends in .l4j.cfg, or an exe file and creates the corresponding config file. + *

+ * If there are multiple .exe or .l4j.cfg files, then this method may grab the wrong file and fail. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - public static boolean updateJrePath(File aPath) + public static File getConfigFile() { - int zios_finish; - return false; + File[] fileArr; + File installPath; + File retFile; + + installPath = DistUtils.getAppPath().getParentFile(); + fileArr = installPath.listFiles(); + + // Attempt to locate the .l4j.ini file + retFile = null; + for (File aFile : fileArr) + { + if (aFile.getName().endsWith(".l4j.ini") == true) + retFile = aFile; + } + + if (retFile == null) + { + for (File aFile : fileArr) + { + if (aFile.getName().endsWith(".exe") == true) + retFile = new File(aFile.getParentFile(), aFile.getName().substring(0, aFile.getName().length() - 4) + ".l4j.ini"); + } + } + + if (retFile == null) + throw new ErrorDM("The config file could not be located."); + + if (retFile.isFile() == false) + { + try + { + retFile.createNewFile(); + } + catch(IOException aExp) + { + throw new ErrorDM(aExp, "A default config file could not be created."); + } + } + + System.out.println("Windows config file: " + retFile); + return retFile; + } + + /** + * Utility method to update the configuration to reflect the specified JRE version. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateJreVersion(JreVersion aJreVersion) + { + // Utilize the system configFile and delegate. + updateJreVersion(aJreVersion, getConfigFile()); + } + + /** + * Utility method to update the configuration to reflect the specified JRE version. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateJreVersion(JreVersion aJreVersion, File aConfigFile) + { + int zzz_incomplete_logic; + throw new ErrorDM("The logic is incomplete."); } /** @@ -25,24 +92,39 @@ public class WindowsUtils *

* Note this method is very brittle, and assumes that there is a single value where the string, -Xmx, is specified in the script. It assumes this string will * be surrounded by a single space character on each side. + *

+ * On failure this method will throw an exception of type ErrorDM. */ - public static String updateMaxMem(long numBytes) + public static void updateMaxMem(long numBytes) + { + // Utilize the system configFile and delegate. + updateMaxMem(numBytes, getConfigFile()); + } + + /** + * Utility method to update the specified max memory (-Xmx) value in the text file (aFile) to the specified maxMemVal. + *

+ * Note this method is very brittle, and assumes that there is a single value where the string, -Xmx, is specified in the script. It assumes this string will + * be surrounded by a single space character on each side. + *

+ * On failure this method will throw an exception of type ErrorDM. + */ + public static void updateMaxMem(long numBytes, File aConfigFile) { - File configFile; List inputList; String strLine, updateStr; boolean isProcessed; - // Bail if we fail to locate the configFile. - configFile = getConfigFile(); - if (configFile != null && configFile.isFile() == true) - return "The config file could not be located."; + // Bail if the configFile is not writable + aConfigFile = getConfigFile(); + if (aConfigFile.setWritable(true) == false) + throw new ErrorDM("The config file is not writeable: " + aConfigFile); isProcessed = false; inputList = new ArrayList<>(); // Process our input - try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(configFile)))) + try (BufferedReader br = MiscUtils.openFileAsBufferedReader(aConfigFile)) { // Read the lines while (true) @@ -72,72 +154,11 @@ public class WindowsUtils } catch(Exception aExp) { - aExp.printStackTrace(); - return "Failed while processing the config file: " + configFile; + throw new ErrorDM(aExp, "Failed while processing the config file: " + aConfigFile); } // Update the script - System.out.println("Updating contents of file: " + configFile); - if (MiscUtils.writeDoc(configFile, inputList) == false) - return "Failed to write the config file: " + configFile; - - // On success return null - return null; - } - - /** - * Returns the l4j runtime configuration file. If one can not be determined then this method will return null. - *

- * If the configuration file is determined but does not exist, then an empty configuration file will be created. - *

- * Note this method looks for a file that ends in .l4j.cfg, or an exe file and creates the corresponding config file. - *

- * If there are multiple .exe or .l4j.cfg files, then this method may grab the wrong file and fail. - */ - public static File getConfigFile() - { - File[] fileArr; - File installPath; - File retFile; - - installPath = DistUtils.getAppPath().getParentFile(); - fileArr = installPath.listFiles(); - - // Attempt to locate the .l4j.ini file - retFile = null; - for (File aFile : fileArr) - { - if (aFile.getName().endsWith(".l4j.ini") == true) - retFile = aFile; - } - - if (retFile == null) - { - for (File aFile : fileArr) - { - if (aFile.getName().endsWith(".exe") == true) - retFile = new File(aFile.getParentFile(), aFile.getName().substring(0, aFile.getName().length() - 4) + ".l4j.ini"); - } - } - - if (retFile == null) - return null; - - if (retFile.isFile() == false) - { - try - { - retFile.createNewFile(); - } - catch(IOException aExp) - { - aExp.printStackTrace(); - return null; - } - } - - System.out.println("Windows config file: " + retFile); - return retFile; + MiscUtils.writeDoc(aConfigFile, inputList); } } diff --git a/template/appLauncher.jar b/template/appLauncher.jar index 0419b685c984bc842f284e3f0ecc1362a8a06a3c..d6ef2a801c85b97fd3464552f95c89148844a1b8 100644 GIT binary patch delta 8650 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 BV(kC` delta 3778 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