package org.thoughtframe.bridge; import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.Platform; import org.osgi.framework.Bundle; import org.thoughtframe.runtime.EngineConfig; import org.thoughtframe.runtime.EngineRuntime; /** * Owns the child classloader island that runs the ThoughtFrame engine. * * The plugin itself only links against the thin runtime API. The engine and its * heavy dependencies (OpenSearch, Lucene, Netty, ...) are loaded here in a * separate classloader, so none of them have to be OSGi bundles and the bridge * interfaces stay shared through the parent classloader. */ public class EngineIsland { protected static final String ENGINE_CLASS = "org.thoughtframe.runtime.DefaultEngineRuntime"; protected static final EngineIsland INSTANCE = new EngineIsland(); /** * System property naming the loopback port for the web stack. Absent or * negative keeps the headless fixture; 0 asks for an ephemeral port. * A host flag, like {@code thoughtframe.eclipse.autostart}. */ protected String fieldPortProperty = "thoughtframe.eclipse.port"; protected URLClassLoader fieldLoader; protected EngineRuntime fieldRuntime; protected Throwable fieldFailure; protected boolean fieldBooting; public static EngineIsland getInstance() { return INSTANCE; } /** * Boots the engine in a child classloader. Idempotent. * * @throws Exception when the engine could not be loaded or started */ public synchronized void start() throws Exception { if (getRuntime() != null) { return; } setBooting(true); try { URLClassLoader loader = createLoader(resolveEngineDir()); Class clazz = loader.loadClass(ENGINE_CLASS); EngineRuntime runtime = (EngineRuntime) clazz.getDeclaredConstructor().newInstance(); runtime.start(EngineConfig.create() .withWebappRoot(resolveWebappRoot()) .withPort(resolvePort()) .withAskPage("/WEB-INF/base/tf-eclipse/frames/tui/cli/ask.html") .withBridge(new EclipseWorkspaceBridge())); setLoader(loader); setRuntime(runtime); setFailure(null); } catch (Exception e) { setFailure(e); throw e; } finally { setBooting(false); } } /** * Stops the engine and closes the island classloader. Safe to call twice. */ public synchronized void stop() { if (getRuntime() != null) { try { getRuntime().close(); } catch (Throwable t) { setFailure(t); } } if (getLoader() != null) { try { getLoader().close(); } catch (Throwable t) { // nothing left to do while shutting down } } setRuntime(null); setLoader(null); } /** * @return true once the engine has finished booting */ public boolean isReady() { return getRuntime() != null && getRuntime().isReady(); } /** * @return the web-stack port. Like jdtbridge, a host gets a listener by * default: when no property is set we ask the OS for an ephemeral * port and publish it in the instance file, so no launch arguments * are needed. Set the property to a fixed port, or to a negative * number to fall back to the headless fixture. */ protected int resolvePort() { String override = System.getProperty(getPortProperty()); if (override == null || override.trim().isEmpty()) { return 0; } try { return Integer.parseInt(override.trim()); } catch (NumberFormatException e) { return 0; } } public String getPortProperty() { return fieldPortProperty; } public void setPortProperty(String inProperty) { fieldPortProperty = inProperty; } protected URLClassLoader createLoader(File inEngineDir) throws Exception { List urls = new ArrayList<>(); File[] files = inEngineDir.listFiles(); if (files != null) { for (File file : files) { if (file.getName().endsWith(".jar")) { urls.add(file.toURI().toURL()); } } } return new URLClassLoader(urls.toArray(new URL[0]), getClass().getClassLoader()); } /** * @return the staged engine folder; override with -Dthoughtframe.engine.dir */ protected File resolveEngineDir() throws Exception { String override = System.getProperty("thoughtframe.engine.dir"); if (override != null) { return new File(override); } Bundle bundle = Platform.getBundle(Constants.PLUGIN_ID); URL entry = bundle.getEntry("engine"); return new File(FileLocator.toFileURL(entry).toURI()); } /** * @return the webapp root; override with -Dthoughtframe.webapp.root */ protected String resolveWebappRoot() throws Exception { String override = System.getProperty("thoughtframe.webapp.root"); if (override != null) { return override; } // The plugin ships its own standalone webapp (node.xml + base/tf-eclipse + // base/tf-engine); it no longer points at the core thoughtframe webapp. Bundle bundle = Platform.getBundle(Constants.PLUGIN_ID); URL entry = bundle.getEntry("webapp"); return new File(FileLocator.toFileURL(entry).toURI()).getAbsolutePath(); } public URLClassLoader getLoader() { return fieldLoader; } public void setLoader(URLClassLoader inLoader) { fieldLoader = inLoader; } public EngineRuntime getRuntime() { return fieldRuntime; } public void setRuntime(EngineRuntime inRuntime) { fieldRuntime = inRuntime; } public Throwable getFailure() { return fieldFailure; } public void setFailure(Throwable inFailure) { fieldFailure = inFailure; } /** * @return true while start() is on the stack, so a view can show progress */ public boolean isBooting() { return fieldBooting; } public void setBooting(boolean inBooting) { fieldBooting = inBooting; } }