diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/AppDataUpgrader.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/AppDataUpgrader.kt new file mode 100644 index 000000000..a3ea4179f --- /dev/null +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/AppDataUpgrader.kt @@ -0,0 +1,224 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.upgrade + +import java.security.PrivilegedActionException +import java.io.IOException +import com.google.gson.JsonSyntaxException +import javafx.scene.control.Alert +import java.io.File +import java.net.URLClassLoader +import java.security.PrivilegedExceptionAction +import java.security.AccessController +import java.util.Arrays +import java.util.ArrayList +import java.util.jar.JarFile +import org.jackhuang.hmcl.Main +import java.util.HashMap +import org.jackhuang.hmcl.task.* +import java.util.logging.Level +import java.util.zip.GZIPInputStream +import java.util.jar.Pack200 +import java.util.jar.JarOutputStream +import org.jackhuang.hmcl.util.* +import java.net.URISyntaxException +import org.jackhuang.hmcl.util.OS +import org.jackhuang.hmcl.i18n +import org.jackhuang.hmcl.ui.alert +import org.jackhuang.hmcl.util.VersionNumber +import java.net.URI + +class AppDataUpgrader : IUpgrader { + + @Throws(IOException::class, PrivilegedActionException::class) + private fun launchNewerVersion(args: Array, jar: File): Boolean { + JarFile(jar).use { jarFile -> + val mainClass = jarFile.manifest.mainAttributes.getValue("Main-Class") + if (mainClass != null) { + val al = ArrayList(Arrays.asList(*args)) + al.add("--noupdate") + AccessController.doPrivileged(PrivilegedExceptionAction { + URLClassLoader(arrayOf(jar.toURI().toURL()), + URLClassLoader.getSystemClassLoader().parent).loadClass(mainClass) + .getMethod("main", Array::class.java).invoke(null, *arrayOf(al.toTypedArray())) + null + }) + return true + } + } + return false + } + + override fun parseArguments(nowVersion: VersionNumber, args: Array) { + val f = AppDataUpgraderPackGzTask.HMCL_VER_FILE + if (!args.contains("--noupdate")) + try { + if (f.exists()) { + val m = GSON.fromJson(f.readText(), Map::class.java) + val s = m["ver"] as? String? + if (s != null && VersionNumber.asVersion(s.toString()) > nowVersion) { + val j = m["loc"] as? String? + if (j != null) { + val jar = File(j) + if (jar.exists() && launchNewerVersion(args, jar)) + System.exit(0) + } + } + } + } catch (ex: JsonSyntaxException) { + f.delete() + } catch (t: IOException) { + LOG.log(Level.SEVERE, "Failed to execute newer version application", t) + } catch (t: PrivilegedActionException) { + LOG.log(Level.SEVERE, "Failed to execute newer version application", t) + } + + } + + override fun download(checker: UpdateChecker, versionNumber: VersionNumber) { + val version = versionNumber as IntVersionNumber + checker.requestDownloadLink().then { + val map: Map? = it["update_checker.request_download_link"] + if (alert(Alert.AlertType.CONFIRMATION, "Alert", i18n("update.newest_version") + version[0] + "." + version[1] + "." + version[2] + "\n" + + i18n("update.should_open_link"))) + if (map != null && map.containsKey("jar") && map["jar"]!!.isNotBlank()) + try { + var hash: String? = null + if (map.containsKey("jarsha1")) + hash = map.get("jarsha1") + if (AppDataUpgraderJarTask(map["jar"]!!, version.toString(), hash!!).test()) { + ProcessBuilder(JavaVersion.fromCurrentEnvironment().binary.absolutePath, "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).absolutePath).directory(File("").absoluteFile).start() + System.exit(0) + } + } catch (ex: IOException) { + LOG.log(Level.SEVERE, "Failed to create upgrader", ex) + } + else if (map != null && map.containsKey("pack") && map["pack"]!!.isNotBlank()) + try { + var hash: String? = null + if (map.containsKey("packsha1")) + hash = map["packsha1"] + if (AppDataUpgraderPackGzTask(map["pack"]!!, version.toString(), hash!!).test()) { + ProcessBuilder(JavaVersion.fromCurrentEnvironment().binary.absolutePath, "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).absolutePath).directory(File("").absoluteFile).start() + System.exit(0) + } + } catch (ex: IOException) { + LOG.log(Level.SEVERE, "Failed to create upgrader", ex) + } + else { + var url = URL_PUBLISH + if (map != null) + if (map.containsKey(OS.CURRENT_OS.checkedName)) + url = map.get(OS.CURRENT_OS.checkedName)!! + else if (map.containsKey(OS.UNKNOWN.checkedName)) + url = map.get(OS.UNKNOWN.checkedName)!! + try { + java.awt.Desktop.getDesktop().browse(URI(url)) + } catch (e: URISyntaxException) { + LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e) + OS.setClipboard(url) + } catch (e: IOException) { + LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e) + OS.setClipboard(url) + } + + } + null + }.execute() + } + + class AppDataUpgraderPackGzTask(downloadLink: String, private val newestVersion: String, private val expectedHash: String) : Task() { + private val tempFile: File = File.createTempFile("hmcl", ".pack.gz") + override val dependents = listOf(FileDownloadTask(downloadLink.toURL(), tempFile, expectedHash)) + + init { + onDone += { event -> if (event.failed) tempFile.delete() } + } + + override fun execute() { + val json = HashMap() + var f = getSelf(newestVersion) + if (!f.parentFile.makeDirectory()) + throw IOException("Failed to make directories: " + f.parent) + + var i = 0 + while (f.exists()) { + f = File(BASE_FOLDER, "HMCL-" + newestVersion + (if (i > 0) "-" + i else "") + ".jar") + i++ + } + if (!f.createNewFile()) + throw IOException("Failed to create new file: " + f) + + JarOutputStream(f.outputStream()).use { jos -> Pack200.newUnpacker().unpack(GZIPInputStream(tempFile.inputStream()), jos) } + json.put("ver", newestVersion) + json.put("loc", f.absolutePath) + val result = GSON.toJson(json) + HMCL_VER_FILE.writeText(result) + } + + val info: String + get() = "Upgrade" + + companion object { + + val BASE_FOLDER = Main.getWorkingDirectory("hmcl") + val HMCL_VER_FILE = File(BASE_FOLDER, "hmclver.json") + + fun getSelf(ver: String): File { + return File(BASE_FOLDER, "HMCL-$ver.jar") + } + } + + } + + class AppDataUpgraderJarTask(downloadLink: String, private val newestVersion: String, expectedHash: String) : Task() { + override var title = "Upgrade" + set(value) {} + private val tempFile = File.createTempFile("hmcl", ".jar") + + init { + onDone += { event -> if (event.failed) tempFile.delete() } + } + + override val dependents = listOf(FileDownloadTask(downloadLink.toURL(), tempFile, expectedHash)) + + override fun execute() { + val json = HashMap() + val f = getSelf(newestVersion) + tempFile.copyTo(f) + json.put("ver", newestVersion) + json.put("loc", f.absolutePath) + val result = GSON.toJson(json) + HMCL_VER_FILE.writeText(result) + } + + companion object { + val BASE_FOLDER = Main.getWorkingDirectory("hmcl") + val HMCL_VER_FILE = File(BASE_FOLDER, "hmclver.json") + + fun getSelf(ver: String): File { + return File(BASE_FOLDER, "HMCL-$ver.jar") + } + } + + } + + companion object { + const val URL_PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html" + } +} \ No newline at end of file diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/IUpgrader.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/IUpgrader.kt new file mode 100644 index 000000000..a112e0faf --- /dev/null +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/IUpgrader.kt @@ -0,0 +1,49 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.upgrade + +import org.jackhuang.hmcl.util.VersionNumber + +/** + * + * @author huangyuhui + */ +interface IUpgrader { + + /** + * Paring arguments to decide on whether the upgrade is needed. + * + * @param nowVersion now launcher version + * @param args Application CommandLine Arguments + */ + fun parseArguments(nowVersion: VersionNumber, args: Array) + + /** + * Just download the new app. + * + * @param checker Should be VersionChecker + * @param versionNumber the newest version + * + * @return should return true + */ + fun download(checker: UpdateChecker, versionNumber: VersionNumber) + + companion object { + val NOW_UPGRADER: IUpgrader = AppDataUpgrader() + } +} diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/UpdateChecker.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/UpdateChecker.kt new file mode 100644 index 000000000..79481d39e --- /dev/null +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/upgrade/UpdateChecker.kt @@ -0,0 +1,110 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.upgrade + +import java.io.IOException +import com.google.gson.JsonSyntaxException +import org.jackhuang.hmcl.task.TaskResult +import org.jackhuang.hmcl.util.* +import java.util.logging.Level + + +/** + * + * @author huangyuhui + */ +class UpdateChecker(var base: VersionNumber, var type: String) { + + @Volatile + var isOutOfDate = false + private set + var versionString: String? = null + private var download_link: Map? = null + + /** + * Get the cached newest version number, use "process" method to + * download! + * + * @return the newest version number + * + * @see process + */ + var newVersion: VersionNumber? = null + internal set + + /** + * Download the version number synchronously. When you execute this method + * first, should leave "showMessage" false. + * + * @param showMessage If it is requested to warn the user that there is a + * new version. + * + * @return the process observable. + */ + fun process(showMessage: Boolean): TaskResult { + return object : TaskResult() { + override val id = "update_checker.process" + override fun execute() { + if (newVersion == null) { + versionString = ("http://huangyuhui.duapp.com/info.php?type=$type").toURL().doGet() + newVersion = VersionNumber.asVersion(versionString!!) + } + + if (newVersion == null) { + LOG.warning("Failed to check update...") + } else if (base < newVersion!!) + isOutOfDate = true + if (isOutOfDate) + result = newVersion + } + } + } + + /** + * Get the download links. + * + * @return a JSON, which contains the server response. + */ + @Synchronized + fun requestDownloadLink(): TaskResult> { + return object : TaskResult>() { + override val id = "update_checker.request_download_link" + override fun execute() { + @Suppress("UNCHECKED_CAST") + if (download_link == null) + try { + download_link = GSON.fromJson(("http://huangyuhui.duapp.com/update_link.php?type=$type").toURL().doGet(), Map::class.java) as Map + } catch (e: JsonSyntaxException) { + LOG.log(Level.WARNING, "Failed to get update link.", e) + } catch (e: IOException) { + LOG.log(Level.WARNING, "Failed to get update link.", e) + } + + result = download_link + } + } + } +/* + val upgrade: EventHandler> = EventHandler() + + fun checkOutdate() { + if (isOutOfDate) + if (EVENT_BUS.fireChannelResulted(OutOfDateEvent(this, newVersion))) + upgrade.fire(SimpleEvent(this, newVersion)) + }*/ +} diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt index 7fd0e6983..5d010fb74 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt @@ -132,6 +132,7 @@ abstract class Task { fun executor() = TaskExecutor(this) fun executor(taskListener: TaskListener) = TaskExecutor(this).apply { this.taskListener = taskListener } fun start() = executor().start() + fun test() = executor().test() fun subscribe(subscriber: Task) = TaskExecutor(with(subscriber)).apply { start() } fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: (AutoTypingMap) -> Unit) = subscribe(task(scheduler, closure)) diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt index 7285d0a5e..93aa0e75f 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt @@ -47,6 +47,20 @@ class TaskExecutor(private val task: Task) { }) } + @Throws(InterruptedException::class) + fun test(): Boolean { + var flag = true + val future = Scheduler.NEW_THREAD.schedule { + if (!executeTasks(listOf(task))) { + taskListener?.onTerminate() + flag = false + } + } + workerQueue.add(future) + future!!.get() + return flag + } + /** * Cancel the subscription ant interrupt all tasks. */ diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt index 23cb9580a..68d4a7d6c 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt @@ -28,23 +28,23 @@ import java.util.* /** * Represents the operating system. */ -enum class OS { +enum class OS(val checkedName: String) { /** * Microsoft Windows. */ - WINDOWS, + WINDOWS("windows"), /** * Linux and Unix like OS, including Solaris. */ - LINUX, + LINUX("linux"), /** * Mac OS X. */ - OSX, + OSX("osx"), /** * Unknown operating system. */ - UNKNOWN; + UNKNOWN("universal"); companion object { /** diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/VersionNumber.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/VersionNumber.kt index 8aa1d5f43..589d05a76 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/VersionNumber.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/VersionNumber.kt @@ -75,6 +75,8 @@ class StringVersionNumber internal constructor(val version: String): VersionNumb */ class IntVersionNumber internal constructor(val version: List): VersionNumber() { + operator fun get(index: Int) = version[index] + override fun compareTo(other: VersionNumber): Int { if (other !is IntVersionNumber) return 0 val len = minOf(this.version.size, other.version.size)