diff --git a/serverpackcreator-api/src/main/i18n/Translations_en_GB.properties b/serverpackcreator-api/src/main/i18n/Translations_en_GB.properties index d9b5aa3e9..369e2a391 100644 --- a/serverpackcreator-api/src/main/i18n/Translations_en_GB.properties +++ b/serverpackcreator-api/src/main/i18n/Translations_en_GB.properties @@ -359,6 +359,11 @@ menubar.gui.config.load.new=New Tab update.dialog.new=An update to ServerPackCreator is available at:\n{0}\n\nWhat would you like to do? update.dialog.available=Update available! update.dialog.yes=Open in Browser +update.dialog.update=Download & Update +update.dialog.update.message=Download is complete, the new version will now be installed. +update.dialog.update.failed.message=Update could not be downloaded. +update.dialog.update.failed.cause=An error has occurred: {0} +update.dialog.update.title=ServerPackCreator update.dialog.no=Neither update.dialog.clipboard=Copy to clipboard filebrowser=File Browser diff --git a/serverpackcreator-app/build.gradle.kts b/serverpackcreator-app/build.gradle.kts index a726346b5..12c9241b8 100644 --- a/serverpackcreator-app/build.gradle.kts +++ b/serverpackcreator-app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.ir.backend.js.compile + plugins { id("serverpackcreator.dokka-conventions") id("org.springframework.boot") apply false @@ -7,6 +9,7 @@ plugins { repositories { mavenCentral() maven { url = uri("https://repo.spring.io/milestone") } + maven { url = uri("https://maven.ej-technologies.com/repository") } } dependencyManagement { @@ -45,6 +48,7 @@ dependencies { api("net.java.balloontip:balloontip:1.2.4.1") api("com.cronutils:cron-utils:9.2.1") api("tokyo.northside:tipoftheday:0.4.2") + compileOnly("com.install4j:install4j-runtime:10.0.9") //WEB api("org.jetbrains.kotlin:kotlin-reflect:1.9.23") diff --git a/serverpackcreator-app/src/main/kotlin/de/griefed/serverpackcreator/app/gui/window/UpdateDialogs.kt b/serverpackcreator-app/src/main/kotlin/de/griefed/serverpackcreator/app/gui/window/UpdateDialogs.kt index 1c2871a62..022e23556 100644 --- a/serverpackcreator-app/src/main/kotlin/de/griefed/serverpackcreator/app/gui/window/UpdateDialogs.kt +++ b/serverpackcreator-app/src/main/kotlin/de/griefed/serverpackcreator/app/gui/window/UpdateDialogs.kt @@ -20,6 +20,13 @@ package de.griefed.serverpackcreator.app.gui.window import Translations +import com.install4j.api.Util +import com.install4j.api.context.UserCanceledException +import com.install4j.api.launcher.ApplicationLauncher +import com.install4j.api.launcher.Variables +import com.install4j.api.update.ApplicationDisplayMode +import com.install4j.api.update.UpdateDescriptor +import com.install4j.api.update.UpdateDescriptorEntry import de.griefed.serverpackcreator.api.ApiProperties import de.griefed.serverpackcreator.api.utilities.common.WebUtilities import de.griefed.serverpackcreator.app.gui.GuiProps @@ -30,11 +37,13 @@ import org.apache.logging.log4j.kotlin.cachedLoggerOf import java.awt.Toolkit import java.awt.datatransfer.Clipboard import java.awt.datatransfer.StringSelection +import java.io.IOException import java.net.URISyntaxException +import java.nio.file.Files +import java.nio.file.Paths import java.util.* -import javax.swing.JFrame -import javax.swing.JOptionPane -import javax.swing.JTextPane +import java.util.concurrent.ExecutionException +import javax.swing.* import javax.swing.text.* /** @@ -56,6 +65,12 @@ class UpdateDialogs( apiProperties.isCheckingForPreReleasesEnabled ) private set + var i4JUpdatable = false + private set + + init { + checkForUpdateWithApi() + } /** * If an update for ServerPackCreator is available, display a dialog letting the user choose whether they want to @@ -81,7 +96,11 @@ class UpdateDialogs( ) jTextPane.isOpaque = false jTextPane.isEditable = false - options[0] = Translations.update_dialog_yes.toString() + if (i4JUpdatable) { + options[0] = Translations.update_dialog_update.toString() + } else { + options[0] = Translations.update_dialog_yes.toString() + } options[1] = Translations.update_dialog_no.toString() options[2] = Translations.update_dialog_clipboard.toString() try { @@ -100,7 +119,11 @@ class UpdateDialogs( options[0] )) { 0 -> try { - webUtilities.openLinkInBrowser(update.get().url().toURI()) + if (i4JUpdatable) { + downloadAndUpdate() + } else { + webUtilities.openLinkInBrowser(update.get().url().toURI()) + } } catch (ex: RuntimeException) { log.error("Error opening browser.", ex) } catch (ex: URISyntaxException) { @@ -121,6 +144,7 @@ class UpdateDialogs( */ fun checkForUpdate(): Boolean { update = updateChecker.checkForUpdate(apiProperties.apiVersion, apiProperties.isCheckingForPreReleasesEnabled) + checkForUpdateWithApi() if (!displayUpdateDialog()) { DialogUtilities.createDialog( Translations.menubar_gui_menuitem_updates_none.toString() + " ", @@ -133,4 +157,129 @@ class UpdateDialogs( } return update.isPresent } + + private fun isUpdatable(): Boolean { + try { + val installationDirectory = Paths.get(Variables.getInstallerVariable("sys.installationDir").toString()) + return !Files.getFileStore(installationDirectory).isReadOnly && (Util.isWindows() || Util.isMacOS() || (Util.isLinux() && !Util.isArchive())) + } catch (ex: IOException) { + log.error("Error checking for install4j updatability.", ex) + } + return false + } + + private fun checkForUpdateWithApi() { + try { + if (isUpdatable()) { + // Here we check for updates in the background with the API. + object : SwingWorker() { + @Throws(Exception::class) + override fun doInBackground(): UpdateDescriptorEntry { + // The compiler variable sys.updatesUrl holds the URL where the updates.xml file is hosted. + // That URL is defined on the "Installer->Auto Update Options" step. + // The same compiler variable is used by the "Check for update" actions that are contained in the update + // downloaders. + val updateUrl: String = Variables.getCompilerVariable("sys.updatesUrl") + val updateDescriptor: UpdateDescriptor = + com.install4j.api.update.UpdateChecker.getUpdateDescriptor(updateUrl, ApplicationDisplayMode.GUI) + // If getPossibleUpdateEntry returns a non-null value, the version number in the updates.xml file + // is greater than the version number of the local installation. + return updateDescriptor.possibleUpdateEntry + } + + override fun done() { + try { + val updateDescriptorEntry: UpdateDescriptorEntry? = get() + // only installers and single bundle archives on macOS are supported for background updates + if (updateDescriptorEntry != null && (!updateDescriptorEntry.isArchive || updateDescriptorEntry.isSingleBundle)) { + // An update is available for download + i4JUpdatable = true + } + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: ExecutionException) { + val cause = e.cause + // UserCanceledException means that the user has canceled the proxy dialog + if (cause !is UserCanceledException) { + e.printStackTrace() + } + } + } + }.execute() + } else { + i4JUpdatable = false + } + } catch (ncdfe: NoClassDefFoundError) { + i4JUpdatable = false + log.debug("Not an install4j installation.") + } + } + + private fun downloadAndUpdate() { + // Here the background update downloader is launched in the background + // See checkForUpdate(), where the interactive updater is launched for comments on launching an update downloader. + object : SwingWorker() { + @Throws(java.lang.Exception::class) + override fun doInBackground(): Any? { + // Note the third argument which makes the call to the background update downloader blocking. + // The callback receives progress information from the update downloader and changes the text on the button + ApplicationLauncher.launchApplication("442", null, true, null) + // At this point, the update downloader has returned, and we can check if the "Schedule update installation" + // action has registered an update installer for execution + // We now switch to the EDT in done() for terminating the application + return null + } + + override fun done() { + try { + get() // rethrow exceptions that occurred in doInBackground() wrapped in an ExecutionException + if (com.install4j.api.update.UpdateChecker.isUpdateScheduled()) { + JOptionPane.showMessageDialog( + mainFrame, + Translations.update_dialog_update_message.toString(), + Translations.update_dialog_update_title.toString(), + JOptionPane.INFORMATION_MESSAGE + ) + // We execute the update immediately, but you could ask the user whether the update should be + // installed now. The scheduling of update installers is persistent, so this will also work + // after a restart of the launcher. + executeUpdate() + } else { + JOptionPane.showMessageDialog( + mainFrame, + Translations.update_dialog_update_failed_message.toString(), + Translations.update_dialog_update_title.toString(), + JOptionPane.ERROR_MESSAGE + ) + } + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: ExecutionException) { + e.printStackTrace() + JOptionPane.showMessageDialog( + mainFrame, + Translations.update_dialog_update_failed_cause(e.cause!!.message.toString()), + Translations.update_dialog_update_title.toString(), + JOptionPane.ERROR_MESSAGE + ) + } + } + }.execute() + } + + private fun executeUpdate() { + // The arguments that are passed to the installer switch the default GUI mode to an unattended + // mode with a progress bar. "-q" activates unattended mode, and "-splash Updating hello world ..." + // shows a progress bar with the specified title. + Thread { + com.install4j.api.update.UpdateChecker.executeScheduledUpdate( + mutableListOf( + "-q", + "-splash", + "Updating ServerPackCreator ...", + "-alerts" + ), true, null + ) + }.start() + } } \ No newline at end of file diff --git a/spc.install4j b/spc.install4j index 3d48059fb..4ba77d355 100644 --- a/spc.install4j +++ b/spc.install4j @@ -1,5 +1,5 @@ - + @@ -1489,6 +1489,283 @@ return true; + + + + + + ${compiler:sys.install4jHome}/resource/updater_16.png + + + + + ${compiler:sys.install4jHome}/resource/updater_32.png + + + + + ${compiler:sys.install4jHome}/resource/updater_48.png + + + + + ${compiler:sys.install4jHome}/resource/updater_128.png + + + + + ${compiler:sys.install4jHome}/resource/updater_256.png + + + + bgupdater + + -Dapple.awt.UIElement=true + ${compiler:sys.fullName} + + + + + + + + + import java.nio.file.*; + +Path dir = context.getInstallationDirectory().toPath(); +// quit if the current installation is on a read only file system, for example a disk image on macOS +// or if the directory is not writable on Linux/Unix. +// If there is no "Request privileges" action in the installer, the condition should also +// check Files.isWritable(dir) +return !Files.getFileStore(dir).isReadOnly() && ((Util.isWindows() && !Util.isArchive()) || Util.isMacOS() || (Util.isLinux() && !Util.isArchive())); + + + + + + + + ${compiler:sys.updatesUrl} + updateDescriptor + + + + + + + + UpdateDescriptorEntry entry = ((UpdateDescriptor)context.getVariable("updateDescriptor")).getPossibleUpdateEntry(); + +if (entry == null) { + return null; +} else if (entry.isArchive() && !entry.isSingleBundle()) { + // only installers and single bundle archives on macOS are supported + return null; +} else if (entry.isDownloaded()) { + // update has been downloaded already + return null; +} else { + return entry; +} + + + updateDescriptorEntry + + + + + + + context.getVariable("updateDescriptorEntry") != null + + + + + + + + + ((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).getNewVersion() + + + updaterNewVersion + + + + + + + ((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).getURL().toExternalForm() + + + updaterDownloadUrl + + + + + + + context.getVariable("sys.updateStorageDir") + File.separator + ((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).getFileName() + + + updaterDownloadFile + + + + + + + ${installer:updaterDownloadFile} + + + ${installer:updaterDownloadUrl} + + + + + + + !((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).isArchive() + + + + + + + + + + ${installer:updaterDownloadFile} + + + + 755 + + + + + + + ${installer:updaterDownloadFile} + + + ${installer:updaterNewVersion} + + + + + + + + + ((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).isArchive() + + + + + + + + + String dirName = context.getVariable("updaterDownloadFile") + "_dir"; +new File(dirName).mkdirs(); +return dirName; + + + updaterStagingDir + + + + + + + + + ${installer:updaterStagingDir} + + + + + + new File((String)context.getVariable("updaterStagingDir")).exists() + + + + + + ${installer:updaterDownloadFile} + + + + + ${installer:updaterStagingDir} + + + + + // only extract app bundle, no other top level files + +import com.install4j.api.unix.UnixFileSystem; + +File realFile = new File(dmgMountPoint, file.getPath()); + +return file.getParent() != null || (file.getName().endsWith(".app") && realFile.isDirectory() && !UnixFileSystem.getFileInformation(realFile).isLink()); + + + + ((String)context.getVariable("updaterDownloadFile")).endsWith(".dmg") + + + + + + ${installer:updaterDownloadFile} + + + + + ${installer:updaterStagingDir} + + + + + // only extract app bundle, no other top level files +file.getParent() != null || (file.getName().endsWith(".app") && directory) + + + + !((String)context.getVariable("updaterDownloadFile")).endsWith(".dmg") + + + + + + ${installer:updaterStagingDir} + + + ${installer:updaterNewVersion} + + + + + + + + + ${installer:updaterDownloadFile} + + + + + + + + + + + + +