diff --git a/src/main/java/com/skcraft/concurrency/DefaultProgress.java b/src/main/java/com/skcraft/concurrency/DefaultProgress.java new file mode 100644 index 0000000..fd8a681 --- /dev/null +++ b/src/main/java/com/skcraft/concurrency/DefaultProgress.java @@ -0,0 +1,28 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +import lombok.Data; + +/** + * A simple default implementation of {@link com.skcraft.concurrency.ProgressObservable} + * with settable properties. + */ +@Data +public class DefaultProgress implements ProgressObservable { + + private String status; + private double progress = -1; + + public DefaultProgress() { + } + + public DefaultProgress(double progress, String status) { + this.progress = progress; + this.status = status; + } +} diff --git a/src/main/java/com/skcraft/concurrency/ObservableFuture.java b/src/main/java/com/skcraft/concurrency/ObservableFuture.java index c4fee92..1458d17 100644 --- a/src/main/java/com/skcraft/concurrency/ObservableFuture.java +++ b/src/main/java/com/skcraft/concurrency/ObservableFuture.java @@ -65,14 +65,19 @@ public class ObservableFuture implements ListenableFuture, ProgressObserva return future.get(timeout, unit); } + @Override + public String toString() { + return observable.toString(); + } + @Override public double getProgress() { return observable.getProgress(); } @Override - public String toString() { - return observable.toString(); + public String getStatus() { + return observable.getStatus(); } } diff --git a/src/main/java/com/skcraft/concurrency/ProgressFilter.java b/src/main/java/com/skcraft/concurrency/ProgressFilter.java new file mode 100644 index 0000000..d9cf69e --- /dev/null +++ b/src/main/java/com/skcraft/concurrency/ProgressFilter.java @@ -0,0 +1,35 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +public class ProgressFilter implements ProgressObservable { + + private final ProgressObservable delegate; + private final double offset; + private final double portion; + + public ProgressFilter(ProgressObservable delegate, double offset, double portion) { + this.delegate = delegate; + this.offset = offset; + this.portion = portion; + } + + @Override + public double getProgress() { + return offset + portion * Math.max(0, delegate.getProgress()); + } + + @Override + public String getStatus() { + return delegate.getStatus(); + } + + public static ProgressObservable between(ProgressObservable delegate, double from, double to) { + return new ProgressFilter(delegate, from, to - from); + } + +} diff --git a/src/main/java/com/skcraft/concurrency/ProgressObservable.java b/src/main/java/com/skcraft/concurrency/ProgressObservable.java index 590a56a..2179e6a 100644 --- a/src/main/java/com/skcraft/concurrency/ProgressObservable.java +++ b/src/main/java/com/skcraft/concurrency/ProgressObservable.java @@ -20,4 +20,11 @@ public interface ProgressObservable { */ double getProgress(); + /** + * Get the current status text. + * + * @return the status text, or null if unavailable + */ + String getStatus(); + } diff --git a/src/main/java/com/skcraft/launcher/Instance.java b/src/main/java/com/skcraft/launcher/Instance.java index 6fc5af3..a8092e9 100644 --- a/src/main/java/com/skcraft/launcher/Instance.java +++ b/src/main/java/com/skcraft/launcher/Instance.java @@ -44,7 +44,7 @@ public class Instance implements Comparable { } @JsonIgnore - public File getVersionManifestPath() { + public File getVersionPath() { return new File(dir, "version.json"); } diff --git a/src/main/java/com/skcraft/launcher/InstanceList.java b/src/main/java/com/skcraft/launcher/InstanceList.java index eb48231..953d731 100644 --- a/src/main/java/com/skcraft/launcher/InstanceList.java +++ b/src/main/java/com/skcraft/launcher/InstanceList.java @@ -6,6 +6,7 @@ package com.skcraft.launcher; +import com.skcraft.concurrency.DefaultProgress; import com.skcraft.concurrency.ProgressObservable; import com.skcraft.launcher.model.modpack.ManifestInfo; import com.skcraft.launcher.model.modpack.PackageList; @@ -67,12 +68,15 @@ public class InstanceList { } public final class Enumerator implements Callable, ProgressObservable { + private ProgressObservable progress = new DefaultProgress(-1, null); + private Enumerator() { } @Override public InstanceList call() throws Exception { log.info("Enumerating instance list..."); + progress = new DefaultProgress(0, _("instanceLoader.loadingLocal")); List local = new ArrayList(); List remote = new ArrayList(); @@ -92,6 +96,8 @@ public class InstanceList { } } + progress = new DefaultProgress(0.3, _("instanceLoader.checkingRemote")); + try { URL packagesURL = launcher.getPackagesURL(); @@ -166,7 +172,12 @@ public class InstanceList { @Override public double getProgress() { - return -1; + return progress.getProgress(); + } + + @Override + public String getStatus() { + return progress.getStatus(); } } } diff --git a/src/main/java/com/skcraft/launcher/Launcher.java b/src/main/java/com/skcraft/launcher/Launcher.java index 2b1d303..05747b7 100644 --- a/src/main/java/com/skcraft/launcher/Launcher.java +++ b/src/main/java/com/skcraft/launcher/Launcher.java @@ -270,6 +270,48 @@ public final class Launcher { } } + /** + * Convenient method to fetch a property. + * + * @param key the key + * @return the property + */ + public String prop(String key) { + return getProperties().getProperty(key); + } + + /** + * Convenient method to fetch a property. + * + * @param key the key + * @param args formatting arguments + * @return the property + */ + public String prop(String key, String... args) { + return String.format(getProperties().getProperty(key), (Object[]) args); + } + + /** + * Convenient method to fetch a property. + * + * @param key the key + * @return the property + */ + public URL propUrl(String key) { + return HttpRequest.url(prop(key)); + } + + /** + * Convenient method to fetch a property. + * + * @param key the key + * @param args formatting arguments + * @return the property + */ + public URL propUrl(String key, String... args) { + return HttpRequest.url(prop(key, args)); + } + /** * Bootstrap. * @@ -317,4 +359,5 @@ public final class Launcher { }); } + } diff --git a/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java b/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java index 1f58d2e..617c2c3 100644 --- a/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java +++ b/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java @@ -61,6 +61,7 @@ public class ClientFileCollector extends DirectoryWalker { task.setHash(hash); task.setLocation(hashedPath); task.setTo(relPath); + task.setSize(file.length()); destPath.getParentFile().mkdirs(); ClientFileCollector.log.info(String.format("Adding %s from %s...", relPath, file.getAbsolutePath())); Files.copy(file, destPath); diff --git a/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java b/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java index acb8c02..35e0ce3 100644 --- a/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java +++ b/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java @@ -17,12 +17,12 @@ import com.skcraft.launcher.auth.Session; import com.skcraft.launcher.launch.InstanceLauncher; import com.skcraft.launcher.launch.LaunchProcessHandler; import com.skcraft.launcher.persistence.Persistence; -import com.skcraft.launcher.selfupdate.LauncherUpdateChecker; -import com.skcraft.launcher.selfupdate.LauncherUpdater; +import com.skcraft.launcher.selfupdate.UpdateChecker; +import com.skcraft.launcher.selfupdate.SelfUpdater; import com.skcraft.launcher.swing.*; -import com.skcraft.launcher.update.InstanceDeleter; -import com.skcraft.launcher.update.InstanceResetter; -import com.skcraft.launcher.update.InstanceUpdater; +import com.skcraft.launcher.update.HardResetter; +import com.skcraft.launcher.update.Remover; +import com.skcraft.launcher.update.Updater; import com.skcraft.launcher.util.SwingExecutor; import lombok.NonNull; import lombok.extern.java.Log; @@ -167,7 +167,7 @@ public class LauncherFrame extends JFrame { } private void checkLauncherUpdate() { - ListenableFuture future = launcher.getExecutor().submit(new LauncherUpdateChecker(launcher)); + ListenableFuture future = launcher.getExecutor().submit(new UpdateChecker(launcher)); Futures.addCallback(future, new FutureCallback() { @Override @@ -187,7 +187,7 @@ public class LauncherFrame extends JFrame { private void selfUpdate() { URL url = updateUrl; if (url != null) { - LauncherUpdater downloader = new LauncherUpdater(launcher, url); + SelfUpdater downloader = new SelfUpdater(launcher, url); ObservableFuture future = new ObservableFuture( launcher.getExecutor().submit(downloader), downloader); @@ -333,7 +333,7 @@ public class LauncherFrame extends JFrame { } // Execute the deleter - InstanceDeleter resetter = new InstanceDeleter(instance); + Remover resetter = new Remover(instance); ObservableFuture future = new ObservableFuture( launcher.getExecutor().submit(resetter), resetter); @@ -357,7 +357,7 @@ public class LauncherFrame extends JFrame { } // Execute the resetter - InstanceResetter resetter = new InstanceResetter(instance); + HardResetter resetter = new HardResetter(instance); ObservableFuture future = new ObservableFuture( launcher.getExecutor().submit(resetter), resetter); @@ -424,7 +424,7 @@ public class LauncherFrame extends JFrame { if (update) { // Execute the updater - InstanceUpdater updater = new InstanceUpdater(launcher, instance); + Updater updater = new Updater(launcher, instance); ObservableFuture future = new ObservableFuture( launcher.getExecutor().submit(updater), updater); diff --git a/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java b/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java index 2b0ef0d..2ef1c00 100644 --- a/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java +++ b/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java @@ -347,6 +347,11 @@ public class LoginDialog extends JDialog { public double getProgress() { return -1; } + + @Override + public String getStatus() { + return _("login.loggingInStatus"); + } } } diff --git a/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java b/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java index da2a951..32cd37b 100644 --- a/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java +++ b/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java @@ -29,6 +29,8 @@ import static com.skcraft.launcher.util.SharedLocale._; @Log public class ProgressDialog extends JDialog { + private final String defaultTitle; + private final String defaultMessage; private final JLabel label = new JLabel(); private final JPanel progressPanel = new JPanel(new BorderLayout(0, 5)); private final JPanel textAreaPanel = new JPanel(new BorderLayout()); @@ -41,9 +43,12 @@ public class ProgressDialog extends JDialog { public ProgressDialog(Window owner, String title, String message) { super(owner, title, ModalityType.DOCUMENT_MODAL); + setResizable(false); initComponents(); label.setText(message); + defaultTitle = title; + defaultMessage = message; setCompactSize(); setLocationRelativeTo(owner); @@ -71,6 +76,9 @@ public class ProgressDialog extends JDialog { } private void initComponents() { + progressBar.setMaximum(1000); + progressBar.setMinimum(0); + buttonsPanel.addElement(detailsButton); buttonsPanel.addGlue(); buttonsPanel.addElement(cancelButton); @@ -174,8 +182,35 @@ public class ProgressDialog extends JDialog { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - dialog.logText.setText(String.valueOf(observable)); - dialog.logText.setCaretPosition(0); + JProgressBar progressBar = dialog.progressBar; + JTextArea logText = dialog.logText; + JLabel label = dialog.label; + + double progress = observable.getProgress(); + if (progress >= 0) { + dialog.setTitle(_("progress.percentTitle", + Math.round(progress * 100 * 100) / 100.0, dialog.defaultTitle)); + progressBar.setValue((int) (progress * 1000)); + progressBar.setIndeterminate(false); + } else { + dialog.setTitle( dialog.defaultTitle); + progressBar.setIndeterminate(true); + } + + String status = observable.getStatus(); + if (status == null) { + status = _("progress.defaultStatus"); + label.setText(dialog.defaultMessage); + } else { + int index = status.indexOf('\n'); + if (index == -1) { + label.setText(status); + } else { + label.setText(status.substring(0, index)); + } + } + logText.setText(status); + logText.setCaretPosition(0); } }); } diff --git a/src/main/java/com/skcraft/launcher/install/Downloader.java b/src/main/java/com/skcraft/launcher/install/Downloader.java new file mode 100644 index 0000000..137daf1 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/Downloader.java @@ -0,0 +1,21 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.concurrency.ProgressObservable; + +import java.io.File; +import java.net.URL; +import java.util.List; + + +public interface Downloader extends ProgressObservable { + + File download(List urls, String key, long size, String name); + + File download(URL url, String key, long size, String name); +} diff --git a/src/main/java/com/skcraft/launcher/install/FileCopy.java b/src/main/java/com/skcraft/launcher/install/FileCopy.java new file mode 100644 index 0000000..34064df --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/FileCopy.java @@ -0,0 +1,47 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.google.common.io.Files; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class FileCopy implements InstallTask { + + private final File from; + private final File to; + + public FileCopy(@NonNull File from, @NonNull File to) { + this.from = from; + this.to = to; + } + + @Override + public void execute() throws IOException { + log.log(Level.INFO, "Copying to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); + to.getParentFile().mkdirs(); + Files.copy(from, to); + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return _("installer.copyingFile", from, to); + } + +} diff --git a/src/main/java/com/skcraft/launcher/install/FileMover.java b/src/main/java/com/skcraft/launcher/install/FileMover.java new file mode 100644 index 0000000..3293eb6 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/FileMover.java @@ -0,0 +1,47 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class FileMover implements InstallTask { + + private final File from; + private final File to; + + public FileMover(@NonNull File from, @NonNull File to) { + this.from = from; + this.to = to; + } + + @Override + public void execute() throws IOException { + log.log(Level.INFO, "Moving to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); + to.getParentFile().mkdirs(); + to.delete(); + from.renameTo(to); + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return _("installer.movingFile", from, to); + } + +} diff --git a/src/main/java/com/skcraft/launcher/install/HttpDownloader.java b/src/main/java/com/skcraft/launcher/install/HttpDownloader.java new file mode 100644 index 0000000..4d36cd2 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/HttpDownloader.java @@ -0,0 +1,265 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.util.HttpRequest; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class HttpDownloader implements Downloader { + + private final HashFunction hf = Hashing.sha1(); + + private final File tempDir; + @Getter @Setter private int threadCount = 6; + @Getter @Setter private int retryDelay = 2000; + @Getter @Setter private int tryCount = 3; + + private List queue = new ArrayList(); + private final Set usedKeys = new HashSet(); + + private final List running = new ArrayList(); + private long downloaded = 0; + private long total = 0; + private int left = 0; + private boolean hasError = false; + + /** + * Create a new downloader using the given executor. + * + * @param tempDir the temporary directory + */ + public HttpDownloader(@NonNull File tempDir) { + this.tempDir = tempDir; + } + + /** + * Make sure that we aren't re-using hash IDs. + * + * @param baseKey the key to make unique + * @return a unique key + */ + private String createUniqueKey(String baseKey) { + String key = baseKey; + int i = 0; + while (usedKeys.contains(key)) { + key = baseKey + "_" + (i++); + } + usedKeys.add(key); + return key; + } + + @Override + public synchronized File download(@NonNull List urls, @NonNull String key, long size, String name) { + if (urls.isEmpty()) { + throw new IllegalArgumentException("Can't download empty list of URLs"); + } + + String hash = hf.hashString(Strings.nullToEmpty(key) + urls.get(0), Charsets.UTF_8).toString(); + hash = createUniqueKey(hash); + File tempFile = new File(tempDir, hash.substring(0, 2) + "/" + hash); + + // If the file is already downloaded (such as from before), then don't re-download + if (!tempFile.exists()) { + total += size; + left++; + queue.add(new HttpDownloadJob(tempFile, urls, size, name != null ? name : tempFile.getName())); + } + + return tempFile; + } + + + @Override + public File download(URL url, String key, long size, String name) { + List urls = new ArrayList(); + urls.add(url); + return download(urls, key, size, name); + } + + /** + * Prevent further downloads from being queued and download queued files. + * + * @throws InterruptedException thrown on interruption + * @throws IOException thrown on I/O error + */ + public void execute() throws InterruptedException, IOException { + synchronized (this) { + queue = Collections.unmodifiableList(queue); + } + + ListeningExecutorService executor = MoreExecutors.listeningDecorator( + Executors.newFixedThreadPool(threadCount)); + + try { + List> futures = new ArrayList>(); + + synchronized (this) { + for (HttpDownloadJob job : queue) { + futures.add(executor.submit(job)); + } + } + + try { + Futures.allAsList(futures).get(); + } catch (ExecutionException e) { + throw new IOException("Something went wrong", e); + } + + if (hasError) { + throw new IOException("Some files could not be downloaded"); + } + } finally { + executor.shutdownNow(); + } + } + + @Override + public synchronized double getProgress() { + if (total <= 0) { + return -1; + } + + long downloaded = this.downloaded; + for (HttpDownloadJob job : running) { + downloaded += Math.max(0, job.getProgress() * job.size); + } + return downloaded / (double) total; + } + + @Override + public synchronized String getStatus() { + if (running.size() > 0) { + StringBuilder builder = new StringBuilder(); + for (HttpDownloadJob job : running) { + builder.append("\n"); + builder.append(job.getStatus()); + } + return _("downloader.downloadingList", queue.size(), left) + builder.toString(); + } else { + return _("downloader.noDownloads"); + } + } + + public class HttpDownloadJob implements Runnable, ProgressObservable { + private final File destFile; + private final List urls; + private final long size; + private String name; + private HttpRequest request; + + private HttpDownloadJob(File destFile, List urls, long size, String name) { + this.destFile = destFile; + this.urls = urls; + this.size = size; + this.name = name; + } + + @Override + public void run() { + try { + synchronized (HttpDownloader.this) { + running.add(this); + } + + download(); + } catch (IOException e) { + hasError = true; + } catch (InterruptedException e) { + log.info("Download of " + destFile + " was interrupted"); + } finally { + synchronized (HttpDownloader.this) { + downloaded += size; + running.remove(this); + } + } + } + + private void download() throws IOException, InterruptedException { + log.log(Level.INFO, "Downloading {0} from {1}...", new Object[] { destFile, urls }); + + File destDir = destFile.getParentFile(); + File tempFile = new File(destDir, destFile.getName() + ".tmp"); + destDir.mkdirs(); + + // Try to download + download(tempFile); + + destFile.delete(); + if (!tempFile.renameTo(destFile)) { + throw new IOException(String.format("Failed to rename %s to %s", tempFile, destFile)); + } + } + + private void download(File file) throws IOException, InterruptedException { + int trial = 0; + boolean first = true; + IOException lastException = null; + + do { + for (URL url : urls) { + // Sleep between each trial + if (!first) { + Thread.sleep(retryDelay); + } + first = false; + + try { + request = HttpRequest.get(url); + request.execute().expectResponseCode(200).saveContent(file); + left--; + return; + } catch (IOException e) { + lastException = e; + log.log(Level.WARNING, "Failed to download " + url, e); + } + } + } while (++trial < tryCount); + + throw new IOException("Failed to download from " + urls, lastException); + } + + @Override + public double getProgress() { + HttpRequest request = this.request; + return request != null ? request.getProgress() : -1; + } + + @Override + public String getStatus() { + double progress = getProgress(); + if (progress >= 0) { + return _("downloader.jobProgress", name, Math.round(progress * 100 * 100) / 100.0); + } else { + return _("downloader.jobPending", name); + } + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/update/InstallLog.java b/src/main/java/com/skcraft/launcher/install/InstallLog.java similarity index 98% rename from src/main/java/com/skcraft/launcher/update/InstallLog.java rename to src/main/java/com/skcraft/launcher/install/InstallLog.java index b86df3a..7398200 100644 --- a/src/main/java/com/skcraft/launcher/update/InstallLog.java +++ b/src/main/java/com/skcraft/launcher/install/InstallLog.java @@ -4,7 +4,7 @@ * Please see LICENSE.txt for license information. */ -package com.skcraft.launcher.update; +package com.skcraft.launcher.install; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; diff --git a/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java b/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java new file mode 100644 index 0000000..a34c093 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java @@ -0,0 +1,50 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class InstallLogFileMover implements InstallTask { + + private final InstallLog installLog; + private final File from; + private final File to; + + public InstallLogFileMover(InstallLog installLog, @NonNull File from, @NonNull File to) { + this.installLog = installLog; + this.from = from; + this.to = to; + } + + @Override + public void execute() throws IOException { + InstallLogFileMover.log.log(Level.INFO, "Installing to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); + to.getParentFile().mkdirs(); + to.delete(); + from.renameTo(to); + installLog.add(to, to); + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return _("installer.movingFile", from, to); + } + +} diff --git a/src/main/java/com/skcraft/launcher/install/InstallTask.java b/src/main/java/com/skcraft/launcher/install/InstallTask.java new file mode 100644 index 0000000..8988002 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/InstallTask.java @@ -0,0 +1,15 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.concurrency.ProgressObservable; + +public interface InstallTask extends ProgressObservable { + + void execute() throws Exception; + +} diff --git a/src/main/java/com/skcraft/launcher/install/Installer.java b/src/main/java/com/skcraft/launcher/install/Installer.java new file mode 100644 index 0000000..0ab03d2 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/Installer.java @@ -0,0 +1,85 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.concurrency.ProgressObservable; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class Installer implements ProgressObservable { + + @Getter private final File tempDir; + private final HttpDownloader downloader; + private InstallTask running; + private int count = 0; + private int finished = 0; + + private List queue = new ArrayList(); + + public Installer(@NonNull File tempDir) { + this.tempDir = tempDir; + this.downloader = new HttpDownloader(tempDir); + } + + public synchronized void queue(@NonNull InstallTask runnable) { + queue.add(runnable); + count++; + } + + public void download() throws IOException, InterruptedException { + downloader.execute(); + } + + public synchronized void execute() throws Exception { + queue = Collections.unmodifiableList(queue); + + try { + for (InstallTask runnable : queue) { + checkInterrupted(); + running = runnable; + runnable.execute(); + finished++; + } + } finally { + running = null; + } + } + + public Downloader getDownloader() { + return downloader; + } + + @Override + public double getProgress() { + return finished / (double) count; + } + + @Override + public String getStatus() { + InstallTask running = this.running; + if (running != null) { + String status = running.getStatus(); + if (status == null) { + status = running.toString(); + } + return _("installer.executing", count - finished) + "\n" + status; + } else { + return _("installer.installing"); + } + } +} diff --git a/src/main/java/com/skcraft/launcher/update/UpdateCache.java b/src/main/java/com/skcraft/launcher/install/UpdateCache.java similarity index 94% rename from src/main/java/com/skcraft/launcher/update/UpdateCache.java rename to src/main/java/com/skcraft/launcher/install/UpdateCache.java index a2dc629..a33d9d8 100644 --- a/src/main/java/com/skcraft/launcher/update/UpdateCache.java +++ b/src/main/java/com/skcraft/launcher/install/UpdateCache.java @@ -4,7 +4,7 @@ * Please see LICENSE.txt for license information. */ -package com.skcraft.launcher.update; +package com.skcraft.launcher.install; import lombok.Data; import lombok.NonNull; diff --git a/src/main/java/com/skcraft/launcher/update/ZipExtract.java b/src/main/java/com/skcraft/launcher/install/ZipExtract.java similarity index 98% rename from src/main/java/com/skcraft/launcher/update/ZipExtract.java rename to src/main/java/com/skcraft/launcher/install/ZipExtract.java index 7a54c77..9cc8c48 100644 --- a/src/main/java/com/skcraft/launcher/update/ZipExtract.java +++ b/src/main/java/com/skcraft/launcher/install/ZipExtract.java @@ -4,7 +4,7 @@ * Please see LICENSE.txt for license information. */ -package com.skcraft.launcher.update; +package com.skcraft.launcher.install; import com.google.common.io.ByteSource; import com.google.common.io.Closer; diff --git a/src/main/java/com/skcraft/launcher/launch/InstanceLauncher.java b/src/main/java/com/skcraft/launcher/launch/InstanceLauncher.java index d3670e9..8146af3 100644 --- a/src/main/java/com/skcraft/launcher/launch/InstanceLauncher.java +++ b/src/main/java/com/skcraft/launcher/launch/InstanceLauncher.java @@ -10,16 +10,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Strings; import com.google.common.io.Files; +import com.skcraft.concurrency.DefaultProgress; import com.skcraft.concurrency.ProgressObservable; import com.skcraft.launcher.AssetsRoot; import com.skcraft.launcher.Configuration; import com.skcraft.launcher.Instance; import com.skcraft.launcher.Launcher; import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.install.ZipExtract; import com.skcraft.launcher.model.minecraft.AssetsIndex; import com.skcraft.launcher.model.minecraft.Library; import com.skcraft.launcher.model.minecraft.VersionManifest; -import com.skcraft.launcher.update.ZipExtract; import com.skcraft.launcher.util.Environment; import com.skcraft.launcher.util.Platform; import lombok.Getter; @@ -36,6 +37,7 @@ import java.util.Map; import java.util.concurrent.Callable; import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.util.SharedLocale._; /** * Handles the launching of an instance. @@ -43,12 +45,15 @@ import static com.skcraft.launcher.LauncherUtils.checkInterrupted; @Log public class InstanceLauncher implements Callable, ProgressObservable { + private ProgressObservable progress = new DefaultProgress(0, _("instanceLauncher.preparing")); + private final ObjectMapper mapper = new ObjectMapper(); private final Launcher launcher; private final Instance instance; private final Session session; private final File extractDir; @Getter @Setter private Environment environment = Environment.getInstance(); + private VersionManifest versionManifest; private AssetsIndex assetsIndex; private File virtualAssetsDir; @@ -64,10 +69,8 @@ public class InstanceLauncher implements Callable, ProgressObservable { * @param session the session * @param extractDir the directory to extract to */ - public InstanceLauncher(@NonNull Launcher launcher, - @NonNull Instance instance, - @NonNull Session session, - @NonNull File extractDir) { + public InstanceLauncher(@NonNull Launcher launcher, @NonNull Instance instance, + @NonNull Session session, @NonNull File extractDir) { this.launcher = launcher; this.instance = instance; this.session = session; @@ -94,12 +97,15 @@ public class InstanceLauncher implements Callable, ProgressObservable { assetsRoot = launcher.getAssets(); // Load versionManifest and assets index - versionManifest = mapper.readValue(instance.getVersionManifestPath(), VersionManifest.class); + versionManifest = mapper.readValue(instance.getVersionPath(), VersionManifest.class); assetsIndex = mapper.readValue(assetsRoot.getIndexPath(versionManifest), AssetsIndex.class); // Copy over assets to the tree + progress = new DefaultProgress(0.1, _("instanceLauncher.preparingAssets")); virtualAssetsDir = assetsRoot.buildAssetTree(versionManifest); + progress = new DefaultProgress(0.9, _("instanceLauncher.collectingArgs")); + addJvmArgs(); addLibraries(); addJarArgs(); @@ -115,6 +121,8 @@ public class InstanceLauncher implements Callable, ProgressObservable { log.info("Launching: " + builder); checkInterrupted(); + progress = new DefaultProgress(1, _("instanceLauncher.startingJava")); + return processBuilder.start(); } @@ -301,4 +309,9 @@ public class InstanceLauncher implements Callable, ProgressObservable { return -1; } + @Override + public String getStatus() { + return null; + } + } diff --git a/src/main/java/com/skcraft/launcher/model/minecraft/Library.java b/src/main/java/com/skcraft/launcher/model/minecraft/Library.java index 923b913..677d1cc 100644 --- a/src/main/java/com/skcraft/launcher/model/minecraft/Library.java +++ b/src/main/java/com/skcraft/launcher/model/minecraft/Library.java @@ -9,15 +9,10 @@ package com.skcraft.launcher.model.minecraft; import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.skcraft.launcher.Launcher; -import com.skcraft.launcher.LauncherUtils; import com.skcraft.launcher.util.Environment; -import com.skcraft.launcher.util.HttpRequest; import com.skcraft.launcher.util.Platform; import lombok.Data; -import java.net.MalformedURLException; -import java.net.URL; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -127,21 +122,6 @@ public class Library { return path; } - public URL getURL(Launcher launcher, Environment environment, URL baseURL) { - if (locallyAvailable && baseURL != null) { - try { - return LauncherUtils.concat(baseURL, getPath(environment)); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } else { - StringBuilder builder = new StringBuilder(); - builder.append(launcher.getProperties().getProperty("librariesUrl")); - builder.append(getPath(environment)); - return HttpRequest.url(builder.toString()); - } - } - @Data public static class Rule { private Action action; diff --git a/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java b/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java index c9c3287..e3dbf12 100644 --- a/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java +++ b/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java @@ -7,30 +7,31 @@ package com.skcraft.launcher.model.modpack; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.skcraft.launcher.update.FileDistribute; -import com.skcraft.launcher.update.UpdateCache; +import com.skcraft.launcher.install.InstallLog; +import com.skcraft.launcher.install.InstallLogFileMover; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.install.UpdateCache; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NonNull; import org.apache.commons.io.FilenameUtils; import java.io.File; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; import static com.skcraft.launcher.LauncherUtils.concat; @Data @EqualsAndHashCode(callSuper = false) -public class FileInstall extends Task { +public class FileInstall extends ManifestEntry { private String version; private String hash; private String location; private String to; + private long size; @JsonIgnore public String getImpliedVersion() { @@ -43,31 +44,23 @@ public class FileInstall extends Task { } @Override - public void run() { - UpdateCache updateCache = getInstaller().getUpdateCache(); + public void install(@NonNull Installer installer, @NonNull InstallLog log, + @NonNull UpdateCache cache, @NonNull File contentDir) throws MalformedURLException { String targetPath = getTargetPath(); - URL url; + File targetFile = new File(contentDir, targetPath); + String fileVersion = getImpliedVersion(); + URL url = concat(getManifest().getObjectsUrl(), getLocation()); - try { - url = concat(getManifest().getObjectsURL(), getLocation()); - } catch (MalformedURLException e) { - throw new RuntimeException("Invalid URL encountered", e); - } - - try { - if (updateCache.mark(FilenameUtils.normalize(targetPath), getImpliedVersion())) { - File targetFile = new File(getInstaller().getDestinationDir(), targetPath); - File sourceFile = getInstaller().download(url, getImpliedVersion()); - List targets = new ArrayList(); - targets.add(targetFile); - getInstaller().submit(new FileDistribute(sourceFile, targets)); + if (cache.mark(FilenameUtils.normalize(targetPath), fileVersion)) { + long size = this.size; + if (size <= 0) { + size = 10 * 1024; } - getInstaller().getCurrentLog().add(to, to); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (IOException e) { - throw new RuntimeException("Failed to download " + url.toString(), e); + File tempFile = installer.getDownloader().download(url, fileVersion, size, to); + installer.queue(new InstallLogFileMover(log, tempFile, targetFile)); + } else { + log.add(to, to); } } diff --git a/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java b/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java index c07f273..c6bcfaf 100644 --- a/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java +++ b/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java @@ -11,7 +11,7 @@ import com.fasterxml.jackson.annotation.JsonManagedReference; import com.google.common.base.Strings; import com.skcraft.launcher.LauncherUtils; import com.skcraft.launcher.model.minecraft.VersionManifest; -import com.skcraft.launcher.update.Installer; +import com.skcraft.launcher.install.Installer; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -31,13 +31,13 @@ public class Manifest extends BaseManifest { private String objectsLocation; private String gameVersion; @JsonManagedReference("manifest") - private List tasks = new ArrayList(); + private List tasks = new ArrayList(); @Getter @Setter @JsonIgnore private Installer installer; private VersionManifest versionManifest; @JsonIgnore - public URL getLibrariesURL() { + public URL getLibrariesUrl() { if (Strings.nullToEmpty(getLibrariesLocation()) == null) { return baseUrl; } @@ -50,7 +50,7 @@ public class Manifest extends BaseManifest { } @JsonIgnore - public URL getObjectsURL() { + public URL getObjectsUrl() { if (Strings.nullToEmpty(getObjectsLocation()) == null) { return baseUrl; } diff --git a/src/main/java/com/skcraft/launcher/model/modpack/Task.java b/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java similarity index 71% rename from src/main/java/com/skcraft/launcher/model/modpack/Task.java rename to src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java index 8297f1b..25d2cf1 100644 --- a/src/main/java/com/skcraft/launcher/model/modpack/Task.java +++ b/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java @@ -10,10 +10,14 @@ import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.skcraft.launcher.update.Installer; +import com.skcraft.launcher.install.InstallLog; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.install.UpdateCache; import lombok.Data; import lombok.ToString; +import java.io.File; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -24,14 +28,11 @@ import lombok.ToString; }) @Data @ToString(exclude = "manifest") -public abstract class Task implements Runnable { +public abstract class ManifestEntry { @JsonBackReference("manifest") private Manifest manifest; - @JsonIgnore - public Installer getInstaller() { - return getManifest().getInstaller(); - } + public abstract void install(Installer installer, InstallLog log, UpdateCache cache, File contentDir) throws Exception; } diff --git a/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java b/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java index 54da83a..1adcf30 100644 --- a/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java +++ b/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java @@ -29,6 +29,7 @@ import java.util.*; * @author Herve Boutemy * @version $Id$ */ +@SuppressWarnings("unchecked") public class ComparableVersion implements Comparable { private String value; diff --git a/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdater.java b/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdater.java deleted file mode 100644 index a32cf2e..0000000 --- a/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdater.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.selfupdate; - -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import com.skcraft.concurrency.ProgressObservable; -import com.skcraft.launcher.Launcher; -import com.skcraft.launcher.update.FileDownloader; -import com.skcraft.launcher.update.Installer; -import lombok.NonNull; - -import java.io.File; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; - -public class LauncherUpdater implements Callable, ProgressObservable { - - private final ListeningExecutorService executor = - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); - private final Launcher launcher; - private final URL url; - private Installer currentInstaller; - - public LauncherUpdater(@NonNull Launcher launcher, @NonNull URL url) { - this.launcher = launcher; - this.url = url; - } - - @Override - public File call() throws Exception { - try { - File dir = launcher.getLauncherBinariesDir(); - File finalPath = new File(dir, String.valueOf(System.currentTimeMillis()) + ".jar.pack"); - List paths = new ArrayList(); - paths.add(finalPath); - - Installer installer = new Installer(executor, launcher.getInstallerDir(), dir, dir); - currentInstaller = installer; - installer.submit(new FileDownloader(installer, url, paths)); - installer.awaitCompletion(); - - return finalPath; - } finally { - executor.shutdownNow(); - } - } - - @Override - public double getProgress() { - return -1; - } - - @Override - public String toString() { - Installer installer = currentInstaller; - if (installer != null) { - return installer.toString(); - } else { - return "..."; - } - } - -} diff --git a/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java b/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java new file mode 100644 index 0000000..76d95cb --- /dev/null +++ b/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java @@ -0,0 +1,70 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.selfupdate; + +import com.skcraft.concurrency.DefaultProgress; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.install.FileMover; +import com.skcraft.launcher.install.Installer; +import lombok.NonNull; + +import java.io.File; +import java.net.URL; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static com.skcraft.launcher.util.SharedLocale._; + +public class SelfUpdater implements Callable, ProgressObservable { + + private final Launcher launcher; + private final URL url; + private final Installer installer; + private ProgressObservable progress = new DefaultProgress(0, _("updater.updating")); + + public SelfUpdater(@NonNull Launcher launcher, @NonNull URL url) { + this.launcher = launcher; + this.url = url; + this.installer = new Installer(launcher.getInstallerDir()); + } + + @Override + public File call() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + try { + File dir = launcher.getLauncherBinariesDir(); + File file = new File(dir, String.valueOf(System.currentTimeMillis()) + ".jar.pack"); + File tempFile = installer.getDownloader().download(url, "", 10000, "launcher.jar.pack"); + + progress = installer.getDownloader(); + installer.download(); + + installer.queue(new FileMover(tempFile, file)); + + progress = installer; + installer.execute(); + + return file; + } finally { + executor.shutdownNow(); + } + } + + @Override + public double getProgress() { + return progress.getProgress(); + } + + @Override + public String getStatus() { + return progress.getStatus(); + } + +} diff --git a/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdateChecker.java b/src/main/java/com/skcraft/launcher/selfupdate/UpdateChecker.java similarity index 75% rename from src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdateChecker.java rename to src/main/java/com/skcraft/launcher/selfupdate/UpdateChecker.java index 4d617e8..439608a 100644 --- a/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdateChecker.java +++ b/src/main/java/com/skcraft/launcher/selfupdate/UpdateChecker.java @@ -18,18 +18,18 @@ import java.util.concurrent.Callable; import static com.skcraft.launcher.util.SharedLocale._; @Log -public class LauncherUpdateChecker implements Callable { +public class UpdateChecker implements Callable { private final Launcher launcher; - public LauncherUpdateChecker(@NonNull Launcher launcher) { + public UpdateChecker(@NonNull Launcher launcher) { this.launcher = launcher; } @Override public URL call() throws Exception { try { - LauncherUpdateChecker.log.info("Checking for update..."); + UpdateChecker.log.info("Checking for update..."); URL url = HttpRequest.url(launcher.getProperties().getProperty("selfUpdateUrl")); @@ -42,13 +42,13 @@ public class LauncherUpdateChecker implements Callable { ComparableVersion current = new ComparableVersion(launcher.getVersion()); ComparableVersion latest = new ComparableVersion(versionInfo.getVersion()); - LauncherUpdateChecker.log.info("Latest version is " + latest + ", while current is " + current); + UpdateChecker.log.info("Latest version is " + latest + ", while current is " + current); if (latest.compareTo(current) >= 1) { - LauncherUpdateChecker.log.info("Update available at " + versionInfo.getUrl()); + UpdateChecker.log.info("Update available at " + versionInfo.getUrl()); return versionInfo.getUrl(); } else { - LauncherUpdateChecker.log.info("No update required."); + UpdateChecker.log.info("No update required."); return null; } } catch (Exception e) { diff --git a/src/main/java/com/skcraft/launcher/update/BaseUpdater.java b/src/main/java/com/skcraft/launcher/update/BaseUpdater.java new file mode 100644 index 0000000..8ee6938 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/BaseUpdater.java @@ -0,0 +1,198 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.update; + +import com.skcraft.launcher.AssetsRoot; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.install.FileMover; +import com.skcraft.launcher.install.InstallLog; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.install.UpdateCache; +import com.skcraft.launcher.model.minecraft.Asset; +import com.skcraft.launcher.model.minecraft.AssetsIndex; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.model.modpack.ManifestEntry; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.HttpRequest; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.LauncherUtils.concat; + +@Log +public abstract class BaseUpdater { + + private static final long JAR_SIZE_ESTIMATE = 5 * 1024 * 1024; + private static final long LIBRARY_SIZE_ESTIMATE = 3 * 1024 * 1024; + + private final Launcher launcher; + private final Environment environment = Environment.getInstance(); + private final List executeOnCompletion = new ArrayList(); + + protected BaseUpdater(@NonNull Launcher launcher) { + this.launcher = launcher; + } + + protected void complete() { + for (Runnable runnable : executeOnCompletion) { + runnable.run(); + } + } + + protected Manifest installPackage(@NonNull Installer installer, @NonNull Instance instance) throws Exception { + final File contentDir = instance.getContentDir(); + final File logPath = new File(contentDir, "install_log.json"); + final File cachePath = new File(contentDir, "update_cache.json"); + + final InstallLog previousLog = Persistence.read(logPath, InstallLog.class); + final InstallLog currentLog = new InstallLog(); + currentLog.setBaseDir(contentDir); + final UpdateCache updateCache = Persistence.read(cachePath, UpdateCache.class); + + Manifest manifest = HttpRequest + .get(instance.getManifestURL()) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(instance.getManifestPath()) + .asJson(Manifest.class); + + if (manifest.getBaseUrl() == null) { + manifest.setBaseUrl(instance.getManifestURL()); + } + + for (ManifestEntry entry : manifest.getTasks()) { + entry.install(installer, currentLog, updateCache, contentDir); + } + + executeOnCompletion.add(new Runnable() { + @Override + public void run() { + for (Map.Entry> entry : previousLog.getEntrySet()) { + for (String path : entry.getValue()) { + if (!currentLog.has(path)) { + new File(contentDir, path).delete(); + } + } + } + + try { + Persistence.write(logPath, currentLog); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to write install log", e); + } + + try { + Persistence.write(cachePath, updateCache); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to write update cache", e); + } + } + }); + + return manifest; + } + + protected void installJar(@NonNull Installer installer, + @NonNull File jarFile, + @NonNull URL url) throws InterruptedException { + // If the JAR does not exist, install it + if (!jarFile.exists()) { + List targets = new ArrayList(); + + File tempFile = installer.getDownloader().download(url, "", JAR_SIZE_ESTIMATE, jarFile.getName()); + installer.queue(new FileMover(tempFile, jarFile)); + log.info("Installing " + jarFile.getName() + " from " + url); + } + } + + protected void installAssets(@NonNull Installer installer, + @NonNull VersionManifest versionManifest, + @NonNull URL indexUrl, + @NonNull List sources) throws IOException, InterruptedException { + AssetsRoot assetsRoot = launcher.getAssets(); + + AssetsIndex index = HttpRequest + .get(indexUrl) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(assetsRoot.getIndexPath(versionManifest)) + .asJson(AssetsIndex.class); + + for (Map.Entry entry : index.getObjects().entrySet()) { + checkInterrupted(); + + String hash = entry.getValue().getHash(); + String path = String.format("%s/%s", hash.subSequence(0, 2), hash); + File targetFile = assetsRoot.getObjectPath(entry.getValue()); + + if (!targetFile.exists()) { + List urls = new ArrayList(); + for (URL sourceUrl : sources) { + try { + urls.add(concat(sourceUrl, path)); + } catch (MalformedURLException e) { + log.log(Level.WARNING, "Bad source URL for library: " + sourceUrl); + } + } + + File tempFile = installer.getDownloader().download( + sources, "", entry.getValue().getSize(), entry.getKey()); + installer.queue(new FileMover(tempFile, targetFile)); + log.info("Fetching " + path + " from " + urls); + } + } + } + + protected void installLibraries(@NonNull Installer installer, + @NonNull VersionManifest versionManifest, + @NonNull File librariesDir, + @NonNull List sources) throws InterruptedException { + + for (Library library : versionManifest.getLibraries()) { + if (library.matches(environment)) { + checkInterrupted(); + + String path = library.getPath(environment); + File targetFile = new File(librariesDir, path); + + if (!targetFile.exists()) { + List urls = new ArrayList(); + for (URL sourceUrl : sources) { + try { + urls.add(concat(sourceUrl, path)); + } catch (MalformedURLException e) { + log.log(Level.WARNING, "Bad source URL for library: " + sourceUrl); + } + } + + File tempFile = installer.getDownloader().download(sources, "", LIBRARY_SIZE_ESTIMATE, + library.getName() + ".jar"); + installer.queue(new FileMover( tempFile, targetFile)); + log.info("Fetching " + path + " from " + urls); + } + } + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/update/FileCopy.java b/src/main/java/com/skcraft/launcher/update/FileCopy.java deleted file mode 100644 index 5a81e36..0000000 --- a/src/main/java/com/skcraft/launcher/update/FileCopy.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.update; - -import com.google.common.io.Files; -import lombok.Data; -import lombok.extern.java.Log; - -import java.io.File; -import java.io.IOException; -import java.util.logging.Level; - -@Data -@Log -public class FileCopy implements Runnable { - - public static final Object driveAccessLock = new Object(); - - private final File from; - private final File to; - - public FileCopy(File from, File to) { - this.from = from; - this.to = to; - } - - @Override - public void run() { - synchronized (driveAccessLock) { - log.log(Level.INFO, "Copying to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); - try { - to.getParentFile().mkdirs(); - Files.copy(from, to); - } catch (IOException e) { - throw new RuntimeException("Failed to copy to " + to, e); - } - } - } -} diff --git a/src/main/java/com/skcraft/launcher/update/FileDistribute.java b/src/main/java/com/skcraft/launcher/update/FileDistribute.java deleted file mode 100644 index f14c6bc..0000000 --- a/src/main/java/com/skcraft/launcher/update/FileDistribute.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.update; - -import com.google.common.io.Files; -import lombok.Data; -import lombok.extern.java.Log; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.logging.Level; - -@Data -@Log -public class FileDistribute implements Runnable { - - private final File from; - private final List to; - - public FileDistribute(File from, List to) { - this.from = from; - this.to = to; - } - - @Override - public void run() { - synchronized (FileCopy.driveAccessLock) { - try { - for (int i = 0; i < to.size(); i++) { - File dest = to.get(i); - dest.getParentFile().mkdirs(); - log.log(Level.INFO, "Copying to {0} (from {1})...", - new Object[]{dest.getAbsoluteFile(), from.getName()}); - if (i == to.size() - 1) { - dest.delete(); - from.renameTo(dest); - } else { - Files.copy(from, dest); - } - } - } catch (IOException e) { - throw new RuntimeException("Failed to copy to " + to, e); - } - } - } -} diff --git a/src/main/java/com/skcraft/launcher/update/FileDownloader.java b/src/main/java/com/skcraft/launcher/update/FileDownloader.java deleted file mode 100644 index a297cb4..0000000 --- a/src/main/java/com/skcraft/launcher/update/FileDownloader.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.update; - -import lombok.Getter; -import lombok.ToString; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.List; - -@ToString(exclude = "installer") -public class FileDownloader implements Runnable { - - private final Installer installer; - @Getter private final URL url; - @Getter private final List targets; - - public FileDownloader(Installer installer, URL url, List targets) { - this.installer = installer; - this.url = url; - this.targets = targets; - } - - @Override - public void run() { - try { - File sourceFile = installer.download(url, ""); - installer.submit(new FileDistribute(sourceFile, targets)); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (IOException e) { - throw new RuntimeException("Failed to download " + url, e); - } - } - -} diff --git a/src/main/java/com/skcraft/launcher/update/GameUpdater.java b/src/main/java/com/skcraft/launcher/update/GameUpdater.java deleted file mode 100644 index eb76424..0000000 --- a/src/main/java/com/skcraft/launcher/update/GameUpdater.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.update; - -import com.skcraft.launcher.AssetsRoot; -import com.skcraft.launcher.Launcher; -import com.skcraft.launcher.model.minecraft.Asset; -import com.skcraft.launcher.model.minecraft.AssetsIndex; -import com.skcraft.launcher.model.minecraft.Library; -import com.skcraft.launcher.model.minecraft.VersionManifest; -import com.skcraft.launcher.util.Environment; -import com.skcraft.launcher.util.HttpRequest; -import lombok.NonNull; -import lombok.extern.java.Log; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static com.skcraft.launcher.LauncherUtils.checkInterrupted; -import static com.skcraft.launcher.util.HttpRequest.url; - -@Log -public class GameUpdater implements Runnable { - - private final Installer installer; - private final Launcher launcher; - private final VersionManifest versionManifest; - private final URL librariesBaseURL; - private Environment environment = Environment.getInstance(); - - public GameUpdater(@NonNull Installer installer, - @NonNull Launcher launcher, - @NonNull VersionManifest versionManifest, URL librariesBaseURL) { - this.installer = installer; - this.launcher = launcher; - this.versionManifest = versionManifest; - this.librariesBaseURL = librariesBaseURL; - } - - @Override - public void run() { - try { - File librariesDir = launcher.getLibrariesDir(); - AssetsRoot assetsRoot = launcher.getAssets(); - File jarPath = launcher.getJarPath(versionManifest); - - URL jarURL = url(String.format( - launcher.getProperties().getProperty("jarUrl"), versionManifest.getId())); - URL assetsIndexURL = url(String.format( - launcher.getProperties().getProperty("assetsIndexUrl"), versionManifest.getAssetsIndex())); - - // If the JAR does not exist, install it - if (!jarPath.exists()) { - List targets = new ArrayList(); - targets.add(jarPath); - installer.submit(new FileDownloader(installer, jarURL, targets)); - } - - // Install libraries - for (Library library : versionManifest.getLibraries()) { - if (library.matches(environment)) { - URL url = library.getURL(launcher, environment, librariesBaseURL); - File file = new File(librariesDir, library.getPath(environment)); - - if (!file.exists()) { - List targets = new ArrayList(); - targets.add(file); - installer.submit(new FileDownloader(installer, url, targets)); - } - - checkInterrupted(); - } - } - - // Install assets - AssetsIndex index = HttpRequest - .get(assetsIndexURL) - .execute() - .expectResponseCode(200) - .returnContent() - .saveContent(assetsRoot.getIndexPath(versionManifest)) - .asJson(AssetsIndex.class); - - for (Map.Entry entry : index.getObjects().entrySet()) { - String hash = entry.getValue().getHash(); - URL url = url(String.format( - launcher.getProperties().getProperty("assetUrl"), hash.subSequence(0, 2), hash)); - File path = assetsRoot.getObjectPath(entry.getValue()); - - checkInterrupted(); - - if (!path.exists()) { - List targets = new ArrayList(); - targets.add(path); - installer.submit(new FileDownloader(installer, url, targets)); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (IOException e) { - throw new RuntimeException("Failed to get resources", e); - } - } - - @Override - public String toString() { - return "GameUpdater{" + - "versionManifest.id=" + versionManifest.getId() + - ", environment=" + environment + - '}'; - } -} diff --git a/src/main/java/com/skcraft/launcher/update/InstanceResetter.java b/src/main/java/com/skcraft/launcher/update/HardResetter.java similarity index 83% rename from src/main/java/com/skcraft/launcher/update/InstanceResetter.java rename to src/main/java/com/skcraft/launcher/update/HardResetter.java index e9301a0..354e48a 100644 --- a/src/main/java/com/skcraft/launcher/update/InstanceResetter.java +++ b/src/main/java/com/skcraft/launcher/update/HardResetter.java @@ -16,12 +16,14 @@ import java.io.File; import java.io.IOException; import java.util.concurrent.Callable; -public class InstanceResetter implements Callable, ProgressObservable { +import static com.skcraft.launcher.util.SharedLocale._; + +public class HardResetter implements Callable, ProgressObservable { private final Instance instance; private File currentDir; - public InstanceResetter(@NonNull Instance instance) { + public HardResetter(@NonNull Instance instance) { this.instance = instance; } @@ -30,6 +32,11 @@ public class InstanceResetter implements Callable, ProgressObservable return -1; } + @Override + public String getStatus() { + return _("instanceResetter.resetting", instance.getTitle()); + } + @Override public Instance call() throws Exception { instance.setInstalled(false); diff --git a/src/main/java/com/skcraft/launcher/update/Installer.java b/src/main/java/com/skcraft/launcher/update/Installer.java deleted file mode 100644 index 0f97416..0000000 --- a/src/main/java/com/skcraft/launcher/update/Installer.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.update; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Charsets; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.skcraft.launcher.util.HttpRequest; -import com.skcraft.launcher.persistence.Persistence; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.java.Log; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.*; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; - -import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; -import static com.skcraft.launcher.LauncherUtils.checkInterrupted; - -@Log -public class Installer { - - private final ObjectMapper mapper = new ObjectMapper(); - private final HashFunction hf = Hashing.sha1(); - private final ListeningExecutorService executor; - @Getter private final File temporaryDir; - @Getter private final File dataFilesDir; - @Getter private final File destinationDir; - @Getter private final InstallLog currentLog = new InstallLog(); - @Getter private final InstallLog previousLog; - @Getter private final UpdateCache updateCache; - @Getter @Setter private int downloadTries = 5; - @Getter @Setter private int tryDelay = 3000; - private final List httpRequests = new ArrayList(); - private final Set usedHashes = new HashSet(); - private final File installLogPath; - private final File updateCachePath; - private final Set running = new HashSet(); - private final ErrorHandler errorHandler = new ErrorHandler(); - private Throwable throwable; - - public Installer(ListeningExecutorService executor, File temporaryDir, File dataFilesDir, File destinationDir) { - this.executor = executor; - this.temporaryDir = temporaryDir; - this.dataFilesDir = dataFilesDir; - this.destinationDir = destinationDir; - - installLogPath = new File(dataFilesDir, "install_log.json"); - updateCachePath = new File(dataFilesDir, "update_cache.json"); - - this.previousLog = Persistence.read(installLogPath, InstallLog.class); - this.updateCache = Persistence.read(updateCachePath, UpdateCache.class); - } - - public File download(URL url, String version) throws IOException, InterruptedException { - String baseId = hf.newHasher() - .putString(url.toString(), Charsets.UTF_8) - .putString(version, Charsets.UTF_8) - .hash() - .toString(); - String id = baseId; - int index = 0; - - while (usedHashes.contains(id)) { - id = baseId + "_" + (index++); - } - usedHashes.add(id); - - File dir = new File(temporaryDir, id.charAt(0) + File.separator + id.charAt(1)); - dir.mkdirs(); - File downloadPath = new File(dir, id + ".filepart"); - File tempPath = new File(dir, id + ".filedownload"); - - if (tempPath.exists()) { - log.log(Level.INFO, "Using existing {0} for {1}...", new Object[]{tempPath, url}); - return tempPath; - } else { - log.log(Level.INFO, "Downloading {0} to {1}...", new Object[]{url, downloadPath}); - - int trial = 0; - while (true) { - HttpRequest request = HttpRequest.get(url); - try { - synchronized (this) { - httpRequests.add(request); - } - request.execute() - .expectResponseCode(200) - .saveContent(downloadPath); - break; - } catch (IOException e) { - if (++trial >= downloadTries) { - throw e; - } - - log.log(Level.WARNING, String.format("Download of %s failed; retrying in %d ms", url, tryDelay), e); - Thread.sleep(tryDelay); - } finally { - synchronized (this) { - httpRequests.remove(request); - } - } - } - - downloadPath.renameTo(tempPath); - return tempPath; - } - } - - public synchronized ListenableFuture submit(Runnable runnable) { - running.add(runnable); - ListenableFuture future = executor.submit(runnable); - Futures.addCallback(future, errorHandler); - future.addListener(new RemoveRunnable(runnable), sameThreadExecutor()); - return future; - } - - public synchronized void submitAll(List tasks) { - for (Runnable runnable : tasks) { - submit(runnable); - } - } - - private synchronized void failAll(Throwable throwable) { - this.throwable = throwable; - running.clear(); - executor.shutdownNow(); - notifyAll(); - } - - public void awaitCompletion() throws ExecutionException, InterruptedException { - try { - synchronized (this) { - while (running.size() > 0) { - wait(); - } - } - - if (throwable != null) { - throw new ExecutionException(throwable); - } - } catch (InterruptedException e) { - executor.shutdownNow(); - throw new InterruptedException(); - } - } - - public void commit() throws IOException, InterruptedException { - deleteOldFiles(); - writeCache(); - } - - protected void deleteOldFiles() throws InterruptedException { - for (Map.Entry> entry : previousLog.getEntrySet()) { - for (String path : entry.getValue()) { - checkInterrupted(); - if (!currentLog.has(path)) { - new File(getDestinationDir(), path).delete(); - } - } - } - } - - protected void writeCache() throws IOException { - Persistence.write(installLogPath, currentLog); - Persistence.write(updateCachePath, updateCache); - } - - private class RemoveRunnable implements Runnable { - private final Runnable runnable; - - public RemoveRunnable(Runnable runnable) { - this.runnable = runnable; - } - - @Override - public void run() { - synchronized (Installer.this) { - running.remove(runnable); - if (running.isEmpty()) { - Installer.this.notifyAll(); - } - } - } - } - - private class ErrorHandler implements FutureCallback { - @Override - public void onSuccess(Object result) { - } - - @Override - public void onFailure(Throwable t) { - if (t instanceof InterruptedException) { - return; - } - log.log(Level.WARNING, "Failed install stage", t); - failAll(t); - } - } - - - @Override - public synchronized String toString() { - StringBuilder builder = new StringBuilder(); - - if (httpRequests.size() > 0) { - builder.append("Downloads:\n"); - for (HttpRequest request : httpRequests) { - builder.append("- "); - builder.append(request.getUrl()); - builder.append(" ("); - double progress = request.getProgress(); - if (progress >= 0) { - builder.append(Math.round(request.getProgress() * 100.0 * 100.0) / 100.0); - builder.append("%)"); - } else { - builder.append("pending)"); - } - builder.append("\n"); - } - builder.append("\n"); - - } - - builder.append("Tasks:\n"); - for (Runnable runnable : running) { - builder.append("- "); - builder.append(runnable.toString()); - builder.append("\n"); - } - return builder.toString(); - } - -} diff --git a/src/main/java/com/skcraft/launcher/update/InstanceUpdater.java b/src/main/java/com/skcraft/launcher/update/InstanceUpdater.java deleted file mode 100644 index d749858..0000000 --- a/src/main/java/com/skcraft/launcher/update/InstanceUpdater.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.update; - -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import com.skcraft.concurrency.ProgressObservable; -import com.skcraft.launcher.Instance; -import com.skcraft.launcher.Launcher; -import com.skcraft.launcher.model.minecraft.VersionManifest; -import com.skcraft.launcher.model.modpack.Manifest; -import com.skcraft.launcher.util.HttpRequest; -import com.skcraft.launcher.persistence.Persistence; -import lombok.NonNull; -import lombok.extern.java.Log; - -import java.io.IOException; -import java.net.URL; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.logging.Level; - -import static com.skcraft.launcher.util.HttpRequest.url; - -@Log -public class InstanceUpdater implements Callable, ProgressObservable { - - private final Launcher launcher; - private final Instance instance; - private final ListeningExecutorService executor = - MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(6)); - private Installer currentInstaller; - - public InstanceUpdater(@NonNull Launcher launcher, @NonNull Instance instance) { - this.launcher = launcher; - this.instance = instance; - } - - private URL getVersionManifestURL(String version) { - return url(String.format(launcher.getProperties().getProperty("versionManifestUrl"), version)); - } - - @Override - public Instance call() throws Exception { - try { - log.info("Checking for an update for '" + instance.getName() + "'..."); - if (instance.getManifestURL() == null) { - log.log(Level.INFO, - "No URL set for {0}, so it can't be updated (the modpack may be removed from the server)", - new Object[] { instance }); - } else if (instance.isUpdatePending() || !instance.isInstalled()) { - log.log(Level.INFO, "Updating {0}...", new Object[]{instance}); - update(instance); - } else { - log.log(Level.INFO, "No update found for {0}.", new Object[] { instance }); - } - - return instance; - } finally { - executor.shutdownNow(); - } - } - - private void update(Instance instance) throws IOException, InterruptedException, ExecutionException { - try { - instance.setLocal(true); - Persistence.commitAndForget(instance); - - Installer installer = new Installer(executor, - launcher.getInstallerDir(), - instance.getDir(), - instance.getContentDir()); - currentInstaller = installer; - - Manifest manifest = HttpRequest - .get(instance.getManifestURL()) - .execute() - .expectResponseCode(200) - .returnContent() - .saveContent(instance.getManifestPath()) - .asJson(Manifest.class); - if (manifest.getBaseUrl() == null) { - manifest.setBaseUrl(instance.getManifestURL()); - } - manifest.setInstaller(installer); - - installer.submitAll(manifest.getTasks()); - installer.awaitCompletion(); - installer.commit(); - - URL url = getVersionManifestURL(manifest.getGameVersion()); - log.log(Level.INFO, instance.getName() + ": Fetching version manifest from " + url + "..."); - - VersionManifest versionManifest = manifest.getVersionManifest(); - - if (versionManifest != null) { - Persistence.write(instance.getVersionManifestPath(), versionManifest); - } else { - // The manifest doesn't come with its own version manifest, so let's download the one for the given - // version of Minecraft - versionManifest = HttpRequest - .get(url) - .execute() - .expectResponseCode(200) - .returnContent() - .saveContent(instance.getVersionManifestPath()) - .asJson(VersionManifest.class); - } - - installer = new Installer(executor, - launcher.getInstallerDir(), - launcher.getCommonDataDir(), - launcher.getCommonDataDir()); - currentInstaller = installer; - - log.log(Level.INFO, instance.getName() + ": Enumerating common data files..."); - installer.submit(new GameUpdater(installer, launcher, versionManifest, manifest.getLibrariesURL())); - installer.awaitCompletion(); - - instance.setVersion(manifest.getVersion()); - instance.setUpdatePending(false); - instance.setInstalled(true); - instance.setLocal(true); - Persistence.commitAndForget(instance); - - log.log(Level.INFO, instance.getName() + " has been updated to version " + manifest.getVersion() + "."); - } finally { - currentInstaller = null; - } - } - - @Override - public double getProgress() { - return -1; - } - - @Override - public String toString() { - Installer installer = currentInstaller; - if (installer != null) { - return installer.toString(); - } else { - return "..."; - } - } -} diff --git a/src/main/java/com/skcraft/launcher/update/InstanceDeleter.java b/src/main/java/com/skcraft/launcher/update/Remover.java similarity index 80% rename from src/main/java/com/skcraft/launcher/update/InstanceDeleter.java rename to src/main/java/com/skcraft/launcher/update/Remover.java index e3e091c..55670c8 100644 --- a/src/main/java/com/skcraft/launcher/update/InstanceDeleter.java +++ b/src/main/java/com/skcraft/launcher/update/Remover.java @@ -16,12 +16,13 @@ import java.io.IOException; import java.util.concurrent.Callable; import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.util.SharedLocale._; -public class InstanceDeleter implements Callable, ProgressObservable { +public class Remover implements Callable, ProgressObservable { private final Instance instance; - public InstanceDeleter(@NonNull Instance instance) { + public Remover(@NonNull Instance instance) { this.instance = instance; } @@ -30,6 +31,11 @@ public class InstanceDeleter implements Callable, ProgressObservable { return -1; } + @Override + public String getStatus() { + return _("instanceDeleter.deleting", instance.getDir()); + } + @Override public Instance call() throws Exception { instance.setInstalled(false); diff --git a/src/main/java/com/skcraft/launcher/update/Updater.java b/src/main/java/com/skcraft/launcher/update/Updater.java new file mode 100644 index 0000000..791c7c6 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/Updater.java @@ -0,0 +1,159 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.update; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.concurrency.DefaultProgress; +import com.skcraft.concurrency.ProgressFilter; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.util.HttpRequest; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.HttpRequest.url; +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class Updater extends BaseUpdater implements Callable, ProgressObservable { + + private final ObjectMapper mapper = new ObjectMapper(); + private final Installer installer; + private final Launcher launcher; + private final Instance instance; + + private List librarySources = new ArrayList(); + private List assetsSources = new ArrayList(); + + private ProgressObservable progress = new DefaultProgress( + -1, _("instanceUpdater.preparingUpdate")); + + public Updater(@NonNull Launcher launcher, @NonNull Instance instance) { + super(launcher); + + this.installer = new Installer(launcher.getInstallerDir()); + this.launcher = launcher; + this.instance = instance; + + librarySources.add(launcher.propUrl("librariesSource")); + assetsSources.add(launcher.propUrl("assetsSource")); + } + + @Override + public Instance call() throws Exception { + Updater.log.info("Checking for an update for '" + instance.getName() + "'..."); + if (instance.getManifestURL() == null) { + Updater.log.log(Level.INFO, + "No URL set for {0}, so it can't be updated (the modpack may be removed from the server)", + new Object[] { instance }); + } else if (instance.isUpdatePending() || !instance.isInstalled()) { + Updater.log.log(Level.INFO, "Updating {0}...", new Object[]{instance}); + update(instance); + } else { + Updater.log.log(Level.INFO, "No update found for {0}.", new Object[] { instance }); + } + + return instance; + } + + private VersionManifest readVersionManifest(Manifest manifest) throws IOException, InterruptedException { + // Check whether the package manifest contains an embedded version manifest, + // otherwise we'll have to download the one for the given Minecraft version + VersionManifest version = manifest.getVersionManifest(); + if (version != null) { + mapper.writeValue(instance.getVersionPath(), version); + return version; + } else { + URL url = url(String.format( + launcher.getProperties().getProperty("versionManifestUrl"), + manifest.getGameVersion())); + + return HttpRequest + .get(url) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(instance.getVersionPath()) + .asJson(VersionManifest.class); + } + } + + /** + * Update the given instance. + * + * @param instance the instance + * @throws IOException thrown on I/O error + * @throws InterruptedException thrown on interruption + * @throws ExecutionException thrown on execution error + */ + protected void update(Instance instance) throws Exception { + // Mark this instance as local + instance.setLocal(true); + Persistence.commitAndForget(instance); + + // Install package and get the manifests + progress = new DefaultProgress(-1, _("instanceUpdater.readingManifest")); + Manifest manifest = installPackage(installer, instance); + progress = new DefaultProgress(-1, _("instanceUpdater.readingVersion")); + VersionManifest version = readVersionManifest(manifest); + + progress = new DefaultProgress(-1, _("instanceUpdater.buildingDownloadList")); + + // Install the .jar + File jarPath = launcher.getJarPath(version); + URL jarSource = launcher.propUrl("jarUrl", version.getId()); + installJar(installer, jarPath, jarSource); + + // Download libraries and assets + installLibraries(installer, version, launcher.getLibrariesDir(), librarySources); + installAssets(installer, version, launcher.propUrl("assetsIndexUrl", version.getAssetsIndex()), assetsSources); + + progress = ProgressFilter.between(installer.getDownloader(), 0, 0.9); + installer.download(); + + progress = ProgressFilter.between(installer, 0.9, 1); + installer.execute(); + + complete(); + + // Update the instance's information + instance.setVersion(manifest.getVersion()); + instance.setUpdatePending(false); + instance.setInstalled(true); + instance.setLocal(true); + Persistence.commitAndForget(instance); + + Updater.log.log(Level.INFO, instance.getName() + + " has been updated to version " + manifest.getVersion() + "."); + } + + @Override + public double getProgress() { + return progress.getProgress(); + } + + @Override + public String getStatus() { + return progress.getStatus(); + } + + +} diff --git a/src/main/java/com/skcraft/launcher/util/HttpRequest.java b/src/main/java/com/skcraft/launcher/util/HttpRequest.java index 84a2193..d1238b4 100644 --- a/src/main/java/com/skcraft/launcher/util/HttpRequest.java +++ b/src/main/java/com/skcraft/launcher/util/HttpRequest.java @@ -288,6 +288,11 @@ public class HttpRequest implements Closeable, ProgressObservable { } } + @Override + public String getStatus() { + return null; + } + @Override public void close() throws IOException { if (conn != null) conn.disconnect(); diff --git a/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/src/main/resources/com/skcraft/launcher/lang/Launcher.properties index 3643a90..11fce1e 100644 --- a/src/main/resources/com/skcraft/launcher/lang/Launcher.properties +++ b/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -118,4 +118,32 @@ console.tray.forceClose=Force close... console.closeWindow=Close Window console.hideWindow=Hide Window console.confirmKill=Are sure that you wish to close the game forcefully? You may lose data. -console.confirmKillTitle=Are you sure? \ No newline at end of file +console.confirmKillTitle=Are you sure? + +downloader.downloadingList=Downloading {0} file(s)... ({1} remaining) +downloader.jobProgress={1,number}%\t{0} +downloader.jobPending=...\t{0} +downloader.noDownloads=No pending downloads. + +progress.defaultStatus=Working... +progress.percentTitle=({0}%) {1} + +installer.installing=Installing... +installer.executing=Executing tasks... ({0} remaining) +installer.copyingFile=Copying from {0} to {1} +installer.movingFile=Moving {0} to {1} + +updater.updating=Updating launcher... + +instanceUpdater.preparingUpdate=Preparing to update... +instanceUpdater.readingManifest=Reading package manifest... +instanceUpdater.readingVersion=Reading version manifest... +instanceUpdater.buildingDownloadList=Collecting files to download... +instanceDeleter.deleting=Deleting {0}... +instanceResetter.resetting=Resetting {0}... +instanceLoader.loadingLocal=Loading local instances from disk... +instanceLoader.checkingRemote=Checking for new modpacks... +instanceLauncher.preparing=Preparing for launch... +instanceLauncher.preparingAssets=Preparing assets for game... +instanceLauncher.collectingArgs=Collecting arguments... +instanceLauncher.startingJava=Starting java... \ No newline at end of file diff --git a/src/main/resources/com/skcraft/launcher/launcher.properties b/src/main/resources/com/skcraft/launcher/launcher.properties index 7e81582..d16a0cd 100644 --- a/src/main/resources/com/skcraft/launcher/launcher.properties +++ b/src/main/resources/com/skcraft/launcher/launcher.properties @@ -9,10 +9,10 @@ agentName=Minecraft offlinePlayerName=Player versionManifestUrl=https://s3.amazonaws.com/Minecraft.Download/versions/%1$s/%1$s.json -librariesUrl=https://libraries.minecraft.net/ +librariesSource=https://libraries.minecraft.net/ jarUrl=http://s3.amazonaws.com/Minecraft.Download/versions/%1$s/%1$s.jar assetsIndexUrl=https://s3.amazonaws.com/Minecraft.Download/indexes/%s.json -assetUrl=http://resources.download.minecraft.net/%s/%s +assetsSource=http://resources.download.minecraft.net/%s/%s yggdrasilAuthUrl=https://authserver.mojang.com/authenticate resetPasswordUrl=https://minecraft.net/resetpassword