From 25a90fc48189e2f998eda15e6a3b3d550a9ef952 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 29 Jul 2018 16:22:49 +0800 Subject: [PATCH 01/32] Remove upgrade feature --- .../java/org/jackhuang/hmcl/Launcher.java | 11 +- .../main/java/org/jackhuang/hmcl/Main.java | 2 + .../org/jackhuang/hmcl/ui/CrashWindow.java | 2 +- .../org/jackhuang/hmcl/ui/SettingsPage.java | 9 +- .../hmcl/upgrade/AppDataUpgrader.java | 253 ------------------ .../org/jackhuang/hmcl/upgrade/IUpgrader.java | 45 ---- .../hmcl/upgrade/NewFileUpgrader.java | 96 ------- .../jackhuang/hmcl/upgrade/UpdateChecker.java | 160 ----------- .../jackhuang/hmcl/util/CrashReporter.java | 3 +- 9 files changed, 12 insertions(+), 569 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IUpgrader.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/NewFileUpgrader.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 2f2264831..79374cc86 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -25,11 +25,7 @@ import javafx.application.Platform; import javafx.stage.Stage; import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.upgrade.AppDataUpgrader; -import org.jackhuang.hmcl.upgrade.IUpgrader; -import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.*; import java.io.File; @@ -37,7 +33,6 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Paths; -import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -56,12 +51,15 @@ public final class Launcher extends Application { primaryStage.setResizable(false); primaryStage.setScene(Controllers.getScene()); + /* + UPDATE: check update UPDATE_CHECKER.process(false) .then(Task.of(Schedulers.javafx(), () -> { if (UPDATE_CHECKER.isOutOfDate()) Controllers.showUpdate(); })) .start(); + */ primaryStage.show(); } catch (Throwable e) { @@ -80,7 +78,6 @@ public final class Launcher extends Application { // NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher"); Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER; - UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args)); LOG.info("*** " + TITLE + " ***"); LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION); @@ -149,8 +146,6 @@ public final class Launcher extends Application { public static final String VERSION = System.getProperty("hmcl.version.override", "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"); public static final String NAME = "HMCL"; public static final String TITLE = NAME + " " + VERSION; - public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION)); - public static final IUpgrader UPGRADER = new AppDataUpgrader(); public static final CrashReporter CRASH_REPORTER = new CrashReporter(); public static final String UPDATE_SERVER = "https://www.huangyuhui.net"; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java index daa04aaf7..596d46911 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -39,6 +39,8 @@ import org.jackhuang.hmcl.setting.ConfigHolder; public final class Main { public static void main(String[] args) { + /* UPDATE: perform auto-update from local source */ + checkJavaFX(); checkDirectoryPath(); checkDSTRootCAX3(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java index eb52b7429..20137f15a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java @@ -39,7 +39,7 @@ public class CrashWindow extends Stage { public CrashWindow(String text) { Label lblCrash = new Label(); - if (Launcher.UPDATE_CHECKER.isOutOfDate()) + if (false/* UPDATE: current version is outdated */) lblCrash.setText(i18n("launcher.crash_out_dated")); else lblCrash.setText(i18n("launcher.crash")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java index 717db8f43..55f52a8d7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java @@ -37,7 +37,6 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.text.Font; -import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.ui.construct.FontComboBox; import org.jackhuang.hmcl.ui.construct.MultiFileItem; @@ -238,10 +237,10 @@ public final class SettingsPage extends StackPane implements DecoratorPage { } public void checkUpdate() { - btnUpdate.setVisible(Launcher.UPDATE_CHECKER.isOutOfDate()); + btnUpdate.setVisible(false /* UPDATE: current version is outdated */); - if (Launcher.UPDATE_CHECKER.isOutOfDate()) { - lblUpdateSub.setText(i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString())); + if (false /* UPDATE: current version is outdated */) { + lblUpdateSub.setText(i18n("update.newest_version", /* UPDATE: latest version number */"")); lblUpdateSub.getStyleClass().setAll("update-label"); lblUpdate.setText(i18n("update.found")); @@ -257,6 +256,6 @@ public final class SettingsPage extends StackPane implements DecoratorPage { @FXML private void onUpdate() { - Launcher.UPDATE_CHECKER.checkOutdate(); + /* UPDATE: Launcher.UPDATE_CHECKER.checkOutdate();*/ } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java deleted file mode 100644 index bfd7af422..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 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 com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; -import com.jfoenix.concurrency.JFXUtilities; -import javafx.scene.layout.Region; -import org.jackhuang.hmcl.Launcher; -import org.jackhuang.hmcl.task.FileDownloadTask; -import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.task.TaskExecutor; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.MessageBox; -import org.jackhuang.hmcl.util.*; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.*; -import java.util.*; -import java.util.concurrent.atomic.AtomicReference; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.jar.Pack200; -import java.util.logging.Level; -import java.util.zip.GZIPInputStream; - -/** - * - * @author huangyuhui - */ -public class AppDataUpgrader extends IUpgrader { - - private void launchNewerVersion(List args, File jar) throws IOException, ReflectiveOperationException { - try (JarFile jarFile = new JarFile(jar)) { - String mainClass = jarFile.getManifest().getMainAttributes().getValue("Main-Class"); - if (mainClass == null) - throw new ClassNotFoundException("Main-Class not found in manifest"); - ArrayList al = new ArrayList<>(args); - al.add("--noupdate"); - ClassLoader pre = Thread.currentThread().getContextClassLoader(); - try { - Logging.stop(); - ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent()); - Thread.currentThread().setContextClassLoader(now); - now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])}); - } finally { - Logging.start(Launcher.LOG_DIRECTORY); - Thread.currentThread().setContextClassLoader(pre); - } - } - } - - @Override - public void parseArguments(VersionNumber nowVersion, List args) { - File f = AppDataUpgraderPackGzTask.HMCL_VER_FILE; - if (!args.contains("--noupdate")) - try { - if (f.exists()) { - Map m = Constants.GSON.fromJson(FileUtils.readText(f), new TypeToken>() { - }.getType()); - String s = m.get("ver"); - if (s != null && VersionNumber.asVersion(s).compareTo(nowVersion) > 0) { - String j = m.get("loc"); - if (j != null) { - File jar = new File(j); - if (jar.exists()) { - launchNewerVersion(args, jar); - System.exit(0); - } - } - } - } - } catch (JsonParseException ex) { - f.delete(); - } catch (IOException | ReflectiveOperationException t) { - Logging.LOG.log(Level.SEVERE, "Unable to execute newer version application", t); - AppDataUpgraderPackGzTask.HMCL_VER_FILE.delete(); // delete version json, let HMCL re-download the newer version. - } - } - - @Override - public void download(UpdateChecker checker, VersionNumber ver) { - if (!(ver instanceof IntVersionNumber)) - return; - IntVersionNumber version = (IntVersionNumber) ver; - checker.requestDownloadLink().then(Task.of(variables -> { - Map map = variables.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID); - - if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar"))) - try { - String hash = null; - if (map.containsKey("jarsha1")) - hash = map.get("jarsha1"); - Task task = new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash); - TaskExecutor executor = task.executor(); - AtomicReference region = new AtomicReference<>(); - JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null))); - if (executor.test()) { - new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath()) - .directory(new File("").getAbsoluteFile()).start(); - System.exit(0); - } - JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent())); - } catch (IOException ex) { - Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex); - } - else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack"))) - try { - String hash = null; - if (map.containsKey("packsha1")) - hash = map.get("packsha1"); - Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash); - TaskExecutor executor = task.executor(); - AtomicReference region = new AtomicReference<>(); - JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null))); - if (executor.test()) { - new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath()) - .directory(new File("").getAbsoluteFile()).start(); - System.exit(0); - } - JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent())); - } catch (IOException ex) { - Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex); - } - else { - String url = Launcher.PUBLISH; - if (map != null) - if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName())) - url = map.get(OperatingSystem.CURRENT_OS.getCheckedName()); - else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName())) - url = map.get(OperatingSystem.UNKNOWN.getCheckedName()); - try { - java.awt.Desktop.getDesktop().browse(new URI(url)); - } catch (URISyntaxException | IOException e) { - Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e); - OperatingSystem.setClipboard(url); - MessageBox.show(i18n("update.no_browser")); - } - } - })).start(); - } - - public static class AppDataUpgraderPackGzTask extends Task { - - public static final File BASE_FOLDER = Launcher.HMCL_DIRECTORY; - public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json"); - - public static File getSelf(String ver) { - return new File(BASE_FOLDER, "HMCL-" + ver + ".jar"); - } - - private final URL downloadLink; - private final String newestVersion, hash; - File tempFile; - - public AppDataUpgraderPackGzTask(URL downloadLink, String newestVersion, String hash) throws IOException { - this.downloadLink = downloadLink; - this.newestVersion = newestVersion; - this.hash = hash; - tempFile = File.createTempFile("hmcl", ".pack.gz"); - - setName("Upgrade"); - } - - @Override - public Collection getDependents() { - return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash))); - } - - @Override - public void execute() throws Exception { - HashMap json = new HashMap<>(); - File f = getSelf(newestVersion); - if (!FileUtils.makeDirectory(f.getParentFile())) - throw new IOException("Failed to make directories: " + f.getParent()); - - for (int i = 0; f.exists() && !f.delete(); i++) - f = new File(BASE_FOLDER, "HMCL-" + newestVersion + (i > 0 ? "-" + i : "") + ".jar"); - if (!f.createNewFile()) - throw new IOException("Failed to create new file: " + f); - - try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(f))) { - Pack200.newUnpacker().unpack(new GZIPInputStream(new FileInputStream(tempFile)), jos); - } - json.put("ver", newestVersion); - json.put("loc", f.getAbsolutePath()); - String result = Constants.GSON.toJson(json); - FileUtils.writeText(HMCL_VER_FILE, result); - } - - } - - public static class AppDataUpgraderJarTask extends Task { - - public static final File BASE_FOLDER = OperatingSystem.getWorkingDirectory("hmcl"); - public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json"); - - public static File getSelf(String ver) { - return new File(BASE_FOLDER, "HMCL-" + ver + ".jar"); - } - - private final URL downloadLink; - private final String newestVersion, hash; - File tempFile; - - public AppDataUpgraderJarTask(URL downloadLink, String newestVersion, String hash) throws IOException { - this.downloadLink = downloadLink; - this.newestVersion = newestVersion; - this.hash = hash; - tempFile = File.createTempFile("hmcl", ".jar"); - - setName("Upgrade"); - } - - @Override - public Collection getDependents() { - return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash))); - } - - @Override - public void execute() throws Exception { - HashMap json = new HashMap<>(); - File f = getSelf(newestVersion); - FileUtils.copyFile(tempFile, f); - json.put("ver", newestVersion); - json.put("loc", f.getAbsolutePath()); - String result = Constants.GSON.toJson(json); - FileUtils.writeText(HMCL_VER_FILE, result); - } - - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IUpgrader.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IUpgrader.java deleted file mode 100644 index c4e3a12b7..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IUpgrader.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 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; - -import java.util.List; - -/** - * - * @author huangyuhui - */ -public abstract class IUpgrader { - - /** - * Paring arguments to decide on whether the upgrade is needed. - * - * @param nowVersion now launcher version - * @param args Application CommandLine Arguments - */ - public abstract void parseArguments(VersionNumber nowVersion, List args); - - /** - * Just download the new app. - * - * @param checker Should be VersionChecker - * @param version the newest version - */ - public abstract void download(UpdateChecker checker, VersionNumber version); -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/NewFileUpgrader.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/NewFileUpgrader.java deleted file mode 100644 index d878fe963..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/NewFileUpgrader.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 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 com.jfoenix.concurrency.JFXUtilities; -import javafx.scene.layout.Region; - -import org.jackhuang.hmcl.task.FileDownloadTask; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.task.TaskExecutor; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.util.Logging; -import org.jackhuang.hmcl.util.VersionNumber; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -/** - * - * @author huangyuhui - */ -public class NewFileUpgrader extends IUpgrader { - - @Override - public void parseArguments(VersionNumber nowVersion, List args) { - int i = args.indexOf("--removeOldLauncher"); - if (i != -1 && i < args.size() - 1) { - File f = new File(args.get(i + 1)); - if (f.exists()) - f.deleteOnExit(); - } - } - - @Override - public void download(UpdateChecker checker, VersionNumber version) { - URL url = requestDownloadLink(); - if (url == null) return; - File newf = new File(url.getFile()); - Controllers.dialog(i18n("message.downloading")); - Task task = new FileDownloadTask(url, newf); - TaskExecutor executor = task.executor(); - AtomicReference region = new AtomicReference<>(); - JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null))); - if (executor.test()) { - try { - new ProcessBuilder(newf.getCanonicalPath(), "--removeOldLauncher", getRealPath()) - .directory(new File("").getAbsoluteFile()) - .start(); - } catch (IOException ex) { - Logging.LOG.log(Level.SEVERE, "Failed to start new app", ex); - } - System.exit(0); - } - JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent())); - } - - private static String getRealPath() { - String realPath = NewFileUpgrader.class.getClassLoader().getResource("").getFile(); - File file = new File(realPath); - realPath = file.getAbsolutePath(); - try { - realPath = java.net.URLDecoder.decode(realPath, UTF_8.name()); - } catch (Exception e) { - e.printStackTrace(); - } - return realPath; - } - - private URL requestDownloadLink() { - return null; - } - -} - diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java deleted file mode 100644 index fb44d4f2c..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 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 com.google.gson.JsonSyntaxException; -import org.jackhuang.hmcl.Launcher; -import org.jackhuang.hmcl.event.Event; -import org.jackhuang.hmcl.event.EventBus; -import org.jackhuang.hmcl.event.OutOfDateEvent; -import org.jackhuang.hmcl.task.GetTask; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.task.TaskResult; -import org.jackhuang.hmcl.ui.construct.MessageBox; -import org.jackhuang.hmcl.util.Constants; -import org.jackhuang.hmcl.util.NetworkUtils; -import org.jackhuang.hmcl.util.VersionNumber; -import static org.jackhuang.hmcl.util.Logging.LOG; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.logging.Level; - -/** - * - * @author huangyuhui - */ -public final class UpdateChecker { - - private volatile boolean outOfDate = false; - private final VersionNumber base; - private String versionString; - private Map download_link = null; - - public UpdateChecker(VersionNumber base) { - this.base = base; - } - - private VersionNumber value; - - public boolean isOutOfDate() { - return outOfDate; - } - - /** - * 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. - */ - public TaskResult process(final boolean showMessage) { - return new TaskResult() { - GetTask http = new GetTask(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update.php?version=" + Launcher.VERSION)); - - @Override - public Collection getDependents() { - return value == null ? Collections.singleton(http) : Collections.emptyList(); - } - - @Override - public void execute() throws Exception { - if (isDevelopmentVersion(Launcher.VERSION)) { - LOG.info("Current version is a development version, skip updating"); - return; - } - - if (value == null) { - versionString = http.getResult(); - value = VersionNumber.asVersion(versionString); - } - - if (value == null) { - LOG.warning("Unable to check update..."); - if (showMessage) - MessageBox.show(i18n("update.failed")); - } else if (base.compareTo(value) < 0) - outOfDate = true; - if (outOfDate) - setResult(value); - } - - @Override - public String getId() { - return "update_checker.process"; - } - }; - } - - private boolean isDevelopmentVersion(String version) { - return version.contains("@") || // eg. @HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@ - version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT - } - - /** - * Get the cached newest version number, use "process" method to - * download! - * - * @return the newest version number - * - * @see #process(boolean) - */ - public VersionNumber getNewVersion() { - return value; - } - - /** - * Get the download links. - * - * @return a JSON, which contains the server response. - */ - public synchronized TaskResult> requestDownloadLink() { - return new TaskResult>() { - @Override - public void execute() { - if (download_link == null) { - try { - download_link = Constants.GSON.>fromJson(NetworkUtils.doGet(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update_link.php")), Map.class); - } catch (JsonSyntaxException | IOException e) { - LOG.log(Level.SEVERE, "Failed to get update link.", e); - } - } - setResult(download_link); - } - - @Override - public String getId() { - return "update_checker.request_download_link"; - } - }; - } - - public static final String REQUEST_DOWNLOAD_LINK_ID = "update_checker.request_download_link"; - - public void checkOutdate() { - if (outOfDate) - if (EventBus.EVENT_BUS.fireEvent(new OutOfDateEvent(this, getNewVersion())) != Event.Result.DENY) { - Launcher.UPGRADER.download(this, getNewVersion()); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index 33e9eb02e..21349498c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -104,8 +104,9 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler { if (checkThrowable(e)) { Platform.runLater(() -> new CrashWindow(text).show()); - if (!Launcher.UPDATE_CHECKER.isOutOfDate()) + if (true /* UPDATE: current version is not outdated */) { reportToServer(text); + } } } catch (Throwable handlingException) { LOG.log(Level.SEVERE, "Unable to handle uncaught exception", handlingException); From a88c8ed85f9bd58bb78e39d9d592291ea8606156 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 29 Jul 2018 19:03:49 +0800 Subject: [PATCH 02/32] Extract class Metadata --- .../java/org/jackhuang/hmcl/Launcher.java | 9 +---- .../java/org/jackhuang/hmcl/Metadata.java | 33 +++++++++++++++++++ .../jackhuang/hmcl/game/HMCLGameLauncher.java | 6 ++-- .../hmcl/setting/VersionSetting.java | 7 ++-- .../org/jackhuang/hmcl/ui/Controllers.java | 5 +-- .../org/jackhuang/hmcl/ui/CrashWindow.java | 4 +-- .../jackhuang/hmcl/util/CrashReporter.java | 7 ++-- 7 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 79374cc86..1bfe196fc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -79,7 +79,7 @@ public final class Launcher extends Application { // NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher"); Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER; - LOG.info("*** " + TITLE + " ***"); + LOG.info("*** " + Metadata.TITLE + " ***"); LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION); LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor")); LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor")); @@ -143,12 +143,5 @@ public final class Launcher extends Application { public static final File HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl"); public static final File LOG_DIRECTORY = new File(Launcher.HMCL_DIRECTORY, "logs"); - public static final String VERSION = System.getProperty("hmcl.version.override", "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"); - public static final String NAME = "HMCL"; - public static final String TITLE = NAME + " " + VERSION; public static final CrashReporter CRASH_REPORTER = new CrashReporter(); - - public static final String UPDATE_SERVER = "https://www.huangyuhui.net"; - public static final String CONTACT = UPDATE_SERVER + "/hmcl.php"; - public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html"; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java new file mode 100644 index 000000000..c0a683140 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -0,0 +1,33 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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; + +/** + * Stores metadata about this application. + */ +public final class Metadata { + private Metadata() {} + + public static final String VERSION = System.getProperty("hmcl.version.override", "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"); + public static final String NAME = "HMCL"; + public static final String TITLE = NAME + " " + VERSION; + + public static final String UPDATE_SERVER_URL = "https://www.huangyuhui.net"; + public static final String CONTACT_URL = UPDATE_SERVER_URL + "/hmcl.php"; + public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html"; +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java index 5b793b618..bf7bf4036 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.game; -import org.jackhuang.hmcl.Launcher; +import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.launch.DefaultLauncher; import org.jackhuang.hmcl.launch.ProcessListener; @@ -46,8 +46,8 @@ public final class HMCLGameLauncher extends DefaultLauncher { @Override protected Map getConfigurations() { Map res = super.getConfigurations(); - res.put("${launcher_name}", Launcher.NAME); - res.put("${launcher_version}", Launcher.VERSION); + res.put("${launcher_name}", Metadata.NAME); + res.put("${launcher_version}", Metadata.VERSION); return res; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java index 061639a6e..93aa40676 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -19,7 +19,8 @@ package org.jackhuang.hmcl.setting; import com.google.gson.*; import javafx.beans.InvalidationListener; -import org.jackhuang.hmcl.Launcher; + +import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.game.LaunchOptions; import org.jackhuang.hmcl.util.*; @@ -503,8 +504,8 @@ public final class VersionSetting { return new LaunchOptions.Builder() .setGameDir(gameDir) .setJava(javaVersion) - .setVersionName(Launcher.TITLE) - .setProfileName(Launcher.TITLE) + .setVersionName(Metadata.TITLE) + .setProfileName(Metadata.TITLE) .setMinecraftArgs(getMinecraftArgs()) .setJavaArgs(getJavaArgs()) .setMaxMemory(getMaxMemory()) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 99fb168ba..504a24ec1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -23,6 +23,7 @@ import javafx.scene.image.Image; import javafx.scene.layout.Region; import javafx.stage.Stage; import org.jackhuang.hmcl.Launcher; +import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; @@ -98,7 +99,7 @@ public final class Controllers { stage.setOnCloseRequest(e -> Launcher.stopApplication()); - decorator = new Decorator(stage, getMainPage(), Launcher.TITLE, false, true); + decorator = new Decorator(stage, getMainPage(), Metadata.TITLE, false, true); decorator.showPage(null); leftPaneController = new LeftPaneController(decorator.getLeftPane()); @@ -115,7 +116,7 @@ public final class Controllers { stage.setMaxHeight(521); stage.getIcons().add(new Image("/assets/img/icon.png")); - stage.setTitle(Launcher.TITLE); + stage.setTitle(Metadata.TITLE); } public static void dialog(Region content) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java index 20137f15a..78e13b930 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java @@ -30,7 +30,7 @@ import javafx.stage.Stage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import org.jackhuang.hmcl.Launcher; +import org.jackhuang.hmcl.Metadata; /** * @author huangyuhui @@ -51,7 +51,7 @@ public class CrashWindow extends Stage { Button btnContact = new Button(); btnContact.setText(i18n("launcher.contact")); - btnContact.setOnMouseClicked(event -> FXUtils.openLink(Launcher.CONTACT)); + btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL)); HBox box = new HBox(); box.setStyle("-fx-padding: 8px;"); box.getChildren().add(btnContact); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index 21349498c..24f84e93b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -18,7 +18,8 @@ package org.jackhuang.hmcl.util; import javafx.application.Platform; -import org.jackhuang.hmcl.Launcher; + +import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.ui.CrashWindow; import org.jackhuang.hmcl.ui.construct.MessageBox; import static java.util.Collections.newSetFromMap; @@ -90,7 +91,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler { CAUGHT_EXCEPTIONS.add(stackTrace); String text = "---- Hello Minecraft! Crash Report ----\n" + - " Version: " + Launcher.VERSION + "\n" + + " Version: " + Metadata.VERSION + "\n" + " Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" + " Thread: " + t.toString() + "\n" + "\n Content: \n " + @@ -117,7 +118,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler { Thread t = new Thread(() -> { HashMap map = new HashMap<>(); map.put("crash_report", text); - map.put("version", Launcher.VERSION); + map.put("version", Metadata.VERSION); map.put("log", Logging.getLogs()); try { String response = NetworkUtils.doPost(NetworkUtils.toURL("https://hmcl.huangyuhui.net/hmcl/crash.php"), map); From e1b6c18017793a0f5b05507f655cd05506a69656 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 29 Jul 2018 19:07:48 +0800 Subject: [PATCH 03/32] Remove HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING --- HMCL/build.gradle | 3 --- HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 8e8fafb67..9cb2c579b 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -30,9 +30,6 @@ dependencies { task generateSources(type: Sync) { from 'src/main/java' into "$buildDir/generated-src" - filter(ReplaceTokens, tokens: [ - 'HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING': mavenVersion - ]) } compileJava.setSource "$buildDir/generated-src" diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index c0a683140..e5f5d8973 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -17,13 +17,15 @@ */ package org.jackhuang.hmcl; +import java.util.Optional; + /** * Stores metadata about this application. */ public final class Metadata { private Metadata() {} - public static final String VERSION = System.getProperty("hmcl.version.override", "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"); + public static final String VERSION = System.getProperty("hmcl.version.override", Optional.ofNullable(Metadata.class.getPackage().getImplementationVersion()).orElse("@develop@")); public static final String NAME = "HMCL"; public static final String TITLE = NAME + " " + VERSION; From 13e227851f26151bbeeb730af918bebf315dbeb2 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 29 Jul 2018 21:10:04 +0800 Subject: [PATCH 04/32] Refactor IconedItem --- .../jackhuang/hmcl/ui/LeftPaneController.java | 4 ++-- .../hmcl/ui/construct/IconedItem.java | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java index 343041dd1..5addd5a91 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java @@ -229,8 +229,8 @@ public final class LeftPaneController { } public void showUpdate() { - launcherSettingsItem.setText(i18n("update.found")); - launcherSettingsItem.setTextFill(Color.RED); + launcherSettingsItem.getLabel().setText(i18n("update.found")); + launcherSettingsItem.getLabel().setTextFill(Color.RED); } private boolean checkedModpack = false; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java index 38ecd9933..7a35595c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/IconedItem.java @@ -21,18 +21,25 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.HBox; -import javafx.scene.paint.Paint; public class IconedItem extends RipplerContainer { + private Label label; + public IconedItem(Node icon, String text) { - super(createHBox(icon, text)); + this(icon); + label.setText(text); } - private static HBox createHBox(Node icon, String text) { + public IconedItem(Node icon) { + super(createHBox(icon)); + label = ((Label) lookup("#label")); + } + + private static HBox createHBox(Node icon) { HBox hBox = new HBox(); icon.setMouseTransparent(true); - Label textLabel = new Label(text); + Label textLabel = new Label(); textLabel.setId("label"); textLabel.setAlignment(Pos.CENTER); textLabel.setMouseTransparent(true); @@ -42,11 +49,7 @@ public class IconedItem extends RipplerContainer { return hBox; } - public void setText(String text) { - ((Label) lookup("#label")).setText(text); - } - - public void setTextFill(Paint paint) { - ((Label) lookup("#label")).setTextFill(paint); + public Label getLabel() { + return label; } } From 1332517a9f93214b84e4d855012d71a7b9b49379 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 30 Jul 2018 23:12:38 +0800 Subject: [PATCH 05/32] Rewrite upgrade feature --- .../java/org/jackhuang/hmcl/Launcher.java | 20 +- .../main/java/org/jackhuang/hmcl/Main.java | 7 +- .../jackhuang/hmcl/event/OutOfDateEvent.java | 53 ---- .../org/jackhuang/hmcl/ui/Controllers.java | 6 - .../org/jackhuang/hmcl/ui/CrashWindow.java | 3 +- .../jackhuang/hmcl/ui/LeftPaneController.java | 24 +- .../org/jackhuang/hmcl/ui/SettingsPage.java | 55 ++-- .../hmcl/upgrade/ExecutableHeaderHelper.java | 124 +++++++++ .../hmcl/upgrade/LocalRepository.java | 114 +++++++++ .../jackhuang/hmcl/upgrade/LocalVersion.java | 96 +++++++ .../jackhuang/hmcl/upgrade/RemoteVersion.java | 71 ++++++ .../jackhuang/hmcl/upgrade/UpdateChecker.java | 98 ++++++++ .../jackhuang/hmcl/upgrade/UpdateHandler.java | 237 ++++++++++++++++++ .../jackhuang/hmcl/util/CrashReporter.java | 4 +- .../resources/assets/lang/I18N.properties | 4 +- .../resources/assets/lang/I18N_zh.properties | 4 +- .../assets/lang/I18N_zh_CN.properties | 4 +- 17 files changed, 820 insertions(+), 104 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/event/OutOfDateEvent.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 1bfe196fc..0fc129d41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl; +import static org.jackhuang.hmcl.util.Lang.thread; import static org.jackhuang.hmcl.util.Logging.LOG; import com.jfoenix.concurrency.JFXUtilities; @@ -26,9 +27,11 @@ import javafx.stage.Stage; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.*; import java.io.File; +import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; @@ -36,6 +39,7 @@ import java.nio.file.Paths; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; public final class Launcher extends Application { @@ -51,15 +55,13 @@ public final class Launcher extends Application { primaryStage.setResizable(false); primaryStage.setScene(Controllers.getScene()); - /* - UPDATE: check update - UPDATE_CHECKER.process(false) - .then(Task.of(Schedulers.javafx(), () -> { - if (UPDATE_CHECKER.isOutOfDate()) - Controllers.showUpdate(); - })) - .start(); - */ + thread(() -> { + try { + UpdateChecker.checkUpdate(); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to check for update", e); + } + }); primaryStage.show(); } catch (Throwable e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java index 596d46911..50a188970 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -35,17 +35,20 @@ import javax.net.ssl.X509TrustManager; import javax.swing.JOptionPane; import org.jackhuang.hmcl.setting.ConfigHolder; +import org.jackhuang.hmcl.upgrade.UpdateHandler; public final class Main { public static void main(String[] args) { - /* UPDATE: perform auto-update from local source */ - checkJavaFX(); checkDirectoryPath(); checkDSTRootCAX3(); checkConfigPermission(); + if (UpdateHandler.processArguments(args)) { + return; + } + ConfigHolder.init(); Launcher.main(args); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/event/OutOfDateEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/event/OutOfDateEvent.java deleted file mode 100644 index fe6fbfd4b..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/event/OutOfDateEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 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.event; - -import org.jackhuang.hmcl.util.ToStringBuilder; -import org.jackhuang.hmcl.util.VersionNumber; - -/** - * - * Result: Deny if do not upgrade HMCL. - * - * @author huangyuhui - */ -public final class OutOfDateEvent extends Event { - private final VersionNumber version; - - public OutOfDateEvent(Object source, VersionNumber version) { - super(source); - this.version = version; - } - - public VersionNumber getVersion() { - return version; - } - - @Override - public boolean hasResult() { - return true; - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("source", getSource()) - .append("version", getVersion()) - .toString(); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 504a24ec1..73feac4c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -179,12 +179,6 @@ public final class Controllers { decorator.showPage(node); } - public static void showUpdate() { - if (stage == null) // shut down - return; - getLeftPaneController().showUpdate(); - } - public static boolean isStopped() { return decorator == null; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java index 78e13b930..84a4c07f7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java @@ -31,6 +31,7 @@ import javafx.stage.Stage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.upgrade.UpdateChecker; /** * @author huangyuhui @@ -39,7 +40,7 @@ public class CrashWindow extends Stage { public CrashWindow(String text) { Label lblCrash = new Label(); - if (false/* UPDATE: current version is outdated */) + if (UpdateChecker.isOutdated()) lblCrash.setText(i18n("launcher.crash_out_dated")); else lblCrash.setText(i18n("launcher.crash")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java index 5addd5a91..3b6febb0d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java @@ -54,6 +54,7 @@ import org.jackhuang.hmcl.ui.construct.ClassTitle; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.IconedItem; import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.MappedObservableList; @@ -92,10 +93,20 @@ public final class LeftPaneController { public LeftPaneController(AdvancedListBox leftPane) { this.leftPane = leftPane; - this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), i18n("settings.launcher")), iconedItem -> { - iconedItem.prefWidthProperty().bind(leftPane.widthProperty()); - iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage())); - }); + launcherSettingsItem = new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20)); + + launcherSettingsItem.getLabel().textProperty().bind( + new When(UpdateChecker.outdatedProperty()) + .then(i18n("update.found")) + .otherwise(i18n("settings.launcher"))); + + launcherSettingsItem.getLabel().textFillProperty().bind( + new When(UpdateChecker.outdatedProperty()) + .then(Color.RED) + .otherwise(Color.BLACK)); + + launcherSettingsItem.prefWidthProperty().bind(leftPane.widthProperty()); + launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage())); leftPane .add(new ClassTitle(i18n("account").toUpperCase(), Lang.apply(new JFXButton(), button -> { @@ -228,11 +239,6 @@ public final class LeftPaneController { Platform.runLater(() -> profilePane.getChildren().setAll(list)); } - public void showUpdate() { - launcherSettingsItem.getLabel().setText(i18n("update.found")); - launcherSettingsItem.getLabel().setTextFill(Color.RED); - } - private boolean checkedModpack = false; private static boolean showNewAccount = true; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java index 55f52a8d7..bc13aaaaf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java @@ -20,6 +20,8 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.*; import com.jfoenix.effects.JFXDepthManager; import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.WeakInvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.binding.When; import javafx.beans.property.ObjectProperty; @@ -38,10 +40,14 @@ import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.text.Font; import org.jackhuang.hmcl.setting.*; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.construct.FontComboBox; import org.jackhuang.hmcl.ui.construct.MultiFileItem; import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.upgrade.RemoteVersion; +import org.jackhuang.hmcl.upgrade.UpdateChecker; +import org.jackhuang.hmcl.upgrade.UpdateHandler; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.i18n.Locales; @@ -104,6 +110,8 @@ public final class SettingsPage extends StackPane implements DecoratorPage { private ObjectProperty selectedProxyType; + private InvalidationListener updateListener; + public SettingsPage() { FXUtils.loadFXML(this, "/assets/fxml/setting.fxml"); @@ -193,8 +201,29 @@ public final class SettingsPage extends StackPane implements DecoratorPage { .orElse(i18n("launcher.common_directory.disabled")), config().commonDirectoryProperty(), config().commonDirTypeProperty())); + // ==== Update ==== FXUtils.installTooltip(btnUpdate, i18n("update.tooltip")); - checkUpdate(); + updateListener = any -> { + btnUpdate.setVisible(UpdateChecker.isOutdated()); + + if (UpdateChecker.isOutdated()) { + lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion())); + lblUpdateSub.getStyleClass().setAll("update-label"); + + lblUpdate.setText(i18n("update.found")); + lblUpdate.getStyleClass().setAll("update-label"); + } else { + lblUpdateSub.setText(i18n("update.latest")); + lblUpdateSub.getStyleClass().setAll("subtitle-label"); + + lblUpdate.setText(i18n("update")); + lblUpdate.getStyleClass().setAll(); + } + }; + UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener)); + UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener)); + updateListener.invalidated(null); + // ==== // ==== Background ==== backgroundItem.loadChildren(Collections.singletonList( @@ -236,26 +265,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage { this.title.set(title); } - public void checkUpdate() { - btnUpdate.setVisible(false /* UPDATE: current version is outdated */); - - if (false /* UPDATE: current version is outdated */) { - lblUpdateSub.setText(i18n("update.newest_version", /* UPDATE: latest version number */"")); - lblUpdateSub.getStyleClass().setAll("update-label"); - - lblUpdate.setText(i18n("update.found")); - lblUpdate.getStyleClass().setAll("update-label"); - } else { - lblUpdateSub.setText(i18n("update.latest")); - lblUpdateSub.getStyleClass().setAll("subtitle-label"); - - lblUpdate.setText(i18n("update")); - lblUpdate.getStyleClass().setAll(); - } - } - @FXML private void onUpdate() { - /* UPDATE: Launcher.UPDATE_CHECKER.checkOutdate();*/ + RemoteVersion target = UpdateChecker.getLatestVersion(); + if (target == null) { + return; + } + UpdateHandler.updateFrom(target); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java new file mode 100644 index 000000000..7aee62983 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java @@ -0,0 +1,124 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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 static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.READ; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; +import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Pair.pair; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.jackhuang.hmcl.util.IOUtils; + +/** + * Helper class for adding/removing executable header from HMCL file. + * + * @author yushijinhun + */ +final class ExecutableHeaderHelper { + private ExecutableHeaderHelper() {} + + private static Map suffix2header = mapOf( + pair("exe", "assets/HMCLauncher.exe")); + + private static Optional getSuffix(Path file) { + String filename = file.getFileName().toString(); + int idxDot = filename.lastIndexOf('.'); + if (idxDot < 0) { + return Optional.empty(); + } else { + return Optional.of(filename.substring(idxDot + 1)); + } + } + + private static Optional readHeader(ZipFile zip, String suffix) throws IOException { + String location = suffix2header.get(suffix); + if (location != null) { + ZipEntry entry = zip.getEntry(location); + if (entry != null && !entry.isDirectory()) { + try (InputStream in = zip.getInputStream(entry)) { + return Optional.of(IOUtils.readFullyAsByteArray(in)); + } + } + } + return Optional.empty(); + } + + private static int detectHeaderLength(ZipFile zip, FileChannel channel) throws IOException { + ByteBuffer buf = channel.map(MapMode.READ_ONLY, 0, channel.size()); + suffixLoop: for (String suffix : suffix2header.keySet()) { + Optional header = readHeader(zip, suffix); + if (header.isPresent()) { + buf.rewind(); + for (byte b : header.get()) { + if (!buf.hasRemaining() || b != buf.get()) { + continue suffixLoop; + } + } + return header.get().length; + } + } + return 0; + } + + /** + * Copies the executable and removes its header. + */ + public static void copyWithoutHeader(Path from, Path to) throws IOException { + try ( + FileChannel in = FileChannel.open(from, READ); + FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING); + ZipFile zip = new ZipFile(from.toFile()) + ) { + in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out); + } + } + + /** + * Copies the executable and appends the header according to the suffix. + */ + public static void copyWithHeader(Path from, Path to) throws IOException { + try ( + FileChannel in = FileChannel.open(from, READ); + FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING); + ZipFile zip = new ZipFile(from.toFile()) + ) { + Optional suffix = getSuffix(to); + if (suffix.isPresent()) { + Optional header = readHeader(zip, suffix.get()); + if (header.isPresent()) { + out.write(ByteBuffer.wrap(header.get())); + } + } + + in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java new file mode 100644 index 000000000..95aaa7336 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -0,0 +1,114 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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 static org.jackhuang.hmcl.util.Logging.LOG; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.logging.Level; + +import org.jackhuang.hmcl.Launcher; +import org.jackhuang.hmcl.task.FileDownloadTask; + +/** + * A class used to manage the local HMCL repository. + * + * @author yushijinhun + */ +final class LocalRepository { + private LocalRepository() {} + + private static Path localStorage = Launcher.HMCL_DIRECTORY.toPath().resolve("hmcl.jar"); + + /** + * Gets the current stored executable in local repository. + */ + public static Optional getStored() { + if (!Files.isRegularFile(localStorage)) { + return Optional.empty(); + } + try (JarFile jar = new JarFile(localStorage.toFile())) { + Attributes attributes = jar.getManifest().getMainAttributes(); + String version = Optional.ofNullable(attributes.getValue("Implementation-Version")) + .orElseThrow(() -> new IOException("Missing Implementation-Version")); + return Optional.of(new LocalVersion(version, localStorage)); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to read HMCL jar: " + localStorage, e); + return Optional.empty(); + } + } + + /** + * Creates a task that downloads the given version to local repository. + */ + public static FileDownloadTask downloadFromRemote(RemoteVersion version) { + try { + return new FileDownloadTask(new URL(version.getUrl()), localStorage.toFile(), version.getIntegrityCheck()); + } catch (MalformedURLException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Copies the current HMCL executable to local repository. + */ + public static void downloadFromCurrent() { + Optional current = LocalVersion.current(); + if (current.isPresent()) { + Path currentPath = current.get().getLocation(); + if (!Files.isRegularFile(currentPath)) { + LOG.warning("Failed to download " + current.get() + ", it isn't a file"); + return; + } + if (isSameAsLocalStorage(currentPath)) { + LOG.warning("Trying to download from self, ignored"); + return; + } + LOG.info("Downloading " + current.get()); + try { + Files.createDirectories(localStorage.getParent()); + ExecutableHeaderHelper.copyWithoutHeader(current.get().getLocation(), localStorage); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to download " + current.get(), e); + } + } + } + + /** + * Writes the executable stored in local repository to the given location. + */ + public static void applyTo(Path target) throws IOException { + if (isSameAsLocalStorage(target)) { + throw new IOException("Cannot apply update to self"); + } + + LOG.info("Applying update to " + target); + ExecutableHeaderHelper.copyWithHeader(localStorage, target); + } + + private static boolean isSameAsLocalStorage(Path path) { + return path.toAbsolutePath().equals(localStorage.toAbsolutePath()); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java new file mode 100644 index 000000000..bc9fb5b5d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java @@ -0,0 +1,96 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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 static java.nio.charset.StandardCharsets.UTF_8; +import static org.jackhuang.hmcl.util.Logging.LOG; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.CodeSource; +import java.util.Optional; +import java.util.logging.Level; + +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.Metadata; + +class LocalVersion { + + public static Optional current() { + CodeSource codeSource = Main.class.getProtectionDomain().getCodeSource(); + if (codeSource == null) { + return Optional.empty(); + } + + URL url = codeSource.getLocation(); + if (url == null) { + return Optional.empty(); + } + + String pathString = url.getFile(); + if (pathString.isEmpty()) { + return Optional.empty(); + } + + try { + pathString = URLDecoder.decode(pathString, UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + + Path path; + try { + path = Paths.get(pathString); + } catch (InvalidPathException e) { + LOG.log(Level.WARNING, "Invalid path: " + pathString, e); + return Optional.empty(); + } + + if (!Files.isRegularFile(path)) { + return Optional.empty(); + } + + return Optional.of(new LocalVersion(Metadata.VERSION, path)); + } + + private String version; + private Path location; + + public LocalVersion(String version, Path location) { + this.version = version; + this.location = location; + } + + public String getVersion() { + return version; + } + + public Path getLocation() { + return location; + } + + @Override + public String toString() { + return "[" + version + " at " + location + "]"; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java new file mode 100644 index 000000000..0f5c6e14f --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java @@ -0,0 +1,71 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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 java.util.Optional; + +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; +import org.jackhuang.hmcl.util.JsonUtils; +import org.jackhuang.hmcl.util.NetworkUtils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +public class RemoteVersion { + + public static RemoteVersion fetch(String url) throws IOException { + try { + JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class); + String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing")); + String downloadUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("jar is missing")); + String sha1 = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("jarsha1 is missing")); + return new RemoteVersion(version, downloadUrl, new IntegrityCheck("SHA-1", sha1)); + } catch (JsonParseException e) { + throw new IOException("Malformed response", e); + } + } + + private String version; + private String url; + private IntegrityCheck integrityCheck; + + public RemoteVersion(String version, String url, IntegrityCheck integrityCheck) { + this.version = version; + this.url = url; + this.integrityCheck = integrityCheck; + } + + public String getVersion() { + return version; + } + + public String getUrl() { + return url; + } + + public IntegrityCheck getIntegrityCheck() { + return integrityCheck; + } + + @Override + public String toString() { + return "[" + version + " from " + url + "]"; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java new file mode 100644 index 000000000..6c8adee7d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -0,0 +1,98 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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 static org.jackhuang.hmcl.util.VersionNumber.asVersion; + +import java.io.IOException; + +import org.jackhuang.hmcl.Metadata; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ObservableBooleanValue; + +public final class UpdateChecker { + private UpdateChecker() {} + + private static ObjectProperty latestVersion = new SimpleObjectProperty<>(); + private static StringProperty updateSource = new SimpleStringProperty("http://localhost/hmcl/update_link"); + private static BooleanBinding outdated = Bindings.createBooleanBinding( + () -> { + RemoteVersion latest = latestVersion.get(); + if (latest == null || isDevelopmentVersion(Metadata.VERSION)) { + return false; + } else { + return asVersion(latest.getVersion()).compareTo(asVersion(Metadata.VERSION)) > 0; + } + }, + latestVersion); + + public static RemoteVersion getLatestVersion() { + return latestVersion.get(); + } + + public static ReadOnlyObjectProperty latestVersionProperty() { + return latestVersion; + } + + public static String getUpdateSource() { + return updateSource.get(); + } + + public static void setUpdateSource(String updateSource) { + UpdateChecker.updateSource.set(updateSource); + } + + public static StringProperty updateSourceProperty() { + return updateSource; + } + + public static boolean isOutdated() { + return outdated.get(); + } + + public static ObservableBooleanValue outdatedProperty() { + return outdated; + } + + public static void checkUpdate() throws IOException { + String source = updateSource.get(); + if (source == null) { + return; + } + + RemoteVersion fetched = RemoteVersion.fetch(source); + Platform.runLater(() -> { + if (source.equals(updateSource.get())) { + latestVersion.set(fetched); + } + }); + } + + private static boolean isDevelopmentVersion(String version) { + return version.contains("@") || // eg. @develop@ + version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java new file mode 100644 index 000000000..fb309a5d6 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -0,0 +1,237 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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 static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread; +import static org.jackhuang.hmcl.util.IntVersionNumber.isIntVersionNumber; +import static org.jackhuang.hmcl.util.Lang.thread; +import static org.jackhuang.hmcl.util.Logging.LOG; +import static org.jackhuang.hmcl.util.VersionNumber.asVersion; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; + +import javax.swing.JOptionPane; + +import org.jackhuang.hmcl.Launcher; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.MessageBox; +import org.jackhuang.hmcl.util.JavaVersion; +import org.jackhuang.hmcl.util.StringUtils; + +import javafx.application.Platform; +import javafx.scene.layout.Region; + +public final class UpdateHandler { + private UpdateHandler() {} + + /** + * @return whether to exit + */ + public static boolean processArguments(String[] args) { + if (!isIntVersionNumber(Metadata.VERSION)) { + return false; + } + + if (isNestedApplication()) { + // updated from old versions + try { + performMigration(); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to perform migration", e); + JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE); + } + return true; + } + + if (args.length == 2 && args[0].equals("--apply-to")) { + try { + applyUpdate(Paths.get(args[1])); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to apply update", e); + JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE); + } + return true; + } + + if (isFirstLaunchAfterUpgrade()) { + JOptionPane.showMessageDialog(null, i18n("fatal.migration_requires_manual_reboot"), "Info", JOptionPane.INFORMATION_MESSAGE); + return true; + } + + Optional local = LocalRepository.getStored(); + if (local.isPresent()) { + int difference = asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION)); + + if (difference < 0) { + LocalRepository.downloadFromCurrent(); + + } else if (difference > 0) { + Optional current = LocalVersion.current(); + if (current.isPresent()) { + try { + requestUpdate(local.get().getLocation(), current.get().getLocation()); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to update from local repository", e); + return false; + } + return true; + } else { + return false; + } + } + + } else { + LocalRepository.downloadFromCurrent(); + } + + return false; + } + + private static void requestUpdate(Path updateTo, Path self) throws IOException { + startJava(updateTo, "--apply-to", self.toString()); + } + + private static void applyUpdate(Path target) throws IOException { + LocalRepository.applyTo(target); + startJava(target); + } + + private static void startJava(Path jar, String... appArgs) throws IOException { + List commandline = new ArrayList<>(); + commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath()); + commandline.add("-jar"); + commandline.add(jar.toAbsolutePath().toString()); + for (String arg : appArgs) { + commandline.add(arg); + } + LOG.info("Starting process: " + commandline); + new ProcessBuilder(commandline) + .directory(Paths.get("").toAbsolutePath().toFile()) + .inheritIO() + .start(); + } + + public static void updateFrom(RemoteVersion version) { + checkFxUserThread(); + + Task task = LocalRepository.downloadFromRemote(version); + TaskExecutor executor = task.executor(); + Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null); + thread(() -> { + boolean success = task.test(); + Platform.runLater(() -> dialog.fireEvent(new DialogCloseEvent())); + if (success) { + try { + Optional current = LocalVersion.current(); + Optional stored = LocalRepository.getStored(); + if (!current.isPresent()) { + throw new IOException("Failed to find current HMCL location"); + } + if (!stored.isPresent()) { + throw new IOException("Failed to find local repository, this shouldn't happen"); + } + requestUpdate(stored.get().getLocation(), current.get().getLocation()); + System.exit(0); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to update to " + version, e); + Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageBox.ERROR_MESSAGE)); + return; + } + } else { + Throwable e = task.getLastException(); + LOG.log(Level.WARNING, "Failed to update to " + version, e); + Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE)); + } + }); + } + + // ==== support for old versions === + private static void performMigration() throws IOException { + LOG.info("Migrating from old versions"); + + Path location = getParentApplicationLocation() + .orElseThrow(() -> new IOException("Failed to get parent application location")); + + Optional local = LocalRepository.getStored(); + if (!local.isPresent() || + asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION)) < 0) { + LocalRepository.downloadFromCurrent(); + } + local = LocalRepository.getStored(); + if (!local.isPresent()) { + throw new IOException("Failed to find local repository"); + } + + requestUpdate(local.get().getLocation(), location); + } + + /** + * This method must be called from the main thread. + */ + private static boolean isNestedApplication() { + StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace(); + for (int i = 0; i < stacktrace.length; i++) { + StackTraceElement element = stacktrace[i]; + if (Main.class.getName().equals(element.getClassName())) { + // we've reached the main method + if (i + 1 == stacktrace.length) { + return false; + } else { + return true; + } + } + } + return false; + } + + private static Optional getParentApplicationLocation() { + String command = System.getProperty("sun.java.command"); + if (command != null) { + Path path = Paths.get(command); + if (Files.isRegularFile(path)) { + return Optional.of(path.toAbsolutePath()); + } + } + return Optional.empty(); + } + + private static boolean isFirstLaunchAfterUpgrade() { + Optional currentPath = LocalVersion.current().map(LocalVersion::getLocation); + if (currentPath.isPresent()) { + Path updated = Launcher.HMCL_DIRECTORY.toPath().resolve("HMCL-" + Metadata.VERSION + ".jar"); + if (currentPath.get().toAbsolutePath().equals(updated.toAbsolutePath())) { + return true; + } + } + return false; + } + // ==== +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index 24f84e93b..3b4a2d5f1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -22,6 +22,8 @@ import javafx.application.Platform; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.ui.CrashWindow; import org.jackhuang.hmcl.ui.construct.MessageBox; +import org.jackhuang.hmcl.upgrade.UpdateChecker; + import static java.util.Collections.newSetFromMap; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -105,7 +107,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler { if (checkThrowable(e)) { Platform.runLater(() -> new CrashWindow(text).show()); - if (true /* UPDATE: current version is not outdated */) { + if (!UpdateChecker.isOutdated()) { reportToServer(text); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 5d437c476..ef9c2442f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -94,6 +94,8 @@ extension.sh=Bash shell fatal.missing_javafx=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Java 8 or 10.\nIf you are using OpenJDK, please ensure OpenJFX is included. fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use HMCL, but HMCL will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause HMCL not to function properly.\nPlease upgrade your Java to 8u101 or later to resolve the problem. fatal.config_access_denied=The configuration at "%s" is not readable or writable.\nPlease grant HMCL read and write access to this file and its parent directory. +fatal.migration_requires_manual_reboot=The upgrade is about to be completed. Please reopen HMCL. +fatal.apply_update_failure=We're sorry that HMCL couldn't finish the upgrade because something went wrong.\nBut you can still manually finish the upgrade by downloading HMCL from %s.\nPlease consider reporting this issue to us. folder.config=Configs folder.coremod=Core Mod @@ -309,7 +311,7 @@ settings.type=Version setting type settings.type.special=Specialized version settings(will not affect other versions) update=Update -update.failed=Failed to check for updates. +update.failed=Failed to perform upgrade update.found=Update Available! update.newest_version=Latest version: %s update.latest=This is latest Version. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 2635abde1..9afce9597 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -94,6 +94,8 @@ extension.sh=Bash 腳本 fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。 fatal.missing_dst_root_ca_x3=當前 Java 平台缺少 DST Root CA X3 證書。\n您依然可以使用 HMCL,但將無法連接到部分站點(如使用 Let's Encrypt 證書的站點),這可能會使 HMCL 無法正常工作。\n請將您的 Java 升級到 8u101 以上以解決此問題。 fatal.config_access_denied=HMCL 無法讀取或寫入位於 "%s" 的配置文件。\n請授予 HMCL 對該文件及其父目錄的讀寫權限。 +fatal.migration_requires_manual_reboot=HMCL 即將完成升級,請重新打開 HMCL。 +fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們反饋該問題。 folder.config=配置文件夾 folder.coremod=核心MOD文件夾 @@ -309,7 +311,7 @@ settings.type=版本設置類型 settings.type.special=單獨版本設置(不會影響到其他版本的設定) update=啓動器更新 -update.failed=檢查更新失敗 +update.failed=更新失敗 update.found=發現更新 update.newest_version=最新版本爲:%s update.latest=當前版本爲最新版本 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 1d6f1b651..a870b6e27 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -94,6 +94,8 @@ extension.sh=Bash 脚本 fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。 fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Let's Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。 fatal.config_access_denied=HMCL 无法读取或写入位于 "%s" 的配置文件。\n请授予 HMCL 对该文件及其父目录的读写权限。 +fatal.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。 +fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 HMCL 来完成升级。\n请考虑向我们反馈该问题。 folder.config=配置文件夹 folder.coremod=核心MOD文件夹 @@ -309,7 +311,7 @@ settings.type=版本设置类型 settings.type.special=单独版本设置(不会影响到其他版本的设定) update=启动器更新 -update.failed=检查更新失败 +update.failed=更新失败 update.found=发现更新 update.newest_version=最新版本为:%s update.latest=当前版本为最新版本 From 2663312dfbc28853ba8a633104b788ebdf0b463a Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 13:28:39 +0800 Subject: [PATCH 06/32] Auto-rename after upgrade --- .../jackhuang/hmcl/upgrade/UpdateHandler.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index fb309a5d6..5c414da7a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -32,6 +32,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.swing.JOptionPane; @@ -121,6 +123,23 @@ public final class UpdateHandler { private static void applyUpdate(Path target) throws IOException { LocalRepository.applyTo(target); + + Optional newVersion = LocalRepository.getStored().map(LocalVersion::getVersion); + if (newVersion.isPresent()) { + Optional newFilename = tryRename(target, newVersion.get()); + if (newFilename.isPresent()) { + LOG.info("Move " + target + " to " + newFilename.get()); + try { + Files.move(target, newFilename.get()); + target = newFilename.get(); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to move target", e); + } + } + } else { + LOG.warning("Failed to find local repository"); + } + startJava(target); } @@ -173,6 +192,18 @@ public final class UpdateHandler { }); } + private static Optional tryRename(Path path, String newVersion) { + String filename = path.getFileName().toString(); + Matcher matcher = Pattern.compile("^(?[hH][mM][cC][lL][.-])(?\\d+(?:\\.\\d+)*)(?\\.[^.]+)$").matcher(filename); + if (matcher.find()) { + String newFilename = matcher.group("prefix") + newVersion + matcher.group("suffix"); + if (!newFilename.equals(filename)) { + return Optional.of(path.resolveSibling(newFilename)); + } + } + return Optional.empty(); + } + // ==== support for old versions === private static void performMigration() throws IOException { LOG.info("Migrating from old versions"); From 6f7d22518e5bf025b53e7a6a4c9992f598a8996d Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 13:34:17 +0800 Subject: [PATCH 07/32] Remove pack.gz build --- HMCL/build.gradle | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 9cb2c579b..b373f41aa 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -1,9 +1,5 @@ -import org.apache.tools.ant.filters.ReplaceTokens - import java.security.MessageDigest import java.util.jar.JarFile -import java.util.jar.Pack200 -import java.util.zip.GZIPOutputStream if (!hasProperty('mainClass')) { ext.mainClass = 'org.jackhuang.hmcl.Main' @@ -68,29 +64,6 @@ processResources { } } -task makePackGZ(dependsOn: jar) doLast { - ext { - jar.classifier = '' - makeExecutableinjar = jar.archivePath - jar.classifier = '' - makeExecutableoutjar = jar.archivePath - jar.classifier = '' - } - def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length()-4)+".pack.gz") - def os = new GZIPOutputStream(new FileOutputStream(loc)) - Pack200.newPacker().pack new JarFile(makeExecutableinjar), os - os.close() - - def messageDigest = MessageDigest.getInstance("SHA1") - loc.eachByte 1024 * 1024, { byte[] buf, int bytesRead -> - messageDigest.update(buf, 0, bytesRead) - } - def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0') - def fileEx = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length()-4)+".pack.gz.sha1") - if (!fileEx.exists()) fileEx.createNewFile() - fileEx.append sha1Hex -} - task makeExecutable(dependsOn: jar) doLast { ext { jar.classifier = '' @@ -115,5 +88,4 @@ task makeExecutable(dependsOn: jar) doLast { } -build.dependsOn makePackGZ -build.dependsOn makeExecutable \ No newline at end of file +build.dependsOn makeExecutable From afa6eb3badc040c8cfc586952dfbcb9ba750437c Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 13:46:31 +0800 Subject: [PATCH 08/32] Refactor build.gradle --- HMCL/build.gradle | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index b373f41aa..1ac671f46 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -1,22 +1,8 @@ import java.security.MessageDigest -import java.util.jar.JarFile -if (!hasProperty('mainClass')) { - ext.mainClass = 'org.jackhuang.hmcl.Main' -} - -def buildnumber = System.getenv("TRAVIS_BUILD_NUMBER") -if (buildnumber == null) - buildnumber = System.getenv("BUILD_NUMBER") -if (buildnumber == null) - buildnumber = "SNAPSHOT" - -def versionroot = System.getenv("VERSION_ROOT") -if (versionroot == null) - versionroot = "3.1" - -String mavenVersion = versionroot + '.' + buildnumber -version = mavenVersion +def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT" +def versionroot = System.getenv("VERSION_ROOT") ?: "3.1" +version = versionroot + '.' + buildnumber dependencies { compile project(":HMCLCore") @@ -41,9 +27,9 @@ jar { manifest { attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.', - 'Main-Class': mainClass, - 'Multi-Release': "true", - 'Implementation-Version': version + 'Main-Class': 'org.jackhuang.hmcl.Main', + 'Multi-Release': 'true', + 'Implementation-Version': version } doLast { From 8192ffec035f83780859faca16bd299c3f88048f Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 13:59:48 +0800 Subject: [PATCH 09/32] Remove duplicated HMCLauncher.exe --- HMCL/HMCLauncher.exe | Bin 92160 -> 0 bytes HMCL/build.gradle | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 HMCL/HMCLauncher.exe diff --git a/HMCL/HMCLauncher.exe b/HMCL/HMCLauncher.exe deleted file mode 100644 index 0dab2c5ca70d15280e5bc00c923aaf6dc9b2c07f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92160 zcmeFaeSB0!mN$O;B}o%HbOQ+lj1na%8pUXX5*xGwbchbY4v`KmB*76fJuNy2_Xbn~ z$=pP8DelP5=&ZY=jx4&%jLtK-I=nceb`neiGQ7G91CCKqD|SZ1iy>&t{e4f}?hw?O z-|q9*^T)y`_tveKQ>RXyI(6#Qsp{gpH%T^0lI-|13`yFFH~sU8=Rbb9Es`{B+^fT+ zSBCug@|~7Bf4=;#@Q+t!FJJMKAFa6O{_K12dEkMc$k{(!n!Q4KAp6G;WY4;@H2eOa zELl1!EiE-C4!X}V>EoS0_H9r6Ilp{q`+wrw`Q^*oo5cG+w?B*bY3IxxzZLIU@wb1+ zCVam%{K)p#@ZS03g6$vSz14O5j#tI|jqSTc{JY}4)iq-Web4*xyXBzfBgY(IrXQ*IA_H-%1+%xe4jM!4Vyjlw+bc zICdaBNnW~I##ilrJP5CY_>1F>kocD^NsA|~SaOeik0jl_7*QSg`yu|`#^0cSJ`iM* znN8X>5(xoRoQn5N7bAdt(Mc;-uDBOTqOEANl!*`Dck=lrtysG3C&)-`mp}u7wNxuUYh<`Pd{RSHDPsP!o;|^c_3m>Yx;8bPJ2tevgntC)4Y6(l z5k$d2L^2Bwlo}pn2l#K3B`L_Gl-CojV8wlvo^gdEK0*6jtF-{I|*Z&v&5H8;9;q^P}<Ib zi%?HPRCe9xd^wepqX%tGn*C+9&FZajD0?a^Sj)olX1f$_v_b?A`KwRjpT7xQ6o=k$ z@EgMjgvVH{lIOVghd<$~be{Hf6z|KQ=6Hrtq}IrA3Yy3%H%2?JL=33b(1FyFl7QZD z0s*{^(yK&oc-#D>!hw>K65|h4g^}R^1lB;YKy{~QPousYVRpVD>Z)%-ParD_pF~`n zcw)AY0JRGdfwBY;yFkO$P>TY3!#+w2kAdWi;7jC8WHj-0HIyM^u!QN9m4A*$2$XU) zkf^x4;VFFU`bIpfv<7;`fwVO|ix1*d-T@({F71!ZpF_4!u-1hAeHu-Y1S#;ZZ~6cw+Y z--k+d5u?6k4e0tZr14ffUL}yf zS;c+v)~g@&^J2uX2I`X9*F<3^m?2_uZI6arvzj!K7E3nspf&E<5h>(-jXKoz)7wCW z1nDL8(FT#5&uoz-a};Nh4DdutgJm@C*=Y>=xG?ym9eu&}2iVtqnk_#5hk_kS*?zte z0d*~i+KRD|T!ztRWaJPZ9rV7dB-_3&DT^IzIIK8~jBLclvP!_0>-&IBl597@P13hzuhm>tcl7`Ts}g4*+yALTd(zs9>3= zr$jG4sVQeQrI)t?jmRwB-xv(=ORoe5yPbN?+n%Ui{I*)t>v#cK8%tgY1|!*c1Ygko zCtWlBCj&tvBNK=2o?hGgc4~EL z8};nR<;`vhvmIs?Z@ELB5G>=5P*%T#eH}^0jCJODb+H?AyRyxfPLi!kb0nq(6f42R zc;0iI6+842YZbGoP1Y)Ji}Gde-b3fTcc!npN?nIh_&!GAxS&3Hh<2wtXef?S-ZfS* zq$gUX*)OYzjYMQ)d7q*5m0%Vengj%TSz9FyJWM~Lo*-r&cC1W{qqT0WrVlW0QVqw- zOy=)nUq6q3+Dv!bOo3!4@f&N~1+uSG+f%$vuGvkVJ($PUs3ktY)pn!qJcQ3WJ$;C4 z_QATZNOfCrxjtij#PBwUT!l>meg`^)Cde6j(RlTP4y~XsHoW1mJaitHZ%|{;m=Ja+Vx?qX^*(IndzqRKpXfFpCs*zr0=|gW>NWdWNX&^UF?*mba;*f zxAJbS!rfLV(AUa&l{SpEYvp}y{%-WXbTM^Vje`iR&vtA6lT5NTd5^Bj%P&lqb61Y` z?8$9uEOxN{AZdUXqA=jbMyS1xm1#u06)BKx4-t9hcd@;CXrR~pTsfQj)}iwy^N?X1 zGEmon$hR@nvERrmrbqTs3eWA|*%iL3J@1cI`XdXt9 z0J<~@0|XsfL82pwGHkY+-K^XFM7aQXZrRO9pPf!J$|)l2&WrvS!Ge8i)A-!edSRd1 zJ8o45J6#gw&w&v`p|;tASd|qm{KZ>AT(C)yonpmKR+!F$?%ZZ?`>GqL$)$Q$Y8j}& z`q7+Qb^DM2P}A*Y0lpM!J?a!}Sc!45FgCITX{~;zWGK!O5W)chR+M980lM1d+({f& zDyl)#*Sd4tz*|2(N`xs)w;j-mdr=5{#;2gc#Bpa$j#J+wu>(7_WZRq0Jm%xz{L_9RAgwqCX7tV65mD$&y=ZGNAgeuFl@ zcf~yBEerBwv=1`IP`V9FkiCh8=c%~=Rq{$qMg7xcP{st?63D-H7PfN%lc zR(t`JV5YT|nuoQRG;~Wm3v}TrSU?k#l59PMNitdep<80z>JQT;_Ejh$XX9eF5^|O` zgY$&=1LqOhO&O^9d&}zji4k4vcM|8#Ob-&Jn-L}vx7YAZ7lO?xxn}Q;52P#fg7IM0 z!sY;%p(P6H))IABk5@nK&}I{L_sBy_c#u+AFQ{t}QCGTcJs8Qopl%s&^^pF70RyRh zE64?*vf|n}&6sUq$OZ(LHLFpH4(GfHA~&nkNdm)OqHGk$bAnya0OInlY_apzCL1m6L9ca3b<4 z!Pa1JbA)R(-89G#1KV#6@&L&EKON*f7!QK_>xuf`9^^+U^?L?6q>>v0eT5giE5`Ux zRE_@m#|(RwVAyJc8Hzc|Fv8ykSqBYoux1@%__F*sYaSAL zyW7o9Lw??hGp@C4U;3*4(lY6Wif zAu+KG@f@zw(hFj#xdm=;NDW## z(vmSN{1TJ#zRni`m!<~b#7^-EfYz4$+y_3)WMXuw_t+)Yq~7C@+6wG1BEHQ+skHh4 zCVHX0Ex8`wYPO^z<&)RSSS>SfxIOpNI?M%PAu!ftLyOvIuk^Cuy#FuE)1qw=!jAM#SD zW#L`9K8brETzCf_0iFcqfL8Cy9sC%@PC{W+I5d3K)yQ}hHH3IKCB*3QCOw|RBf!5v zjUoOL!T_JdPv3@=4fygzTOujkKxiM|ZGQ85D9!wSd<1wla16CX(!(7f9KQl6MJ#+f z8ib{atNu1{>y5`7%ReRS0FNNL(SKsIyby+_Jq3_6^_L+7kiA_?qsZ`GANZv3G(7ki zA3zYPhGPYck!9>8Q%5JqBRrLIO{84^3@o7=B~f7-IL-Sl^5o;N{2^!B*w8VIaj}_UFX=IR}u$h@0#!dWm0Rg!TG<5BV2Ka{{ zZlEj-DafR5J_TujxEM>3B>oJtm!bVWswWgjoVCWLu;MOeVVJNwF!pb{nNnNW zQqIqzUX-3#Tl1-e@^_+qqLjabj|Fl>{GVbH4hrCg(!q<6X&-+MRdhChFknLBZj8pZ zl%6ifJ&HlT9G6!$-2p$ha1P@DRHcH8Lh67%(!X;w5;tt}6%4kBsKCUJ`th z6tRZ>eD#(Ep#n)uyNjL~0eW6Khn`c4=sA5Bo>dz&mWx-~67jm!CtgX@#B0J-@w#p@ zUQa!i6vm7Br7erZ+rDKn-n!qhWf^@poalpmNZPufK(gTfW=h<$1Yd1Q3-Bbo+maTE zR8T7^91lh83+{_bZMDnrZ3Qcs-%N~IZB9}zzMDEEmS)Eortk0xVxSK|K~mE@bx;+g zjEktQuSi63XCL&5{Vem6GFlkO|!MTb`+%RC7HH^2qfRvJ<)dB`;I&Yn!ooB=ZaJ| z!(p4@^tP{Xs?8H&RQn93)bbYA96Qf7G4vJNjnS4Rn=?U@;Ona~PFTPmvwIK9Zqg9C zT`vv8`0KtgTG6u2QX$$?e<8 zBYpnD{FDn}4*Y?jj;@?71BKq8+=5J4$tK7IH4qaJlt8S8-=Okl1d5n(m90sHjRozZ zE{-Jed@#noh$B383M#9{2qLkxv-oZ-^z+e9pQMy;o`zQ)G);^PG*p?hYp8{i8#5^x zdZ1$$rR6pSpgn?lgp9gMU7aJXxja5}w^?H2plUP7xVotT8+AS2#F}Wl$1~g*<+q`7 zA~rvX44{dtmZ%O(^(H}Ur&AwDa&m2rQ&L9I1oB^?5WNq8g+ie?;RU4_n1`sO zelQLhjh(04&uYx(v=%YpPj!ajVo~3D! z?2!-;b2=~({JkC`c%P9`gwGo&Fv|?+{w`$liA>#kai0h441b@eiKuQ4z+l-Mb3gI@ z5rzQ6@E(xu4f~ZV`Oi@=Yxf>dQg+>m#Mm&zrlt*_ElEus?xF3P7`o>URcd0)Rac4?cK&JN%Ze}5k)PzmD0Dp@ZP{55qjAg5T6&_lOzBt(@i^f{Q#?Y0{PWcj z8-D{#(q_-uXN8(81Xu)fOu6{97}?z-|5)fiXtRD@Mj$pdq#f78vJ_>;A2z(+NmpkE65uTg-Wbc5%#! z15|ZlvU8x(djTPI!CpLz46uKq>V5_566(Si3M~Nm9zVCp$*vv#u9z*7xZXeE_ep&G6=f>N)z$K_>i<%Aa?~rio`|-D{m_0)1gU#4 zIGI#8_1=~DcPJCo)ozvv zunoK+wUh>5Z}${Pf24=^%*xC%c58zn4}o0=nw(_3tG3#`XI6ZMl^tb0NLlG|an`hN zXEi3|eBhI4w{73PaBaR%T4G>*j$WCSYFTohWzfh&Q%q%%A!Iw$`0{Kwi3x z`T3L|Kd|(6AjA9}Q)VxHKmng%V46ks@)Md(tflQk+lE3s|G=S@I+dA#GNJo6!! zdA4Vd%W@D_OKpBPN%P`vwIf;WO;#ptCbFsR4zr*QxwlYEPfNBuLT%4BqaoJKXirp( z5Rxji14V2r^ny%tek*OP6d0Gz?ab4I#7p z4JDIS$^0eS#)a+>k7h#V3iwk?>uMb1(b>v~oyH`Lvtp-aK{s`h+l9Kc9g60@J*Xj$ zM!Tm;^B-jXOwIqcz~U(LXKCnqe>SvMb?Hfz;rQ#4w52Dt`*{MTcHQ4@q7>gM#vl|8 zfwlH5cMJo1K1B3kR|UIL7y`P#o7R;^o-w}<3t3DsYE73Uk6ywqrA9JzQM+baqKU8A z5S9Rs`6nT2O2{_k#3r?a-RWe9SfyJl?rZ2-ebaqS>1xzrYf;Y|^6jj~t@8BS^H=vF zV@)bPy;Bidn}k#)h0X7qD@r|d800R1{RvHXd&``w+}^#boPbnWVQUdJ@Knk;Y&%JwawogLkKNm6bWka*?J?5P6D7d5YzY&C9XnMR%63p4`ncj0)->qj zi%AUnoy=!fJHDZQDzGnMut98?eINjv>+tO7Ral2aq@YoqZd5x7 ztlOw|)3p9BR6ccgHnc$!jm1vV#k65up*^jl9Br|}Le{L%3V-K?2&Lwh_EA%c9G+q) z-`kC87NntuRmsDV(B=~kFyO=3vREWyL6osPQ#@hNMA&SC2#Qt=H(P-S(y^4GXc;KS zw?OrZfQsgS-QO1o6R&d%<~((G76cIW$GutXzHFldwIx_b?Q)pJVP5bIj7Z)#+l28G(S9WQHI> z(~NQlE6RfTXeKMM!z4G3w9R>FF2^!5vCfNK>4{GDJ4r0`Sln_Z^JOvz%D9-Nge}L; z25+W~n|F8`suydT{)Uk;rn!3AtS z3^x|`pxxGt5tGJlcd+zP%$aK2Z)=LPiaCqUn>n*XzN~I)@tt^&insW3eiQ0Z7iUVH ze}XB4Po%HanbKMZzZ5~E2nONuma-s|bKDHOd2CIN6Ef+Lt<~FK4et!sPalV%RnKR` z!Uzqx?tCpMv2vvA)d?g)Ev}#b46&Ubp(hbV;@`4z$eHIbM>;DiTUeW?^*7{tqzt=I zpab$Zo>y|Izd^}!Z6uOi4Np_J)eMue<9hW|>&i52+;rOYYP*)Aud^e{wsQCx*VFXTlTCsAHr~WL0`@EHQSBPKhtLyef}g{Uzb<2L$7vr{yq-6SFi5XZ>L)R zF>D|>2sV8Uf=!mtNZ>NINL9ROJ9TF(&qqNyL#TeX5<;yPe3uZUX7Nm z1I64i?t1lu?^5@J@1riW>yxKIZR;>QN*=i(xiRH-wbi1wSa=3F z;6s~{^Mojtqt8O=P4t+9zXkYPhQBzEY$9NT!y5`Mjk7KB7|@u~&A1JP*2dY^L|%RJ zFnyX!)LLS)Uz{T!M<2y8O=s@{k=(r-3XR6u#@svA&!DV|B4oy5hkz>5)z9tf7xr(? zJl_YDJAm4zZajMN=eWryW#KPT=Y~RC<80d?+{E|Ac$s<3xQp6yu8oF*?7DsKozy1t zQ#oZ0sTJY6&ufuf_XR!CUCH>H>&7FPjYmZu9#uX(9thx3y%>+R%g?yhrQq*QWLsB; zZ0jnKZQYNMZQW0hZQUwlTeog`T_?dj*HjC!q-;X-SBzIb&sM)ciGgb3fYi?Hb4?wT z@4a(Pu<96c21o+}v1Ba;FD&Zk7HW#xX;DtasS>kc7G6u>CffhTN3=8Ox#Z6EJ%a00>g1#3jXdS1}#Gr+Jg8hA07_`@Tgvl$J*sc z_z~jJK0H<-4(-#o*8-~k20iakwg9(EbnRzJkl>f=ze1$+-G=Jb7}VK!C?8|>E$6W& zZ~`RwC~dUbX2*KLY*D-Vfz9Oh!r8Yg=i=ytClefyXwR!yK1sO;bN808W+qAv*)Xbc zv?$fYt#TSch75xQD^kvcIy+4>ASpz(>+F(T#9Y97hF)UXU@3sz=6vqn(x4u)l=5v? zfL`4pm}JHnH)BuFGa28QuJ>cL=IPNKtv+8+MYoq= z0$QHU_OsQQ{N!nx-8ASqHGRnC>`8IRDQ*6@+;fD%OOsKh_g zUBvEujT7R%w;u$WYquTnIH&R=j!C=F!R9)}cV$I+=M+RGN}6w!SG%%+xieVS>Nl{* z=+IZXO3r+nbwzE~E zAAWm?XT`nxY^!dc&9drrdmU^KSXrQx!t5P>=?F=B$CMyO(4y|ftS*mbQz(Y*k61`;`89Mv;~l1StGl7cI;h(Y@~;Gj&ivI*Wln6- zhGK(#t$y9_YIqQN>-Wf$V%bDy{ELkSn(}2~GCc-g1uq%wEq)ZGVGQl%zea=rd)A~q zOf8g;y$esEm(40DpSHlsWn_h=n{vMc#ey$F@;?4mIur$s9>2mj*pUp9{AUpD2&eF7 zM8^IG6ApSo7{NyHok-)qMLcXd8&EJ|=Q5k-ZBp!{WvER_YBXts` z#85W}_$G)M5>7P7uA7}rk34$#=s|*L_F{T0H$_lF+`@8ZC~w9FbfuNI4%A69I193w zr1xyM+LjHui@23t=bFX=H~EM98dt|adK~|AO{5yh!x~d&Lp+&Z%IR}WP{1&}A)X<* zNJ96bUO79_0wMok9++)c+Z<4u%ydlHCQ(Q&6)N(u=}~BJa!P<-lZOrzR+b@vOcsb| z5!0eh&juSA@ZXBIl$M#UiN+L)W4-(t^gTNoQolCj(GACyP{c5k_!~$NyIXe8@z{)D zWFj_O9tSwcG!kkfg7JL+oX9uAbG$Sjr;50ko&D9*6Xd_R9*`92VeD4@yMR02c~* zV=u)fYJ4>bn$3q$Va0ANA^A6u2f;GqARLI^D@WXX7Q>b>7*t8xg^?kZ@Z-o)9$L7k z**R#>V(N#qXOZ_I#KQM%_D)57n}t$aB2&XrTF@weqXi8X{3q3=yRp=CvYA*Iy^UGK zSe2>!cLsP1x%H8AexiH_D;9KB;pPT4E?&{5>?x+zdNTYq!POK?Y&BFnmD z*XuJKF&ktu>=4+ghfS*ZX#O3}>RawRmadhb@Xqquk$vqIOoVA2zHStj?`+moJMidW zcQ}|GwvCS1kOGvXO_SEZyoglFI~^oW-KK4$PeQxncD6Qu6g5j6#tVtH@LzP%$_wOy zy}|!Bq`ILTTVMj$bh;I7RWp5)+JVl>k_NOb>`~u zdRBf?t=TOpS2rA&CxGVMgUOKgJTujrC|Qc&RUCkeqUZ&Xp!{S@#J+PW`P8w0CJ_mh zN?n~LDVGvO`5CYq|0UwE0Hk%XR+PnmgyGAFUu&)y)tYPxvs@N@m};Cd*1Xvmd6+&f z!@dj%!Ti@^!DwY|hzsxnbQe={jGN5NiMz*9<10U){3%O)4&Q=?edxP zG^@#M^`DSvx~^MiuSHK0rz|8+S?GY^aBchqW5ru(cj@mT0QI2mR;u6C@B}{9wQifM zVLwXMtu?!66ar%Lo`wJ|-dzp9N3yy$OL8?lj}V^*e1N;;s&4=UejPs9ayOrVH!!il z(hU8ep~qE!Ka$PLlw7JvnUJVtM52-*@k*3bQ9=DJ1o1dl_|$f}fFC4VgAevM&fi4P zPO;-tn0C@2rxImH32`glE9Y&wCXLhz-4%6sTen?^E z0RvuKd=CgJI;)sVu;K8pX)N~Op@D5a#~Vr^#0B%`V8|?v-Gj~}`d42@wJX=41@RG@ z4lNjHSD<lCfZhy2KYT^QJLQf-}w7t zu$vhsF`WEUuoDJ8u;p98#-_QFmJxNgA*I$;|23+_GNFKwEzeX}J0#gpt^~_EeB_yO zHRAJ^JLDVcZWCsrtLttPW~0mFxDVLY%6PbwXr4u@xaX)HkATtYHzP#0JF&Y&GeveO z&-zRd1~UarwBEfdAJUg&3nI%~cy`Ub-ooB>6)c&pwKsLx*_x?mv0@m>f_ApZeHOWE z;mi#*m8r;}wsqUu(2^d>r$9>pLi${I`DB-1e+oPuxA~Rvk7=aMhYvH@0UH`w1!^mJ zq1U^2gf-4So=8md!34xZVH*h@EOzq1xj1 z-n{0$8|kP=Ikrw?=b(-V<-;^53Y7u`!X$!NVPSMjuz{{7m0`xmL@g*PPW2Y4#?H>i zlJ$q2wuc=w0Wl>#IIjebiKTp@U{%=PE%N1fD3c==z0eUbR%UVq92g1(jOxrXu>%~- zOr$IvloG}ui_gYkmIPj+UikH~$Gbo)9cBpSJTm0oRy+ z5-#sp0y}z|9dZ`CJ(FsnIf1(WKk(Lu>9ZYylGx2XDHeIU`cNh~`muQWdc2KQ>BfVZ zLH;*zG&&g`=5{5h-YUr@R1OR12oj)PCC7Y#o?n@%jK_*F06lgNiVE0#w#X6)@~tM2 zSO&6Ohb&k3q}U_ULL6F|sg-xcNyS=Xa8>k?nK`5PH zPcW7{_;|va0hhgJNYDxE@*4=ky4=G3Mg`%Y)$@yPGu}XFvWjT{)1UcDxlp9Xm+{WtV_vPA9mYTc=8`k zub#waWWkD${>pJ}_vb*4lSWK%>Bs^!Fy}n};RnF@9SHW=I5Qct%B~NYA{m?bC*hlxB-Y4)ghh2MwoYo_WXsb-eZBPiS zwgGH5)2`n1C2%j4$1P!L*sp;a;$XGA5%3l`RwQ`~?A1w;H29GTWpGgbtU>vG?(bwy zESRXVcVR-;Ll;|^p4~WG1G%sDjA6^2 zYJb1%1&qn+SN*cxd%Aj(KDk<7^s<``)ry?zBX&vI7q*blnCaje_HK6(YtzTC@Dba# zR?wJ4`xAT7!OY5De+?PPnaJpD`jx(!qJ(-o*s{3 z99jrNR?8;#8N7#^umpY=(}+mkgA9i832cfG>L6Q0niu}@9G_HTZZZl6LcMHRj$>X4 zYES3SirU3C5}b56#Gt1K$911|!IP2e&@09CBxe{&(ju&STQk+ROtn9A#c|9WDV(T6 zGf7Jj8=Oc7qi5gOnpvjrJP1fwU9X0sV5EtnUu43*tHdhfZf3MCiYcpeGeEF$y8UT8 z%CkHy8v$e}iJf{CNwFH1^>opPa9EVET*I=>$aqpHA8;ara|xYjRC~*mDUp_EUyitL5x0LJE-e@wfIw#|03{V$)9@-=Z;6dUh_$Y_&zf-g;LN3cwv*5D@OQ&@&Xrh)wi+v_<9Q}B_!aMimThGRP%^6oOV4o-#~ zF&9I_bD{~4Q#Db{FlONog*)J0cJRnvkBC`6IyOddcqAL1aPSm7vfnd>>REp?VPo25 z!;X)LMD(B#3}=<72InnyyFpI!J;qMr>h}=$4oXy%8xv|#EgBFEXHlVadQb@5L<%k~ z2fI;<`T`|y6(wWIM#e@IOVoKS70RIph2X#(GX>?~lJFI^}} z8?o_f+fER|RBhv+!go=*IrN}V#BSOh<4q;Sj%2IY&RuVoN0>g1j5Q_{LCsfl&}BhT z6#P#9mpiEoLE1egalHpGIjny8wc4Cipyla4yLYeBBiti}jq%^`!nY-0n4KjzD z`~q`0?EQ891Wf4wp+sG%MbzPmCVUsSQ0)nekU3W8)0#azCV&A$xRO&qIR-4Lpx+TV zHwsx;)CYl6bw~&tHw#RhhdugvoTgKCx8pnVhQ`~h9QXV{q(6K)YKc1_#f4`8=P>PY zXNFlh6G?cI*fiu@)HONR8gjB%&bhYwI*3i?d|875mE`7{mmxSun}T!J%MN}gs%8{e zia=}!p&fToic3yU6cQh4Qr$y#!U26%;$-uj`6Ui`*;|m%0@+ns^2a>zcWl^&FhDuU zuV-~V$l81_oGFz9Q|#S-b9ZR_~ZEN!ykqk`7HduwjaIxF5p^=M z+Hn1LySE$jDs2ym0(D&zp&Of165T{Yhos8nA(#+(`P)#NA_3f}f<7WAZscId{lh>?oW6<^;knUUX1*&IRe5F`z!)&qRO}qK*=DN$I|wjJcVsr1R7(q%r!0h<~kI`p*9H*P3$B?Bi^0v`Evz##H&D7 zX!w|vNw-S(Ore^@NdxzhxUvOJ`E?GD0BN6xJnI4h{ty}mFhCEzk6Sl_L0)A>nIAOW z?}IP>u{8p*q(~B$h{1^E_0c%uL+9Z-K};+^po$MVrc_}zmyXpJJ6dNuB^X3fnFTwN za7B!b+WcGvmLXAP#AbI1Oggjeo72VH5t{`|3>K$)8GOIy>8r3_o1>+&qM`ttA}@im z5HxP)KD?QWl;64lh*YuFs-&syR!w}NG7FCN4=^kG67)=C5-uR}J6t>L1u-W-HU8ohG-x==**iL}uDBKva+HP4p46A#go*qHom^1OURnR6;MD3#0zh2zU(-2Rd5D_~K z13KUdn$*_x!9Yz;$(sq$ZwA;P-hO!)xq>?QHe?UsI3iHmya3%tc&Q!sK|B*Co|y|q z{63zc{$$4YV)T-WxcZsVCWfy1ncycj zitUBhdzt=I+FryVr(sxP=I6^p)FzyfQodwwp&wG%Lk7>4P{g#NrBZ&3psq9 z-=LIgSX}j|2$@1M#nR9%w5t!<9j*p4Y3MU@^uV%MvbF$cW1Ze+rH55GcK#EN`Q%$x z-6_wi%hwT>J$3m%qxThc`H#_icwIhFiYh;8D|FNWOson4va7|)5l|^`7Xm+}zz-0p zr@)N5{GZ2x{UQ$R6{`vC4|pqAAR}s%Vq*|Mby5tMYB(Awkb=Ml1dM72I}pKof~+*g zyCJQ@5px?4+Rj z39B72ck*RaowHJ#8oPl8GVOT4L0Pvy8o(j-hfoeuk}y+7A-ZVGwWx}5XqZZ^(#c`1~EsY=$>%OmL5R3qS zei%j)UT9<)-V0Go z0oy&0o#3^D!7|ql8#_Z(x;`>}A#O&JEi>4D{h;-hz>*)c58}dUKU$Jub)}YIPS`~5CQe?VbV1ZXkZ&m(Kd1}NO;QvGg zO&6dWvAG8}p9L!jNCEgbdDN%nd3vx9hKob`_`hPFA$g}~&8*7zD9^-3l<})U01upk z)*=n3ru(5RQHUBU>@Nw8=1!R6_lYq>y@g&Jhx^u8f6N)+Gf@Ekm@I_|*$vN10x-}I zgQqNZs!U&Nj}6CwgJvVd@_a{#PL-WgVvuRL1+ju}dP4}cgkB8s$|P^nCiDsURFP&y zlREKDJc+uCl7#RpZwX)eL!Y#(p8D_+WDZZIPwH2`44=jng2ZnWLDA7yA&4{Sorj=u zZ6=9F?6C83JPgf$IU0!SO+BzMTqiY z*R6YiK`v;Zm`F*wIC5LnsIAOnz>_s!ab9x#+>!A|Vf{=-hk|v6vi1Z~dO?%f-mkGs z0iuZe*dBg;kf^_irns9aHd-9i8+Kt@LmN_wu8LDRDHIh!+9`v9c%nEZCHz7jhf5)p zmz|`%3{}7*3M{9}2!2-^+$C|;g|w-gAP+-OOxF*`n>3o*>Cb{ha1Hnu;-IK+*OT~M zZ;5Y>3CCAu8V)v*;(SDnn|6g25C>zQLSCGv$?zN#{KfVjd5gUjZ~T$)Hl)Thh&?9sZW$>$jB%-gS zrd)%tIHYXk@zY4f4um*e@^Ol2IvV*I+=)Uhyht|pDsWc03W5Wq$X>FUBnI<^o45lq z6^bhKi$N+n!3X@}f&E5i8`^6{3kw`XrTfWc*W%AjDwa zc5u374l*M?t}aqfi9?`LZ=`hL-D#2&`izyB4|JeM@}Jw$4{4P@yf#S zE}VnR-~q5wn=}g4;=AGf6&balyk%F`J;X)iKxPzGUb!cX zr5#D~HL!s9(5y}j84Ob#&%Ykup)C|nbUyHvcst6%hl~$%QQw-9jK7P%!g`!_N2CSp zXSAH?X|m$0O+zQGK>$Eu0Ku@p7! z*&uv7$@;w&7!1fMbnjTT*8(VaUx2b7-fe(_R#RR&`M&c-D%5!jn@bpw(#>ekUg&tV zngAKWuXL)=Tu|VQSqx*6bq`uckbN2Ji1$zg{*hQoV4a3Smrx-Z;n@W6p^E_SK$!u6 z4^#Q?0l4v70QXWMvu)zWDu-TdK!)Miqv_5fZqVfn18-n)_7C!nxHzAAI#k@o%iz4o zOR>t#ge$BAr0VP>3^Pcmz@+bGr`bZg`pG!YSDMnr_Iv(v{S}MzZSt4Q16L@|=_C7bo0}~zfZie3C{YJH4d4V?5-VMw z-Ln@CGM;9(|44D~k#@Mvc#F>}Ljl_B@0On>rzZHWoTo#eM`J$&0&-dz&#@;^4zXs> z;lzf>k-ZEz9-GzB{U~m6OA1aG(nr8iY2FJ=S8=Y}`7JtvD!oBqln03>sB#$?K@3{f zUdk@RvI7@d**!<$*Wl^(bb~1SkG$oD|NASTM(&xN zRnwPfHFW?xV4g3aQcK2SQY%EA2|W~-787XP z7Db0=X?&1*2db%AL72qZE+M&gV=ss9bD-&ugLw?xsvxGnRS4rCKnuSdN*Fm>3;q{- zqR<#G?25w5182n`zXblPLH-GPDah*~V}krctT(aJqWxz~0r~;JYuW6Bq*C`7!ERTR zL%BqJzMg#-CgxT-Ex_-m2_K6;5@%C<_X4Gbh%u(jLJ;R=%%~y}l}BqXk-%>j5w!Bs zrcgA$5)tSKZ3@Lv{o#pSn60;AqT>%@CWhuUDCR>m#?RKSkQ*D!V&^}=pBzFRl$_u*B9PkOv5JJk=)zb!e*}GwX(t=y$Tt$f zjNk>Re-cpTW3e>e4nS>^6Ri`};lD&DAh#njihqJ&s3*w>9<|Ds!(cfvEYgy2^Di#{ zu!J)t$%j4cTZD_F6QSq7y-sjOIy{FLoa^!Wohv50eox(E;&&V#lqP=b&}nh}9EnyB z!f)cm_?gT>21o^`uj#-KdV}2UGF&@utRm*9^4 zcB*ixS(KB0t_35@kOR4U&ox7m;V8aqyFK+5t5O8SZkeIREY*{Hnr%v_Zcm+HKyjGf zhsUbFFng-ksvN;QQk|}EGy41caXuV3%e4Nc7XRc!pyR?8aehyD7~umNOf9ioAE|0$ zCcibI(^3ntSs)pC%mNX29>RIx0xb^54~eI}X3pngGM`K32;Q><@5KRlpM#Ihi5!R8 zXuc{c0&iwQW?_6=6j$AO8ryP83I7F2w**s*3J3}*45EU+5g@7{N)`NIpaMNn0T$d; z1#m6?F0S_s2K8?VP>TtaGyrNw0+av*Tzy5|#(=seP#QBB&fNlzIY7GL68ru728U)l zAhZ90zOnqjneY66GhY^XXrNwkKOlVfBQv!k9PW3tN&GV(DQ{XMsh0?Z+I&US6$n;o zMa#siaRtOALh7?~hz|!%MoHqn#P3WYc0LNV(hQfFq=e)U7bOh?1k%r6_!|8z&L0WN z;mnPLIqJ?6K5}Xa--u*Hg^cCX#O2!v8_V;!Vn!RwC-X=mIF(l>f)n{&iQu*T_C(Of zZ%qVeaZfzBZ86#+4hdY;o+%*fpa|24McqAlpd~_*t(_yu6OHOQ{J|?gnZh|CZxT(C zxW$GhOgT+mjeDW6=zlaD+zK|*3g=W{&?4p>4t@y>0ogIhNs4uE4xkJjTY|Ns4(1Wo z3zg^g@Dkt$r=PimZ!cM1lpz7Gz$-cwn_6Z}p+bBsvP06?%3>dj^+g%a0REUni;<7a zEC32y1|6jm#-VtaZ$~mF3wWL+knkKIjK){EJW-%gl0c&kB@$?q^3Nf?L$TZh8kc_u zjh|r@m-03-u>%WbNWA7)BEa4kzw3aTf*!AV>Jcd{a213iwWLh7EVlmPz z5`=;RU!WSl?l3xxGg?DM013OV*a28IVHi& zRouaE`xqTR(=we*tE-0MR0E?unFX#r5L;t7$9X4s@S=%WWG2l?{%+yR#Mw*(A)3gkY5msGJKZ9dxT}dB-SOdBaI`!5nx@3ZG2{#pd`gnZ-g> z?21?$be^|NuA)$?Ex*{Q;A)VV2lLlu7}@|b8fj3+sx(XN6BCC^#20rC z;82G%fWU$JtTgu&JBnRWEaH_>7h`iR=5(biHsRvfRKx;N+-dM>!(m{fV22MasBAdC z(ABVl1T7%BfQsjEBv7T@RnWg6_JoN5Lw7{3=zJKNL}SW3CnkbulPBtRzS-RafqRut z?Y(ZrDZSutYVYt>@Ck;oL$8~KypdrcZ~GVKQWFMy5`8>nXgPJj}Qt)fRjY+O23h8u&DV~ARDNB%JRp2#%| zFc{B9ZlhNZQHK^iIL3Yx{}8R9@vNlN*(x&mA@X)W!vq#{>BUD&DB>LvfnU493ci87 z>0!Y1?8zUdJdg8@tmY_d!AYguHqRc0b5q?-U%`{xHiMRq?`!kj^Z2q)(NgSQ zk(Mg<1XjTqfbA-}APQ?!?)_R`M2l8hyShXr-4|EV4I6)f1wX-3d@x|#OqKwD*P-*c zGjSd#-wQMtWSE6JwW;Rxsc^6bQhNFf0b9Xs)o;UMA7#A(-gO4u4cc(o3K>g3l3;F6 zF2`;;aXFEZd=38ae!?0vN0L^#jOG!LDBJ`?`HCbc)gdA2pDW#fsB|{A+ZS*n*ix z_+kW#Uvn71Zu9_lbzO_mkU`uwpAOT|E()Br7ga=V(Vo}}*lpr>ZD7QcDq~~BKA4D* zhgV@;itVR18MkCuy>BTWxpv%8g^h!;`b;Zq$s^}^4BCt|ExPW`^)xWAXRRLN?kaDy zoE}*u-pOO}$o4 z++v6wMe!R+$|Y6#ul)rXQC zMI8;%$fO0zzvChzoi0kmNg$s+w;5G>+gtLREk>_O@F4FP`QN--!qy-31D zLart^$A)5XR=|!Rc4}7Hm2S9;%%$(dW*G9F5+-&UyRx|Zc@m|A#=~$Q1vE>|q88e* zbBApqv9W_Ydtsw;9w<=}b8EO*e5Y=I5?6kaQ3me){0Ta}%vy1b zz?6*TooAqUVYwqsg}S%s_ox(#a2|gK%hLhFXc3s{q7~D_$TzTJ`XgmEq5Xqp$G;Fs zg+_tKN1!5zbr1SuiyM>mz^7wN`k>F+EHLF3=Y<$JUIbvj$V3c348@Hl@3LS7mIz>K zG!s8mmN^e;+s!o6wam1nF$f|xyA-KUBGt72?9PJ;NLYYIiiqjvDiQV{7?DD8thSIY z6Axpd7N;EOblr1Mq2-Axx5L4Jr@u_{4%->6TX%^eO*nBNAUkk)bOq6Pu8!kTR zkcr1JXeQ*KL1G2e+pmmhOQKKU3-ia@;N{r0YVS7}N54YR8ES7I)Jy-_3k7iH-rsc) zGG&eM+4dMZuD1;GE-W*(M}V>R2wiBR{gLP`D5^i=eBp%`-XL=1e!andb7OKLnQp$+ z84>bT!ft~-ztK`aYQvWU$4hWgYBu(fE{Hj?@uxR%@JJbs{@IO;qd?tr{0)LYcS3Zd znXIL;K8bd!d^l2dc0&Ulk=1p2wl+{S4oJDSm(Wjb#nQZOE7Mxd+}S>FbM+T6b8N8S zVDx*X0IxUnp;Vv@ZuD`$Q4Uzd`big zya!e^#X^x^>%XLU_SS;jX1E6emhBJ@h&G%=NR4Kzy<=9T3dnjvc5I~j8pV%UHB7e; zt1TLntrv7OT4UD6u}l;-VG-z82Sn{1n-ilAS}aB-?}A(*lXfbqrPc8&;7i_wG?U+6 zgo9eb&!!X$ws1Du%>RQdiM{we7uu1Z#FV~Wjnq-qMYz?c7n9^!^(ve+p*CRm-Frs4 zl-FWE%-kPhXUh0>WYns}d?>cj#WluBd?J=W%#YKuMHq6KKTtC8QMX!{1F);NyojHJ z)iBhS0t!xlTJ2W97E{*x0y2v_}QfM1|S^AI(i2Hc8I>b~Z1d2`jbxE-?^#f?F!{H+l za}Fbr|0^aXY)os95=8P`vuSIwoY2>jRO+Ptt^DaEB;}#7?}84UFChv#UgwJ!qc;@j z0k`}7Cvm>4L$93*a^SQtDrW8Slnn(C9CEH}`*0F1qwxzppx(-n8`kF_n`73fF*97- z3oPD))gMF1;H;&xA8YN|Sf~JmYS>roEJf4dZb$seKNYr!ILcUS>vNrvWPQ`M099GV ztol4F)^GalRyxQ1MUd}D3(;Yf7=yyY;mN1bn}b%|>EJd9=Ggt1XQ1AUpvCxr1=qp-_T`&AY(y_ZKHX%Jxth28M)xiCa$8n|Kgt+rr)W6;q+ht~p z9gA;`HV>T1JK*_hy_Zxw9I6*nJ8+|efklKFUB@;QD*1I-a8QwCtROQaeaQte5e(n;4R)I5&_f&ywDD$0zl#qB zCboR(pdB3x1T&HklJi%B7ZW~67eQS_00n4qXwc4=0V-+z@k4{SQf@HRjTfM9|JT0{ zbqPTgpv8g0BtGgNLZuUhaae%N4-Z=u_G0dPtjFm9F* zTcm?YyNLid3QBzBlOpN-M*y54ZN{L3hy$cRbyTBy5;2bY^U#Sg3lF*D(={Oj_bd%~^AFVd?KEtEVdf{33Yfg>LQ5}=S}Oc;sorBKAdKcUGmeiHPmxQh`4;00z> zdmn}gZqKU(tzdmB4lz3;)&;xhbG-#Wf+6tbBTRHnG!h3sX(a#s1E16u9QdSPWH7rC zKNcCui}?7LWESkjv{++W*X``w{JS=YcRNM04QFj{mpHVru1i(WoJ)BLezA7k-aE6o6ZDbo$M)Oyoy+wx5 zGhvx|)J?pZ?9+*rU~II`aLFHRJLpzm^SZDmU=67`&QYd_mHt zN1PRdymYW{)%4H%;I<4nNlQ!EH^w$@lF6mJQVSo>>lH znapN6GGYAe2dMy@4;d)WYiQOa3#Z^iwX2A(ax;I1p-Qd!3>w}OKnoT*(&IKD2VI$d8Y_bn>j&UP(M2I|_gchY&WErJlJXAx4cUiA$leVjdzvjW_J|$I zP+T-{7WBZxbU92u2jV?7w)C*fS$@gRODKM8rXhvd=`V-hSnGabH`pT?Ezj zLh0^2+hkExU5k!!;@K}J*Up_-8+(McwH8jSQ#l>O#-ckVau%;xGWy0E=~;kQKSiB$ zlr9oXgVX#FXt0DHom&BlmrXFxe61o=?X@Zd(HFZ~?X@X62s&byQAAr|Cb-zxJO^BZ z5~<9-v&~1p-)qcP4O}5MA0Z%4O9&qgqRXGbkV)ZHq_24Bhp%waOey~tQp+(0Ts(}T zd<+V`Y(Zi|@h4}=4K4f{6mHp2{wV<5PXN$^I61{0rEW*%xKZ#D08;;*zOl_s=getk zy5c=@DG|MGj(t~KqjG5+5+8B>XH)DM8X^zKO z&6ru`khV$J|RXiHfjCV2Tu2Q0$Mg_T2qylMQ$NdRv7% z^D*45cN4z>C1&H6-Vg*Xesm(C7bd#sAo@I#667y{8T7)e2s#N$CW2EV*r0tv1j7{E zK*2|!r0j5YUxP1W`?L7s2TxG&ISRfhf?Fy0DuVi>uOLVui$!6xt^mIcU+V5z`lDTx z4A_%Ia+byX$`)U9=nFNCz?ZT8E`0GS0e%4m7mDB_3I;@QF$Mhys=Fs%pbvgJ1e$pj z?xDXHi&WQ+TiO09sP*tp$z<96qKyhF4)vs~c-T&mQDe{xVpBR=Wl{K>`>Q z48551{x(cS#kf{q!R~1nEM55JV1E{FKQZ$C*=5L;j*Ana1?l!TffUYD-!Zm5h0=U3Xgx4I#P4(9_e38hV-ecBY`PeYOIhI>d{wAe?}yl< z0w22T5}zZn(+=#7?Nox_04)xi>5tZ$#S=L?jt%5U>!Tb-^&0-xg~%v^R7^A{D*Fkr zk3G5xP_84>LV!Pm9PH7@5q_f(-e>x|GpEES;)z1^6ul!oxjy0T~=&UI)UBVzc{@$4Pn6Q==%6(pqp& z9nD+-sBFV!gGUiRFb|S$Q%t`FF{$B$s%%e@%FCL_MItkkzWjN0SMM&YabAw8fv281A8kkF*&=}=uCIP0fsnAi# zif~;J`tXy)e~@Qil<{}^fgG-F3@IPh8Nw~+FJ=5oN{_(>Zr}t^c_*qhS5}w-u(K*( zGT;;FUg_dRACSUGxIurMK}PyAC@e!u`On~i690)}I1ajTV(|BfiJt<@@b<49o_kKW zd*Rji|FQQjU{#iB-}lW%T?)F$)JQFtiA5=fsf~)pLPg22Fi@dPMF9a37+A~QXktre z#1%6(Q%y5%#%7usXQn3SnKrf+#1z{Ynp2QsSXMW)oFa47%H;c<*LB~pFspf<_xq0b zJC65Vdh);aoAcVA*XHK_`|nl1qptUf2`%GdtF@@)8qo z9C^c85b;{dX%0{md#{e`CDztQcPDp|FIGM{lG@=X%lC5fMd! z6&N*Ti`10g-m7a0g~skxvy9p5JnI;B181MegvN}82c0e+bB-E^wLLpTn79xlhDjHFJpm5 zA~H)YjYMuPvNRI6IU5bDh<`|5SM8`R9G+hxY>aSI4G&e`ad7Y*uglByA8;C?&@n&Z z^xA08n5wC5lE*7lPOp-`yf5F!cP@g<9p{{^kwSyD4K~~p z%-!UT#SAZEzlds&AsrtUS1jqWT8c0%Y)Q`wrC@%ti?ST*Tj1c$+{i$yYInhx0rXV> z|BQyp_Pbgm39VvHo#9jwL}mpZv~swT?QWqSpQVFtEC zRvV{k9G=jKJ*R5IJzaXre2L3KqH>6e14KRfwwm#>h|o{h@HsUD$GiReUohB18S<8A z5D%jDOL)ft|r4L5*CmuxBWA;$<&?y7bx3XIo#8e%JrD&L4@qk}Uu-M%-UYwmgv}wm0s{LZC;)n*s4l{?>gYCfJ_# z_L9&-3}pk8o);eEZP=+};cXK>OmUjdfzvsk4cQaXn(&!({U6BQY%>v#W^qX9Y~#|i zWZ^7XI6LZ;&cZyEg?%~;`*areg{my<(^=RjS#UmE?|*g3Yo2T1l%#qfs8ju#x8J&b zD%HI}Vj_L}RLb91DUTw5n38$|QmF?!r5=5_;hQKQ$P01aT`8IVcu<%l%=y?8B z3n?VsMBkPsO4E=gN{x~xN(J-pU-v$Z6yD<)*WRuXdjd4_`ST=X-h$O#fHhEY!aW`&m39vNx}iZb#=t7H{(6$$;nSkofrQ_N|ne!TzwC1$T8m z!yWsOQEk&&)?+N2zL>DNMFmmkWZnKXCw*&qVC5>FeP2D4FD~*b%YD^pBAH$uKD)4c z+j2zIvKYi6J$?kv%0^V&8N%TW|Jv}tNZZG#Ht|CWrp*ywrIs(5O`BWjEV(wn$GC8MqwByx6EdLr*PKU3piRLA0su=<)Agk{~OtYgltg?4USJ(|McJi9QA z!`$9(>ZV$tYr^g|-Tb?MCpL0d{t-6$3XR{1T*};|_sM({MW>Ds&8{3H4$T!ot{&~| zN=`w}t3{&s@>WC~h5E_Qs;Mp>UPtz%1YZ7kYQF4RIeNQ)4|2REbx0uZ6Va219_1W) zclp##xFm7UR3#Gg?H)UA-MO?mNPnM{t9wkIW}8cUu$WpxEgE%71==>ct9cf!(q#F= zyK>S2LEQ?91&NmLT%@<5ZH&rRDW_I%TL)ae;zkU2VTM%lD?v+lQh4F(HpkXk5lcg(x;0`-E`m@{5Gem&XRe zc%zUbfOFWK)K~fY@b)5)eTVwb;Wn%_;qzt9`1iN^-{RQhVCwv3oTOnNIAl}mC2bq0 zK-1gQ!>BmN;jjimPiS2J2Ss#*IP&y}<&OjbpQNle3@4QB|6Mq+Sr*m+lPx|!+DC*MgA+h2K~ zL)r+}K!??wK3$a=;`_?6ydMIdr^S4xxkfjZmS<_$`vcA=-wSKxTPgc0A8y6M-eb~S zj^Y^N(~@H3LNe2g)M2pSqf9$H?rX(h`FN(Y4@ARy_sR}g2gL$RyYp{QsXpUZA^YzW zp74SfmYWlfEc-#&iOQ#C!-LNN5A|0G>(cEiai8e42OaiX2}hPsw8DEyEcv8+>+(VL zlkLl$Am`fQornA{Nn|<}zbYIaUw=>-tQ`;gGU2ObmvS7Sedkrhw)UvD_N(MoszA8p zI@_HX7xBCkM^qySozR~NGYk+8`irkm5EHih=!5enbV5a(aQWBBYU<5SzFu9!vguwRK*Z?%+&R;OE{)yu>R(dzW; zsI`YT9CEG~Y4e^iC0?~G)69|bmY5uAa;}e4)=I=$i-?u)EnpapA+l8(7`uugRw_ig zE8Pv-q89Jy8`J=7KGsZlt@3RD5geROj*@8%r{t4e;^}qF68LnVvhL%_fNA% zok};~6$O{tbC0(RkKHSE1=B|b0@ZWOr^Qj=fW3ZVmLkLDmTfc^Z!D9m3!W=TGoPfc z`*y{AW9DNErqz6pS;Y$5P1&B80RgeC7OU3Hf8@6X@m-f8!+42T!8a z?ZL}rz9y46HtbeJO*}@j%e!+SrT%L<%G)l05w}d??qPyu4TfDf4Nb_%-GVPJ8%Nvk zvB+>OEsL3nc{7)8vjQtxNSc5qN)KO%{DVB zAX4TzB#*n>_k_cV1hWRKIAN*mF84G)XUeB{p#x*tRgSTCl?Q%vSDDY@IbwaytgFqJ z+dY}raXhO%$Amh&>W3WJjtQF(st&B*W{nwv-`<=aJIQL!tYd1SF4nvUx8%halZk`e-W`}k^F~RU%!LQHB_9h6M79-;oBbwEoO8@!maR9ed-7q4QGZb^`5+VBz9zT4K3476 z>h?_cO_b?3pOBL1oPV9=2#ip#{HzudmM^$U!G(fy5sDFvR;jP@V#bj(sakk2hoV!2{);@5ffRn`F4*k-G0_WWt;hlyzwIC8%FusZuM$I7A)=Vl6Mfg()nVx8B+rf+lAR(YEAMn z^A=JWpw*oUA36xySFyzCxx^eSM$eR}b&kJ7j4pQo zJIYd6!zVlF!A7=67PyJ?J3p|_?{KBb|F#@1`Lb)rx>VIY$W7DkCQrm>82_D zM^qgBYNI;DUKqM@>AyE(l@=IpZe~DmKI42|maBOi`lNVb64o4c`f8|9g`u3r=AiF= z+)klyrG7fsAHaNCAe!lroYXc~1zA0d)u_FE%x&h(!Y*8fz`-=O#l3ZU`hWymra|{e zz(&=K=9%0aKF;i-p{?&0>NqXqhkQYISaLo2oys+)*T*F{m*rKg`LKs~I5#*84aAqn zs*;;k$j3T)9P3-t98xyEZK|V^U6~(?oJ*F$ca{d%5Ow@wR5isCX)|1w+my)Jgfr!d&SwXuoBcRelf|^b z8_Tt;P~TTQynPFM-kxrrV@OiwQN%^se0xF@)|@SSts~m@8w*+zAk}fU`REzy64H+F z>#75poT~GEq>hiB>Nq|YA6wxWWNxe`q3%>$dez%H8&p5*>x2oi?rX`$V*@Au9 zk*W>i4spVUlOQ{ba$5$K9Hy}9#2~-23z7Z3GOnPAN56_Jp`q-T9jpv0!552zN>FF- z*4Y1c_QGm)o<<$exvK0}IXk>^wjbi?usLq% z0aEE1RN)S*rF&3?9mWljrzLIeOu8!UVX16><%wGGnt#tQo;#<8yd+61caA#Lax?BP z(6}Eh(#$IClb<5P*SpKm>oFkO?7x$4&c!1dpzgz4o+0C`=QiFC>LJ1D+a%05Qu9?; z`CPn+E4h;G4}VyCZ)$W@L2OQ6{31F@E#pv%U^Jk&X_3Km(174 zSKe{V${M%XxmUeuZq(HkU$IkH!OS(*Uvs*imm4oLU#7DX-pc)6YaKI@Fk5C4AS2eP zDN(U5k5isdWKC+`%(|H~F|*BMUy?`81dQXeVpp7djWC@*`2;?&Xl3wVXqt@!VN`32 z9G|g_`TiuY++!Za0|vZ&5veLX=bcVX38Cwp>U_CYj&hEW?d41P z2<%ZrDDu5e6(@o3BGSO4{wX2RtmEZ_o_vIH#z~g740dN&Fk5|>x4TsXlaE(j`pf>AIi%3z=Sk2A5~Uhlz&aV0Xi8UtQQ4U@lXh2a*Fb6D3@h^-I{WADQ$WXA(Y_ zPmc3_o}m@&c}=Vsgpl9aI3Xs_A0CtEYhIh@JF~VbC&uBO)$%J!XtU*?Rmy+EZciBdHZH&uzB^iG*jMlZD zd4N&jO=>*exVP~$Rw61B8;0Yvs^K zUTJDL6z3w??NmY|aD};oCKec=oxY--zASED9fX@u+_cA4jmx08%;$*fHoi4(@$(#6 z`fu>Wx#0~w1dPv}cf$NUXKddJask#sXY!oo^`rBgcUI>)GrDY17GKuqIp2C!Z+xas zY$Nj3Yx10*zdG?c*79p3@|@snN6RMO7T2FCis&0pG%G3igP4pUjjvzt2o5Qcb2Saz@5}9W>P;Tpgx} zUKk0ahv;;!jb1sHu&oa2p@(rnQ<{k=Kwin#%cpU(%~?!(XY={^hq=@?+q~zP2nUL# z$mP9>mFgvni{*RET+UxM(7fkw1d}&Vp0&=A&0x7!#sVrTKVL?mOqp&n0(zS{Dt?No z8Qnu)^S3Xwr=p*g3cl*fvN3Wt3pa;;WHex^!)iBu2sdOIK3j{EIvqlzY~{?+SYnZn z9kh9y&1=QI+M+@JJ7;z}!+*qH&kQxJL_VAJ_uHOvi5&RK9>+Fy^IV1Gmy=Q03=kYM|ZFo;>Hp@KahbHQeGZZhN{_^gHX| z!YehB?BywPTVvk3FV2zI&53z@#*Q-)Fq$bYM*a`HGy03 z+E*N(aLyx@y?ZV|{XAy_i~yzByJwoR%Bp!D8Dd+@B`o%QH6ietXxDuqf6K?`Ip^%f z_ul8bnpb~Ny0%hOo4+yuLt z8f28Jh9^3L(Avg-;pW(bO)o7V|w38$6jxGZR7DksCYs1SKPGR zF44XT-y~!Vdo_L{H|SXz7VxKoXaEeiC>Y(N1Znpu$_; zA&=*0BTN$ar0t=$vAXf-p|~UD@<`+PIlB+bvmLQ*x=%RTxVPbGyIJGW##Ra9Y(pbr zzel|N8>P0Ji+IrWNH zUYqh9O}v}pk@xVrwA7Q0jvlIFRj)4Ii}L&r&m}b#Z-!QUeD3dwf$+Q+5--lHe0ZKD z@d^3+!p-y~jUQItCu5S8HC;}|gPW=(EA>a~zo@U-R1@QT$N6&gx})I?#{PH18u-X| z+mQbkqG{Z#<7xcRiXMX}RKy?Fe_j8|rdNn~Q*%%%>W|eQ>#^&bX)!s;Wi;+>YN$1L z2^UXEp*DU}t6{yW)mmfL9(rjZSv&NE{5`dSDQcVBJmgi8zIoL$A^}V{5=gnfeHWtNnt!1Vv=dM)Ut3BLh z0fB0@e^NNp)z-CaQ!w`pYYMH!#CN<0BWc^($|*5j(fWHhAFGR~IU0dJyfQN8(C`@F zt{yV^HL$H^BJBtYgZBk7sI{inVU5FMZmU&O{E5u8FK43f^7N%WC`IIU>2%FT{@hlp zTuKw@6C(Rq(V3E=!m4L z%iS;l&cQLbc=YgPtIIn%J&@Z`49$y9Zg-#d^x-XZR(8mEkyNJ5yw!Y?0fI$J6JG=2 z@+$cWYh3EEW;nj%F&(RG-^2`AkIbIK^_wvpQMUDsZFY0{CeUTa*K<)IIXlze`Ln@v z70&fe{`$`Ja{la(q}I9qr~EAp%^FtYSs{zlLZ8WPH?=~25Qlq`o41k~nHRmuQJg)= zhcPsoFo4$faBsw=_;m*97dFUQvFg{x3HMq(dhpq5h zP(nHZhWl;4__`nR4iSvPxNq_We}nlu0ptPs&<$c7z{T zZN(6Hp%D>wR8w?|R9~MtfKS82iRnJGuV z+w@A?TJ5CE!FMV{ej-a!b2KyWCJK))D>G|i2X;ixdO&=E*9FYiJsW}U$eO|t1IyIQ-Z ztm-|xsWH8&!Q8>}gj!1=m!GP2d0Qj^I_UNQqRgxD&b&vTjpT;IMp>6sFJexWYkvu$ zVW+jm&>K7P{!cHud1 zv8{2;F*hsroM}TMb1HE2qaq5F52|!ORk0CX@MRTWTc~&7DR(9x1>-^bY@8Q{@VI2A z_~S^7r*4j!Dr%NL)y=XO;(bOhcDV~}K3anfcbq>~tJ`<%)$Q&n7sbl6qlgP9K7k8a zs&e1BW9KfJ%aObu$u?DrdKE53)V`D`q`Vl($yohra%IXuv8ayx2~T3m*^fdloQLI; za(8|MRj#e?z@xm&BC9x_7*lIGxgO1-%x+eCt%cr9YM`pk=Bvm@va+^r_D*j%S{W)$ zMODwNtfoe(qQegNjJi6>zzdakV2n9iIXUxg%X`gPS>m7zZzcq~vw<84Z))J7MtjeK zaGsOlu<JJuc(fYqZ-Z)cVW=@NmFu^PDqk- zeK|F%p;4uZxPP>eV9mhXamkV1EOojg9mkq&(j_UsSy>JH{dM9c$$N1}MtG6J>tdcg z&e{$4p ze|aQd)pAr;t(L~sReId3<&R7LTq1vZ$)CRTz236Is6em6-hm#47jXkVoEz|t`7?Q* zDQ%7^&p|OrQhlfSZ{lo76jzeI=;w*>eQ}wmt1q|uT%flg;l&|>%UtL(!9os6C ztHo>8R7cuOHh&~~wgoHF%t`d`d|cJ9Qj{;9=uGqEdo;fr9K5^L$22yp&n3~_3_&IP zWqcdmbTLZHTOjJN+fx~;qws%ldr* zUKkpXP4X^h7LJPg--xl*{0h7A;jU1Zf{ z7cO+3e@OkuAAPZSng=hqkZ1Oz(?JA&HOEATHcgIZB@#{V9YsBnK74W%Er)RCnCYq; zj|oK2F)u~Q_bk-VW4?TZ0bp`8x3?_5Je1y% z-Cp?^UGk~t!p>4xo1NT;VB7i>-V#L*WtLOPtyC*ET0*AnH%}#>{$HB;N zRG`|w^rQ&X-fc>5<9;Bkeq5RYN;_I5(%N4laZsE zJAQ^m_e?H*gz}gX`d^^;rG$sdVH;%t_ryS53sDBlX~ls1Ar9?=`XL5#3LR-M2fb>V z`JTh5B!{m#YGe`z931|xVbzw2!#3G(S|;JPNsSq18HI`woA{0zvx_nsQj3Xs(_w00 zy*wJy4LjeBkx{31hNG5IFw((0BDOmG)Xped9eQdfr+#qLJ(Hx_m3NG;yjSSTD|aNX zmS7xVfx(jA5MT2-`F?K*9h}yF9Z?%#h2ikI&PpisW;6lUM0I~jykDX0J_z);+<(kX znm~l532>dJl9O5RqEF4yE~6aljyAW27$c=Tx@Wq4Yg`8J9T4~e*HOCrMw$LLW=1L7 z1^o5jG0?YQ0alL2u*OVIc}TcPF4gOXRwlV*h!8(b?S8PoW1v52f$c;>X$ot~Of!q# zW!E>EcWC&c8Ao7qn%#1g@>G&TZn|B04uxC;y7DU&au4Xr(Ix7ak%(&YtQ5S9h(1-IZO1P4hh68k5o*2L!q_C8aeDm~X$1 zLG}<_F`!kS1D8je@sHW_d5$IyQJTRt5xafNiu4_K@fdc^xIV9JWzOQ^^}sWit^SPj zosq9RjsXituBOkT!(-*h8jlCD;j&(3%ASY3e3|T00~zOD)jRqz2>t0y%*z^u!_8@C zRwYbQ!zb@`%#!Y(fofi0fbGltya7Y@F2(^($qW>c?ZX&TB1Qz}{&QsF)V-;3R9nlF zWMFol-`mUi?Cw9wV#B+dzK*NDrFtH|D{{M0fj**D^dzTHCU(r~Myj4B zGB5;#rUqqnIY!QB&yoI`Ps7)Jti-mTicEZ-@NX<9QLXh8Jr7?FkkE!Y37nz3s#4t5 z91T|||IP)E+QyTpf3ZtHsU!#^fH;e13|}(UeBL_7kNWo&Eu|Tcwtz; zd|sF%x{FRHN4Ak1SX0duAUyC!`>|BzQMvQif<}Iw-sFcz2M@^RW zRDx+lXkj=JtLs(FB%a;7?xOrB#vI zvbidWe2IG9Q3tVBt&@v%90ndbftIPn&h38?Y`~0B2e?w}Pni%_=X^G)kMr3qPB%w~ z)o?>@fZP`?jpUX_cYlf_Y+u3`Wj!YOs$&!0Dl@GYQrg231>qH*%iqfKFu!7)+sJv8 zR2pdre~3WMCr5HG_7&zbtvSCFtIAxH^NFHjG2!NWO{kcP!rBbd6(?qC+U8br!rB#^ z@qTwqjXCIay|3B6qs*D>oJtuVSfg=q)Jz{XwJsm1xXuxde1Y?P4>}u~AJ5IW zjl$-|7~Z?2q6uU8I_J2wWZFmM^AT_}Z`{miMo=tgoNSL-M?LcdhOMLCIAPA7-qv=a zh0d`pEvIdaq^@d8Oysbf7@q8zQyJ?wtr<24)y~2N+|kHSYQ~(X{T@H`8~-D3xfk|k zmEvrqc6}nMIhMgr2x`8taGH-dh%^vv_1B)x!Mx7DTQF>YU>a&>nn)}o#m)Yf{fGAd zbALm`Uhm1%pZi;J)dv^snw|~T5z}YXAyf`jk*D81 z-QauI;ML5x{EZUNyq!_ORaVz3%}fcF^-5F~G_1L!f}NEQog7P}YH`+rX_a*VR(<}! zs&-OD$E1kk)PGT`F&P^x>zEl7{rtM=dsxr7+EqqhW8|-Z^=% z&#X#~aSW^T9d(A)4A>h{lhyJiwWZy(n{9dwQ;BiT>{+AQM*WLu;QE@r(~{w2)ai!P z@A?`HB(<^&FU2qr%RbYvi|()+h+VW*RM8SZdr?KT7ge;dkV1%7Mb$u_q6wrAuA}w` z_BX1cIz&-Xw743miYmIjsQOq%6{U+RngSZAib@sQjFsKKQChcnZL>S(%@@{GeBmzd zTk(afysPtx`nUdBD_89*_zb&=6k*hX+F<+N?|iGd)?YuWTI}Dq?De5?!}9}?z{*Ch zNC8ebhlj`Pt&WL$kLn_v z6eiG{OgPfQ_;q>Xz!-mI%(~BLq!sHJ``Xs}>J8zQ?*Px8TejM~L7TtJVJvZQ8mUa% zTJQG8xiQf~dxHi1Qu5)$_?!s0zw7+Fh9Zh+OQ<_h7U8eSx|b&#!edU<#>rjo4MyN& z>VtKNzHC0{#Hfk&HO-C)b{C7^J>e`pWH|dBBF6CN*e{k7JVRc6t9jV|&z&Vb@|>R@ z$a8ww5%#>6=L~@FygH|h2CJ725`+sXdqyKKToIg@MP?k2)d_css$K{~AO zt@Yj1gL_@$U-M+~1jS5k=nX{h@s`?}529vAH28b$;PYyC^8JF-qh1SZ4g13PS(~r5 zZPc5icJn06{41w3yFgzzcxkA$h-U3`gx&H1WHZ7jAYogH86xt=xaKgWOI zxoZ6}&-*oI6h?h9dIzJf7`+*|XWo3}ylT=pJmd9i4QB;ZgQa}uSUhvy^)>MzY+JtE z`OK_H|IV8C&*Poq+mJV53Vi!w)B!l>?94gEwP?7vcSxP{rJArk8}>Sr>xGMZq5}2l zSTqi4*ngB~7;A>q-8kg6+igcvc5;?MqGi4`ZdCQ@JyJxxG4_lz;Z^4pg7LVfX+v*- z;@nAidCu{RrnS1OQpdGAC)IfWMF88W9c@F{O#if+k~FOJK96DBTkjM3nIT_#`&hPa zPE!)bJMx8xE5;ze`T-xbR!QtZ%U5*8r`0dvE9@?8J;3IE2l0t zsE22pS(NjIBg6N5(VnZSx*KSjG)Yc^P${Y=Mfx}8V>GJH+%%bm()6>aDo|~27=_I2 z2O=d%Ze_upYYN62@^k7*wc?=HBzhwUXPW@5Kk!o z_xHb%1Ft897%xIwp$DM#koCkv62AbdwF-$OgLWpq%bUid3%7ZGP`=Q67-OypE z6$+ggV)TQqfJQ@8pbV%8S`R%0?Skr|BhYc^6cl~~{-9WBEHnkW4Jw81gzkkNf&L6V z1MPz9p<~cVsB2<~(HpuPiiajabD?F>kD>da$Dn7RI_MzuK6DD|NqZiZe#MRZle+NZ~MiMNHS&&b{oHj zW)&|fE?Hje%3ZN2x71TmQjDh+LG}x?b6nX)MJ0=}wdJlLyQphB!dZZNVvO6!%uF)2 z3I*AzFv`lZSGtOGm%EB`ix+#CZX3oUBi)94i(cngrzoz?yA_59A3ANyi3kadsYT^U!dh)2Ptho|sb#sj z%E;1B%k|98F7kFVOSjF0cjE5G;sQ@Wc2U8q+{tCA7G)Qu=Pr^WOI~4wf9N)%plB!t ziiajZDNrVq0~J9Yr~0E0_29KKq*ivlnLcPMbHXp zGxQL&4XTG)p{S1u2TFrH&}Qf<=pb|!iZ+Q48V-$yra<{nDYP288`=uhL9I}93+aH0 zppDSuP(5@Oiv1`4p$tg)m=CUo9)KQ)UWew-UzlB7l z+~PhCH`jo}hM{IT&3^->aiy}m#<&(73K5Jk0slBRZUBdau~!?$o!~r3*5y~?X*G&# zupb7+LDxb%`M3jYh!_Q zV@0lV4}IAp*J5vWncPspTo_ZlSgV&wxxfL;yuw^e7iczXw&1X{E%rINdD-3~k84p$ zvF**ZsHmjeTc*<`WtUy{?f$LsgS{7cG=C*g9r3q^9&Fuiuakb^P!=pJDD!y9eNI7n zX;Jo~+@-n29@o=&GER(ay{O% zVpSXql2eQYw`OGA*im-vrEQg^rN3~1QlF)+YR2b^(pXTkw79?}b)&pMN_;^FGjy#F zD!6ah-F`=qEq<3~7iTZd&G|m&o$u~~43xA5m&18n>fm%IcQ&uT)%Aj$mBrahsqO{s z14NlCucXW+dAIGdJ-I6iJVDjkc7dDrit(*cw7dD<#7G{$!L#oOw^Mud;k$u!<9kXcTLGHUsP65s++Rmnk3D8a#419d611O+vAGu zWS1oK6?kMhO(~!`mMzL(8RQ9Du`gL#D!rxamW74%#;#PEDyeS6=)wk5ggD}N8R~GY zG`PjpuC7Auu*_wL%2xj@NG%&xwz+I;+3KGMsb&AmHh(I#L(TcucJoN$k@;13NaiB; z>?qe5VdOA>k+EiSNm(*=A+4-rQEquT2K+*nxD}4E@c2fz@vBP4zpFKU<#8M7Ubivl ze#SaTer32_RPHu@2Gv%%jZ1&(Htt=o-7j3H-H1C0Lw-NPy{O~Bi!HUX_&@lM%Q*jE ztYQB<-{wO46l6Z%^>4lL&3~QUuQNLSg&6D%v;=u7(#7C8a+sEBAPpEnl&6)$MbU-7ffc`yZ`d zbLZOs-TD9D9smC_|C1(7o{~IuTFQ;nZ%UnU^DUeoPS2P%droH7ZJPhzZvX!a{-=A3 zjTuNJGFbc0VXcyp?=_~D6&UH+9>$yEoa~i`*ek?MxhQZ~tW}1%FH?pzC;TaQ`1O`4 z74P$97aLP^7aB9N%Z#MbGH|6aoj9j^i;N`iVk14b)VPHO)6FHzj48Q`#64?}hSuhc1iKqzIHj?n$)k!l9BMq7{ zCo%=#8rpcbi$Oh zP9?O3;0*M_pCn{Cf})pHO}FwUC_Lu9O=B^bj=xgy7G^S%mz&A^GH?p!B2>}gdsV0s zj*h1c-x80VUr~!}$Bwr^#i8R=MXBO1vfS&qz2shEwA0`r#-)~g$xcs6yI!X&*oB?O z&Spr{RG4WN&H%54Upf3;X~i!(`8S-of!NDGSG$S0(k@es6k|MTTFT#Y{>d*FZpRt3 z;G6!%o!G`$ zhoHxy7omFS7<3YHoJ0l$MMEwq7K(?)LT)GpN`o??1yDY;0=gS|0223)LEE4gq1T~i zs1*wTH{n7qC>ELkIOwZGM^U|Gr=B^ z*hN6LorEiX1!X%(!i|E&>>{YQMPfb}5_35vAo?UrKiSf!f#T;@%UtS-xSs<__*sy| zEp1TDmqIS66dDLgo0YUmKE!%? z+&v3P`u0I${|+Sf(J)2$lYfT#098PDLxr^SI9jjdYn77TPNt7Aj3x9FamGz5pV~Y_ z<u_mTU7TcK7c?Mvzvv>EdTF#0RpK%1dvXzW?kPy-bEwU$Ru06WWpZJD%~ zmcqLdTCGTpdW#dIw%p%#gWBxvW zMdV=-aTk&DRVobym`ESfAtfIPzyIgqNFQ2Gn36ilVQ|Rtgsgwx7ru-pl2RG7q~Fd* zpG&H8(AgnMp06Xg?+Z)(%LrP`2o&sBQuFdf*nTC| znh)RScZJHit|#K#F6VDgx5U#?N2Gqrs3$up_Wnu^xmf-mr$ziOA`Ic4hfyky(1ag) z?95YbK*n)hhdP&x&SU3&6uUj0zXKCT>&hmkCH!4XoU#ifu}YasF22dT3y0e|$2p`+ zYI+G_F68fu|6w>f)tYmXcPWw1ttPlkbC}QQlC}4i>@u+R(ho`>|7|Gssu#p2(qwO0 z8A9K*>_tnETHctW#t`u<_f-+k^1Fi*) zZb?Q66)PMJ1*5<)a3B~C%J|#`j78rS91V5@-Jk;%i&YLGUK98B7I_gYt~l8SoY`ya)9N zi~?^32ZFP~Sa1$F8oUjh0_K7l;1VzgECM~?Iul!OdWQ@F6e(dX!L=6 z){UZ0#$2x;r>V89)Q31mcNx#6PKf$e)KV|vFr}KJ_M%Qf3Z(0Yn5QeX%#3eDZm8=; z9HYOuFQ(K)Epq^=A92hA#9Zo#q*=n3dJ>09+(=y!wS+JAMdA`SQfDN;GPjU=6GzS^ zoN_oK=8_kwKXIghW`$b(W+=Z>k0f64D|JcINfRU{33HakL&@VLC8Ln~C1DC5WZfwI zl<=jV#SyE_U!<;yTJkIPO~RCPN}Y?th0J}V-bp%Vpq9EP>f896iq2z|wbVtsyre$H z;abuu_HnpQLcK!eORHBY_gXFW)^;OxH;x)0>C~&ksfwRchb3*2o~0_yGgO*oHD#BJ z)NhG53pY~7MYbVzM(Vk!Wu7N>UDOi3)OX?A`FWB0ZQpk_+6yZJdKi+_9x}2+k$ROwr|Ogv?(cN$+g|K zBorxKgBqrDIZ1I*>ap-h^L?q}>CK8qx*gtV%v51W9ww{$r)8x&9U?c7kTY?k+tnn+ zN9k8>_tKY2{S*EctNhq)M%<)XWu{Y{hDmT8(rrMu+u(XOS>>^a5GB8oHr-Bic~4UO z3NFiF_jdl1)$HHyZ<1AA(Bn>us;9ayNmA+4c}`MoSLa#tUgvqTs>|gT*Ch|Z@#*yG zJlJukDQ?(tr>c_Dai^o_K>Hm~!i(;@OEDaR!Kb}l0wpDqDAz8NYlnr}C%-0E=Cl(`Of zhAIKgw+n|Wtw`4vJKPLawmMv0XLYz!RGxyv>Rbj(RjcZp##^YldhQ^yP_33c&Q|Fy zr&kf3E;&2?EGsX%Or$N^{E|Li{D@6wKe{FAG*43HVDm!Sgl=ckRr28X2->PrQPo=Rrj>tNkQ(@g3M=HWhvq4 zUPH&5u6h)$PO-{G+}v!9FQQIU>F=1&$*3ZHDpM$HVc}sKYvbWyRKFxOa}t?}{05?L z0As;-!O`H~KsUGrOa*@iW`b{mIp9CQQt$w{8vGNu5qu837u*GI0S|+ZgCYai4!#cV z0(XG*U>!J?bX^S|M*kpq5|nw1$T9|lk(VUVFQX^DAz(Cm8GmEIHDEmWENI|96i7iY zypKj74rZX2F=zq!6VQcu7qAGujP}f9gBG85YN-!UNKA1v&dVn7Eso(h&9!D>8fqd+*1<#=OfZMT;2E+R&8Bc;y zpv-d?VBQlPi2fEZ9)Gc5Ec!xFWOReT(de_meDu9QH+q(AN`~kJQ_2!`laA;&<8ePJ`p^F{>NY)`UEh1K$6;P zD@7lVJ_`NA;6QLW7z=vA(cmm_BkAr9y3yYcrh*w@CiqKmGxisQIq0jvErfeLSc?8m z@B!@mfUD8Z7JKx4!HwwWfcJtAh&gySxDEUbSPgCkx8lAZSdTta?9pEW9!4*-y?WxC z0GjAGfG5E}g2uokwZ4i3A3|G=ze~Yr^tX#W{;mLG(C2{h;LD&JtOe7+ZQufMJ6H<7 z23CN3z&gU~4{k<37nHUA2jD~KcY;rWFM+Rve*lkxzXI#=Hvl|~{@0-6GCqn9ZX`XU zz&_|#fvxD>pbPyba2xuuU_AN?a69^O-~{yRzy`t{2&SN)2WEi9U^C{IfeX;j2Oq%S z7_bQaYEWd=E^r0N7l4~_KUxs|Bj8r>M_?@GSAo^&3&7XG60iXb15NOK z@C>*a48J_dcod8R{|g)l{uzt~r-GxwQc$mhWCzh6yR%h4rN`@qs!!7D9K|QC&Q)zh zs|(b4s?~)mO>we$^Ib|H6|mZ6h5nXGpf ztA0?=!?IN$qSXskI<#6#yKJ?jNvn1Gw7NjWt<{C9479p9D4bGN545?K)M&Nrh}mK4 zI;724sj}DeHYp){9x1zbwtA5o|Fj#~>+CS^a4&mxwz)o6q}^-2YxQEqQ?1TdWv10j z)wrP5#fqm|ttCvhTKI0OJK`->bwZoV3ek4s4KiP5@lDp4xt3bi=iTNR_oVU#jMYdabVG)iMHIceU(EpQ+Na4BK4R zbyBBA&LV0pUDWa^Ey2|CmuyuZwA@Ea5=E{V2R9{;dJV4Yu~utor#|~psQMQzTamqB zTRyTx)hjKh(CM+|3tA`cM5eVw{<^mKV>d8wR!)UtUwDIw#6mLciC!Fdig56**@kuFp9 zRm(PYyn6jBZBbgLedbE^QiD76QqQ&9d=(-$TQdQV$c`893uCh^?S0B>h zgHe3+r>*U0lWfGk0)w^fxZ*cy7f*RBG4t=e2aWu6(8SK+>QLpcZLYB+{hiHiUHoLN zva!|j-?}zLS#;nR`+m8*Pvsc*#C2EabpM5;de-_eg%x{;Rv#F1|KZTWvx%pF6@BM# z{?gAq=5NanpICKbha+jkj-pk!>HOKzNuDG}@*{VL|M<>x8}femx1Mfg0&jY{PhUSd z@_|cF-S@TSvpF#=cZE#&^pEYzbvxX~rk&gpg8o+R*5>x#>bABqJM#zs_Fd=RwnSTX z&I-%shQsX$cQ}ODe${08Z%teqYWuput-1QCmEtJN%>&h1@qk;SysM>u)FH#JN0R8$ zlI2Erd4W`2BYRPas^H~6EYlTRd1+0Y%<5C*c(Yh7#pqzZ^SUi`00puf1TLBwD^(z19$?k zI_q@96I&7=dugC6=@ z;=x(zmnSxpzUths2lV}W;(d$n4zGBhhcg$vQgPKUo=W`u_Y2>6d>6NKD_Z82cX=l9 zH)|dp@@DNNgg3ov`I2W7R~2vRZH5iR|FN4R-rk;gSL)5Le`t^&cgz(}je0(D%$&ue zOFz3F|9{MQ=Drsa_uRZJW?rwWCmPKyp;!L%#l+WM8*$gw`vVhyrA%JU8;5@{&D%Uzy1ABxhd?L@XCWrs}qyIJo-u1y6c3WkKfRAp|+aqi2QS9c}O%>QW8WY>s^Mtt|) z-i5Cu#$EE~zaM`O|Hi5Q{ZG7-cyaa(aRWY0M8CP;Z>x4E*8l4Mv%~NG*92BTMVYQW ziEm`M7jAWNt2%99+kH>%NqlA0J^8~!$lsQIdB07pOZ?4{pZvIHH;*;8&W`N$dR^js zTi$&1sSn^+{l}lam$f(X!{)YCp{^Lxb70z_mc5Ctm=i_4Mo~Z9VgAR8Urk(l`>^L8 z4~)Y9t*<<5yp~x0=bY4nlf+m0#p;hXzLr?FWyKXk+TfRa#Pc6?eLZpYOS9hS`P~6i{5#0$d5?h zf;(@W*l%Crv0schQSk6c(jR(w_^?WqtSL+k4a}t{s{_ideFFJJ54pXh)7 zib<{sq-S%%rN3OYKk-*{;|8xhLH*BsJbu;F`xEb6-?s3EH{r+j@qG`q?oWKLSLBcp znO99TQYPKf>&gR(UkrLIHtNp4gm=*&7A`!H*!$AR5&gIIo@g{ouKwWz2NGBQ;E~4* z#&Ju%B4Sz1KMo}Ba5!H+^3Uuszbsr5FN9{-1OZ)4OSwcf_qT&=g!Jw)qmtUq>8 z`L}VwAGF@a{3Tj%uubAnbzC5h59M=#Kw)aT5n_N{aSBhW`Wk*xIIzpZQRmZ>uoez z>Q(qQHtf`T8^``i>uroG*LoXEZ`67ltFP308y_&V-p1yq=nweWSW%+&HoC6SdK;^c z?Njb;T<{yMx6!yw>uuaTKZ81N8WoZeslsp`WeO{SmzcSvKzj7p0Y|Lk(BXXJf z#kngy*?GJ?Auf?iLh`kZ%Ru=qg~&eTgB&8uln;Om0ykfyWx)@CgV9G{t!3--`3sTB zSA$N_g}KbSV!cAhgD73s|FrZTsi zW5|9$GBRMfVJ0WE(^|i=D{HN?2#f%?>$H?qUZ?ODN>-*ncOyTUn<` z2>Om>Inq(I`A-*k90 z8=8-)>^zGn)qB!h;sTq7dI}}vYGrFk1uhRf~lN+pGpozwsk8y zeO7)V((GW@*@W<2u18w#MJ7~+Pdh!L*Qp2&?f?6@dBsIuk@1w2xQa@O7aMmEL+%ba zpzX9H`K8RtP+JzEsAVszG(N;AjR_H}b~|Y&(nfb-x9dDRX?w@<+<;xfd3L?93;t~h z*8F$XY3!lJ>;2Z4qwdbWb#gnK{V(bsn#11n2+bo2N}|X>Ki4u7d;2HtSJZAO*pHZ1 z;O_3AaD>kfq2C%5mbkIA5FA#pKM89)?i$Vu%S3-NC@gVvp|FDeNmvcIi{Z^P`zQM< zqtQ=+g2NMcl4kh@r!Cm8*qyPg(K;D9sOY_ zm}lbdLg@gZ0`&-< z)bYgRi1-pg6=&IStj(6HvHjyFL+uS}vlUE)*q0`RMP0S{AGP_Z{8t(JV#Bj=aW|J{X z!A$xe9mkDgWMp)9r{c)$WR{jB?&f0Fk-oG{G0W-fj*@3zptG3@uc(t*hBz}yF@wcc zS~8To3e2{pV#Z~EPc}Rm5n{L~D6NlCGn_Q~Mwj?gZeq$;Xs4>L&7gu?=Brk%%2^2e zja{Qcba|9{7RB6{l!TE{*U9WQ75D4GW*ORDJ!UG;;x|LPYryPy7vfP7$507~V;pZ# zf>u3{>7Q{NGwJ6ovr%GZj1LK}FQdgs-pssS!qau=dNDK7LW~OJnIo*cRm@>Lz*y;F zLuMxLT{6)cvCw+$r!>T<4nbd5UbZMEUXC{y)jtd|QkaBUW-3i$e*W(Y-fhIIah7*m zFZ2^TX~!QoxeeQGEbp~m=x4hfdBm-(Ck)o;#=W$+R4^Zsa+iKues&)r_3{>%TZ{~Q zJP5YC%d*>k0e^$7c~o+z+jA<@I;5laIm*ivIl}ZsQar zKRfKmF*@vlpq(GFmp)W}!C`M4>Q>>1xqN*=P{MudD!1_wWa|Zk?az&L8`oUt7UK*0 zkD2B+VxeIB`k?T|yw8np0pm!D+b|(Je_PLU_gJEK_r(ovBMi0HiBH40$?a~>58hPz zwT&LbL(rJ;LFz>)>NBXt%yGTuMX*~t{@`@ZAYC&d`Puv}!mot20+hNUKN(ZxXQx5x z$^_CYKReum=jE&S?b@C%*r{}%Z@)Q5+phpOLUuU8$lsmORU%sBhEpIVR91cOB zT+G{^CB^cf=j`mV0(lK}4(I45^IW1Q*Luhgzc(e{dUJB>sOv`L$UCydp||EPp6Sgk zTdCd_PF{hFY%#l~G`IMs+?8UG-|ptd;=B?JadV59B^P^_T5jS+Kbf~qb1^WIrp!)C zyU~v0t?*gJ`Ps!eMY%c2_VdzYM#-0cAI=lT-)dbg`Lge;TTc;IhiBxLEiEYS$P}~V zup7&*9F~+#CBMA5n#Pm1u)_Fbn95#SZW+&A^6KQG+{q>0Vu`%wo1t?X{*@rp4E5F| zhQ>2n69&gGhSelzuE~OzV3(7qyJWt8vE;p7>$zAAOyg0YaPRSw_ zh9{c`SWAVIbBySY(oIneg0s=#w|a_Z6)!I+&N2QLc58M)d2Tz`kO2z6f=d3MMso3@ zlAPSMk^+^dPtcRM^B#ZZo04Ivx!KFke-zr_obnX)$nW_cJ_yRyd3H&~Iq9VZ#fpo@ zE8!XKuUGF8HEdEf$*2=u`}@?pMWfTCk9ZG*=qQ!T~9|q?{ZI3>oGPVW}nA zY)~nCRUdOS-*PmIcmuf09NxB8?ZH?aHiH*;3ns1f@F>BYf}GsR`PpU0MO|!VMoGK9 zacM~Kla`m9&wCYDzLb8_yD#*dY{WGPh^ zZWQNoX8t^rImWwU64dVXqYdTKh834DWEB^d+VQ@_|I^;rz-3isdqbk4si9J$p*{)< zC?DthIo}5q1r+d65m8Y<`4$ub6%`XdN-Ik%Eiy|~G%Kd;rVW)f*4X5f&Dg}9v2q$K zn$dD5n{3AJIIZ`;&m)GG_x5(@{oQ%*z32Bk&pBtGz1P}%?X~`Uo%5W%Hgc|K8a1`{ zvUI;-G+pgR)YO17zwFtIDi%c*`6sZHOUA||gzu4E=}bk;@qmC|x#v@g{IZ%JIKEcxr)vNmXq2!Tu`-vGngyYV^!NE#L{>(*>G+Nw zk{5xnzw@8$+~Ys4-f`+8J4?K!!9DIn{+D;ZyW8Wx z{jJ~CU;C31FC7nl&-_W3@;{6GzueWnYf!+)yH4+NUr%4b<1+nQcDf}dcg4Hwcjvt8 z3CMf5{M~QAHdy`kiT6)^h5r?9=_q*xxgT&5a?Q<8%yZ4{2Qd5REpqL+s{T4(JMQN{ zZ=3!hHM^#Ig!su{Q$0NY5cdD&ME-jy2E)pD$dc`B!$U_v*P-0C#-D$W3M+T@ICri5SJS_`{)-#0EP3LCL^mPcNy?sEP?(3s z5Ejl$#Cju%i2_peU%FmS~V9>cd%XfACu59K1Uj6?efo|^m^+deJ?}iHJ;MfC4JYF>P!EpnQ z44j*A48yS%M;tnBfv$ZW|BiJ0?GbSLJJKszY5Xn14&Q8n?ziY+-;vJj3x5?ge3O@* z`T3@PM>>l~m4CHSB0o#V-z!&b{`7`xq;I}Pdee8Lvv_Iyk`C~W6W}H#hPTD zxHhZ_bpJQ$NGioO`+uop+l=cYoxo=U%CGFiFB%y=*b}h_x@S1)#1B_Lc#Cj+*NqsP zor%cvuU;3_^|b+UhHpTe)u}c=nDoC}Flu~G@E{?nFd}jCZ{jlxsY}=H)TLW@3YW)I zSiJqtfLN!vZ={4C{KswsW0jEDjJ)O1DKpQ+q|Bn^$~$S&%10=udocE%l#soMJoZ!? z5Sv7ON62SFWAyso10(wVXKN9jU=9I|O#|c9FQl1Ol(g_J%DknC^n&Fya$Gh+_Tb-x zMx^9YxH6G$7JU>sK9`2-KI$`Q#22h=??Dla#{boZzU4-x&E6BvS$_-Y8cgvSg_K(N z0FAA^hwO#7Q{~!w$e3PEU4pt%P;gJ;va2aFX%>wbTZZ!*5@sx=K?x3fK14kt{x%@a z+|xfw`S!!bRke!EC`gXWEPNp*Z8lkj4HP$_fC8@XPop!6Xu|S`DXID{5^`^)JuN?` zC@YNuuj@jAUAodhVH`z`FQ5_0WyDXfb@xGBqRangehdwFrc&rAoq|Fr@~r!1i4+RZd&%^u=1iz%RIKN^`blN_{LowuBB7|v7usx_35@{Rl> z#?FH5lW8E9u#6a!L!*7AbeX?6jz&)|r%}Fg8m#)LU$jOcBcxyA;W4|T9;_e7R^Co} z=~@zJHIQ%BE}Go1o49neKPZ@nc*j%xFl*JFw69<1ndmnv_6kq-&{yp9HN>d6cC3il>PhCLK z6XsH%@R4rW{v$XVk(x_U6Z2^V_(rAArPwL+(QiTQzPmC0;}+jb zOSX^<`9IwJIPKikOi@M(L2q0P8ptKlFmEcQ=gcK7bs9~cIghTtafr)*K&(bF86`A4 zF`K&g8B8I=1&Wzacm;nZi%}Ei&}i)Aax18A%^jr8siV>9MHJBcCK@$y9$6*pXxqML z=;c>lb@?09N-x_V(|M0Mg+{AmXkK|W$;p!_xc@*u|2UnZ)8|3{MKn~+r6KwhS3jVC zSI9qg$pbWd!3x^B=W&<6JZlm9YlhqYf$=(Qdm7#IU=uy{^a0AAiM~wqx&7Y-7&tO* z4h@#ZQMX*8>w;jvlPC&2jFf5Avu`*B28V+G6tur9b?_e$tJC%1IWQrSdh`vW zUSY#r{vku6fX!LZ|M|p0_nB=n9lV_fjct za;1xxSMUdps?N|?6D9Un%Hn{@?$X8&mzUt)k0H^rD3YZ`%pJ1Kfh7F*wjJjys4 zTX6?vtZX8qcon5CebAMU%OCBJpH@ZDu(2U}CVT-OMU2g%kfAYt`HvLnW+8(HdwsA0 z)FWdaFskxf{ckT=PjcQeib*d=J+T9C2g_sq_zz6Cfz7GQwz=b=uVEKuF5ODQqehcw zhiz`tFhCf}{{mb}gl&^tq z6wys_2F8%-l(_ItU_udfyGf>Q{YTLtX*@a5&-9v2WKWt+X)_9``jO|TddDG3n^j5) zb_&I$%%CAgI-My61|D3!w1OxG4V>mS3h=+s&0n+lTVEF&qm zp17e@GodfuEuQOQebgpo(yv~rq|x16$T9;BJ|k5JyaM=7^q56xJy18I#k5%=P} z4h@P_ssFG{@E=KmfgvEAfZveB#18$eXv6k{8{JwiHv; zn8h?W9=154p0a9MsAAi1sd~>Rv|z{kG-vHIGbhd`Q)M&rsS<&X_s%wmS0fz zl3g^l>Om^M|5sG@z&kXd1iF}i8)cN6R|Rjn*Tu6yW45z z@@7)UW{{Z5#zkB+>miz4xrOGe zdzK3Bc!7LnE2#cQZ_u=bt4XoO{#?`?Uq}CU97RR7B}F@^C8kgTc-QRxlqz?8K$EH- zr*U)EyX{=G^FyScrd-G%Eq^VI&s_!iG*iX)4~X5HSh~f{o45W2s)FomAN`c(u5TrM z%mh-*F$ZNM$?o8Ht)t}CW0CLSu~Un`D!A=g=wCaPZTlS+-u)_N*FHj%$~I9>-5#oZ z7-{#tN4~1vZrar9hbaG!m)-i+z)tefCu66Ud<9xN7|m<(yH+~-Wss~px7#UMpUqmn zn`)YW4}CmM#rGY9KJKK+Rgii4cId?;G;j0IU43JDWIV@Wk@S()!k|NG{X6uled zF$eowlS`J+@|HKK?(x^jhjXO84LQ$&%-AcE|L2>eCr(hTfXhzvD2|gvE|!bGh?y%)|_oNw^;n@M6$sU;AnJ-~IUM zgRy&;eXAXGiNmD?93t`NA`tXgGzK=aqy*GhG?XGx_OAniOl}?yb89%nU1IxO_kE3y z8B~FY+{b3uuoxZ-5Emj7$Bwx+EJ_MXEkP^T9H0`+gYmBd#@6t;5oJ4gvAIZWy-!vL z7URRe;smQ@65ikV=i9L89@U`7<}rEP*q4kJi{8N2{9s$O+rAE(*={*5_SLVH1Vjc2gFAV~rd2Du4M~ymZM?fTK z;p=+bWxdTJqjb!KTEJN0F@8VGaMc!cMSc7?uS_$Yy2=VHUR+&+b>gikKAwy66tS<0 z@X?1AH7#p=oEqi9YS~y}5A&3Ytf*y0HBpY;yQhiObFo@o#WD{FEU2-fuwt`bGq1F$ zvY;joiw##VtXWuG8#ixZrCv}|8NY;&!b;N%%8Ie5c6MiOAm#CxSZKSZmVNqiiQ4f0 zfSNemEr*gdMe`Qpi=btfu8~q*v_z(OOleJn#(sk-yT&GjYDxW zp+j!CxT?4QM_mGmeQ59iE}EOh6?5ykC%A)LJNGHKh5tE!z2Fm;34aj&F8IV^@w_-y zDwE!q+NFP!M#(GXC*>gJ2F0Uzl}u%Ya-H{G^&H|q;On_S%wOe}ax1yJxTD;cTqw`; zNqi2!k>ABX#+yQs@Tzc5=qmOUZxA_A7RQMD#WV@=C!`h98fllbPkK^1AU!V~mR^&7 zA^od#Qu;{xMEYF%N(z*F$T!MEH%1h)%dB6Ou{JQ)H`761XQm)*u z98T~MP)c4geZI~u& zhE}XqX!mLlX*;!5`VRdu{eAr_T{jYq4aPmj{l+2VW#jk8=SCmXXU;K~nd{6jYnT;d z6DfS>|5+J_GfHR;xI3kDHgYr4-yBU)nmoarD!=p zmSj~nAJIS4&+G7C zj5%hJd55{z{F&KizGr@5c0)T7EW=8%W?74@2dtl3Z&+Vj!S*0K!WQjhJI&6t7o#<6 z?G1K|-D^{MPx*InUY-1>1r{ zFYZRJn!APj3)i0?%5UKxOS=ub+{IzWoX&j94$jH(O2r5^e6OR>YwOe>by~8t}q`qpD=%HzHYu{zGMCw zvI?=nEY6x{-EB2mk66vt4R)A4+0JD$%12B{9OiPHxvg9~f02(7l7%c`hA>Yk6>5ZA zgw4W(!f%DXqAE@iH;RYEH^dLbzlwdN8>L~ABx%w(X@WFMdR00Gi}+OfyVO;_K@Njn z8ge%Jav!YY6}hi6M2S&EMN=}A8OpDe_muA55bt2`0`HBgqHa>Rsb|zcZKC#pc0pUB zKd&FtuQR$Ep@zp$j5MR(xYO8d>@*%V{>6CSc-45r__cA;_{8|axL|ZOdzy9TR&%fw zVaZkv^lz`#-yUdxY8y_4v)%avlY5n4?jLb~p+O^pLbu+6((1C6AFO z$hSeZKavm1C*?1JJ|U2AqB2F9t;|(Qlr_pmWuNkl(yF|uoKQYgzE*m8`*{0%o4oafee4syBM*CXiPG)jY?y!@e=yzMl%MM^@#Zk z^Nbk@TdK5nTR*crHfPVWEA3VGJ@#(MA4?6X!_s`z1 zya8%&H4_$7sxDSHs#CS&+B@3kS{I$u=jc1lN@usj{=jAS*pG|l3@#10U%)NoR&iT^ z{lDPOz&?Zce*9qcPzwJ5{~BK}#EY}U4dT0EZ)u8jP8uecC@&~UUPJv#t<&~szt&D_ zhMuI)*H`H?jb+9yhG1G|hPeuO6>JTHW-Nwg+-vQ#4qDG!N3BeIwSB++s2%5wb=EpN zoR^$GIV{-gKGbPB_aOHx?km(Fl)saIh5vy6D<3F$1RGlWYk`Ep;zPiSKZ-r1doezI zF7*a-&X!lnU&}L=-XUZ1_(USsdDpR!-DU$@_9{c{*` z3vjrBo6K$C_HfT}N4UQHgM5H+P>2!7i`n86VvBT0dK2T;$I@S)$CbX`PRtkwerM<3+x~AKDvOZ0}512j4 zcoDYW+nj4{w|{KEX@3O$VS%vO(^9gKBIbzi%j1=apu9raq-<3ddUt!@_3l((Qt#ED z)Yll#n$f_$r8b)f;)DG~^07R}OMD`q%%}3{ypPX?#uf4n{5pOE-^4fb`}jlrVg3kz zlt0EF=iB%b{7L>4-_D=r&+@@StiTD9kSe6ZL#PsK#X7NGY!KH$OEv;Yw!n6`i%nv) zxKC`s=zahge@Hxh8Rgr6G$+MVV!L=+JS(1qXK_KiC}I&UDOl)urQ}AR?OJ||c=cNnMMPS~3jI)Q0BgRqVm~q@_GfqI} z?Z#>2tZ~jb57fPA1em?dP&3R7H$7&g3E`hZoD0ZvgS=7REN_7)vR!VHn}Po=@JSBH zt@0uHuzW;5Dj$=N%Wd)rAXmpIe@;FxUyv`#SS(BlR(b)=LX|Kj93EAq60O84oMHmq zl9f~?UGXVdN{*5XyDwBql?tUwsa5KfdZj^G2VL5zY*w}?jmmatRWmTKMcJ<$01h5f z4l75Lqwt1~D{a8UlgcTjT{*4fsJUvsTBtUv+tntu8Dnb;JfQ<>t9nR1tR7L1s>jsh zYMXjOJ*l389kp2}t#<3Ib>6yY1=+pqP&?d?v}0|_HsKqj+gZ@9Lc7AQwd?J5_C|Y) zz1?nxpK!oFWFN7Q*=_boyWKtu{J&@iIlY`vC)|m2V&OHIPO_5@1k7~`F>cj5_0BqH zqq7BA*zB}82b@FB5$BlG=A3leowM*JE;1X$6WRvIJc#SXg>vCsByddPOfH#A=d!q5 zu8^zXYPouD9k&sfww-I{TDSw;Az11$u8lhh@AfQrp1TN)=>?Pt=b0@?ya`K4$C#80 zE2!XW`FdEwMtCXPVGAw%0a(KkSV9}DpdA))9v(xG&`Ssv!i7j!gCv;nAJXALnN2oQtC5HU>jh|zc+ zQN=_tRrCQ-^TkrJ3dq&~RND+hYXX|>2a+9zuX-E^b_(cq4#*V%)CvJ&d4N_NkSY-< zn zc*1+Z8>Xsij#>w7Yz6+cs~6P(&7&o1`C5b41eqSz*Z`V^`sYBe8==#!(C1UYlyg9p b5Fkq8ccS&xNZsJweu-lUID*`N$@YH$)PUQz diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 1ac671f46..30f3e457d 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -60,7 +60,7 @@ task makeExecutable(dependsOn: jar) doLast { } def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length() - 4) + ".exe") def fos = new FileOutputStream(loc) - def is = new FileInputStream(new File(project.buildDir, '../HMCLauncher.exe')) + def is = new FileInputStream(new File(project.projectDir, "src/main/resources/assets/HMCLauncher.exe")) int read def bytes = new byte[8192] while((read = is.read(bytes)) != -1) From b791878651709c708412a0ac2646a5cc7f5a33e0 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 14:02:26 +0800 Subject: [PATCH 10/32] Remove generateSources task --- HMCL/build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 30f3e457d..b41432454 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -9,14 +9,6 @@ dependencies { compile rootProject.files("lib/JFoenix.jar") } -task generateSources(type: Sync) { - from 'src/main/java' - into "$buildDir/generated-src" -} - -compileJava.setSource "$buildDir/generated-src" -compileJava.dependsOn generateSources - configurations { coreCompile.extendsFrom compile coreRuntime.extendsFrom runtime From e91e1cab47921500126833f96a30a81cc09e3af7 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 14:04:36 +0800 Subject: [PATCH 11/32] Remove coreCompile/Runtime.extendsFrom --- HMCL/build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index b41432454..300e05303 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -9,11 +9,6 @@ dependencies { compile rootProject.files("lib/JFoenix.jar") } -configurations { - coreCompile.extendsFrom compile - coreRuntime.extendsFrom runtime -} - jar { from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } From c815fa80e89a02bccfb996c5e8acc8ab8083710d Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 14:06:54 +0800 Subject: [PATCH 12/32] Prevent /build being created --- build.gradle | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 8a8ac1fc2..b5b9b7983 100644 --- a/build.gradle +++ b/build.gradle @@ -19,14 +19,7 @@ group 'org.jackhuang' version '3.0' -repositories { - mavenCentral() -} - -allprojects { - group 'org.jackhuang' - version '3.0' - +subprojects { apply plugin: 'java' apply plugin: 'idea' From e0608cdf3e6354604e1c01bd90cf914684f07768 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 14:14:01 +0800 Subject: [PATCH 13/32] Remove unused files --- HMCL/HMCL.keystore | Bin 2284 -> 0 bytes HMCL/build.gradle | 6 ------ HMCL/icon.ico | Bin 4286 -> 0 bytes HMCL/src/main/icon.icns | Bin 158805 -> 0 bytes 4 files changed, 6 deletions(-) delete mode 100644 HMCL/HMCL.keystore delete mode 100644 HMCL/icon.ico delete mode 100644 HMCL/src/main/icon.icns diff --git a/HMCL/HMCL.keystore b/HMCL/HMCL.keystore deleted file mode 100644 index ea91d8309b26af5720b3b9e7d72da1d231a9a66d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2284 zcmd5-=Tp-O6HcQ;r~*oF0s-+CzyOK@(xoWvC?XOkq+`6 zh>05MO^S3xiY20ffB~hv=-kcR%=-_#ANIqu&+g97vpc){Yx`>;5D0Q;;9myAUc&?& z0=FQ0cMJ%`1_H>iT>#F`t;P<9fe{cvFqjPlBf~xr+HO=P*%E@dE?w3Nx^y6C93dAl zB}%O3rCR~lyH%%n_BbuW6_(P0;3oZ2MowATc@t`sHQsL1va90vR|~>UaZftAF3Sgx zH=a2sD&Ag*{D|t$7HM-HD^B4WMsT>2?pM8YyTwsO>fSlMU0466%c8(~z0m^mCa7Xo zdEJJ+^EZ*<@G=K+(};-Rf$3%RYOM{Ms3tok5Qfw0EjBKRAhWhx zLPI-JTLG_E zd1dqNM5H}8F_cs0I#yeSTKV9QNiW2H3M35pJ1=J`7c~`-7afij8pRF3x?ec#O2z!{ zEoLVhwfH@acJWd#=GFF)f3%KP7@vmxi$cq}E$SA(@1SAq36phG8Fa<*N6|V+Fb~BpJB)@=~ zTelec{C%3?XG3lTr^)g137yAcn5m~$;p;26bDCdv5H8LymxYs(pinLJ9F)Z~sNzcaS8*Ykq2>zM5up9BR0lm?!=CBuVH zD&q~@<5?bMOW$qez*D*=w_Xm~>)H45a2ijJ{CIkh7ANW}2Z)Ax3@lO1N+RjfSEc9O z>UO4LBS#c2Fj+y1O0^n!brGJ!)BUYOwXoW@p2B+sU)Wjo$IZNG;U-6c0EueJzMK}a z+8lC!MCNh<@kD!|w2q4**9E+n&+9jorBP>jgBn8wwL`4gj#~M-L4dIl+fwOF9%6G= zgUi>lyHcuQDv^jX+&Y+~&~tgl$!aElPI8d#z0%BCGV9h$$4OhefKtWH_jZN?63wP_ zJT-{gJBE8n=54)oUl7o!QToHx`J$sv&2&WLsD$ly49}e6n-1DhqOp?gQ6KkcXHzbx z*|R+K7lWZ2U)k#_68&p}bssG6`Z0G)rSQ9xeHd~!D``>5ompwLb<+MWuWHw7>uQht zVl7VhVpr0Ukm+~uR!WrO;{&^|Hm^=FPw9d13T*~pRtVBLpuW!lFp9j}`i#1#@ z?as`@m&S9VqG_CZ9SUB#4itFgE7i!E-QkwH!87B+927Rxv;j|E;J$OLxKe;l>KbFz z*HJvWaP8$a`CcTmT%4EY)0_N|H$%I>RFYyXTGD73svNi}635g}BEEl3D}SqFUTrWl z9C$1I{Y^(_vEWU{C!^UIsn==zV2Vi;Kd}TfG{a2Z+^UcBx~cg`dflARx9Pk_ndH8Z zcdjm};ufUd^Ms5)?B}E?&&~rSw)0)Cb1k&6Jvymh-drGbqs~|Vn&fFK zK|2e9fa)1mVbvR%tI7}2KvMtlHzIY|n&VEI>R$41Y^3j-h)4xSL+`mH+!Js&a zz_5K|5j*_OG+{NYvcI%Hg6L+S5)k*zn!ezS$)%I5QKeq+nLA&sAyy+KDkGAivjZ(H zCvKZBQ(DYfp-yVN?E)(g)P>XW7xpux8^+KHBBRm9i$#{LR%4_0y=}tdhr^8`>(w(t zEq88?%iXYJY?FsqPfDPW>s99^_Y`^%v=lth+yRsJ+e>7cWG_#CE%ZzzqJtFkcXD%9 zYWb?yXXxV1@~sXvGQ Qiu9DKtSpsjVq#1AFIyA)5C8xG diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 300e05303..509bf5a19 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -31,12 +31,6 @@ jar { } } -processResources { - from(sourceSets.main.resources.srcDirs) { - exclude 'icon.icns' - } -} - task makeExecutable(dependsOn: jar) doLast { ext { jar.classifier = '' diff --git a/HMCL/icon.ico b/HMCL/icon.ico deleted file mode 100644 index fe7ac703b65263266b6fe864bc89bae4c49bb646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 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�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
MZJ~#!I_ys4`PaAliz) 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 Date: Tue, 31 Jul 2018 14:20:58 +0800 Subject: [PATCH 14/32] Remove unnecessary saveConfig() call --- HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index 483be6292..095af6305 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -60,8 +60,6 @@ public class Settings { profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged); profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.saveConfig()); } - - config().addListener(source -> ConfigHolder.saveConfig()); } public Font getFont() { From e38bfdfc736f07b7e803341a43a8d618085c6786 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 15:48:29 +0800 Subject: [PATCH 15/32] Refactor build.gradle --- HMCL/build.gradle | 48 +++++++++++++++++------------------------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 509bf5a19..467d4fcd4 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -9,6 +9,12 @@ dependencies { compile rootProject.files("lib/JFoenix.jar") } +def createChecksum(File file) { + def algorithm = "SHA-1" + def suffix = "sha1" + new File(file.parentFile, file.name + "." + suffix).text = MessageDigest.getInstance(algorithm).digest(file.bytes).encodeHex().toString() + "\n" +} + jar { from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } @@ -20,39 +26,19 @@ jar { } doLast { - def messageDigest = MessageDigest.getInstance("SHA1") - archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead -> - messageDigest.update(buf, 0, bytesRead) - } - def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0') - def fileEx = new File(project.buildDir, "libs/" + archivePath.getName() + ".sha1") - if (!fileEx.exists()) fileEx.createNewFile() - fileEx.append sha1Hex + createChecksum(archivePath) } } -task makeExecutable(dependsOn: jar) doLast { - ext { - jar.classifier = '' - makeExecutableinjar = jar.archivePath - jar.classifier = '' - makeExecutableoutjar = jar.archivePath - jar.classifier = '' - } - def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length() - 4) + ".exe") - def fos = new FileOutputStream(loc) - def is = new FileInputStream(new File(project.projectDir, "src/main/resources/assets/HMCLauncher.exe")) - int read - def bytes = new byte[8192] - while((read = is.read(bytes)) != -1) - fos.write(bytes, 0, read) - is.close() - is = new FileInputStream(makeExecutableoutjar) - while((read = is.read(bytes)) != -1) - fos.write(bytes, 0, read) - is.close() - fos.close() - +def createExecutable(String suffix, String header) { + def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix) + output.bytes = new File(project.projectDir, header).bytes + output << jar.archivePath.bytes + createChecksum(output) } -build.dependsOn makeExecutable +task makeExecutables(dependsOn: jar) doLast { + createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe") +} + +build.dependsOn makeExecutables From dd45e1b3dbbd7f63810e9635f5768be742c77690 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 19:39:21 +0800 Subject: [PATCH 16/32] Add JAR integrity check --- HMCL/build.gradle | 35 ++++- .../hmcl/upgrade/IntegrityChecker.java | 130 ++++++++++++++++++ .../hmcl/upgrade/LocalRepository.java | 2 + .../jackhuang/hmcl/upgrade/UpdateChecker.java | 4 + .../jackhuang/hmcl/upgrade/UpdateHandler.java | 6 +- .../jackhuang/hmcl/util/CrashReporter.java | 3 +- .../assets/hmcl_signature_publickey.der | Bin 0 -> 550 bytes 7 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java create mode 100644 HMCL/src/main/resources/assets/hmcl_signature_publickey.der diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 467d4fcd4..792d68e18 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -1,4 +1,9 @@ +import java.nio.file.FileSystems +import java.security.KeyFactory import java.security.MessageDigest +import java.security.Signature +import java.security.spec.PKCS8EncodedKeySpec +import java.util.zip.ZipFile def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT" def versionroot = System.getenv("VERSION_ROOT") ?: "3.1" @@ -9,10 +14,37 @@ dependencies { compile rootProject.files("lib/JFoenix.jar") } +def digest(String algorithm, byte[] bytes) { + return MessageDigest.getInstance(algorithm).digest(bytes) +} + def createChecksum(File file) { def algorithm = "SHA-1" def suffix = "sha1" - new File(file.parentFile, file.name + "." + suffix).text = MessageDigest.getInstance(algorithm).digest(file.bytes).encodeHex().toString() + "\n" + new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n" +} + +def attachSignature() { + def keyLocation = System.getenv("HMCL_SIGNATURE_KEY"); + if(keyLocation == null) + return; + def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes)) + + def signer = Signature.getInstance("SHA512withRSA") + signer.initSign(privatekey) + new ZipFile(jar.archivePath).withCloseable { zip -> + zip.stream() + .sorted(Comparator.comparing({ it.name })) + .forEach({ + signer.update(digest("SHA-512", it.name.getBytes("UTF-8"))) + signer.update(digest("SHA-512", zip.getInputStream(it).bytes)) + }) + } + def signature = signer.sign() + + FileSystems.newFileSystem(URI.create("jar:" + jar.archivePath.toURI()), [:]).withCloseable { zipfs -> + zipfs.getPath("META-INF/hmcl_signature").bytes = signature + } } jar { @@ -26,6 +58,7 @@ jar { } doLast { + attachSignature() createChecksum(archivePath) } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java new file mode 100644 index 000000000..e6184b575 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java @@ -0,0 +1,130 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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 static java.nio.charset.StandardCharsets.UTF_8; +import static org.jackhuang.hmcl.util.Logging.LOG; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.TreeMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.IOUtils; + +/** + * A class that checks the integrity of HMCL. + * + * @author yushijinhun + */ +public final class IntegrityChecker { + private IntegrityChecker() {} + + private static final String SIGNATURE_FILE = "META-INF/hmcl_signature"; + private static final String PUBLIC_KEY_FILE = "assets/hmcl_signature_publickey.der"; + + private static PublicKey getPublicKey() throws IOException { + try (InputStream in = IntegrityChecker.class.getResourceAsStream("/" + PUBLIC_KEY_FILE)) { + if (in == null) { + throw new IOException("Public key not found"); + } + return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFullyAsByteArray(in))); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to load public key", e); + } + } + + private static boolean verifyJar(Path jarPath) throws IOException { + PublicKey publickey = getPublicKey(); + + byte[] signature = null; + Map fileFingerprints = new TreeMap<>(); + try (ZipFile zip = new ZipFile(jarPath.toFile())) { + for (ZipEntry entry : zip.stream().toArray(ZipEntry[]::new)) { + String filename = entry.getName(); + try (InputStream in = zip.getInputStream(entry)) { + if (SIGNATURE_FILE.equals(filename)) { + signature = IOUtils.readFullyAsByteArray(in); + } else { + fileFingerprints.put(filename, DigestUtils.digest("SHA-512", in)); + } + } + } + } + + if (signature == null) { + throw new IOException("Signature is missing"); + } + + try { + Signature verifier = Signature.getInstance("SHA512withRSA"); + verifier.initVerify(publickey); + for (Entry entry : fileFingerprints.entrySet()) { + verifier.update(DigestUtils.digest("SHA-512", entry.getKey().getBytes(UTF_8))); + verifier.update(entry.getValue()); + } + return verifier.verify(signature); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to verify signature", e); + } + } + + static void requireVerifiedJar(Path jar) throws IOException { + if (!verifyJar(jar)) { + throw new IOException("Invalid signature: " + jar); + } + } + + private static Boolean selfVerified = null; + + /** + * Checks whether the current application is verified. + * This method is blocking. + */ + public static synchronized boolean isSelfVerified() { + if (selfVerified != null) { + return selfVerified; + } + try { + verifySelf(); + LOG.info("Successfully verified current JAR"); + selfVerified = true; + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to verify myself, is the JAR corrupt?", e); + selfVerified = false; + } + return selfVerified; + } + + private static void verifySelf() throws IOException { + Path self = LocalVersion.current().orElseThrow(() -> new IOException("Failed to find myself")) + .getLocation(); + requireVerifiedJar(self); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index 95aaa7336..6df548a27 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -88,6 +88,7 @@ final class LocalRepository { } LOG.info("Downloading " + current.get()); try { + IntegrityChecker.requireVerifiedJar(current.get().getLocation()); Files.createDirectories(localStorage.getParent()); ExecutableHeaderHelper.copyWithoutHeader(current.get().getLocation(), localStorage); } catch (IOException e) { @@ -105,6 +106,7 @@ final class LocalRepository { } LOG.info("Applying update to " + target); + IntegrityChecker.requireVerifiedJar(localStorage); ExecutableHeaderHelper.copyWithHeader(localStorage, target); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java index 6c8adee7d..3280cebad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -83,6 +83,10 @@ public final class UpdateChecker { return; } + if (!IntegrityChecker.isSelfVerified()) { + return; + } + RemoteVersion fetched = RemoteVersion.fetch(source); Platform.runLater(() -> { if (source.equals(updateSource.get())) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 5c414da7a..7ff5a7100 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -97,7 +97,7 @@ public final class UpdateHandler { } else if (difference > 0) { Optional current = LocalVersion.current(); - if (current.isPresent()) { + if (current.isPresent() && IntegrityChecker.isSelfVerified()) { try { requestUpdate(local.get().getLocation(), current.get().getLocation()); } catch (IOException e) { @@ -118,6 +118,7 @@ public final class UpdateHandler { } private static void requestUpdate(Path updateTo, Path self) throws IOException { + IntegrityChecker.requireVerifiedJar(updateTo); startJava(updateTo, "--apply-to", self.toString()); } @@ -177,6 +178,9 @@ public final class UpdateHandler { if (!stored.isPresent()) { throw new IOException("Failed to find local repository, this shouldn't happen"); } + if (!IntegrityChecker.isSelfVerified()) { + throw new IOException("Current JAR is not verified"); + } requestUpdate(stored.get().getLocation(), current.get().getLocation()); System.exit(0); } catch (IOException e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index 3b4a2d5f1..c08edfd74 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -22,6 +22,7 @@ import javafx.application.Platform; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.ui.CrashWindow; import org.jackhuang.hmcl.ui.construct.MessageBox; +import org.jackhuang.hmcl.upgrade.IntegrityChecker; import org.jackhuang.hmcl.upgrade.UpdateChecker; import static java.util.Collections.newSetFromMap; @@ -107,7 +108,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler { if (checkThrowable(e)) { Platform.runLater(() -> new CrashWindow(text).show()); - if (!UpdateChecker.isOutdated()) { + if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) { reportToServer(text); } } diff --git a/HMCL/src/main/resources/assets/hmcl_signature_publickey.der b/HMCL/src/main/resources/assets/hmcl_signature_publickey.der new file mode 100644 index 0000000000000000000000000000000000000000..d01d9ea4cb4e039c3ee8e9603aafe5f730e36794 GIT binary patch literal 550 zcmV+>0@?jAf&wBi4F(A+hDe6@4FLfG1potr0uKN%f&vNxf&u{m%9$6Oj6&S3(6E4j zE7?S9ed2K+4p9*sTU+V?+~V$cv=MMJ79CE*T?ZkLk|AN$hQcIE`L8!6GFz0$J0`a0~i!yUX8eq9djRXpYmO3BL?d@f01)JVbBLJrDvX#?8PcjdMgzE{va4 zXyasIVfU5Kuc&S%iKqWvW8;yN2husizR8?lHvwz_KP+|UKC&FCxO$w|C)|atGP#@t z&=@^O+Ak}14jI14#S@9~5rozaIVARU9cInvnrE zRut-)Ag=Zannij77v|jm4rHr)*uVzFH{D>>0PjQ=Fu=87|As+_oeSy<1CPOl4N|+% zA5lk87~LB2uC8g3SLF<%1OAGw6#~{RikWH`= oCggN^>FKXCmnCv@kuJYi9a@wBU^Ngt$jFs5*+`080s{d60c$l1=Kufz literal 0 HcmV?d00001 From 4f52bb2ffe8a12e7bc046af4bf3da96033ec5a65 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 19:41:50 +0800 Subject: [PATCH 17/32] Add option hmcl.update_source.override --- HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java | 2 +- .../src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index e5f5d8973..9a810eed4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -29,7 +29,7 @@ public final class Metadata { public static final String NAME = "HMCL"; public static final String TITLE = NAME + " " + VERSION; - public static final String UPDATE_SERVER_URL = "https://www.huangyuhui.net"; + public static final String UPDATE_SERVER_URL = System.getProperty("hmcl.update_source.override", "https://www.huangyuhui.net"); public static final String CONTACT_URL = UPDATE_SERVER_URL + "/hmcl.php"; public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html"; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java index 3280cebad..0d10dff8f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -37,7 +37,7 @@ public final class UpdateChecker { private UpdateChecker() {} private static ObjectProperty latestVersion = new SimpleObjectProperty<>(); - private static StringProperty updateSource = new SimpleStringProperty("http://localhost/hmcl/update_link"); + private static StringProperty updateSource = new SimpleStringProperty(Metadata.UPDATE_SERVER_URL + "/api/update_link"); private static BooleanBinding outdated = Bindings.createBooleanBinding( () -> { RemoteVersion latest = latestVersion.get(); From 3b42ba937549bfe11e9a3d389c72482423d8885b Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Tue, 31 Jul 2018 19:46:22 +0800 Subject: [PATCH 18/32] Add version query when checking for update --- .../jackhuang/hmcl/upgrade/UpdateChecker.java | 6 ++++- .../org/jackhuang/hmcl/util/NetworkUtils.java | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java index 0d10dff8f..805ec3ffa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -17,11 +17,14 @@ */ package org.jackhuang.hmcl.upgrade; +import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.VersionNumber.asVersion; import java.io.IOException; import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.util.NetworkUtils; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -37,7 +40,8 @@ public final class UpdateChecker { private UpdateChecker() {} private static ObjectProperty latestVersion = new SimpleObjectProperty<>(); - private static StringProperty updateSource = new SimpleStringProperty(Metadata.UPDATE_SERVER_URL + "/api/update_link"); + private static StringProperty updateSource = new SimpleStringProperty( + NetworkUtils.withQuery(Metadata.UPDATE_SERVER_URL + "/api/update_link", mapOf(pair("version", Metadata.VERSION)))); private static BooleanBinding outdated = Bindings.createBooleanBinding( () -> { RemoteVersion latest = latestVersion.get(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java index 91fecb24f..3e87a5fb8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java @@ -20,9 +20,11 @@ package org.jackhuang.hmcl.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.net.*; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.function.Supplier; @@ -48,6 +50,27 @@ public final class NetworkUtils { NetworkUtils.userAgentSupplier = Objects.requireNonNull(userAgentSupplier); } + public static String withQuery(String baseUrl, Map params) { + try { + StringBuilder sb = new StringBuilder(baseUrl); + boolean first = true; + for (Entry param : params.entrySet()) { + if (first) { + sb.append('?'); + first = false; + } else { + sb.append('&'); + } + sb.append(URLEncoder.encode(param.getKey(), "UTF-8")); + sb.append('='); + sb.append(URLEncoder.encode(param.getValue(), "UTF-8")); + } + return sb.toString(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + public static HttpURLConnection createConnection(URL url) throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setDoInput(true); From 38f27d134dcc331df5a6bfe93fb0080e48a2aed1 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Wed, 1 Aug 2018 12:18:07 +0800 Subject: [PATCH 19/32] Change task.test() to executor.test() --- .../src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 7ff5a7100..3b2a6e4cf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -166,7 +166,7 @@ public final class UpdateHandler { TaskExecutor executor = task.executor(); Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null); thread(() -> { - boolean success = task.test(); + boolean success = executor.test(); Platform.runLater(() -> dialog.fireEvent(new DialogCloseEvent())); if (success) { try { From b78beb3305ff9b4f43037320317e51c136696aa9 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Wed, 1 Aug 2018 12:32:37 +0800 Subject: [PATCH 20/32] Copy downloaded HMCL to .hmcl only after it's verified --- .../hmcl/upgrade/LocalRepository.java | 24 ++++++++++++------- .../jackhuang/hmcl/upgrade/UpdateHandler.java | 8 ++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index 6df548a27..03358beda 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -19,11 +19,10 @@ package org.jackhuang.hmcl.upgrade; import static org.jackhuang.hmcl.util.Logging.LOG; import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.Optional; import java.util.jar.Attributes; import java.util.jar.JarFile; @@ -63,12 +62,21 @@ final class LocalRepository { /** * Creates a task that downloads the given version to local repository. */ - public static FileDownloadTask downloadFromRemote(RemoteVersion version) { - try { - return new FileDownloadTask(new URL(version.getUrl()), localStorage.toFile(), version.getIntegrityCheck()); - } catch (MalformedURLException e) { - throw new UncheckedIOException(e); - } + public static FileDownloadTask downloadFromRemote(RemoteVersion version) throws IOException { + Path stage = Files.createTempFile("hmcl-update-", ".jar"); + return new FileDownloadTask(new URL(version.getUrl()), stage.toFile(), version.getIntegrityCheck()) { + @Override + public void execute() throws Exception { + try { + super.execute(); + IntegrityChecker.requireVerifiedJar(stage); + Files.createDirectories(localStorage.getParent()); + Files.copy(stage, localStorage, StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(stage); + } + } + }; } /** diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 3b2a6e4cf..67f710adb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -162,7 +162,13 @@ public final class UpdateHandler { public static void updateFrom(RemoteVersion version) { checkFxUserThread(); - Task task = LocalRepository.downloadFromRemote(version); + Task task; + try { + task = LocalRepository.downloadFromRemote(version); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to create upgrade download task", e); + return; + } TaskExecutor executor = task.executor(); Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null); thread(() -> { From be37014206d1c467f2ab0544f3e3756e45fb33b5 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 4 Aug 2018 21:17:52 +0800 Subject: [PATCH 21/32] Use Paths.get(url.toURI()) in LocalVersion.current() --- .../jackhuang/hmcl/upgrade/LocalVersion.java | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java index bc9fb5b5d..b1f613742 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java @@ -17,14 +17,12 @@ */ package org.jackhuang.hmcl.upgrade; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.jackhuang.hmcl.util.Logging.LOG; -import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; import java.net.URL; -import java.net.URLDecoder; +import java.nio.file.FileSystemNotFoundException; import java.nio.file.Files; -import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.security.CodeSource; @@ -47,22 +45,11 @@ class LocalVersion { return Optional.empty(); } - String pathString = url.getFile(); - if (pathString.isEmpty()) { - return Optional.empty(); - } - - try { - pathString = URLDecoder.decode(pathString, UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - Path path; try { - path = Paths.get(pathString); - } catch (InvalidPathException e) { - LOG.log(Level.WARNING, "Invalid path: " + pathString, e); + path = Paths.get(url.toURI()); + } catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) { + LOG.log(Level.WARNING, "Invalid path: " + url, e); return Optional.empty(); } From 7096135cdf044c4d97be1896ff09a64fad49f0c2 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 4 Aug 2018 21:26:46 +0800 Subject: [PATCH 22/32] Change update source to hmcl.huangyuhui.net --- HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index 9a810eed4..702b379af 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -29,7 +29,7 @@ public final class Metadata { public static final String NAME = "HMCL"; public static final String TITLE = NAME + " " + VERSION; - public static final String UPDATE_SERVER_URL = System.getProperty("hmcl.update_source.override", "https://www.huangyuhui.net"); + public static final String UPDATE_SERVER_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net"); public static final String CONTACT_URL = UPDATE_SERVER_URL + "/hmcl.php"; public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html"; } From bdb7afa8601e308673483ed24e2f0e2d162a4195 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 4 Aug 2018 21:39:19 +0800 Subject: [PATCH 23/32] Replace updateSource with updateChannel --- .../java/org/jackhuang/hmcl/Metadata.java | 4 +- .../jackhuang/hmcl/upgrade/UpdateChecker.java | 45 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index 702b379af..4ece46b5c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -29,7 +29,7 @@ public final class Metadata { public static final String NAME = "HMCL"; public static final String TITLE = NAME + " " + VERSION; - public static final String UPDATE_SERVER_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net"); - public static final String CONTACT_URL = UPDATE_SERVER_URL + "/hmcl.php"; + public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link"); + public static final String CONTACT_URL = "https://www.huangyuhui.net/hmcl.php"; public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html"; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java index 805ec3ffa..7d0497c9a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -39,9 +39,12 @@ import javafx.beans.value.ObservableBooleanValue; public final class UpdateChecker { private UpdateChecker() {} + public static final String CHANNEL_STABLE = "stable"; + public static final String CHANNEL_DEV = "dev"; + + private static StringProperty updateChannel = new SimpleStringProperty(CHANNEL_STABLE); + private static ObjectProperty latestVersion = new SimpleObjectProperty<>(); - private static StringProperty updateSource = new SimpleStringProperty( - NetworkUtils.withQuery(Metadata.UPDATE_SERVER_URL + "/api/update_link", mapOf(pair("version", Metadata.VERSION)))); private static BooleanBinding outdated = Bindings.createBooleanBinding( () -> { RemoteVersion latest = latestVersion.get(); @@ -53,6 +56,18 @@ public final class UpdateChecker { }, latestVersion); + public static String getUpdateChannel() { + return updateChannel.get(); + } + + public static void setUpdateChannel(String updateChannel) { + UpdateChecker.updateChannel.set(updateChannel); + } + + public static StringProperty updateChannelProperty() { + return updateChannel; + } + public static RemoteVersion getLatestVersion() { return latestVersion.get(); } @@ -61,18 +76,6 @@ public final class UpdateChecker { return latestVersion; } - public static String getUpdateSource() { - return updateSource.get(); - } - - public static void setUpdateSource(String updateSource) { - UpdateChecker.updateSource.set(updateSource); - } - - public static StringProperty updateSourceProperty() { - return updateSource; - } - public static boolean isOutdated() { return outdated.get(); } @@ -82,18 +85,18 @@ public final class UpdateChecker { } public static void checkUpdate() throws IOException { - String source = updateSource.get(); - if (source == null) { - return; - } - if (!IntegrityChecker.isSelfVerified()) { return; } - RemoteVersion fetched = RemoteVersion.fetch(source); + String channel = getUpdateChannel(); + String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf( + pair("version", Metadata.VERSION), + pair("channel", channel))); + + RemoteVersion fetched = RemoteVersion.fetch(url); Platform.runLater(() -> { - if (source.equals(updateSource.get())) { + if (channel.equals(getUpdateChannel())) { latestVersion.set(fetched); } }); From 04553c2c5688cfa260edc54c866f97f3ee53ad74 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sat, 4 Aug 2018 22:04:59 +0800 Subject: [PATCH 24/32] Support packed update archive --- .../hmcl/upgrade/LocalRepository.java | 20 +++++++++++--- .../jackhuang/hmcl/upgrade/RemoteVersion.java | 26 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index 03358beda..dd873d820 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -26,7 +26,10 @@ import java.nio.file.StandardCopyOption; import java.util.Optional; import java.util.jar.Attributes; import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Pack200; import java.util.logging.Level; +import java.util.zip.GZIPInputStream; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -63,17 +66,26 @@ final class LocalRepository { * Creates a task that downloads the given version to local repository. */ public static FileDownloadTask downloadFromRemote(RemoteVersion version) throws IOException { - Path stage = Files.createTempFile("hmcl-update-", ".jar"); + Path stage = Files.createTempFile("hmcl-update-", ""); return new FileDownloadTask(new URL(version.getUrl()), stage.toFile(), version.getIntegrityCheck()) { @Override public void execute() throws Exception { + Path jar = stage; try { super.execute(); - IntegrityChecker.requireVerifiedJar(stage); + if (version.getType() == RemoteVersion.Type.PACK) { + Path unpacked = Files.createTempFile("hmcl-update-", ".jar"); + try (GZIPInputStream stream = new GZIPInputStream(Files.newInputStream(jar)); + JarOutputStream out = new JarOutputStream(Files.newOutputStream(unpacked))) { + Pack200.newUnpacker().unpack(stream, out); + } + jar = unpacked; + } + IntegrityChecker.requireVerifiedJar(jar); Files.createDirectories(localStorage.getParent()); - Files.copy(stage, localStorage, StandardCopyOption.REPLACE_EXISTING); + Files.copy(jar, localStorage, StandardCopyOption.REPLACE_EXISTING); } finally { - Files.deleteIfExists(stage); + Files.deleteIfExists(jar); } } }; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java index 0f5c6e14f..33fda5f62 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java @@ -34,9 +34,16 @@ public class RemoteVersion { try { JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class); String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing")); - String downloadUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("jar is missing")); - String sha1 = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("jarsha1 is missing")); - return new RemoteVersion(version, downloadUrl, new IntegrityCheck("SHA-1", sha1)); + String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null); + String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null); + String packUrl = Optional.ofNullable(response.get("pack")).map(JsonElement::getAsString).orElse(null); + String packHash = Optional.ofNullable(response.get("packsha1")).map(JsonElement::getAsString).orElse(null); + if (packUrl != null && packHash != null) + return new RemoteVersion(version, packUrl, Type.PACK, new IntegrityCheck("SHA-1", packHash)); + else if (jarUrl != null && jarHash != null) + return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash)); + else + throw new IOException("Missing both jar and pack download URL"); } catch (JsonParseException e) { throw new IOException("Malformed response", e); } @@ -44,11 +51,13 @@ public class RemoteVersion { private String version; private String url; + private Type type; private IntegrityCheck integrityCheck; - public RemoteVersion(String version, String url, IntegrityCheck integrityCheck) { + public RemoteVersion(String version, String url, Type type, IntegrityCheck integrityCheck) { this.version = version; this.url = url; + this.type = type; this.integrityCheck = integrityCheck; } @@ -60,6 +69,10 @@ public class RemoteVersion { return url; } + public Type getType() { + return type; + } + public IntegrityCheck getIntegrityCheck() { return integrityCheck; } @@ -68,4 +81,9 @@ public class RemoteVersion { public String toString() { return "[" + version + " from " + url + "]"; } + + public enum Type { + PACK, + JAR + } } From b1ce3c31f52c7d26fa11da01a89cf92a5eec3fe3 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sat, 4 Aug 2018 22:12:02 +0800 Subject: [PATCH 25/32] Build packed archive --- HMCL/build.gradle | 58 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 792d68e18..b4f925494 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -1,9 +1,15 @@ import java.nio.file.FileSystems +import java.nio.file.StandardOpenOption import java.security.KeyFactory import java.security.MessageDigest import java.security.Signature import java.security.spec.PKCS8EncodedKeySpec +import java.util.jar.JarFile +import java.util.jar.JarOutputStream +import java.util.jar.Pack200 +import java.util.zip.GZIPOutputStream import java.util.zip.ZipFile +import java.nio.file.Files def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT" def versionroot = System.getenv("VERSION_ROOT") ?: "3.1" @@ -24,26 +30,27 @@ def createChecksum(File file) { new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n" } -def attachSignature() { +def attachSignature(File jar) { def keyLocation = System.getenv("HMCL_SIGNATURE_KEY"); - if(keyLocation == null) - return; + if (keyLocation == null) + return def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes)) def signer = Signature.getInstance("SHA512withRSA") signer.initSign(privatekey) - new ZipFile(jar.archivePath).withCloseable { zip -> + new ZipFile(jar).withCloseable { zip -> zip.stream() - .sorted(Comparator.comparing({ it.name })) - .forEach({ - signer.update(digest("SHA-512", it.name.getBytes("UTF-8"))) - signer.update(digest("SHA-512", zip.getInputStream(it).bytes)) - }) + .sorted(Comparator.comparing { it.name }) + .filter { it.name != "META-INF/hmcl_signature" } + .forEach { + signer.update(digest("SHA-512", it.name.getBytes("UTF-8"))) + signer.update(digest("SHA-512", zip.getInputStream(it).bytes)) + } } def signature = signer.sign() - FileSystems.newFileSystem(URI.create("jar:" + jar.archivePath.toURI()), [:]).withCloseable { zipfs -> - zipfs.getPath("META-INF/hmcl_signature").bytes = signature + FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs -> + Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature"), StandardOpenOption.CREATE, StandardOpenOption.WRITE).bytes = signature } } @@ -52,13 +59,13 @@ jar { manifest { attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.', - 'Main-Class': 'org.jackhuang.hmcl.Main', - 'Multi-Release': 'true', - 'Implementation-Version': version + 'Main-Class': 'org.jackhuang.hmcl.Main', + 'Multi-Release': 'true', + 'Implementation-Version': version } doLast { - attachSignature() + attachSignature(archivePath) createChecksum(archivePath) } } @@ -70,8 +77,29 @@ def createExecutable(String suffix, String header) { createChecksum(output) } +task makePackGz(dependsOn: jar) doLast { + def tmp = new File(project.buildDir, "tmp") + def unpackedJar = new File(tmp, jar.archivePath.name) + def packGz = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack.gz") + + def originalStream = new ByteArrayOutputStream() + def unpackedJarStream = new JarOutputStream(new FileOutputStream(unpackedJar)) + Pack200.newPacker().pack(new JarFile(jar.archivePath), originalStream) + Pack200.newUnpacker().unpack(new ByteArrayInputStream(originalStream.toByteArray()), unpackedJarStream) + unpackedJarStream.close() + attachSignature(unpackedJar) + + new GZIPOutputStream(new FileOutputStream(packGz)).withCloseable { stream -> + Pack200.newPacker().pack(new JarFile(unpackedJar), stream) + } + + createChecksum(packGz) +} + + task makeExecutables(dependsOn: jar) doLast { createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe") } +build.dependsOn makePackGz build.dependsOn makeExecutables From 86030e64ca292a43f2d85f628935cdf57598aa29 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 4 Aug 2018 23:10:14 +0800 Subject: [PATCH 26/32] Extract writeToStorage() --- .../hmcl/upgrade/LocalRepository.java | 55 +++++++++++++------ .../jackhuang/hmcl/upgrade/RemoteVersion.java | 9 +-- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index dd873d820..27e87ba22 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.upgrade; import static org.jackhuang.hmcl.util.Logging.LOG; import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -62,30 +63,50 @@ final class LocalRepository { } } + private static void writeToStorage(Path source, boolean checkHeaders) throws IOException { + IntegrityChecker.requireVerifiedJar(source); + Files.createDirectories(localStorage.getParent()); + if (checkHeaders) { + ExecutableHeaderHelper.copyWithoutHeader(source, localStorage); + } else { + Files.copy(source, localStorage, StandardCopyOption.REPLACE_EXISTING); + } + } + /** * Creates a task that downloads the given version to local repository. */ public static FileDownloadTask downloadFromRemote(RemoteVersion version) throws IOException { - Path stage = Files.createTempFile("hmcl-update-", ""); - return new FileDownloadTask(new URL(version.getUrl()), stage.toFile(), version.getIntegrityCheck()) { + Path downloaded = Files.createTempFile("hmcl-update-", null); + return new FileDownloadTask(new URL(version.getUrl()), downloaded.toFile(), version.getIntegrityCheck()) { @Override public void execute() throws Exception { - Path jar = stage; + super.execute(); + try { - super.execute(); - if (version.getType() == RemoteVersion.Type.PACK) { - Path unpacked = Files.createTempFile("hmcl-update-", ".jar"); - try (GZIPInputStream stream = new GZIPInputStream(Files.newInputStream(jar)); - JarOutputStream out = new JarOutputStream(Files.newOutputStream(unpacked))) { - Pack200.newUnpacker().unpack(stream, out); - } - jar = unpacked; + switch (version.getType()) { + case JAR: + writeToStorage(downloaded, false); + break; + + case PACK: + Path unpacked = Files.createTempFile("hmcl-update-unpack-", null); + try { + try (InputStream in = new GZIPInputStream(Files.newInputStream(downloaded)); + JarOutputStream out = new JarOutputStream(Files.newOutputStream(unpacked))) { + Pack200.newUnpacker().unpack(in, out); + } + writeToStorage(unpacked, false); + } finally { + Files.deleteIfExists(unpacked); + } + break; + + default: + throw new IllegalArgumentException("Unknown type: " + version.getType()); } - IntegrityChecker.requireVerifiedJar(jar); - Files.createDirectories(localStorage.getParent()); - Files.copy(jar, localStorage, StandardCopyOption.REPLACE_EXISTING); } finally { - Files.deleteIfExists(jar); + Files.deleteIfExists(downloaded); } } }; @@ -108,9 +129,7 @@ final class LocalRepository { } LOG.info("Downloading " + current.get()); try { - IntegrityChecker.requireVerifiedJar(current.get().getLocation()); - Files.createDirectories(localStorage.getParent()); - ExecutableHeaderHelper.copyWithoutHeader(current.get().getLocation(), localStorage); + writeToStorage(current.get().getLocation(), true); } catch (IOException e) { LOG.log(Level.WARNING, "Failed to download " + current.get(), e); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java index 33fda5f62..c7523a83a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java @@ -38,12 +38,13 @@ public class RemoteVersion { String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null); String packUrl = Optional.ofNullable(response.get("pack")).map(JsonElement::getAsString).orElse(null); String packHash = Optional.ofNullable(response.get("packsha1")).map(JsonElement::getAsString).orElse(null); - if (packUrl != null && packHash != null) + if (packUrl != null && packHash != null) { return new RemoteVersion(version, packUrl, Type.PACK, new IntegrityCheck("SHA-1", packHash)); - else if (jarUrl != null && jarHash != null) + } else if (jarUrl != null && jarHash != null) { return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash)); - else - throw new IOException("Missing both jar and pack download URL"); + } else { + throw new IOException("No download url is available"); + } } catch (JsonParseException e) { throw new IOException("Malformed response", e); } From 3c2f232acb2c0a2a731d3a555838e5909f479abe Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 4 Aug 2018 23:11:31 +0800 Subject: [PATCH 27/32] Rename PACK -> PACK_GZ --- .../main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java | 2 +- .../main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index 27e87ba22..605675574 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -89,7 +89,7 @@ final class LocalRepository { writeToStorage(downloaded, false); break; - case PACK: + case PACK_GZ: Path unpacked = Files.createTempFile("hmcl-update-unpack-", null); try { try (InputStream in = new GZIPInputStream(Files.newInputStream(downloaded)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java index c7523a83a..16f732adf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java @@ -39,7 +39,7 @@ public class RemoteVersion { String packUrl = Optional.ofNullable(response.get("pack")).map(JsonElement::getAsString).orElse(null); String packHash = Optional.ofNullable(response.get("packsha1")).map(JsonElement::getAsString).orElse(null); if (packUrl != null && packHash != null) { - return new RemoteVersion(version, packUrl, Type.PACK, new IntegrityCheck("SHA-1", packHash)); + return new RemoteVersion(version, packUrl, Type.PACK_GZ, new IntegrityCheck("SHA-1", packHash)); } else if (jarUrl != null && jarHash != null) { return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash)); } else { @@ -84,7 +84,7 @@ public class RemoteVersion { } public enum Type { - PACK, + PACK_GZ, JAR } } From ace830bced2297ae2a8694315074d8f43136ba4e Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 5 Aug 2018 00:42:05 +0800 Subject: [PATCH 28/32] Refactor build.gradle --- HMCL/build.gradle | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index b4f925494..986eedfa1 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -1,5 +1,4 @@ import java.nio.file.FileSystems -import java.nio.file.StandardOpenOption import java.security.KeyFactory import java.security.MessageDigest import java.security.Signature @@ -43,17 +42,27 @@ def attachSignature(File jar) { .sorted(Comparator.comparing { it.name }) .filter { it.name != "META-INF/hmcl_signature" } .forEach { - signer.update(digest("SHA-512", it.name.getBytes("UTF-8"))) - signer.update(digest("SHA-512", zip.getInputStream(it).bytes)) - } + signer.update(digest("SHA-512", it.name.getBytes("UTF-8"))) + signer.update(digest("SHA-512", zip.getInputStream(it).bytes)) + } } def signature = signer.sign() FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs -> - Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature"), StandardOpenOption.CREATE, StandardOpenOption.WRITE).bytes = signature + Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it.bytes = signature } } } +ext.packer = Pack200.newPacker() +packer.properties()["pack.effort"] = "9" +ext.unpacker = Pack200.newUnpacker() + +def repack(File file) { + def packed = new ByteArrayOutputStream() + new JarFile(file).withCloseable { packer.pack(it, packed) } + new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) } +} + jar { from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } @@ -65,6 +74,7 @@ jar { } doLast { + repack(archivePath) attachSignature(archivePath) createChecksum(archivePath) } @@ -78,22 +88,12 @@ def createExecutable(String suffix, String header) { } task makePackGz(dependsOn: jar) doLast { - def tmp = new File(project.buildDir, "tmp") - def unpackedJar = new File(tmp, jar.archivePath.name) - def packGz = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack.gz") - - def originalStream = new ByteArrayOutputStream() - def unpackedJarStream = new JarOutputStream(new FileOutputStream(unpackedJar)) - Pack200.newPacker().pack(new JarFile(jar.archivePath), originalStream) - Pack200.newUnpacker().unpack(new ByteArrayInputStream(originalStream.toByteArray()), unpackedJarStream) - unpackedJarStream.close() - attachSignature(unpackedJar) - - new GZIPOutputStream(new FileOutputStream(packGz)).withCloseable { stream -> - Pack200.newPacker().pack(new JarFile(unpackedJar), stream) + def outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack.gz") + new GZIPOutputStream(outputPath.newOutputStream()).withCloseable { out -> + new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) } } - createChecksum(packGz) + createChecksum(outputPath) } From 2b8843ca19b46d2d674f49126720dce15c4c9727 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 5 Aug 2018 01:02:42 +0800 Subject: [PATCH 29/32] Replace .pack.gz with .pack.xz --- HMCL/build.gradle | 12 +++++++----- .../org/jackhuang/hmcl/upgrade/LocalRepository.java | 6 +++--- .../org/jackhuang/hmcl/upgrade/RemoteVersion.java | 10 +++++----- build.gradle | 10 ++++++++++ 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 986eedfa1..358a44e75 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -6,10 +6,12 @@ import java.security.spec.PKCS8EncodedKeySpec import java.util.jar.JarFile import java.util.jar.JarOutputStream import java.util.jar.Pack200 -import java.util.zip.GZIPOutputStream import java.util.zip.ZipFile import java.nio.file.Files +import org.tukaani.xz.LZMA2Options +import org.tukaani.xz.XZOutputStream + def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT" def versionroot = System.getenv("VERSION_ROOT") ?: "3.1" version = versionroot + '.' + buildnumber @@ -87,9 +89,9 @@ def createExecutable(String suffix, String header) { createChecksum(output) } -task makePackGz(dependsOn: jar) doLast { - def outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack.gz") - new GZIPOutputStream(outputPath.newOutputStream()).withCloseable { out -> +task makePackXz(dependsOn: jar) doLast { + def outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack.xz") + new XZOutputStream(outputPath.newOutputStream(), new LZMA2Options(9)).withCloseable { out -> new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) } } @@ -101,5 +103,5 @@ task makeExecutables(dependsOn: jar) doLast { createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe") } -build.dependsOn makePackGz +build.dependsOn makePackXz build.dependsOn makeExecutables diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index 605675574..19616cb2a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -30,10 +30,10 @@ import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Pack200; import java.util.logging.Level; -import java.util.zip.GZIPInputStream; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.tukaani.xz.XZInputStream; /** * A class used to manage the local HMCL repository. @@ -89,10 +89,10 @@ final class LocalRepository { writeToStorage(downloaded, false); break; - case PACK_GZ: + case PACK_XZ: Path unpacked = Files.createTempFile("hmcl-update-unpack-", null); try { - try (InputStream in = new GZIPInputStream(Files.newInputStream(downloaded)); + try (InputStream in = new XZInputStream(Files.newInputStream(downloaded)); JarOutputStream out = new JarOutputStream(Files.newOutputStream(unpacked))) { Pack200.newUnpacker().unpack(in, out); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java index 16f732adf..504cdaeea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java @@ -36,10 +36,10 @@ public class RemoteVersion { String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing")); String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null); String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null); - String packUrl = Optional.ofNullable(response.get("pack")).map(JsonElement::getAsString).orElse(null); - String packHash = Optional.ofNullable(response.get("packsha1")).map(JsonElement::getAsString).orElse(null); - if (packUrl != null && packHash != null) { - return new RemoteVersion(version, packUrl, Type.PACK_GZ, new IntegrityCheck("SHA-1", packHash)); + String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null); + String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null); + if (packXZUrl != null && packXZHash != null) { + return new RemoteVersion(version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash)); } else if (jarUrl != null && jarHash != null) { return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash)); } else { @@ -84,7 +84,7 @@ public class RemoteVersion { } public enum Type { - PACK_GZ, + PACK_XZ, JAR } } diff --git a/build.gradle b/build.gradle index b5b9b7983..0c17f3cc7 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,16 @@ group 'org.jackhuang' version '3.0' +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'org.tukaani:xz:1.8' + } +} + + subprojects { apply plugin: 'java' apply plugin: 'idea' From 055379f569eddd92ed4c85ef776c554730f4fd6e Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 5 Aug 2018 09:37:55 +0800 Subject: [PATCH 30/32] Add .pack.gz build back --- HMCL/build.gradle | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 358a44e75..5042b3985 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -6,6 +6,7 @@ import java.security.spec.PKCS8EncodedKeySpec import java.util.jar.JarFile import java.util.jar.JarOutputStream import java.util.jar.Pack200 +import java.util.zip.GZIPOutputStream import java.util.zip.ZipFile import java.nio.file.Files @@ -51,7 +52,7 @@ def attachSignature(File jar) { def signature = signer.sign() FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs -> - Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it.bytes = signature } + Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it << signature } } } @@ -89,13 +90,26 @@ def createExecutable(String suffix, String header) { createChecksum(output) } -task makePackXz(dependsOn: jar) doLast { - def outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack.xz") - new XZOutputStream(outputPath.newOutputStream(), new LZMA2Options(9)).withCloseable { out -> - new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) } +task makePack(dependsOn: jar) { + ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack") + doLast { + outputPath.newOutputStream().withCloseable { out -> + new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) } + } + createChecksum(outputPath) } +} - createChecksum(outputPath) +task makePackXz(dependsOn: makePack) doLast { + def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz") + new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(9)).withCloseable { it << makePack.outputPath.bytes } + createChecksum(packXz) +} + +task makePackGz(dependsOn: makePack) doLast { + def packGz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".gz") + new GZIPOutputStream(packGz.newOutputStream()).withCloseable { it << makePack.outputPath.bytes } + createChecksum(packGz) } @@ -104,4 +118,5 @@ task makeExecutables(dependsOn: jar) doLast { } build.dependsOn makePackXz +build.dependsOn makePackGz build.dependsOn makeExecutables From 7f2a392a337849e7c8b6a8134685ca40251e8efd Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 5 Aug 2018 11:06:50 +0800 Subject: [PATCH 31/32] Use com.github.johnrengelman.shadow plugin --- HMCL/build.gradle | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/HMCL/build.gradle b/HMCL/build.gradle index 5042b3985..eba7524a3 100644 --- a/HMCL/build.gradle +++ b/HMCL/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '2.0.4' +} + import java.nio.file.FileSystems import java.security.KeyFactory import java.security.MessageDigest @@ -67,19 +71,30 @@ def repack(File file) { } jar { - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } - manifest { attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.', 'Main-Class': 'org.jackhuang.hmcl.Main', 'Multi-Release': 'true', 'Implementation-Version': version } + finalizedBy shadowJar +} + +shadowJar { + classifier = null + + exclude 'META-INF/maven/**' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/LICENSE.txt' + + dependencies { + exclude(dependency('org.jetbrains:annotations')) + } doLast { - repack(archivePath) - attachSignature(archivePath) - createChecksum(archivePath) + repack(jar.archivePath) + attachSignature(jar.archivePath) + createChecksum(jar.archivePath) } } From a2b74b8e783fd71f5e2c454b06199e83cb6634da Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Wed, 8 Aug 2018 10:02:34 +0800 Subject: [PATCH 32/32] Fix getImplementationVersion returning null on Java 9. --- .../java/org/jackhuang/hmcl/Metadata.java | 4 +- .../hmcl/upgrade/LocalRepository.java | 24 +++---- .../jackhuang/hmcl/upgrade/LocalVersion.java | 41 ++--------- .../org/jackhuang/hmcl/game/GameVersion.java | 22 +++--- .../org/jackhuang/hmcl/launch/StreamPump.java | 18 +++-- .../org/jackhuang/hmcl/util/JarUtils.java | 72 +++++++++++++++++++ .../jackhuang/hmcl/util/ToStringBuilder.java | 17 +++++ 7 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/JarUtils.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index 4ece46b5c..b9ce083ea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl; -import java.util.Optional; +import org.jackhuang.hmcl.util.JarUtils; /** * Stores metadata about this application. @@ -25,7 +25,7 @@ import java.util.Optional; public final class Metadata { private Metadata() {} - public static final String VERSION = System.getProperty("hmcl.version.override", Optional.ofNullable(Metadata.class.getPackage().getImplementationVersion()).orElse("@develop@")); + public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@")); public static final String NAME = "HMCL"; public static final String TITLE = NAME + " " + VERSION; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java index 19616cb2a..9c8080ec6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalRepository.java @@ -17,7 +17,11 @@ */ package org.jackhuang.hmcl.upgrade; -import static org.jackhuang.hmcl.util.Logging.LOG; +import org.jackhuang.hmcl.Launcher; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.util.JarUtils; +import org.tukaani.xz.XZInputStream; + import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -25,15 +29,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Optional; -import java.util.jar.Attributes; -import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Pack200; import java.util.logging.Level; -import org.jackhuang.hmcl.Launcher; -import org.jackhuang.hmcl.task.FileDownloadTask; -import org.tukaani.xz.XZInputStream; +import static org.jackhuang.hmcl.util.Logging.LOG; /** * A class used to manage the local HMCL repository. @@ -52,15 +52,9 @@ final class LocalRepository { if (!Files.isRegularFile(localStorage)) { return Optional.empty(); } - try (JarFile jar = new JarFile(localStorage.toFile())) { - Attributes attributes = jar.getManifest().getMainAttributes(); - String version = Optional.ofNullable(attributes.getValue("Implementation-Version")) - .orElseThrow(() -> new IOException("Missing Implementation-Version")); - return Optional.of(new LocalVersion(version, localStorage)); - } catch (IOException e) { - LOG.log(Level.WARNING, "Failed to read HMCL jar: " + localStorage, e); - return Optional.empty(); - } + return Optional.of(localStorage) + .flatMap(JarUtils::getImplementationVersion) + .map(version -> new LocalVersion(version, localStorage)); } private static void writeToStorage(Path source, boolean checkHeaders) throws IOException { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java index b1f613742..ada1acff8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/LocalVersion.java @@ -17,47 +17,16 @@ */ package org.jackhuang.hmcl.upgrade; -import static org.jackhuang.hmcl.util.Logging.LOG; - -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.CodeSource; -import java.util.Optional; -import java.util.logging.Level; - -import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.util.JarUtils; + +import java.nio.file.Path; +import java.util.Optional; class LocalVersion { public static Optional current() { - CodeSource codeSource = Main.class.getProtectionDomain().getCodeSource(); - if (codeSource == null) { - return Optional.empty(); - } - - URL url = codeSource.getLocation(); - if (url == null) { - return Optional.empty(); - } - - Path path; - try { - path = Paths.get(url.toURI()); - } catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) { - LOG.log(Level.WARNING, "Invalid path: " + url, e); - return Optional.empty(); - } - - if (!Files.isRegularFile(path)) { - return Optional.empty(); - } - - return Optional.of(new LocalVersion(Metadata.VERSION, path)); + return JarUtils.thisJar().map(path -> new LocalVersion(Metadata.VERSION, path)); } private String version; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java index ea73afbf5..3329e67d3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java @@ -73,19 +73,17 @@ public final class GameVersion { if (file == null || !file.exists() || !file.isFile() || !file.canRead()) return Optional.empty(); - try { - try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) { - Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class"); - if (Files.exists(minecraft)) { - Optional result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft)); - if (result.isPresent()) - return result; - } - Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class"); - if (Files.exists(minecraftServer)) - return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer)); - return Optional.empty(); + try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) { + Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class"); + if (Files.exists(minecraft)) { + Optional result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft)); + if (result.isPresent()) + return result; } + Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class"); + if (Files.exists(minecraftServer)) + return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer)); + return Optional.empty(); } catch (IOException e) { return Optional.empty(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/StreamPump.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/StreamPump.java index ed193b7e2..be692dca1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/StreamPump.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/StreamPump.java @@ -49,17 +49,15 @@ final class StreamPump implements Runnable { @Override public void run() { - try { - try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Constants.SYSTEM_CHARSET))) { - String line; - while ((line = bufferedReader.readLine()) != null) { - if (Thread.currentThread().isInterrupted()) { - Thread.currentThread().interrupt(); - break; - } - - callback.accept(line); + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Constants.SYSTEM_CHARSET))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + if (Thread.currentThread().isInterrupted()) { + Thread.currentThread().interrupt(); + break; } + + callback.accept(line); } } catch (IOException e) { Logging.LOG.log(Level.SEVERE, "An error occurred when reading stream", e); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/JarUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/JarUtils.java new file mode 100644 index 000000000..3147c39be --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/JarUtils.java @@ -0,0 +1,72 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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.util; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.CodeSource; +import java.util.Optional; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +public final class JarUtils { + + public static Optional thisJar() { + CodeSource codeSource = FileUtils.class.getProtectionDomain().getCodeSource(); + if (codeSource == null) { + return Optional.empty(); + } + + URL url = codeSource.getLocation(); + if (url == null) { + return Optional.empty(); + } + + Path path; + try { + path = Paths.get(url.toURI()); + } catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) { + return Optional.empty(); + } + + if (!Files.isRegularFile(path)) { + return Optional.empty(); + } + + return Optional.of(path); + } + + public static Optional getManifest(Path jar) { + try (JarFile file = new JarFile(jar.toFile())) { + return Optional.ofNullable(file.getManifest()); + } catch (IOException e) { + return Optional.empty(); + } + } + + public static Optional getImplementationVersion(Path jar) { + return Optional.of(jar).flatMap(JarUtils::getManifest) + .flatMap(manifest -> Optional.ofNullable(manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION))); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ToStringBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ToStringBuilder.java index 335a45d67..a9b923739 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ToStringBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ToStringBuilder.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 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.util; public class ToStringBuilder {