/* * $Id$ * * Copyright (c) 2000-2008 by Rodney Kinney, 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.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.PrintStream; import java.io.RandomAccessFile; import java.lang.reflect.InvocationTargetException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import javax.swing.JFrame; import javax.swing.JMenuBar; import javax.swing.SwingUtilities; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import VASSAL.Info; import VASSAL.build.module.metadata.AbstractMetaData; import VASSAL.build.module.metadata.MetaDataFactory; import VASSAL.build.module.metadata.SaveMetaData; import VASSAL.configure.IntConfigurer; import VASSAL.configure.LongConfigurer; import VASSAL.i18n.Resources; import VASSAL.i18n.TranslateVassalWindow; import VASSAL.preferences.Prefs; import VASSAL.tools.ErrorDialog; import VASSAL.tools.ThrowableUtils; import VASSAL.tools.io.IOUtils; import VASSAL.tools.io.ZipArchive; import VASSAL.tools.logging.LoggedOutputStream; import VASSAL.tools.menu.MacOSXMenuManager; import VASSAL.tools.menu.MenuBarProxy; import VASSAL.tools.menu.MenuManager; /** * Tracks recently-used modules and builds the main GUI window for * interacting with modules. * * @author rodneykinney * @since 3.1.0 */ public class ModuleManager { private static final Logger logger = LoggerFactory.getLogger(ModuleManager.class); private static final String NEXT_VERSION_CHECK = "nextVersionCheck"; public static final String MAXIMUM_HEAP = "maximumHeap"; //$NON-NLS-1$ public static final String INITIAL_HEAP = "initialHeap"; //$NON-NLS-1$ public static void main(String[] args) { // FIXME: We need to catch more exceptions in main() and then exit in // order to avoid situations where the main thread ends due to an uncaught // exception, but there are other threads still running, and so VASSAL // does not quit. For example, this can happen if an IllegalArgumentException // is thrown here... // parse command-line arguments LaunchRequest lr = null; try { lr = LaunchRequest.parseArgs(args); } catch (LaunchRequestException e) { // FIXME: should be a dialog... System.err.println("VASSAL: " + e.getMessage()); System.exit(1); } // do this before the graphics subsystem fires up or it won't stick System.setProperty("swing.boldMetal", "false"); if (lr.mode == LaunchRequest.Mode.TRANSLATE) { // show the translation window in translation mode SwingUtilities.invokeLater(new Runnable() { public void run() { // FIXME: does this window exit on close? new TranslateVassalWindow(null).setVisible(true); } }); return; } // // How we start exactly one request server: // // To ensure that exactly one process acts as the request server, we // acquire a lock on the ~/VASSAL/key file, and then attempt to acquire // a lock on the ~/VASSAL/lock file. If we cannot lock ~/VASSAL/lock, // then there is already a server running; in that case, we read the // port number and security key from ~/VASSAL/key. If we can lock // ~/VASSAL/lock, then we start the server, write the port number and // key to ~/VASSAL/key, and continue to hold the lock on ~/VASSAL/lock. // Finally, we unlock ~/VASSAL/key and proceed to act as a client, // sending requests over localhost:port using the security key. // // The advantages of this method are: // // (1) No race conditions between processes started at the same time. // (2) No port collisions, because we don't use a predetermined port. // final File keyfile = new File(Info.getConfDir(), "key"); final File lockfile = new File(Info.getConfDir(), "lock"); int port = 0; long key = 0; RandomAccessFile kraf = null; FileLock klock = null; try { // acquire an exclusive lock on the key file kraf = new RandomAccessFile(keyfile, "rw"); try { klock = kraf.getChannel().lock(); } catch (OverlappingFileLockException e) { throw (IOException) new IOException().initCause(e); } // determine whether we are the server or a client // Note: We purposely keep lout open in the case where we are the // server, because closing lout will release the lock. FileLock lock = null; final FileOutputStream lout = new FileOutputStream(lockfile); try { lock = lout.getChannel().tryLock(); } catch (OverlappingFileLockException e) { throw (IOException) new IOException().initCause(e); } if (lock != null) { // we have the lock, so we will be the request server // bind to an available port on the loopback device final ServerSocket serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)); // write the port number where we listen to the key file port = serverSocket.getLocalPort(); kraf.writeInt(port); // create new security key and write it to the key file key = (long) (Math.random() * Long.MAX_VALUE); kraf.writeLong(key); // create a new Module Manager new ModuleManager(serverSocket, key, lout, lock); } else { // we do not have the lock, so we will be a request client lout.close(); // read the port number we will connect to from the key file port = kraf.readInt(); // read the security key from the key file key = kraf.readLong(); } kraf.close(); } catch (IOException e) { // FIXME: should be a dialog... System.err.println("VASSAL: IO error"); e.printStackTrace(); System.exit(1); } finally { // this will also release the lock on the key file IOUtils.closeQuietly(kraf); } lr.key = key; // pass launch parameters on to the ModuleManager via the socket Socket clientSocket = null; ObjectOutputStream out = null; InputStream in = null; try { clientSocket = new Socket((String) null, port); out = new ObjectOutputStream( new BufferedOutputStream(clientSocket.getOutputStream())); out.writeObject(lr); out.flush(); in = clientSocket.getInputStream(); IOUtils.copy(in, System.err); } catch (IOException e) { // FIXME: should be a dialog... System.err.println("VASSAL: Problem with socket on port " + port); e.printStackTrace(); System.exit(1); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly((Closeable) out); IOUtils.closeQuietly(clientSocket); } } private static ModuleManager instance = null; public static ModuleManager getInstance() { return instance; } private final long key; private FileOutputStream lout; private FileLock lock; private final ServerSocket serverSocket; public ModuleManager(ServerSocket serverSocket, long key, FileOutputStream lout, FileLock lock) throws IOException { if (instance != null) throw new IllegalStateException(); instance = this; this.serverSocket = serverSocket; this.key = key; // we hang on to these to prevent the lock from being lost this.lout = lout; this.lock = lock; // truncate the errorLog final File errorLog = new File(Info.getHomeDir(), "errorLog"); new FileOutputStream(errorLog).close(); final StartUp start = SystemUtils.IS_OS_MAC_OSX ? new ModuleManagerMacOSXStartUp() : new StartUp(); start.startErrorLog(); // log everything which comes across our stderr System.setErr(new PrintStream(new LoggedOutputStream(), true)); Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler()); start.initSystemProperties(); // try to migrate old preferences if there are no current ones final File pdir = Info.getPrefsDir(); if (!pdir.exists()) { // Check the 3.2.0 through 3.2.7 location File pzip = new File(Info.getHomeDir(), "Preferences"); if (!pzip.exists()) { // Check the pre-3.2 location. pzip = new File(System.getProperty("user.home"), "VASSAL/Preferences"); } if (pzip.exists()) { FileUtils.forceMkdir(pdir); final byte[] buf = new byte[4096]; try { final ZipArchive za = new ZipArchive(pzip); try { for (String f : za.getFiles()) { final File ofile = new File( pdir, "VASSAL".equals(f) ? "V_Global" : Prefs.sanitize(f) ); InputStream in = null; try { in = za.getInputStream(f); OutputStream out = null; try { out = new FileOutputStream(ofile); IOUtils.copy(in, out, buf); out.close(); } finally { IOUtils.closeQuietly(out); } in.close(); } finally { IOUtils.closeQuietly(in); } } za.close(); } finally { IOUtils.closeQuietly(za); } } catch (IOException e) { logger.error("Failed to convert legacy preferences file.", e); } } } if (SystemUtils.IS_OS_MAC_OSX) new MacOSXMenuManager(); else new ModuleManagerMenuManager(); SwingUtilities.invokeLater(new Runnable() { public void run() { launch(); } }); // ModuleManagerWindow.getInstance() != null now, so listen on the socket final Thread socketListener = new Thread( new SocketListener(serverSocket), "socket listener"); socketListener.setDaemon(true); socketListener.start(); final Prefs globalPrefs = Prefs.getGlobalPrefs(); // determine when we should next check on the current version of VASSAL final LongConfigurer nextVersionCheckConfig = new LongConfigurer(NEXT_VERSION_CHECK, null, -1L); globalPrefs.addOption(null, nextVersionCheckConfig); long nextVersionCheck = nextVersionCheckConfig.getLongValue(-1L); if (nextVersionCheck < System.currentTimeMillis()) { new UpdateCheckRequest().execute(); } // set the time for the next version check if (nextVersionCheck == -1L) { // this was our first check; randomly check after 0-10 days to // to spread version checks evenly over a 10-day period nextVersionCheck = System.currentTimeMillis() + (long) (Math.random() * 10 * 86400000); } else { // check again in 10 days nextVersionCheck += 10 * 86400000; } nextVersionCheckConfig.setValue(nextVersionCheck); // FIXME: the importer heap size configurers don't belong here // the initial heap size for the module importer final IntConfigurer initHeapConf = new IntConfigurer( INITIAL_HEAP, Resources.getString("GlobalOptions.initial_heap"), //$NON-NLS-1$ Integer.valueOf(256) ); globalPrefs.addOption("Importer", initHeapConf); // the maximum heap size for the module importer final IntConfigurer maxHeapConf = new IntConfigurer( MAXIMUM_HEAP, Resources.getString("GlobalOptions.maximum_heap"), //$NON-NLS-1$ Integer.valueOf(512) ); globalPrefs.addOption("Importer", maxHeapConf); } public void shutDown() throws IOException { lock.release(); lout.close(); } private class SocketListener implements Runnable { private final ServerSocket serverSocket; public SocketListener(ServerSocket serverSocket) { this.serverSocket = serverSocket; } public void run() { try { ObjectInputStream in = null; PrintStream out = null; Socket clientSocket = null; while (true) { try { clientSocket = serverSocket.accept(); in = new ObjectInputStream( new BufferedInputStream(clientSocket.getInputStream())); final String message = execute(in.readObject()); in.close(); clientSocket.close(); if (message == null || clientSocket.isClosed()) continue; out = new PrintStream( new BufferedOutputStream(clientSocket.getOutputStream())); out.println(message); out.close(); } catch (IOException e) { ErrorDialog.showDetails( e, ThrowableUtils.getStackTrace(e), "Error.socket_error" ); } catch (ClassNotFoundException e) { ErrorDialog.bug(e); } finally { IOUtils.closeQuietly((Closeable) in); IOUtils.closeQuietly(out); IOUtils.closeQuietly(clientSocket); } } } finally { IOUtils.closeQuietly(serverSocket); } } } protected void launch() { logger.info("Manager"); final ModuleManagerWindow window = ModuleManagerWindow.getInstance(); window.setVisible(true); final boolean isFirstTime = !Info.getPrefsDir().exists(); if (isFirstTime) new FirstTimeDialog(window).setVisible(true); } protected String execute(Object req) { if (req instanceof LaunchRequest) { final LaunchRequest lr = (LaunchRequest) req; if (lr.key != key) { // FIXME: translate return "incorrect key"; } final LaunchRequestHandler handler = new LaunchRequestHandler(lr); try { SwingUtilities.invokeAndWait(handler); } catch (InterruptedException e) { return "interrupted"; // FIXME } catch (InvocationTargetException e) { ErrorDialog.bug(e); return null; } return handler.getResult(); } else { return "unrecognized command"; // FIXME } } private static class LaunchRequestHandler implements Runnable { private final LaunchRequest lr; private String result; public LaunchRequestHandler(LaunchRequest lr) { this.lr = lr; } public void run() { result = handle(); } public String getResult() { return result; } private String handle() { final ModuleManagerWindow window = ModuleManagerWindow.getInstance(); switch (lr.mode) { case MANAGE: window.toFront(); break; case LOAD: if (Player.LaunchAction.isEditing(lr.module)) return "module open for editing"; // FIXME if (lr.module == null && lr.game != null) { // attempt to find the module for the saved game or log final AbstractMetaData data = MetaDataFactory.buildMetaData(lr.game); if (data != null && data instanceof SaveMetaData) { // we found save metadata final String moduleName = ((SaveMetaData) data).getModuleName(); if (moduleName != null && moduleName.length() > 0) { // get the module file by module name lr.module = window.getModuleByName(moduleName); } else { // this is a pre 3.1 save file, can't tell the module name // FIXME: show some error here return "cannot find module"; } } } if (lr.module == null) { return "cannot find module"; // FIXME: show some error here } else if (lr.game == null) { new Player.LaunchAction(window, lr.module).actionPerformed(null); } else { new Player.LaunchAction( window, lr.module, lr.game).actionPerformed(null); } break; case EDIT: if (Editor.LaunchAction.isInUse(lr.module)) return "module open for play"; // FIXME if (Editor.LaunchAction.isEditing(lr.module)) return "module open for editing"; // FIXME new Editor.LaunchAction(window, lr.module).actionPerformed(null); break; case IMPORT: new Editor.ImportLaunchAction( window, lr.importFile).actionPerformed(null); break; case NEW: new Editor.NewModuleLaunchAction(window).actionPerformed(null); break; case EDIT_EXT: return "not yet implemented"; // FIXME case NEW_EXT: return "not yet implemented"; // FIXME default: return "unrecognized mode"; // FIXME } return null; } } private static class ModuleManagerMenuManager extends MenuManager { private final MenuBarProxy menuBar = new MenuBarProxy(); @Override public JMenuBar getMenuBarFor(JFrame fc) { return (fc instanceof ModuleManagerWindow) ? menuBar.createPeer() : null; } @Override public MenuBarProxy getMenuBarProxyFor(JFrame fc) { return (fc instanceof ModuleManagerWindow) ? menuBar : null; } } }