/**
* Copyright (C) 2012 - 2014 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.FileWriter;
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.Iterator;
import java.util.List;
import org.eclim.Services;
import org.eclim.command.ReloadCommand;
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.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.jobs.IJobManager;
import org.eclipse.core.runtime.jobs.Job;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import com.google.gson.Gson;
import com.martiansoftware.nailgun.NGServer;
/**
* Class which handles starting/stopping of the eclim nailgun daemon along with
* the loading/unloading of eclim plugins.
*
* @author Eric Van Dewoestine
*/
public class EclimDaemon
implements FrameworkListener
{
private static final Logger logger =
Logger.getLogger(EclimDaemon.class);
private static String BASE = "org.eclim";
private static String CORE = "org.eclim.core";
private static EclimDaemon instance = new EclimDaemon();
private boolean started;
private boolean starting;
private boolean stopping;
private NGServer server;
private EclimDaemon()
{
}
public static EclimDaemon getInstance()
{
return instance;
}
public void start()
{
if (started || starting){
return;
}
String host = Services.getPluginResources("org.eclim")
.getProperty("nailgun.server.host");
String portString = Services.getPluginResources("org.eclim")
.getProperty("nailgun.server.port");
try{
String workspace = getWorkspace();
logger.info("Workspace: " + workspace);
String home = getHome();
logger.info("Home: " + home);
starting = true;
logger.info("Starting eclim...");
int port = Integer.parseInt(portString);
InetAddress address = InetAddress.getByName(host);
server = new NGServer(address, port, getExtensionClassLoader());
server.setCaptureSystemStreams(false);
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){
logger.error(Services.getMessage("plugin.load.failed", CORE));
return;
}
bundle.start(Bundle.START_TRANSIENT);
bundle.getBundleContext().addFrameworkListener(this);
// wait up to 10 seconds for bundles to activate.
synchronized(this){
wait(10000);
}
// headless
if (EclimApplication.isEnabled()){
logger.info("Waiting on running jobs before starting eclimd...");
long timeout = System.currentTimeMillis() + (30 * 1000);
IJobManager manager = Job.getJobManager();
while(System.currentTimeMillis() < timeout &&
jobsRunning(manager))
{
try{
Thread.sleep(1000);
}catch(InterruptedException ie){
// ignore
}
}
}
starting = false;
started = true;
logger.info("Eclim Server Started on: {}:{}", host, port);
registerInstance(home, workspace, port);
server.run();
}catch(NumberFormatException nfe){
logger.error("Error starting eclim:",
new RuntimeException("Invalid port number: '" + portString + "'"));
}catch(BindException be){
logger.error("Error starting eclim on {}:{}", host, portString, be);
}catch(Throwable t){
logger.error("Error starting eclim:", t);
}finally{
try{
unregisterInstance();
}catch(Exception e){
throw new RuntimeException(e);
}
starting = false;
started = false;
}
}
public void stop()
throws Exception
{
if(started && !stopping){
try{
stopping = true;
logger.info("Shutting down eclim...");
if(server != null && server.isRunning()){
server.shutdown(false /* exit vm */);
}
unregisterInstance();
logger.info("Stopping plugin " + CORE);
Bundle bundle = Platform.getBundle(CORE);
if (bundle != null){
try{
bundle.stop();
}catch(IllegalStateException ise){
// thrown because eclipse osgi BaseStorage attempts to register a
// shutdown hook during shutdown.
}
}
logger.info("Eclim stopped.");
}finally{
stopping = false;
}
}
}
/**
* Register the current instance in the eclimd instances file for use by vim.
*/
private void registerInstance(String home, String workspace, int port)
throws Exception
{
File instances = new File(FileUtils.concat(
System.getProperty("user.home"), ".eclim/.eclimd_instances"));
Gson gson = new Gson();
FileWriter out = null;
try{
List<Instance> entries = readInstances();
if (entries != null){
boolean updated = false;
// remove any stale entries
for (Iterator<Instance> i = entries.iterator(); i.hasNext();){
Instance entry = i.next();
if (!entry.exists()){
i.remove();
updated = true;
logger.info("Removed stale instance entry: " +
entry.home + ':' + entry.port);
}
}
// register new instance
Instance instance = new Instance(home, workspace, port);
if (!entries.contains(instance)){
entries.add(instance);
updated = true;
}
// update the instances file
if (updated){
out = new FileWriter(instances);
for (Instance entry : entries) {
out.write(gson.toJson(entry) + '\n');
}
}
}
}catch(IOException ioe){
logger.error(
"\nError writing to eclimd instances file: " + ioe.getMessage() +
"\n" + instances);
}finally{
IOUtils.closeQuietly(out);
}
}
/**
* Unregister the current instance in the eclimd instances file for use by vim.
*/
private synchronized void unregisterInstance()
throws Exception
{
File instances = new File(FileUtils.concat(
System.getProperty("user.home"), ".eclim/.eclimd_instances"));
Gson gson = new Gson();
FileWriter out = null;
try{
List<Instance> entries = readInstances();
if (entries == null){
return;
}
Instance instance = new Instance(getHome(), getWorkspace(), getPort());
entries.remove(instance);
if (entries.size() == 0){
if (!instances.delete()){
logger.error("Error deleting eclimd instances file: " + instances);
}
}else{
out = new FileWriter(instances);
for (Instance entry : entries) {
out.write(gson.toJson(entry) + '\n');
}
}
}catch(IOException ioe){
logger.error(
"\nError writing to eclimd instances file: " + ioe.getMessage() +
"\n" + instances);
return;
}finally{
IOUtils.closeQuietly(out);
}
}
@SuppressWarnings("unchecked")
private List<Instance> 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;
}
}
Gson gson = new Gson();
FileInputStream in = null;
try{
in = new FileInputStream(instances);
List<String> lines = IOUtils.readLines(in);
List<Instance> entries = new ArrayList<Instance>();
for (String line : lines){
if (!line.startsWith("{")) {
continue;
}
entries.add(gson.fromJson(line, Instance.class));
}
return entries;
}finally{
IOUtils.closeQuietly(in);
}
}
private String getWorkspace()
{
return ResourcesPlugin.getWorkspace()
.getRoot().getRawLocation().toOSString().replace('\\', '/');
}
private String getHome()
throws IOException
{
Bundle bundle = Platform.getBundle(BASE);
IPath p = Path.fromOSString(FileLocator.getBundleFile(bundle).getPath());
return p.addTrailingSeparator().toOSString();
}
private int getPort()
{
String portString = Services.getPluginResources("org.eclim")
.getProperty("nailgun.server.port");
return Integer.parseInt(portString);
}
/**
* 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);
}
}
}
@Override
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();
}
}
private boolean jobsRunning(IJobManager manager)
{
Job[] jobs = manager.find(null);
for (Job job : jobs){
if (job.getState() == Job.RUNNING){
logger.debug("Waiting on job: " + job);
return true;
}
}
logger.info("Jobs finished.");
return false;
}
@SuppressWarnings("unused")
private class Instance
{
private String home;
private String workspace;
private int port;
public Instance(String home, String workspace, int port)
{
this.home = home;
this.workspace = workspace;
this.port = port;
}
public boolean exists()
{
return new File(home).exists();
}
public boolean equals(Object other)
{
if (!(other instanceof Instance)){
return false;
}
Instance otheri = (Instance)other;
return workspace.equals(otheri.workspace) &&
home.equals(otheri.home) &&
port == otheri.port;
}
}
}