/*
* $Id$
*
* Copyright (c) 2008-2009 by Joel Uckelman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.launch;
import java.awt.Dimension;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipFile;
import javax.swing.AbstractAction;
import javax.swing.SwingUtilities;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.SystemUtils;
import org.jdesktop.swingworker.SwingWorker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import VASSAL.Info;
import VASSAL.build.module.ExtensionsManager;
import VASSAL.build.module.GlobalOptions;
import VASSAL.build.module.metadata.AbstractMetaData;
import VASSAL.build.module.metadata.MetaDataFactory;
import VASSAL.build.module.metadata.ModuleMetaData;
import VASSAL.configure.DirectoryConfigurer;
import VASSAL.preferences.Prefs;
import VASSAL.preferences.ReadOnlyPrefs;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.ThrowableUtils;
import VASSAL.tools.WarningDialog;
import VASSAL.tools.concurrent.FutureUtils;
import VASSAL.tools.concurrent.listener.EventListener;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.filechooser.ModuleFileFilter;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.io.ProcessLauncher;
import VASSAL.tools.io.ProcessWrapper;
import VASSAL.tools.ipc.IPCMessage;
import VASSAL.tools.ipc.IPCMessenger;
import VASSAL.tools.ipc.SimpleIPCMessage;
import VASSAL.tools.lang.MemoryUtils;
/**
*
* The base class for {@link Action}s which launch processes from the
* {@link ModuleManagerWindow}.
*
* @author Joel Uckelman
* @since 3.1.0
*/
public abstract class AbstractLaunchAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private static final Logger logger =
LoggerFactory.getLogger(AbstractLaunchAction.class);
//
// memory-related constants
//
protected static final int PHYS_MEMORY;
protected static final int DEFAULT_INITIAL_HEAP = 256;
protected static final int DEFAULT_MAXIMUM_HEAP = 512;
protected static final int FAILSAFE_INITIAL_HEAP = 64;
protected static final int FAILSAFE_MAXIMUM_HEAP = 128;
static {
// Determine how much physical RAM this machine has
// Assume 4GB if we can't determine how much RAM there is
final long physMemoryBytes = MemoryUtils.getPhysicalMemory();
PHYS_MEMORY = physMemoryBytes < 0 ? 4096 : (int)(physMemoryBytes >> 20);
}
protected final Window window;
protected final String entryPoint;
protected final LaunchRequest lr;
protected static final Set<File> editing =
Collections.synchronizedSet(new HashSet<File>());
protected static final Map<File,Integer> using =
Collections.synchronizedMap(new HashMap<File,Integer>());
/*
protected static final List<ObjectOutputStream> children =
Collections.synchronizedList(new ArrayList<ObjectOutputStream>());
*/
protected static final List<IPCMessenger> children =
Collections.synchronizedList(new ArrayList<IPCMessenger>());
protected static final AtomicInteger nextId = new AtomicInteger(1);
public AbstractLaunchAction(String name, Window window,
String entryPoint, LaunchRequest lr) {
super(name);
this.window = window;
this.entryPoint = entryPoint;
this.lr = lr;
}
/**
* @param file the file to check
* @return <code>true</code> iff the file is in use
*/
public static boolean isInUse(File file) {
return using.containsKey(file);
}
/**
* @param file the file to check
* @return <code>true</code> iff the file is being edited
*/
public static boolean isEditing(File file) {
return editing.contains(file);
}
/**
* Ask child processes to close.
*
* @return <code>true</code> iff all child processes will terminate
*/
public static boolean shutDown() {
ModuleManagerWindow.getInstance().toBack();
final List<Future<IPCMessage>> futures =
new ArrayList<Future<IPCMessage>>();
// must synchronize when iterating over a Collections.synchronizedList()
synchronized (children) {
for (IPCMessenger ipc : children) {
try {
futures.add(ipc.send(new Launcher.CloseRequest()));
}
catch (IOException e) {
// FIXME
e.printStackTrace();
}
}
}
// FIXME: not working!
for (Future<IPCMessage> f : futures) {
try {
if (f.get() instanceof Launcher.CloseReject) {
System.out.println("rejected!");
return false;
}
}
catch (ExecutionException e) {
e.printStackTrace();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
/** {@inheritDoc} */
public void actionPerformed(ActionEvent e) {
ModuleManagerWindow.getInstance().setWaitCursor(true);
getLaunchTask().execute();
}
protected abstract LaunchTask getLaunchTask();
protected File promptForFile() {
// prompt the user to pick a file
final FileChooser fc = FileChooser.createFileChooser(window,
(DirectoryConfigurer)
Prefs.getGlobalPrefs().getOption(Prefs.MODULES_DIR_KEY));
addFileFilters(fc);
// loop until cancellation or we get an existing file
if (fc.showOpenDialog() == FileChooser.APPROVE_OPTION) {
lr.module = fc.getSelectedFile();
if (lr.module != null) {
if (lr.module.exists()) {
final AbstractMetaData metadata =
MetaDataFactory.buildMetaData(lr.module);
if (metadata == null || ! (metadata instanceof ModuleMetaData)) {
ErrorDialog.show(
"Error.invalid_vassal_module", lr.module.getAbsolutePath());
logger.error(
"-- Load of {} failed: Not a Vassal module",
lr.module.getAbsolutePath()
);
lr.module = null;
}
}
else {
lr.module = null;
}
// FIXME: do something to warn about nonexistant file
// FileNotFoundDialog.warning(window, lr.module);
}
}
return lr.module;
}
protected void addFileFilters(FileChooser fc) {
fc.addChoosableFileFilter(new ModuleFileFilter());
}
protected class LaunchTask extends SwingWorker<Void,Void> {
protected final int id = nextId.getAndIncrement();
// lr might be modified before the task is over, keep a local copy
protected final LaunchRequest lr =
new LaunchRequest(AbstractLaunchAction.this.lr);
protected ServerSocket serverSocket = null;
protected Socket clientSocket = null;
protected IPCMessenger ipc = null;
@Override
public Void doInBackground() throws InterruptedException,
IOException {
// FIXME: this should be in an abstract method and farmed out to subclasses
// send some basic information to the log
if (lr.module != null) {
logger.info("Loading module file {}", lr.module.getAbsolutePath());
// slice tiles for module
final String aname = lr.module.getAbsolutePath();
final ModuleMetaData meta = new ModuleMetaData(new ZipFile(aname));
final String hstr =
DigestUtils.shaHex(meta.getName() + "_" + meta.getVersion());
final File cdir = new File(Info.getConfDir(), "tiles/" + hstr);
final TilingHandler th = new TilingHandler(
aname,
cdir,
new Dimension(256, 256),
PHYS_MEMORY,
nextId.getAndIncrement()
);
try {
th.sliceTiles();
}
catch (CancellationException e) {
cancel(true);
return null;
}
// slice tiles for extensions
final ExtensionsManager mgr = new ExtensionsManager(lr.module);
for (File ext : mgr.getActiveExtensions()) {
final TilingHandler eth = new TilingHandler(
ext.getAbsolutePath(),
cdir,
new Dimension(256, 256),
PHYS_MEMORY,
nextId.getAndIncrement()
);
try {
eth.sliceTiles();
}
catch (CancellationException e) {
cancel(true);
return null;
}
}
}
if (lr.game != null) {
logger.info("Loading game file {}", lr.game.getAbsolutePath());
}
if (lr.importFile != null) {
logger.info(
"Importing module file {}",
lr.importFile.getAbsolutePath()
);
}
// end FIXME
// set default heap sizes
int initialHeap = DEFAULT_INITIAL_HEAP;
int maximumHeap = DEFAULT_MAXIMUM_HEAP;
String moduleName = null;
// FIXME: this should be in an abstract method and farmed out to subclasses,
// rather than a case structure for each kind of thing which may be loaded.
// find module-specific heap settings, if any
if (lr.module != null) {
final AbstractMetaData data = MetaDataFactory.buildMetaData(lr.module);
if (data == null) {
ErrorDialog.show(
"Error.invalid_vassal_file", lr.module.getAbsolutePath());
ModuleManagerWindow.getInstance().setWaitCursor(false);
return null;
}
if (data instanceof ModuleMetaData) {
moduleName = ((ModuleMetaData) data).getName();
// log the module name
logger.info("Loading module {}", moduleName);
// read module prefs
final ReadOnlyPrefs p = new ReadOnlyPrefs(moduleName);
// read initial heap size
initialHeap = getHeapSize(
p, GlobalOptions.INITIAL_HEAP, DEFAULT_INITIAL_HEAP);
// read maximum heap size
maximumHeap = getHeapSize(
p, GlobalOptions.MAXIMUM_HEAP, DEFAULT_MAXIMUM_HEAP);
}
}
else if (lr.importFile != null) {
final Prefs p = Prefs.getGlobalPrefs();
// read initial heap size
initialHeap = getHeapSize(
p, GlobalOptions.INITIAL_HEAP, DEFAULT_INITIAL_HEAP);
// read maximum heap size
maximumHeap = getHeapSize(
p, GlobalOptions.MAXIMUM_HEAP, DEFAULT_MAXIMUM_HEAP);
}
// end FIXME
//
// Heap size sanity checks: fall back to failsafe heap sizes in
// case the given initial or maximum heap is not usable.
//
// FIXME: The heap size messages are too nonspecific. They should
// differientiate between loading a module and importing a module,
// since the heap sizes are set in different places for those two
// actions.
// maximum heap must fit in physical RAM
if (maximumHeap > PHYS_MEMORY) {
initialHeap = FAILSAFE_INITIAL_HEAP;
maximumHeap = FAILSAFE_MAXIMUM_HEAP;
FutureUtils.wait(WarningDialog.show(
"Warning.maximum_heap_too_large",
FAILSAFE_MAXIMUM_HEAP
));
}
// maximum heap must be at least the failsafe size
else if (maximumHeap < FAILSAFE_MAXIMUM_HEAP) {
initialHeap = FAILSAFE_INITIAL_HEAP;
maximumHeap = FAILSAFE_MAXIMUM_HEAP;
FutureUtils.wait(WarningDialog.show(
"Warning.maximum_heap_too_small",
FAILSAFE_MAXIMUM_HEAP
));
}
// initial heap must be at least the failsafe size
else if (initialHeap < FAILSAFE_INITIAL_HEAP) {
initialHeap = FAILSAFE_INITIAL_HEAP;
maximumHeap = FAILSAFE_MAXIMUM_HEAP;
FutureUtils.wait(WarningDialog.show(
"Warning.initial_heap_too_small",
FAILSAFE_INITIAL_HEAP
));
}
// initial heap must be less than or equal to maximum heap
else if (initialHeap > maximumHeap) {
initialHeap = FAILSAFE_INITIAL_HEAP;
maximumHeap = FAILSAFE_MAXIMUM_HEAP;
FutureUtils.wait(WarningDialog.show(
"Warning.initial_heap_too_large",
FAILSAFE_INITIAL_HEAP
));
}
// create a socket for communicating which the child process
final InetAddress lo = InetAddress.getByName(null);
serverSocket = new ServerSocket(0, 0, lo);
final int port = serverSocket.getLocalPort();
// build the argument list
final ArrayList<String> al = new ArrayList<String>();
al.add(Info.javaBinPath);
al.add(""); // reserved for initial heap
al.add(""); // reserved for maximum heap
al.add("-DVASSAL.id=" + id); // instance id
al.add("-DVASSAL.port=" + port); // MM socket port
// pass on the user's home, if it's set
final String userHome = System.getProperty("user.home");
if (userHome != null) al.add("-Duser.home=" + userHome);
// pass on the user's working dir, if it's set
final String userDir = System.getProperty("user.dir");
if (userDir != null) al.add("-Duser.dir=" + userDir);
// set the classpath
al.add("-cp");
al.add(System.getProperty("java.class.path"));
if (SystemUtils.IS_OS_MAC_OSX) {
// set the MacOS X dock parameters
// use the module name for the dock if we found a module name
// FIXME: should "Unnamed module" be localized?
final String d_name = moduleName != null && moduleName.length() > 0
? moduleName : "Unnamed module";
// get the path to the app icon
final String d_icon = new File(Info.getBaseDir(),
"Contents/Resources/VASSAL.icns").getAbsolutePath();
al.add("-Xdock:name=" + d_name);
al.add("-Xdock:icon=" + d_icon);
// Quartz can cause font rendering problems; turn it off
al.add("-Dapple.awt.graphics.UseQuartz=false");
}
else if (SystemUtils.IS_OS_WINDOWS) {
// Disable the 2D to Direct3D pipeline?
final Boolean disableD3d =
(Boolean) Prefs.getGlobalPrefs().getValue(Prefs.DISABLE_D3D);
if (Boolean.TRUE.equals(disableD3d)) {
al.add("-Dsun.java2d.d3d=false");
}
}
al.add(entryPoint);
al.addAll(Arrays.asList(lr.toArgs()));
final String[] args = al.toArray(new String[al.size()]);
// try to start a child process with the given heap sizes
args[1] = "-Xms" + initialHeap + "M";
args[2] = "-Xmx" + maximumHeap + "M";
ProcessWrapper proc = new ProcessLauncher().launch(args);
try {
proc.future.get(1000L, TimeUnit.MILLISECONDS);
}
catch (CancellationException e) {
cancel(true);
return null;
}
catch (ExecutionException e) {
logger.error("", e);
}
catch (TimeoutException e) {
// this is expected
}
// if launch failed, use conservative heap sizes
if (proc.future.isDone()) {
args[1] = "-Xms" + FAILSAFE_INITIAL_HEAP + "M";
args[2] = "-Xmx" + FAILSAFE_MAXIMUM_HEAP + "M";
proc = new ProcessLauncher().launch(args);
try {
proc.future.get(1000L, TimeUnit.MILLISECONDS);
}
catch (ExecutionException e) {
logger.error("", e);
}
catch (TimeoutException e) {
// this is expected
}
if (proc.future.isDone()) {
throw new IOException("failed to start child process");
}
else {
FutureUtils.wait(WarningDialog.show(
"Warning.maximum_heap_too_large",
FAILSAFE_MAXIMUM_HEAP
));
}
}
clientSocket = serverSocket.accept();
ipc = new IPCMessenger(clientSocket);
ipc.addEventListener(
NotifyOpenModuleOk.class,
new NotifyOpenModuleOkListener()
);
ipc.addEventListener(
NotifyNewModuleOk.class,
new NotifyNewModuleOkListener()
);
ipc.addEventListener(
NotifyImportModuleOk.class,
new NotifyImportModuleOkListener()
);
ipc.addEventListener(
NotifyOpenModuleFailed.class,
new NotifyOpenModuleFailedListener()
);
ipc.addEventListener(
NotifySaveFileOk.class,
new NotifySaveFileOkListener()
);
ipc.start();
children.add(ipc);
// block until the process ends
try {
proc.future.get();
}
catch (ExecutionException e) {
logger.error("", e);
}
return null;
}
protected int getHeapSize(ReadOnlyPrefs p, String key, int defaultHeap) {
// read heap size, if it exists
final String val = p.getStoredValue(key);
if (val == null) return defaultHeap;
try {
return Integer.parseInt(val);
}
catch (NumberFormatException ex) {
return -1;
}
}
protected int getHeapSize(Prefs p, String key, int defaultHeap) {
// read heap size, if it exists
final Object val = p.getValue(key);
if (val == null) return defaultHeap;
try {
return Integer.parseInt(val.toString());
}
catch (NumberFormatException ex) {
return -1;
}
}
@Override
protected void done() {
try {
get();
}
catch (CancellationException e) {
// this means that loading was cancelled
ModuleManagerWindow.getInstance().setWaitCursor(false);
}
catch (InterruptedException e) {
ErrorDialog.bug(e);
}
catch (ExecutionException e) {
// determine what kind of exception occurred
final Throwable c = e.getCause();
if (c instanceof IOException) {
ErrorDialog.showDetails(
e,
ThrowableUtils.getStackTrace(e),
"Error.socket_error"
);
}
else {
ErrorDialog.bug(e);
}
}
finally {
IOUtils.closeQuietly(clientSocket);
IOUtils.closeQuietly(serverSocket);
children.remove(ipc);
}
}
}
//
// Commands
//
protected abstract static class LaunchRequestMessage
extends SimpleIPCMessage {
protected final LaunchRequest lr;
public LaunchRequestMessage(LaunchRequest lr) {
this.lr = lr;
}
}
public static class NotifyOpenModuleOk extends LaunchRequestMessage {
private static final long serialVersionUID = 1L;
public NotifyOpenModuleOk(LaunchRequest lr) {
super(lr);
}
}
public static class NotifyNewModuleOk extends LaunchRequestMessage {
private static final long serialVersionUID = 1L;
public NotifyNewModuleOk(LaunchRequest lr) {
super(lr);
}
}
public static class NotifyImportModuleOk extends LaunchRequestMessage {
private static final long serialVersionUID = 1L;
public NotifyImportModuleOk(LaunchRequest lr) {
super(lr);
}
}
public static class NotifyOpenModuleFailed extends LaunchRequestMessage {
private static final long serialVersionUID = 1L;
public final Throwable thrown;
public NotifyOpenModuleFailed(LaunchRequest lr, Throwable thrown) {
super(lr);
this.thrown = thrown;
}
}
public static class NotifySaveFileOk extends SimpleIPCMessage {
private static final long serialVersionUID = 1L;
public final File file;
public NotifySaveFileOk(File file) {
this.file = file;
}
}
//
// Listeners
//
protected static class NotifyOpenModuleOkListener
implements EventListener<NotifyOpenModuleOk> {
public void receive(Object src, final NotifyOpenModuleOk msg) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final ModuleManagerWindow mmw = ModuleManagerWindow.getInstance();
mmw.addModule(msg.lr.module);
mmw.setWaitCursor(false);
}
});
}
}
protected static class NotifyNewModuleOkListener
implements EventListener<NotifyNewModuleOk> {
public void receive(Object src, NotifyNewModuleOk msg) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
ModuleManagerWindow.getInstance().setWaitCursor(false);
}
});
}
}
protected static class NotifyImportModuleOkListener
implements EventListener<NotifyImportModuleOk> {
public void receive(Object src, NotifyImportModuleOk msg) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
ModuleManagerWindow.getInstance().setWaitCursor(false);
}
});
}
}
protected static class NotifyOpenModuleFailedListener
implements EventListener<NotifyOpenModuleFailed> {
public void receive(Object src, NotifyOpenModuleFailed msg) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
ModuleManagerWindow.getInstance().setWaitCursor(false);
}
});
ErrorDialog.showDetails(
msg.thrown,
ThrowableUtils.getStackTrace(msg.thrown),
"Error.module_load_failed",
msg.thrown.getMessage()
);
}
}
protected static class NotifySaveFileOkListener
implements EventListener<NotifySaveFileOk> {
public void receive(Object rc, final NotifySaveFileOk msg) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
ModuleManagerWindow.getInstance().update(msg.file);
}
});
}
}
}