1
0
mirror of https://github.com/SKCraft/Launcher.git synced 2025-02-08 00:21:21 +01:00

Rewrote updating mechanism to be more reliable.

This commit is contained in:
sk89q 2014-01-09 01:52:39 -08:00
parent 9843b5c883
commit ae2a1f091f
43 changed files with 1256 additions and 821 deletions

View File

@ -0,0 +1,28 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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;
}
}

View File

@ -65,14 +65,19 @@ public class ObservableFuture<V> implements ListenableFuture<V>, 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();
}
}

View File

@ -0,0 +1,35 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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);
}
}

View File

@ -20,4 +20,11 @@ public interface ProgressObservable {
*/
double getProgress();
/**
* Get the current status text.
*
* @return the status text, or null if unavailable
*/
String getStatus();
}

View File

@ -44,7 +44,7 @@ public class Instance implements Comparable<Instance> {
}
@JsonIgnore
public File getVersionManifestPath() {
public File getVersionPath() {
return new File(dir, "version.json");
}

View File

@ -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<InstanceList>, 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<Instance> local = new ArrayList<Instance>();
List<Instance> remote = new ArrayList<Instance>();
@ -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();
}
}
}

View File

@ -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 {
});
}
}

View File

@ -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);

View File

@ -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<URL> future = launcher.getExecutor().submit(new LauncherUpdateChecker(launcher));
ListenableFuture<URL> future = launcher.getExecutor().submit(new UpdateChecker(launcher));
Futures.addCallback(future, new FutureCallback<URL>() {
@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<File> future = new ObservableFuture<File>(
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<Instance> future = new ObservableFuture<Instance>(
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<Instance> future = new ObservableFuture<Instance>(
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<Instance> future = new ObservableFuture<Instance>(
launcher.getExecutor().submit(updater), updater);

View File

@ -347,6 +347,11 @@ public class LoginDialog extends JDialog {
public double getProgress() {
return -1;
}
@Override
public String getStatus() {
return _("login.loggingInStatus");
}
}
}

View File

@ -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);
}
});
}

View File

@ -0,0 +1,21 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<URL> urls, String key, long size, String name);
File download(URL url, String key, long size, String name);
}

View File

@ -0,0 +1,47 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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);
}
}

View File

@ -0,0 +1,47 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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);
}
}

View File

@ -0,0 +1,265 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<HttpDownloadJob> queue = new ArrayList<HttpDownloadJob>();
private final Set<String> usedKeys = new HashSet<String>();
private final List<HttpDownloadJob> running = new ArrayList<HttpDownloadJob>();
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<URL> 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<URL> urls = new ArrayList<URL>();
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<ListenableFuture<?>> futures = new ArrayList<ListenableFuture<?>>();
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<URL> urls;
private final long size;
private String name;
private HttpRequest request;
private HttpDownloadJob(File destFile, List<URL> 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);
}
}
}
}

View File

@ -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;

View File

@ -0,0 +1,50 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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);
}
}

View File

@ -0,0 +1,15 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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;
}

View File

@ -0,0 +1,85 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<InstallTask> queue = new ArrayList<InstallTask>();
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");
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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<Process>, 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<Process>, 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<Process>, 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<Process>, ProgressObservable {
log.info("Launching: " + builder);
checkInterrupted();
progress = new DefaultProgress(1, _("instanceLauncher.startingJava"));
return processBuilder.start();
}
@ -301,4 +309,9 @@ public class InstanceLauncher implements Callable<Process>, ProgressObservable {
return -1;
}
@Override
public String getStatus() {
return null;
}
}

View File

@ -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;

View File

@ -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<File> targets = new ArrayList<File>();
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);
}
}

View File

@ -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<Task> tasks = new ArrayList<Task>();
private List<ManifestEntry> tasks = new ArrayList<ManifestEntry>();
@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;
}

View File

@ -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;
}

View File

@ -29,6 +29,7 @@ import java.util.*;
* @author <a href="mailto:hboutemy@apache.org">Herve Boutemy</a>
* @version $Id$
*/
@SuppressWarnings("unchecked")
public class ComparableVersion
implements Comparable {
private String value;

View File

@ -1,71 +0,0 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<File>, 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<File> paths = new ArrayList<File>();
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 "...";
}
}
}

View File

@ -0,0 +1,70 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<File>, 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();
}
}

View File

@ -18,18 +18,18 @@ import java.util.concurrent.Callable;
import static com.skcraft.launcher.util.SharedLocale._;
@Log
public class LauncherUpdateChecker implements Callable<URL> {
public class UpdateChecker implements Callable<URL> {
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<URL> {
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) {

View File

@ -0,0 +1,198 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Runnable> executeOnCompletion = new ArrayList<Runnable>();
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<String, Set<String>> 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<File> targets = new ArrayList<File>();
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<URL> 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<String, Asset> 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<URL> urls = new ArrayList<URL>();
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<URL> 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<URL> urls = new ArrayList<URL>();
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);
}
}
}
}
}

View File

@ -1,43 +0,0 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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);
}
}
}
}

View File

@ -1,51 +0,0 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<File> to;
public FileDistribute(File from, List<File> 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);
}
}
}
}

View File

@ -1,42 +0,0 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<File> targets;
public FileDownloader(Installer installer, URL url, List<File> 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);
}
}
}

View File

@ -1,120 +0,0 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<File> targets = new ArrayList<File>();
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<File> targets = new ArrayList<File>();
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<String, Asset> 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<File> targets = new ArrayList<File>();
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 +
'}';
}
}

View File

@ -16,12 +16,14 @@ import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
public class InstanceResetter implements Callable<Instance>, ProgressObservable {
import static com.skcraft.launcher.util.SharedLocale._;
public class HardResetter implements Callable<Instance>, 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<Instance>, ProgressObservable
return -1;
}
@Override
public String getStatus() {
return _("instanceResetter.resetting", instance.getTitle());
}
@Override
public Instance call() throws Exception {
instance.setInstalled(false);

View File

@ -1,248 +0,0 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<HttpRequest> httpRequests = new ArrayList<HttpRequest>();
private final Set<String> usedHashes = new HashSet<String>();
private final File installLogPath;
private final File updateCachePath;
private final Set<Runnable> running = new HashSet<Runnable>();
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<? extends Runnable> 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<String, Set<String>> 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<Object> {
@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();
}
}

View File

@ -1,151 +0,0 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Instance>, 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 "...";
}
}
}

View File

@ -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<Instance>, ProgressObservable {
public class Remover implements Callable<Instance>, 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<Instance>, ProgressObservable {
return -1;
}
@Override
public String getStatus() {
return _("instanceDeleter.deleting", instance.getDir());
}
@Override
public Instance call() throws Exception {
instance.setInstalled(false);

View File

@ -0,0 +1,159 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Instance>, ProgressObservable {
private final ObjectMapper mapper = new ObjectMapper();
private final Installer installer;
private final Launcher launcher;
private final Instance instance;
private List<URL> librarySources = new ArrayList<URL>();
private List<URL> assetsSources = new ArrayList<URL>();
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();
}
}

View File

@ -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();

View File

@ -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?
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...

View File

@ -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