/*
* 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;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.skcraft.launcher.auth.AccountList;
import com.skcraft.launcher.auth.LoginService;
import com.skcraft.launcher.auth.YggdrasilLoginService;
import com.skcraft.launcher.launch.LaunchSupervisor;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.persistence.Persistence;
import com.skcraft.launcher.swing.SwingHelper;
import com.skcraft.launcher.update.UpdateManager;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SharedLocale;
import com.skcraft.launcher.util.SimpleLogFormatter;
import com.sun.management.OperatingSystemMXBean;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.java.Log;
import org.apache.commons.io.FileUtils;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import static com.skcraft.launcher.util.SharedLocale.tr;
/**
* The main entry point for the launcher.
*/
@Log
public final class Launcher {
public static final int PROTOCOL_VERSION = 2;
@Getter
private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
@Getter @Setter private Supplier<Window> mainWindowSupplier = new DefaultLauncherSupplier(this);
@Getter private final File baseDir;
@Getter private final Properties properties;
@Getter private final InstanceList instances;
@Getter private final Configuration config;
@Getter private final AccountList accounts;
@Getter private final AssetsRoot assets;
@Getter private final LaunchSupervisor launchSupervisor = new LaunchSupervisor(this);
@Getter private final UpdateManager updateManager = new UpdateManager(this);
@Getter private final InstanceTasks instanceTasks = new InstanceTasks(this);
/**
* Create a new launcher instance with the given base directory.
*
* @param baseDir the base directory
* @throws java.io.IOException on load error
*/
public Launcher(@NonNull File baseDir) throws IOException {
this(baseDir, baseDir);
}
/**
* Create a new launcher instance with the given base and configuration
* directories.
*
* @param baseDir the base directory
* @param configDir the config directory
* @throws java.io.IOException on load error
*/
public Launcher(@NonNull File baseDir, @NonNull File configDir) throws IOException {
SharedLocale.loadBundle("com.skcraft.launcher.lang.Launcher", Locale.getDefault());
this.baseDir = baseDir;
this.properties = LauncherUtils.loadProperties(Launcher.class, "launcher.properties", "com.skcraft.launcher.propertiesFile");
this.instances = new InstanceList(this);
this.assets = new AssetsRoot(new File(baseDir, "assets"));
this.config = Persistence.load(new File(configDir, "config.json"), Configuration.class);
this.accounts = Persistence.load(new File(configDir, "accounts.dat"), AccountList.class);
setDefaultConfig();
if (accounts.getSize() > 0) {
accounts.setSelectedItem(accounts.getElementAt(0));
}
executor.submit(new Runnable() {
@Override
public void run() {
cleanupExtractDir();
}
});
updateManager.checkForUpdate();
}
/**
* Updates any incorrect / unset configuration settings with defaults.
*/
public void setDefaultConfig() {
double configMax = config.getMaxMemory() / 1024.0;
double suggestedMax = 2;
double available = Double.MAX_VALUE;
try {
OperatingSystemMXBean bean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
available = bean.getTotalPhysicalMemorySize() / 1024.0 / 1024.0 / 1024.0;
if (available <= 6) {
suggestedMax = available * 0.48;
} else {
suggestedMax = 4;
}
} catch (Exception ignored) {
}
if (config.getMaxMemory() <= 0 || configMax >= available - 1) {
config.setMaxMemory((int) (suggestedMax * 1024));
}
}
/**
* Get the launcher version.
*
* @return the launcher version
*/
public String getVersion() {
String version = getProperties().getProperty("version");
if (version.equals("${project.version}")) {
return "1.0.0-SNAPSHOT";
}
return version;
}
/**
* Get a login service.
*
* @return a login service
*/
public LoginService getLoginService() {
return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")));
}
/**
* Get the directory containing the instances.
*
* @return the instances dir
*/
public File getInstancesDir() {
return new File(getBaseDir(), "instances");
}
/**
* Get the directory to store temporary files.
*
* @return the temporary directory
*/
public File getTemporaryDir() {
return new File(getBaseDir(), "temp");
}
/**
* Get the directory to store temporary install files.
*
* @return the temporary install directory
*/
public File getInstallerDir() {
return new File(getTemporaryDir(), "install");
}
/**
* Get the directory to store temporarily extracted files.
*
* @return the directory
*/
private File getExtractDir() {
return new File(getTemporaryDir(), "extract");
}
/**
* Delete old extracted files.
*/
public void cleanupExtractDir() {
log.info("Cleaning up temporary extracted files directory...");
final long now = System.currentTimeMillis();
File[] dirs = getExtractDir().listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
try {
long time = Long.parseLong(pathname.getName());
return (now - time) > (1000 * 60 * 60);
} catch (NumberFormatException e) {
return false;
}
}
});
if (dirs != null) {
for (File dir : dirs) {
log.info("Removing " + dir.getAbsolutePath() + "...");
try {
FileUtils.deleteDirectory(dir);
} catch (IOException e) {
log.log(Level.WARNING, "Failed to delete " + dir.getAbsolutePath(), e);
}
}
}
}
/**
* Create a new temporary directory to extract files to.
*
* @return the directory path
*/
public File createExtractDir() {
File dir = new File(getExtractDir(), String.valueOf(System.currentTimeMillis()));
dir.mkdirs();
log.info("Created temporary directory " + dir.getAbsolutePath());
return dir;
}
/**
* Get the directory to store the launcher binaries.
*
* @return the libraries directory
*/
public File getLauncherBinariesDir() {
return new File(getBaseDir(), "launcher");
}
/**
* Get the directory to store common data files.
*
* @return the common data directory
*/
public File getCommonDataDir() {
return getBaseDir();
}
/**
* Get the directory to store libraries.
*
* @return the libraries directory
*/
public File getLibrariesDir() {
return new File(getCommonDataDir(), "libraries");
}
/**
* Get the directory to store versions.
*
* @return the versions directory
*/
public File getVersionsDir() {
return new File(getCommonDataDir(), "versions");
}
/**
* Get the directory to store a version.
*
* @param version the version
* @return the directory
*/
public File getVersionDir(String version) {
return new File(getVersionsDir(), version);
}
/**
* Get the path to the JAR for the given version manifest.
*
* @param versionManifest the version manifest
* @return the path
*/
public File getJarPath(VersionManifest versionManifest) {
return new File(getVersionDir(versionManifest.getId()), versionManifest.getId() + ".jar");
}
/**
* Get the news URL.
*
* @return the news URL
*/
public URL getNewsURL() {
try {
return HttpRequest.url(
String.format(getProperties().getProperty("newsUrl"),
URLEncoder.encode(getVersion(), "UTF-8")));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Get the packages URL.
*
* @return the packages URL
*/
public URL getPackagesURL() {
try {
String key = Strings.nullToEmpty(getConfig().getGameKey());
return HttpRequest.url(
String.format(getProperties().getProperty("packageListUrl"),
URLEncoder.encode(key, "UTF-8")));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* 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));
}
/**
* Show the launcher.
*/
public void showLauncherWindow() {
mainWindowSupplier.get().setVisible(true);
}
/**
* Create a new launcher from arguments.
*
* @param args the arguments
* @return the launcher
* @throws ParameterException thrown on a bad parameter
* @throws IOException throw on an I/O error
*/
public static Launcher createFromArguments(String[] args) throws ParameterException, IOException {
LauncherArguments options = new LauncherArguments();
new JCommander(options, args);
Integer bsVersion = options.getBootstrapVersion();
log.info(bsVersion != null ? "Bootstrap version " + bsVersion + " detected" : "Not bootstrapped");
File dir = options.getDir();
if (dir != null) {
log.info("Using given base directory " + dir.getAbsolutePath());
} else {
dir = new File(".");
log.info("Using current directory " + dir.getAbsolutePath());
}
return new Launcher(dir);
}
/**
* Setup loggers and perform initialization.
*/
public static void setupLogger() {
SimpleLogFormatter.configureGlobalLogger();
}
/**
* Bootstrap.
*
* @param args args
*/
public static void main(final String[] args) {
setupLogger();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
Launcher launcher = createFromArguments(args);
SwingHelper.setSwingProperties(tr("launcher.appTitle", launcher.getVersion()));
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
launcher.showLauncherWindow();
} catch (Throwable t) {
log.log(Level.WARNING, "Load failure", t);
SwingHelper.showErrorDialog(null, "Uh oh! The updater couldn't be opened because a " +
"problem was encountered.", "Launcher error", t);
}
}
});
}
}