From 0c1700b5f4387ae144711e1c8b01e135e2a67d96 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 17:59:38 +0300 Subject: [PATCH 1/8] fix(jar): getRunningJarFileAsUrl() was not working as expected before --- sync-script/src/main/kotlin/utils/JarUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync-script/src/main/kotlin/utils/JarUtils.kt b/sync-script/src/main/kotlin/utils/JarUtils.kt index f2fcb3a..6741a96 100644 --- a/sync-script/src/main/kotlin/utils/JarUtils.kt +++ b/sync-script/src/main/kotlin/utils/JarUtils.kt @@ -7,7 +7,7 @@ import java.net.URL * @throws IllegalStateException if the application is not being run from a JAR file * */ fun getRunningJarFileAsUrl(): Result { - val codeSource = object {}.javaClass.enclosingClass.protectionDomain?.codeSource + val codeSource = object {}.javaClass.protectionDomain?.codeSource codeSource?.location?.let { if (!it.file.endsWith(".jar", ignoreCase = true)) { return Result.failure( From 797135fdfeb9d14e2ad263979529904f527a3c48 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 18:01:35 +0300 Subject: [PATCH 2/8] feat(gradle): add GenerateBuildConfigTask task in buildSrc --- .../main/kotlin/GenerateBuildConfigTask.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 buildSrc/src/main/kotlin/GenerateBuildConfigTask.kt diff --git a/buildSrc/src/main/kotlin/GenerateBuildConfigTask.kt b/buildSrc/src/main/kotlin/GenerateBuildConfigTask.kt new file mode 100644 index 0000000..875b0bb --- /dev/null +++ b/buildSrc/src/main/kotlin/GenerateBuildConfigTask.kt @@ -0,0 +1,49 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.mapProperty +import org.gradle.kotlin.dsl.property + +/** + * A task to extract data from the build script into the source code to use it, such as the version of the project. + * */ +open class GenerateBuildConfigTask : DefaultTask() { + @get:Input + val fieldsToGenerate: MapProperty = project.objects.mapProperty() + + @get:Input + val classFullyQualifiedName: Property = project.objects.property() + + @get:OutputDirectory + val generatedOutputDirectory: DirectoryProperty = project.objects.directoryProperty() + + @TaskAction + fun execute() { + val directory = generatedOutputDirectory.get().asFile + directory.deleteRecursively() + directory.mkdirs() + + val packageNameParts = classFullyQualifiedName.get().split(".") + val className = packageNameParts.last() + val generatedFile = directory.resolve("$className.kt") + val generatedFileContent = + buildString { + if (packageNameParts.size > 1) { + appendLine("package ${packageNameParts.dropLast(1).joinToString(".")}") + } + + appendLine() + appendLine("// GENERATED FILE, Manual changes will be overwritten") + appendLine("object $className {") + for ((key, value) in fieldsToGenerate.get().entries.sortedBy { it.key }) { + appendLine(" const val $key = ${if (value is String) "\"$value\"" else value.toString()}") + } + appendLine("}") + } + generatedFile.writeText(generatedFileContent) + } +} From aa7b561a8699d5f1552f6f5c945905902ec744e8 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 18:04:12 +0300 Subject: [PATCH 3/8] chore(gradle): configure common module to use GenerateBuildConfigTask and auto generate BuildConfig on compile Kotlin code, configure kotlin generated src directory --- common/build.gradle.kts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 7802bb7..8bf77b8 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -22,5 +22,26 @@ tasks.test { } kotlin { - jvmToolchain(libs.versions.java.get().toInt()) + jvmToolchain( + libs.versions.java + .get() + .toInt(), + ) +} + +val generateBuildConfig = + tasks.register("generateBuildConfig") { + val buildConfigDirectory = project.layout.buildDirectory.dir("generated") + + classFullyQualifiedName.set("generated.BuildConfig") + generatedOutputDirectory.set(buildConfigDirectory) + fieldsToGenerate.put("PROJECT_VERSION", libs.versions.project.get()) + } + +sourceSets.main.configure { + kotlin.srcDirs(generateBuildConfig.flatMap { it.generatedOutputDirectory }) +} + +tasks.compileKotlin.configure { + dependsOn(generateBuildConfig) } From 7354007e193488886eecd47f505aacb3105f928a Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 18:06:20 +0300 Subject: [PATCH 4/8] docs(development-notes): add a note regarding generated BuildConfig --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7da0338..3650812 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -161,6 +161,8 @@ discussed above - Usually when we add a new fields or modify existing ones in the data classes, like, for example, adding `description` field in the `Mod` data class, we will try to update the [Admin](./admin) module too to convert the new data from other launchers to make the process easier for administrations +- The project generate `BuildConfig` object using a Gradle task once you start the application or building it, + you might get `Unresolved reference: BuildConfig` which can be solved by either start the application or building it. ### Development Known Issues 🚧 From cd840a73d53afe1e8d2aeb3f58908116335627b0 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 18:07:41 +0300 Subject: [PATCH 5/8] feat(command-line): a new executeBatchScriptInSeparateWindow() function to run Windows batch script in a separate window --- common/src/main/kotlin/utils/CommandLine.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/common/src/main/kotlin/utils/CommandLine.kt b/common/src/main/kotlin/utils/CommandLine.kt index 36bbae2..c86d522 100644 --- a/common/src/main/kotlin/utils/CommandLine.kt +++ b/common/src/main/kotlin/utils/CommandLine.kt @@ -1,6 +1,7 @@ package utils import utils.os.OperatingSystem +import java.io.File import java.util.concurrent.TimeUnit /** @@ -74,3 +75,15 @@ fun powerShellCommandLine( isLoggingEnabled = isLoggingEnabled, ) } + +/** + * Launch a bat script file on **Microsoft Windows** in a new window which will **prevent blocking the code execution** + * the code execution of the bat script will continue to work even if the application has been closed + * @throws IllegalStateException If the current operating system is not [OperatingSystem.Windows] + * */ +fun executeBatchScriptInSeparateWindow(batScriptFile: File) { + if (!OperatingSystem.current.isWindows()) { + throw IllegalStateException("Bat script can be only executed on Windows.") + } + ProcessBuilder("cmd", "/c", "start", batScriptFile.absolutePath).start() +} From f9ff55b5025f9e5214d2f410567e93955b46f76b Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 18:09:47 +0300 Subject: [PATCH 6/8] chore(constants): add URLs for the latest version and latest jar file in ProjectInfoConstants --- common/src/main/kotlin/constants/ProjectInfoConstants.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/src/main/kotlin/constants/ProjectInfoConstants.kt b/common/src/main/kotlin/constants/ProjectInfoConstants.kt index a1f7ffe..cee50d1 100644 --- a/common/src/main/kotlin/constants/ProjectInfoConstants.kt +++ b/common/src/main/kotlin/constants/ProjectInfoConstants.kt @@ -7,4 +7,9 @@ object ProjectInfoConstants { // At the moment, we don't have a website; this will make it easier to provide a link to it later const val WEBSITE = REPOSITORY_LINK + + const val LIBS_VERSIONS_TOML_FILE_URL = + "https://raw.githubusercontent.com/ellet0/kraft-sync/main/gradle/libs.versions.toml" + + const val LATEST_SYNC_SCRIPT_JAR_FILE_URL = "https://github.com/ellet0/kraft-sync/releases/download/latest/kraft-sync.min.jar" } From 4194824cd61ae656e01dabd7aa0f40ba73e7c352 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 18:13:11 +0300 Subject: [PATCH 7/8] feat: implement highly experimental auto update feature which disabled by default --- sync-script/src/main/kotlin/JarAutoUpdater.kt | 147 ++++++++++++++++++ sync-script/src/main/kotlin/Main.kt | 6 + .../main/kotlin/config/models/ScriptConfig.kt | 17 +- 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 sync-script/src/main/kotlin/JarAutoUpdater.kt diff --git a/sync-script/src/main/kotlin/JarAutoUpdater.kt b/sync-script/src/main/kotlin/JarAutoUpdater.kt new file mode 100644 index 0000000..8e4ddcd --- /dev/null +++ b/sync-script/src/main/kotlin/JarAutoUpdater.kt @@ -0,0 +1,147 @@ +import constants.ProjectInfoConstants +import constants.SyncScriptInstanceFiles +import generated.BuildConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Request +import utils.FileDownloader +import utils.HttpService +import utils.SystemInfoProvider +import utils.executeAsync +import utils.executeBatchScriptInSeparateWindow +import utils.getBodyOrThrow +import utils.getRunningJarFileAsUrl +import utils.os.OperatingSystem +import utils.terminateWithOrWithoutError +import java.io.File + +object JarAutoUpdater { + private suspend fun downloadLatestJarFile(): Result = + try { + val newJarFile = + SyncScriptInstanceFiles.SyncScriptData.Temp.file + .resolve("${ProjectInfoConstants.NORMALIZED_NAME}-new.jar") + if (newJarFile.exists()) { + newJarFile.delete() + } + FileDownloader( + downloadUrl = ProjectInfoConstants.LATEST_SYNC_SCRIPT_JAR_FILE_URL, + targetFile = newJarFile, + progressListener = { _, _, _ -> }, + ).downloadFile() + Result.success(newJarFile) + } catch (e: Exception) { + e.printStackTrace() + Result.failure(e) + } + + private suspend fun getLatestProjectVersion(): Result = + try { + val url = ProjectInfoConstants.LIBS_VERSIONS_TOML_FILE_URL + println("\uD83D\uDCE5 Sending GET request to: $url") + val request = + Request + .Builder() + .url(url) + .get() + .build() + val response = HttpService.client.newCall(request).executeAsync() + val responseBody: String = response.getBodyOrThrow().string() + + val projectVersionRegex = Regex("""project\s*=\s*"(.+?)"""") + + val projectVersion = + projectVersionRegex + .find(responseBody) + ?.groups + ?.get(1) + ?.value + Result.success(projectVersion) + } catch (e: Exception) { + e.printStackTrace() + Result.failure(e) + } + + suspend fun updateIfAvailable() { + val currentRunningJarFile = + File( + getRunningJarFileAsUrl() + .getOrElse { + println("⚠\uFE0F Auto update feature is only supported when running using JAR.") + return + }.file, + ) + val latestProjectVersion = + getLatestProjectVersion().getOrElse { + println("❌ We couldn't get the latest project version: ${it.message}") + return + } + if (latestProjectVersion == null) { + println( + "⚠\uFE0F It seems that the project version is missing, it could have been moved somewhere else. " + + "Consider updating manually.", + ) + return + } + if (latestProjectVersion == BuildConfig.PROJECT_VERSION) { + println("✨ You're using the latest version of the project.") + return + } + val newJarFile = + downloadLatestJarFile().getOrElse { + println("❌ An error occurred while downloading the latest version: ${it.message}") + return + } + println("ℹ\uFE0F The new update has been downloaded, will close the application.") + updateApplication( + currentRunningJarFile = currentRunningJarFile, + newJarFile = newJarFile, + ) + } + + private suspend fun updateApplication( + currentRunningJarFile: File, + newJarFile: File, + ) { + when (OperatingSystem.current) { + OperatingSystem.Linux, OperatingSystem.MacOS -> { + Runtime.getRuntime().addShutdownHook( + Thread { + currentRunningJarFile.delete() + newJarFile.renameTo(currentRunningJarFile) + }, + ) + } + + OperatingSystem.Windows -> { + // On Windows, we can't rename, delete or modify the current running JAR file due to file locking + val updateBatScriptFile = + SyncScriptInstanceFiles.SyncScriptData.Temp.file + .resolve("update.bat") + withContext(Dispatchers.IO) { + updateBatScriptFile.parentFile.mkdirs() + updateBatScriptFile.createNewFile() + } + updateBatScriptFile.writeText( + """ + @echo off + echo Waiting for 2 seconds to ensure application closure... + timeout /t 2 > nul + del "${currentRunningJarFile.absolutePath}" + move "${newJarFile.absolutePath}" "${currentRunningJarFile.absolutePath}" + exit + """.trimIndent(), + ) + executeBatchScriptInSeparateWindow( + batScriptFile = updateBatScriptFile, + ) + } + + OperatingSystem.Unknown -> { + println("⚠\uFE0F Auto update feature is not supported on ${SystemInfoProvider.getOperatingSystemName()}.") + } + } + // Will require the user to launch once again after the update. + terminateWithOrWithoutError() + } +} diff --git a/sync-script/src/main/kotlin/Main.kt b/sync-script/src/main/kotlin/Main.kt index c03cb5e..e078154 100644 --- a/sync-script/src/main/kotlin/Main.kt +++ b/sync-script/src/main/kotlin/Main.kt @@ -3,6 +3,7 @@ import config.data.ScriptConfigDataSource import config.models.ScriptConfig import constants.Constants import constants.SyncScriptInstanceFiles +import generated.BuildConfig import gui.GuiState import gui.dialogs.CreateScriptConfigDialog import gui.dialogs.QuickPreferencesDialog @@ -40,6 +41,7 @@ suspend fun main(args: Array) { passedArgs = args + println("📋 Current project version: ${BuildConfig.PROJECT_VERSION}") println("\uD83D\uDCC1 Current working directory: ${SystemInfoProvider.getCurrentWorkingDirectoryPath()}") when (OperatingSystem.current) { @@ -152,6 +154,10 @@ suspend fun main(args: Array) { ) } + if (scriptConfig.autoUpdateEnabled) { + JarAutoUpdater.updateIfAvailable() + } + // TODO: Plan if we should implement this in non GUI mode if (GuiState.isGuiEnabled && !SyncScriptInstanceFiles.SyncScriptData.IsPreferencesConfigured.file diff --git a/sync-script/src/main/kotlin/config/models/ScriptConfig.kt b/sync-script/src/main/kotlin/config/models/ScriptConfig.kt index bf607f8..3120488 100644 --- a/sync-script/src/main/kotlin/config/models/ScriptConfig.kt +++ b/sync-script/src/main/kotlin/config/models/ScriptConfig.kt @@ -64,7 +64,22 @@ data class ScriptConfig( * then consider passing false to [isGuiEnabledOverride] * */ val environment: Environment = Environment.Client, - // TODO: add `autoUpdate` property, plan how it work and add some kind of caching or how often it will update + /** + * Currently, the auto update feature is **highly experimental**, might be removed, changed at anytime. + * And for now, this feature has the following known issues: + * 1. It will always update even if the next version has as **breaking changes** + * that can't be automatically migrated. + * 2. It will always update even the next version is not a stable release; + * we haven't implemented an update channel for now (e.g., stable, beta, alpha, development etc...). + * 3. Once the update is finished, the application will close with + * exit code 1 which will indicate as an error by the launcher. + * The user will have to launch once again to run the updated JAR. + * 4. Currently, it lacks the ability to check for updates, such as on a weekly basis. + * 5. Lacks the option to ask if the user wants to update or skip. + * 6. At the moment we have minimized JAR and the fat JAR, + * the update process will always update to the minimized JAR. + * */ + val autoUpdateEnabled: Boolean = false, ) { companion object { var instance: ScriptConfig? = null From 67d690d4c253460ec09fad2ca01c3184ed16b262 Mon Sep 17 00:00:00 2001 From: Ellet Date: Fri, 21 Jun 2024 18:15:55 +0300 Subject: [PATCH 8/8] docs: fix a few typos related to the new autoUpdateEnabled property --- .../src/main/kotlin/config/models/ScriptConfig.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sync-script/src/main/kotlin/config/models/ScriptConfig.kt b/sync-script/src/main/kotlin/config/models/ScriptConfig.kt index 3120488..fdad983 100644 --- a/sync-script/src/main/kotlin/config/models/ScriptConfig.kt +++ b/sync-script/src/main/kotlin/config/models/ScriptConfig.kt @@ -65,14 +65,14 @@ data class ScriptConfig( * */ val environment: Environment = Environment.Client, /** - * Currently, the auto update feature is **highly experimental**, might be removed, changed at anytime. + * Currently, the auto-update feature is **highly experimental**, and might be removed, or changed at any time. * And for now, this feature has the following known issues: - * 1. It will always update even if the next version has as **breaking changes** + * 1. It will always update even if the next version has **breaking changes** * that can't be automatically migrated. - * 2. It will always update even the next version is not a stable release; - * we haven't implemented an update channel for now (e.g., stable, beta, alpha, development etc...). + * 2. It will always update even if the next version is not a stable release; + * we haven't implemented an update channel for now (e.g., stable, beta, alpha, development, etc...). * 3. Once the update is finished, the application will close with - * exit code 1 which will indicate as an error by the launcher. + * exit code 1 which will indicate an error by the launcher. * The user will have to launch once again to run the updated JAR. * 4. Currently, it lacks the ability to check for updates, such as on a weekly basis. * 5. Lacks the option to ask if the user wants to update or skip.