/*
Copyright 2009 Hauke Rehfeld
This file is part of QuakeInjector.
QuakeInjector is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
QuakeInjector is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QuakeInjector. If not, see <http://www.gnu.org/licenses/>.
*/
package de.haukerehfeld.quakeinjector;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
public class Installer {
private static final int simultanousDownloads = 1;
private static final int simultanousInstalls = 1;
private static final int simultanousInspectors = 1;
private static final int simultanousWaiters = 15;
private Configuration.EnginePath installDirectory;
private Configuration.DownloadPath downloadDirectory;
private ExecutorService activeDownloaders = Executors.newFixedThreadPool(simultanousDownloads);
private ExecutorService activeInspectors = Executors.newFixedThreadPool(simultanousInspectors);
private ExecutorService activeInstallers = Executors.newFixedThreadPool(simultanousInstalls);
private ExecutorService activeWaiters = Executors.newFixedThreadPool(simultanousWaiters);
private Map<Package,Worker> queue = new HashMap<Package,Worker>();
public Installer(Configuration.EnginePath installDirectory, Configuration.DownloadPath downloadDirectory) {
this.installDirectory = installDirectory;
this.downloadDirectory = downloadDirectory;
}
public boolean checkInstallDirectory() {
if (!installDirectory.existsOrDefault()) {
return false;
}
return installDirectory.get().canWrite();
}
public boolean checkDownloadDirectory() {
if (!downloadDirectory.existsOrDefault()) {
return false;
}
return downloadDirectory.get().canWrite();
}
public void cancelAll() {
synchronized (queue) {
for (Package inQueue: new java.util.HashSet<Package>(getQueue())) {
cancel(inQueue);
System.out.println("Canceling " + inQueue);
}
}
}
public boolean working() {
return !queue.isEmpty();
}
public Set<Package> getQueue() {
return queue.keySet();
}
public boolean alreadyQueued(final Package map) {
return queue.get(map) != null;
}
public void install(final Package selectedMap,
final String url,
final InstallErrorHandler errorHandler,
final PropertyChangeListener downloadProgressListener) {
//map already in the instalation queue
if (alreadyQueued(selectedMap)) {
return;
}
//wait for the download to finish, then start
//installation. after that, handle errors or save status
Worker saveInstalled = new Worker(url,
selectedMap,
errorHandler,
downloadProgressListener);
synchronized (queue) { queue.put(selectedMap, saveInstalled); }
synchronized (activeWaiters) { activeWaiters.submit(saveInstalled); }
}
public void cancel(Package installerMap) {
Worker w;
synchronized (queue) { w = queue.get(installerMap); }
w.cancel();
}
public void uninstall(PackageFileList map,
final UninstallErrorHandler errorHandler,
PropertyChangeListener progressListener) {
final UninstallWorker uninstall = new UninstallWorker(map, installDirectory.get().getAbsolutePath());
uninstall.addPropertyChangeListener(progressListener);
uninstall.execute();
//wait until finished and set finished status
new SwingWorker<Void,Void>() {
@Override
public Void doInBackground() {
try {
uninstall.get();
errorHandler.success();
}
catch (java.util.concurrent.ExecutionException e) {
Throwable er = e.getCause();
if (er instanceof Exception) {
errorHandler.error((Exception) er);
}
else {
errorHandler.error(e);
}
}
catch (java.lang.InterruptedException e) {
System.out.println("Installer: " + e.getMessage());
e.printStackTrace();
}
return null;
}
}.execute();
}
public interface InstallErrorHandler {
/**
* Decide which files in the package should be overwritten
* @param existingFiles in the package
* @return all the files that should be overwritten or an empty list to overwrite no files and cancel install
*/
public List<File> overwrite(Map<String,File> existingFiles);
public void success(PackageFileList installedFiles);
public void handle(OnlineFileNotFoundException error);
public void handle(FileNotWritableException error, PackageFileList alreadyInstalledFiles);
public void handle(IOException error, PackageFileList alreadyInstalledFiles);
public void handle(java.net.SocketException error, PackageFileList alreadyInstalledFiles);
public void handle(CancelledException error, PackageFileList alreadyInstalledFiles);
}
public interface UninstallErrorHandler {
public void success();
public void error(Exception e);
}
public static class CancelledException extends RuntimeException {}
private class Worker extends SwingWorker<Void,Void> {
public InstallWorker installer;
public DownloadWorker downloader;
private Throwable error;
private final String url;
private final Package map;
private final InstallErrorHandler handler;
private final PropertyChangeListener downloadProgressListener;
public Worker(String url,
Package map,
InstallErrorHandler handler,
PropertyChangeListener downloadProgressListener) {
this.url = url;
this.map = map;
this.handler = handler;
this.downloadProgressListener = downloadProgressListener;
}
@Override
public Void doInBackground() {
try {
final File downloadFile = new File(downloadDirectory.get().getAbsolutePath() + File.separator + map.getId() + ".zip");
System.out.println("Downloading to " + downloadFile);
long downloadSize;
if (!downloadFile.exists()) {
downloadFile.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(downloadFile);
//make sure file streams get closed
try {
downloadSize = download(url, out);
out.flush();
out.close();
}
catch (Exception e) {
out.close();
System.out.print("Error downloading file, removing...");
if (!downloadFile.delete()) {
System.err.print("Couldn't delete partially downloaded file ("
+ downloadFile + ")! ");
}
System.out.println("done.");
throw e;
}
}
else {
downloadSize = downloadFile.length();
System.out.println("Skipping download, already existing with length " + downloadSize);
}
System.out.println("Inspecting downloaded archive..." + downloadFile);
FileInputStream in = new FileInputStream(downloadFile);
Map<String,File> existingFiles = inspect(in);
in.close();
System.out.println("done.");
List<File> overwrites = null;
if (existingFiles != null) {
overwrites = askForOverwrite(existingFiles);
}
System.out.println("After asking");
if (overwrites == null || !overwrites.isEmpty()) {
//and start install
System.out.println("Starting install");
String mapDir = installDirectory.getUnzipDir(map).getAbsolutePath();
in = new FileInputStream(downloadFile);
installer = new InstallWorker(in,
downloadSize,
map,
installDirectory.get(),
mapDir,
overwrites);
synchronized (activeInstallers) { activeInstallers.submit(installer); }
//make sure file streams get closed
try {
installer.get();
}
catch (Exception e) {
throw e;
}
finally {
in.close();
}
}
else {
System.out.println("Canceling install");
cancel();
}
}
catch (java.io.FileNotFoundException e) {
error = e;
}
catch (IOException e) {
error = e;
}
catch (java.util.concurrent.CancellationException e) {
error = e;
}
catch (java.lang.InterruptedException e) {
error = e;
}
catch (java.util.concurrent.ExecutionException e) {
error = e.getCause();
}
catch (Exception e) {
error = e;
System.err.println("Caught 'unchecked' exception in Installer.Worker.doBackground(): " + e);
e.printStackTrace();
}
return null;
}
private long download(String url,
FileOutputStream out) throws
IOException,
InterruptedException,
ExecutionException {
//first, download the file
Download download = Download.create(url);
downloader = new DownloadWorker(download, out);
downloader.addPropertyChangeListener(downloadProgressListener);
synchronized (activeDownloaders) { activeDownloaders.submit(downloader); }
return downloader.get();
}
private Map<String,File> inspect(final InputStream in) throws
IOException,
InterruptedException,
ExecutionException {
//see what files the zip wants to extract
InspectZipWorker inspector = new InspectZipWorker(in);
synchronized (activeInspectors) { activeInspectors.submit(inspector); }
System.out.println("Waiting for inspection...");
final List<ZipEntry> entries = inspector.get();
//check files
final Map<String,File> files = new HashMap<String,File>();
boolean existingFile = false;
for (ZipEntry z: entries) {
if (z.isDirectory()) {
continue;
}
File f = new File(installDirectory.getUnzipDir(map).getAbsolutePath() + File.separator + z.getName());
String name
= RelativePath.getRelativePath(installDirectory.get(), f).toString();
files.put(name, f);
if (f.exists()) {
existingFile = true;
}
}
if (!existingFile) {
return null;
}
return files;
}
private List<File> askForOverwrite(final Map<String,File> files) throws
InterruptedException,
InvocationTargetException,
ExecutionException {
//popup overwrite dialog
SwingWorker<List<File>,Void> dialogue = new SwingWorker<List<File>,Void>() {
public List<File> doInBackground() {
return handler.overwrite(files);
}
};
SwingUtilities.invokeAndWait(dialogue);
List<File> overwrites = dialogue.get();
return overwrites;
}
public void cancel() {
if (downloader != null) { downloader.cancel(true); }
if (installer != null) { installer.cancel(true); }
cancel(true);
}
@Override
public void done() {
PackageFileList files;
if (installer != null) {
files = installer.getInstalledFiles();
}
else {
files = new PackageFileList(map.getId());
}
//see if there was an error
if (error != null) {
try {
throw error;
}
catch (OnlineFileNotFoundException error) {
handler.handle((OnlineFileNotFoundException) error);
}
catch (java.net.SocketException error) {
handler.handle((java.net.SocketException) error, files);
}
catch (FileNotWritableException error) {
handler.handle((FileNotWritableException) error, files);
}
catch (IOException error) {
handler.handle((IOException) error, files);
}
catch (Throwable e) {
System.out.println("unhandled exception from install worker" + error);
error.printStackTrace();
}
installer = null;
downloader = null;
}
else if (isCancelled()) {
System.out.println("CancelledException!");
handler.handle(new CancelledException(), files);
}
else {
System.out.println("Success installing");
handler.success(files);
}
System.out.println("Done saving installedmaps");
synchronized (queue) { queue.remove(map); }
}
}
}