Skip to content

Commit

Permalink
Merge branch 'develop' into 'main'
Browse files Browse the repository at this point in the history
improv: Allow starting of update from within the app when using installers

See merge request Griefed/ServerPackCreator!605
Griefed committed Aug 30, 2024
2 parents 1e99a7b + be6e0b0 commit 19fb457
Showing 4 changed files with 441 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions serverpackcreator-app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
@@ -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<UpdateDescriptorEntry, Any?>() {
@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<Any?, Any?>() {
@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()
}
}
279 changes: 278 additions & 1 deletion spc.install4j
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<install4j version="10.0.8" transformSequenceNumber="10">
<install4j version="10.0.9" transformSequenceNumber="10">
<directoryPresets config="./img" />
<application name="ServerPackCreator" applicationId="3565-3228-6904-0931" mediaDir="./media" mediaFilePattern="ServerPackCreator-${compiler:sys.version}-Installer-${compiler:sys.platform}" compression="9" lzmaCompression="true" createChecksums="false" shrinkRuntime="false" shortName="SPC" publisher="Griefed" publisherWeb="https://serverpackcreator.de" version="${compiler:projectVersion}" allPathsRelative="true" autoSave="true" macVolumeId="521d0a10ddf6d897" javaMinVersion="21">
<variables>
@@ -1489,6 +1489,283 @@ return true;</property>
</group>
</screens>
</application>
<application name="Background update downloader" id="442" beanClass="com.install4j.runtime.beans.applications.CustomApplication">
<serializedBean>
<property name="customIconImageFiles">
<add>
<object class="com.install4j.api.beans.ExternalFile">
<string>${compiler:sys.install4jHome}/resource/updater_16.png</string>
</object>
</add>
<add>
<object class="com.install4j.api.beans.ExternalFile">
<string>${compiler:sys.install4jHome}/resource/updater_32.png</string>
</object>
</add>
<add>
<object class="com.install4j.api.beans.ExternalFile">
<string>${compiler:sys.install4jHome}/resource/updater_48.png</string>
</object>
</add>
<add>
<object class="com.install4j.api.beans.ExternalFile">
<string>${compiler:sys.install4jHome}/resource/updater_128.png</string>
</object>
</add>
<add>
<object class="com.install4j.api.beans.ExternalFile">
<string>${compiler:sys.install4jHome}/resource/updater_256.png</string>
</object>
</add>
</property>
<property name="executableName" type="string">bgupdater</property>
<property name="useCustomIcon" type="boolean" value="true" />
<property name="vmParameters" type="string">-Dapple.awt.UIElement=true</property>
<property name="windowTitle" type="string">${compiler:sys.fullName}</property>
</serializedBean>
<startup>
<screen id="443" beanClass="com.install4j.runtime.beans.screens.StartupScreen" rollbackBarrierExitCode="0">
<actions>
<action name="Check prerequisites" id="444" beanClass="com.install4j.runtime.beans.actions.control.RunScriptAction" rollbackBarrierExitCode="0" failureStrategy="quit">
<serializedBean>
<property name="script">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">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() &amp;&amp; ((Util.isWindows() &amp;&amp; !Util.isArchive()) || Util.isMacOS() || (Util.isLinux() &amp;&amp; !Util.isArchive()));
</property>
</object>
</property>
</serializedBean>
</action>
<action id="445" beanClass="com.install4j.runtime.beans.actions.update.CheckForUpdateAction" actionElevationType="none" failureStrategy="quit">
<serializedBean>
<property name="url" type="string">${compiler:sys.updatesUrl}</property>
<property name="variable" type="string">updateDescriptor</property>
</serializedBean>
</action>
<action name="Update descriptor entry" id="446" beanClass="com.install4j.runtime.beans.actions.control.SetVariableAction" failureStrategy="quit">
<serializedBean>
<property name="failIfNull" type="boolean" value="true" />
<property name="script">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">UpdateDescriptorEntry entry = ((UpdateDescriptor)context.getVariable("updateDescriptor")).getPossibleUpdateEntry();

if (entry == null) {
return null;
} else if (entry.isArchive() &amp;&amp; !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;
}</property>
</object>
</property>
<property name="variableName" type="string">updateDescriptorEntry</property>
</serializedBean>
</action>
<group name="Update available" id="447" beanClass="com.install4j.runtime.beans.groups.ActionGroup">
<serializedBean>
<property name="conditionExpression">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">context.getVariable("updateDescriptorEntry") != null</property>
</object>
</property>
</serializedBean>
<beans>
<action name="New version" id="448" beanClass="com.install4j.runtime.beans.actions.control.SetVariableAction">
<serializedBean>
<property name="script">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).getNewVersion()</property>
</object>
</property>
<property name="variableName" type="string">updaterNewVersion</property>
</serializedBean>
</action>
<action name="Download URL" id="449" beanClass="com.install4j.runtime.beans.actions.control.SetVariableAction">
<serializedBean>
<property name="script">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).getURL().toExternalForm()</property>
</object>
</property>
<property name="variableName" type="string">updaterDownloadUrl</property>
</serializedBean>
</action>
<action name="Download location" id="450" beanClass="com.install4j.runtime.beans.actions.control.SetVariableAction">
<serializedBean>
<property name="script">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">context.getVariable("sys.updateStorageDir") + File.separator + ((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).getFileName()</property>
</object>
</property>
<property name="variableName" type="string">updaterDownloadFile</property>
</serializedBean>
</action>
<action id="451" beanClass="com.install4j.runtime.beans.actions.net.DownloadFileAction" failureStrategy="quit">
<serializedBean>
<property name="targetFile">
<object class="java.io.File">
<string>${installer:updaterDownloadFile}</string>
</object>
</property>
<property name="url" type="string">${installer:updaterDownloadUrl}</property>
</serializedBean>
</action>
<group name="Installer" id="452" beanClass="com.install4j.runtime.beans.groups.ActionGroup">
<serializedBean>
<property name="conditionExpression">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">!((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).isArchive()</property>
</object>
</property>
</serializedBean>
<beans>
<action id="453" beanClass="com.install4j.runtime.beans.actions.files.SetModeAction" actionElevationType="elevated">
<serializedBean>
<property name="files" type="array" class="java.io.File" length="1">
<element index="0">
<object class="java.io.File">
<string>${installer:updaterDownloadFile}</string>
</object>
</element>
</property>
<property name="mode" type="string">755</property>
</serializedBean>
</action>
<action id="454" beanClass="com.install4j.runtime.beans.actions.update.ScheduleUpdateAction" actionElevationType="none" failureStrategy="quit">
<serializedBean>
<property name="installerFile">
<object class="java.io.File">
<string>${installer:updaterDownloadFile}</string>
</object>
</property>
<property name="version" type="string">${installer:updaterNewVersion}</property>
</serializedBean>
</action>
</beans>
</group>
<group name="Single Bundle Archive" id="455" beanClass="com.install4j.runtime.beans.groups.ActionGroup">
<serializedBean>
<property name="conditionExpression">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">((UpdateDescriptorEntry)context.getVariable("updateDescriptorEntry")).isArchive()</property>
</object>
</property>
</serializedBean>
<beans>
<action name="Staging Directory" id="456" beanClass="com.install4j.runtime.beans.actions.control.SetVariableAction">
<serializedBean>
<property name="script">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">String dirName = context.getVariable("updaterDownloadFile") + "_dir";
new File(dirName).mkdirs();
return dirName;</property>
</object>
</property>
<property name="variableName" type="string">updaterStagingDir</property>
</serializedBean>
</action>
<action name="Delete Staging Directory" id="457" beanClass="com.install4j.runtime.beans.actions.files.DeleteFileAction" actionElevationType="elevated" rollbackBarrierExitCode="0">
<serializedBean>
<property name="backupForRollback" type="boolean" value="false" />
<property name="files" type="array" class="java.io.File" length="1">
<element index="0">
<object class="java.io.File">
<string>${installer:updaterStagingDir}</string>
</object>
</element>
</property>
<property name="recursive" type="boolean" value="true" />
</serializedBean>
<condition>new File((String)context.getVariable("updaterStagingDir")).exists()</condition>
</action>
<action id="458" beanClass="com.install4j.runtime.beans.actions.files.ExtractDmgFileAction" actionElevationType="elevated" rollbackBarrierExitCode="0" failureStrategy="quit">
<serializedBean>
<property name="archiveFile">
<object class="java.io.File">
<string>${installer:updaterDownloadFile}</string>
</object>
</property>
<property name="destinationDirectory">
<object class="java.io.File">
<string>${installer:updaterStagingDir}</string>
</object>
</property>
<property name="fileFilter">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">// 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") &amp;&amp; realFile.isDirectory() &amp;&amp; !UnixFileSystem.getFileInformation(realFile).isLink());</property>
</object>
</property>
</serializedBean>
<condition>((String)context.getVariable("updaterDownloadFile")).endsWith(".dmg")</condition>
</action>
<action id="459" beanClass="com.install4j.runtime.beans.actions.files.ExtractTarFileAction" actionElevationType="elevated" rollbackBarrierExitCode="0" failureStrategy="quit">
<serializedBean>
<property name="archiveFile">
<object class="java.io.File">
<string>${installer:updaterDownloadFile}</string>
</object>
</property>
<property name="destinationDirectory">
<object class="java.io.File">
<string>${installer:updaterStagingDir}</string>
</object>
</property>
<property name="fileFilter">
<object class="com.install4j.api.beans.ScriptProperty">
<property name="value" type="string">// only extract app bundle, no other top level files
file.getParent() != null || (file.getName().endsWith(".app") &amp;&amp; directory)</property>
</object>
</property>
</serializedBean>
<condition>!((String)context.getVariable("updaterDownloadFile")).endsWith(".dmg")</condition>
</action>
<action id="460" beanClass="com.install4j.runtime.beans.actions.update.ScheduleUpdateAction" actionElevationType="none" failureStrategy="quit">
<serializedBean>
<property name="installerFile">
<object class="java.io.File">
<string>${installer:updaterStagingDir}</string>
</object>
</property>
<property name="version" type="string">${installer:updaterNewVersion}</property>
</serializedBean>
</action>
<action name="Delete archive" id="461" beanClass="com.install4j.runtime.beans.actions.files.DeleteFileAction" actionElevationType="elevated" rollbackBarrierExitCode="0">
<serializedBean>
<property name="backupForRollback" type="boolean" value="false" />
<property name="files" type="array" class="java.io.File" length="1">
<element index="0">
<object class="java.io.File">
<string>${installer:updaterDownloadFile}</string>
</object>
</element>
</property>
</serializedBean>
</action>
</beans>
</group>
</beans>
</group>
</actions>
</screen>
</startup>
</application>
</applications>
<styles defaultStyleId="1">
<style name="Standard" id="1" beanClass="com.install4j.runtime.beans.styles.FormStyle">

0 comments on commit 19fb457

Please sign in to comment.