/** * Copyright (C) 2005 - 2012 Eric Van Dewoestine * * This program 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. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package org.eclim.eclipse; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.BindException; import java.net.InetAddress; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import org.eclim.Services; import org.eclim.annotation.Command; import org.eclim.command.CommandLine; import org.eclim.eclipse.ui.internal.EclimWorkbenchWindow; import org.eclim.logging.Logger; import org.eclim.plugin.PluginResources; import org.eclim.util.IOUtils; import org.eclim.util.file.FileUtils; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.adaptor.LocationManager; import org.eclipse.equinox.app.IApplication; import org.eclipse.equinox.app.IApplicationContext; import org.eclipse.osgi.internal.baseadaptor.AdaptorUtil; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.EclimDisplay; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkEvent; import org.osgi.framework.FrameworkListener; import com.martiansoftware.nailgun.NGContext; import com.martiansoftware.nailgun.NGServer; /** * Abstract base class containing shared functionality used by implementations * of an eclim application. * * @author Eric Van Dewoestine */ public abstract class AbstractEclimApplication implements IApplication, FrameworkListener { private static final Logger logger = Logger.getLogger(AbstractEclimApplication.class); private static final String CORE = "org.eclim.core"; private static AbstractEclimApplication instance; private String workspace; private NGServer server; private boolean starting; private boolean stopping; private boolean registered; /** * {@inheritDoc} * @see IApplication#start(IApplicationContext) */ public Object start(IApplicationContext context) throws Exception { workspace = ResourcesPlugin .getWorkspace().getRoot().getRawLocation().toOSString().replace('\\', '/'); logger.info("Workspace: " + workspace); starting = true; logger.info("Starting eclim..."); instance = this; int exitCode = 0; final String host = Services.getPluginResources("org.eclim") .getProperty("nailgun.server.host"); final String portString = Services.getPluginResources("org.eclim") .getProperty("nailgun.server.port"); try{ if (!onStart()){ return EXIT_OK; } // add shutdown hook. Runtime.getRuntime().addShutdownHook(new ShutdownHook()); // register that server is running to external processes. registered = registerInstance(); // setup nailgun final int port = Integer.parseInt(portString); InetAddress address = InetAddress.getByName(host); server = new NGServer(address, port, getExtensionClassLoader()); server.setCaptureSystemStreams(false); starting = false; // for headed eclimd, plugins can be loaded and nailgun started on our // current non-main thread. if (isHeaded()){ load(); logger.info("Eclim Server Started on: {}:{}", host, port); server.run(); // for headless eclimd, handle initializing workbench and running nailgun // on a separate thread. }else{ Display display = new EclimDisplay(); //PlatformUI.createDisplay(); final Shell shell = display.getActiveShell(); // hack to set an active window before workbench.runUI is called (needed // by at least cdt on startup), also load the eclim plugins here to // ensure they load after the workbench is setup. display.asyncExec(new Runnable(){ public void run() { try{ shell.setData(new EclimWorkbenchWindow()); load(); }catch(Exception e){ throw new RuntimeException(e); } } }); final NGServer theServer = server; new Thread(){ public void run() { logger.info("Eclim Server Started on: {}:{}", host, port); theServer.run(); } }.start(); PlatformUI.createAndRunWorkbench(display, new WorkbenchAdvisor()); } }catch(NumberFormatException nfe){ logger.error("Error starting eclim:", new RuntimeException("Invalid port number: '" + portString + "'")); return new Integer(1); }catch(BindException be){ logger.error("Error starting eclim on {}:{}", host, portString, be); return new Integer(1); }catch(Throwable t){ logger.error("Error starting eclim:", t); return new Integer(1); }finally{ starting = false; } shutdown(); return new Integer(exitCode); } /** * {@inheritDoc} * @see IApplication#stop() */ public void stop() { try{ shutdown(); }catch(Exception e){ logger.error("Error shutting down.", e); } } /** * Invoked during application startup, allowing subclasses to perform any * necessary startup initialization. * * @return true if the application should continue to start, false otherwise. */ public boolean onStart() throws Exception { return true; } /** * Invoked during application shutdown, allowing subclasses to perform any * necessary shutdown cleanup. */ public void onStop() throws Exception { } /** * Test for "headed" environment. * * @return true if running in "headed" environment. */ public abstract boolean isHeaded(); /** * Determines if this application is in the process of starting. * * @return True if starting, false if stopped or finished starting. */ public boolean isStarting() { return starting; } /** * Determines if this application is in the process of stopping. * * @return True if stopping, false if stopped or finished stopping. */ public boolean isStopping() { return stopping; } /** * Determines if the underlying nailgun server is running or not. * * @return True if the nailgun server is running, false otherwise. */ public boolean isRunning() { return server != null && server.isRunning(); } /** * Gets the running instance of this application. * * @return The AbstractEclimApplication instance. */ public static AbstractEclimApplication getInstance() { return instance; } /** * Loads the core bundle which in turn loads the eclim plugins. */ private synchronized void load() throws Exception { logger.info("Loading plugin org.eclim"); PluginResources defaultResources = Services.getPluginResources("org.eclim"); defaultResources.registerCommand(ReloadCommand.class); logger.info("Loading plugin org.eclim.core"); Bundle bundle = Platform.getBundle(CORE); if(bundle == null){ String diagnosis = EclimPlugin.getDefault().diagnose(CORE); logger.error(Services.getMessage("plugin.load.failed", CORE, diagnosis)); return; } bundle.start(); bundle.getBundleContext().addFrameworkListener(this); // wait up to 10 seconds for bundles to activate. wait(10000); } /** * Shuts down the eclim server. */ private synchronized void shutdown() throws Exception { if(!stopping){ stopping = true; logger.info("Shutting down eclim..."); if(server != null && server.isRunning()){ server.shutdown(false /* exit vm */); } logger.info("Stopping plugin " + CORE); Bundle bundle = Platform.getBundle(CORE); if (bundle != null){ try{ bundle.stop(); }catch(IllegalStateException ise){ // thrown because eclipse osgi BaseStorage attempt to register a // shutdown hook during shutdown. } } EclimPlugin plugin = EclimPlugin.getDefault(); if(plugin != null){ plugin.stop(null); } unregisterInstance(); onStop(); logger.info("Clean osgi config..."); File osgiConfig = LocationManager.getOSGiConfigurationDir(); AdaptorUtil.rm(osgiConfig); logger.info("Eclim stopped."); } } /** * Register the current instance in the eclimd instances file for use by vim. * * @return true if the instance was registered, false otherwise. */ private boolean registerInstance() throws Exception { File instances = new File(FileUtils.concat( System.getProperty("user.home"), ".eclim/.eclimd_instances")); FileOutputStream out = null; try{ List<String> entries = readInstances(); if (entries == null){ return false; } String port = Services.getPluginResources("org.eclim") .getProperty("nailgun.server.port"); String instance = workspace + ':' + port; if (!entries.contains(instance)){ entries.add(0, instance); out = new FileOutputStream(instances); IOUtils.writeLines(entries, out); } return true; }catch(IOException ioe){ logger.error( "\nError writing to eclimd instances file: " + ioe.getMessage() + "\n" + instances); }finally{ IOUtils.closeQuietly(out); } return false; } /** * Unregister the current instance in the eclimd instances file for use by vim. */ private void unregisterInstance() throws Exception { if (!registered){ return; } File instances = new File(FileUtils.concat( System.getProperty("user.home"), ".eclim/.eclimd_instances")); FileOutputStream out = null; try{ List<String> entries = readInstances(); if (entries == null){ return; } String port = Services.getPluginResources("org.eclim") .getProperty("nailgun.server.port"); String instance = workspace + ':' + port; entries.remove(instance); if (entries.size() == 0){ if (!instances.delete()){ logger.error("Error deleting eclimd instances file: " + instances); } }else{ out = new FileOutputStream(instances); IOUtils.writeLines(entries, out); } }catch(IOException ioe){ logger.error( "\nError writing to eclimd instances file: " + ioe.getMessage() + "\n" + instances); return; }finally{ IOUtils.closeQuietly(out); } } private List<String> readInstances() throws Exception { File doteclim = new File(FileUtils.concat(System.getProperty("user.home"), ".eclim")); if (!doteclim.exists()){ if (!doteclim.mkdirs()){ logger.error("Error creating ~/.eclim directory: " + doteclim); return null; } } File instances = new File(FileUtils.concat( doteclim.getAbsolutePath(), ".eclimd_instances")); if (!instances.exists()){ try{ instances.createNewFile(); }catch(IOException ioe){ logger.error( "\nError creating eclimd instances file: " + ioe.getMessage() + "\n" + instances); return null; } } FileInputStream in = null; try{ in = new FileInputStream(instances); List<String> entries = IOUtils.readLines(in); return entries; }finally{ IOUtils.closeQuietly(in); } } /** * Builds the classloader used for third party nailgun extensions dropped into * eclim's ext dir. * * @return The classloader. */ private ClassLoader getExtensionClassLoader() throws Exception { File extdir = new File(FileUtils.concat(Services.DOT_ECLIM, "resources/ext")); if (extdir.exists()){ FileFilter filter = new FileFilter(){ public boolean accept(File file){ return file.isDirectory() || file.getName().endsWith(".jar"); } }; ArrayList<URL> urls = new ArrayList<URL>(); listFileUrls(extdir, filter, urls); return new URLClassLoader( urls.toArray(new URL[urls.size()]), this.getClass().getClassLoader()); } return null; } private void listFileUrls(File dir, FileFilter filter, ArrayList<URL> results) throws Exception { File[] files = dir.listFiles(filter); for (File file : files) { if(file.isFile()){ results.add(file.toURI().toURL()); }else{ listFileUrls(file, filter, results); } } } /** * {@inheritDoc} * @see FrameworkListener#frameworkEvent(FrameworkEvent) */ public synchronized void frameworkEvent(FrameworkEvent event) { // We are using a framework INFO event to announce when all the eclim // plugins bundles have been started (but not necessarily activated yet). Bundle bundle = event.getBundle(); if (event.getType() == FrameworkEvent.INFO && CORE.equals(bundle.getSymbolicName())) { logger.info("Loaded plugin org.eclim.core"); notify(); } } /** * Shutdown hook for non-typical shutdown. */ private class ShutdownHook extends Thread { /** * Runs the shutdown hook. */ public void run() { if (!stopping){ Display.getDefault().syncExec(new Runnable(){ public void run() { try{ shutdown(); }catch(Exception e){ logger.error("Error running shutdown hook.", e); } } }); } } } @Command(name = "reload") public static class ReloadCommand implements org.eclim.command.Command { private NGContext context; /** * {@inheritDoc} * @see org.eclim.command.Command#execute(CommandLine) */ public Object execute(CommandLine commandLine) throws Exception { Bundle bundle = Platform.getBundle(CORE); bundle.update(); bundle.start(); return Services.getMessage("plugins.reloaded"); } /** * {@inheritDoc} * @see org.eclim.command.Command#getContext() */ public NGContext getContext() { return context; } /** * {@inheritDoc} * @see org.eclim.command.Command#setContext(NGContext) */ public void setContext(NGContext context) { this.context = context; } /** * {@inheritDoc} * @see org.eclim.command.Command#cleanup(CommandLine) */ public void cleanup(CommandLine commandLine) { } } }