diff options
author | James Moger <james.moger@gitblit.com> | 2014-04-10 17:33:19 -0400 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2014-04-10 19:01:30 -0400 |
commit | b2fec20f1f1081607b54b3e7dd20b12d03cef113 (patch) | |
tree | 4979033daec189d6a56c983b73478e782b7e91c9 /src/main/java/com/gitblit/manager/PluginManager.java | |
parent | d433fd3ab6185c2fb5eab998d1fcab4dd0940e5f (diff) | |
download | gitblit-b2fec20f1f1081607b54b3e7dd20b12d03cef113.tar.gz gitblit-b2fec20f1f1081607b54b3e7dd20b12d03cef113.zip |
Improve plugin manager based on upstreamed contributions to pf4j
Diffstat (limited to 'src/main/java/com/gitblit/manager/PluginManager.java')
-rw-r--r-- | src/main/java/com/gitblit/manager/PluginManager.java | 329 |
1 files changed, 258 insertions, 71 deletions
diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java index 47154b87..9cefc88d 100644 --- a/src/main/java/com/gitblit/manager/PluginManager.java +++ b/src/main/java/com/gitblit/manager/PluginManager.java @@ -18,13 +18,18 @@ package com.gitblit.manager; import java.io.BufferedInputStream; import java.io.File; import java.io.FileFilter; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -33,11 +38,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.fortsoft.pf4j.DefaultPluginManager; +import ro.fortsoft.pf4j.PluginClassLoader; +import ro.fortsoft.pf4j.PluginState; +import ro.fortsoft.pf4j.PluginStateEvent; +import ro.fortsoft.pf4j.PluginStateListener; import ro.fortsoft.pf4j.PluginVersion; import ro.fortsoft.pf4j.PluginWrapper; import com.gitblit.Keys; import com.gitblit.models.PluginRegistry; +import com.gitblit.models.PluginRegistry.InstallState; import com.gitblit.models.PluginRegistry.PluginRegistration; import com.gitblit.models.PluginRegistry.PluginRelease; import com.gitblit.utils.Base64; @@ -49,16 +59,18 @@ import com.google.common.io.InputSupplier; /** * The plugin manager maintains the lifecycle of plugins. It is exposed as - * Dagger bean. The extension consumers supposed to retrieve plugin manager - * from the Dagger DI and retrieve extensions provided by active plugins. + * Dagger bean. The extension consumers supposed to retrieve plugin manager from + * the Dagger DI and retrieve extensions provided by active plugins. * * @author David Ostrovsky * */ -public class PluginManager extends DefaultPluginManager implements IPluginManager { +public class PluginManager implements IPluginManager, PluginStateListener { private final Logger logger = LoggerFactory.getLogger(getClass()); + private final DefaultPluginManager pf4j; + private final IRuntimeManager runtimeManager; // timeout defaults of Maven 3.0.4 in seconds @@ -67,47 +79,168 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage private int readTimeout = 12800; public PluginManager(IRuntimeManager runtimeManager) { - super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins")); + File dir = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); this.runtimeManager = runtimeManager; + this.pf4j = new DefaultPluginManager(dir); + } + + @Override + public void pluginStateChanged(PluginStateEvent event) { + logger.debug(event.toString()); } @Override public PluginManager start() { - logger.info("Loading plugins..."); - loadPlugins(); - logger.info("Starting loaded plugins..."); - startPlugins(); + pf4j.loadPlugins(); + logger.debug("Starting plugins"); + pf4j.startPlugins(); return this; } @Override public PluginManager stop() { - logger.info("Stopping loaded plugins..."); - stopPlugins(); + logger.debug("Stopping plugins"); + pf4j.stopPlugins(); return null; } + /** + * Installs the plugin from the url. + * + * @param url + * @param verifyChecksum + * @return true if successful + */ @Override - public boolean deletePlugin(PluginWrapper pw) { - File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); - File pluginFolder = new File(folder, pw.getPluginPath()); - File pluginZip = new File(folder, pw.getPluginPath() + ".zip"); + public synchronized boolean installPlugin(String url, boolean verifyChecksum) throws IOException { + File file = download(url, verifyChecksum); + if (file == null || !file.exists()) { + logger.error("Failed to download plugin {}", url); + return false; + } - if (pluginFolder.exists()) { - FileUtils.delete(pluginFolder); + String pluginId = pf4j.loadPlugin(file); + if (StringUtils.isEmpty(pluginId)) { + logger.error("Failed to load plugin {}", file); + return false; } - if (pluginZip.exists()) { - FileUtils.delete(pluginZip); + + PluginState state = pf4j.startPlugin(pluginId); + return PluginState.STARTED.equals(state); + } + + @Override + public synchronized boolean disablePlugin(String pluginId) { + return pf4j.disablePlugin(pluginId); + } + + @Override + public synchronized boolean enablePlugin(String pluginId) { + if (pf4j.enablePlugin(pluginId)) { + return PluginState.STARTED == pf4j.startPlugin(pluginId); } - return true; + return false; + } + + @Override + public synchronized boolean deletePlugin(String pluginId) { + PluginWrapper pluginWrapper = getPlugin(pluginId); + final String name = pluginWrapper.getPluginPath().substring(1); + if (pf4j.deletePlugin(pluginId)) { + + // delete the checksums + File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + File [] checksums = pFolder.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + if (!file.isFile()) { + return false; + } + + return file.getName().startsWith(name) && + (file.getName().toLowerCase().endsWith(".sha1") + || file.getName().toLowerCase().endsWith(".md5")); + } + + }); + + if (checksums != null) { + for (File checksum : checksums) { + checksum.delete(); + } + } + return true; + } + return false; + } + + @Override + public synchronized PluginState startPlugin(String pluginId) { + return pf4j.startPlugin(pluginId); + } + + @Override + public synchronized PluginState stopPlugin(String pluginId) { + return pf4j.stopPlugin(pluginId); + } + + @Override + public synchronized void startPlugins() { + pf4j.startPlugins(); + } + + @Override + public synchronized void stopPlugins() { + pf4j.stopPlugins(); + } + + @Override + public synchronized List<PluginWrapper> getPlugins() { + return pf4j.getPlugins(); + } + + @Override + public synchronized PluginWrapper getPlugin(String pluginId) { + return pf4j.getPlugin(pluginId); + } + + @Override + public synchronized List<Class<?>> getExtensionClasses(String pluginId) { + List<Class<?>> list = new ArrayList<Class<?>>(); + PluginClassLoader loader = pf4j.getPluginClassLoader(pluginId); + for (String className : pf4j.getExtensionClassNames(pluginId)) { + try { + list.add(loader.loadClass(className)); + } catch (ClassNotFoundException e) { + logger.error(String.format("Failed to find %s in %s", className, pluginId), e); + } + } + return list; } @Override - public boolean refreshRegistry() { + public synchronized <T> List<T> getExtensions(Class<T> type) { + return pf4j.getExtensions(type); + } + + @Override + public synchronized PluginWrapper whichPlugin(Class<?> clazz) { + return pf4j.whichPlugin(clazz); + } + + @Override + public synchronized boolean refreshRegistry() { String dr = "http://gitblit.github.io/gitblit-registry/plugins.json"; String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr); try { - return download(url); + File file = download(url, true); + if (file != null && file.exists()) { + URL selfUrl = new URL(url.substring(0, url.lastIndexOf('/'))); + // replace ${self} with the registry url + String content = FileUtils.readContent(file, "\n"); + content = content.replace("${self}", selfUrl.toString()); + FileUtils.writeContent(file, content); + } } catch (Exception e) { logger.error(String.format("Failed to retrieve plugins.json from %s", url), e); } @@ -124,7 +257,7 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage } }; - File [] files = folder.listFiles(jsonFilter); + File[] files = folder.listFiles(jsonFilter); if (files == null || files.length == 0) { // automatically retrieve the registry if we don't have a local copy refreshRegistry(); @@ -140,6 +273,7 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage try { String json = FileUtils.readContent(file, "\n"); registry = JsonUtils.fromJsonString(json, PluginRegistry.class); + registry.setup(); } catch (Exception e) { logger.error("Failed to deserialize " + file, e); } @@ -151,18 +285,17 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage } @Override - public List<PluginRegistration> getRegisteredPlugins() { + public synchronized List<PluginRegistration> getRegisteredPlugins() { List<PluginRegistration> list = new ArrayList<PluginRegistration>(); Map<String, PluginRegistration> map = new TreeMap<String, PluginRegistration>(); for (PluginRegistry registry : getRegistries()) { - List<PluginRegistration> registrations = registry.registrations; - list.addAll(registrations); - for (PluginRegistration reg : registrations) { + list.addAll(registry.registrations); + for (PluginRegistration reg : list) { reg.installedRelease = null; map.put(reg.id, reg); } } - for (PluginWrapper pw : getPlugins()) { + for (PluginWrapper pw : pf4j.getPlugins()) { String id = pw.getDescriptor().getPluginId(); PluginVersion pv = pw.getDescriptor().getVersion(); PluginRegistration reg = map.get(id); @@ -174,75 +307,129 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage } @Override - public PluginRegistration lookupPlugin(String idOrName) { - for (PluginRegistry registry : getRegistries()) { - PluginRegistration reg = registry.lookup(idOrName); - if (reg != null) { - return reg; + public synchronized List<PluginRegistration> getRegisteredPlugins(InstallState state) { + List<PluginRegistration> list = getRegisteredPlugins(); + Iterator<PluginRegistration> itr = list.iterator(); + while (itr.hasNext()) { + if (state != itr.next().getInstallState()) { + itr.remove(); } } - return null; + return list; } @Override - public PluginRelease lookupRelease(String idOrName, String version) { - for (PluginRegistry registry : getRegistries()) { - PluginRegistration reg = registry.lookup(idOrName); - if (reg != null) { - PluginRelease pv; - if (StringUtils.isEmpty(version)) { - pv = reg.getCurrentRelease(); - } else { - pv = reg.getRelease(version); - } - if (pv != null) { - return pv; - } + public synchronized PluginRegistration lookupPlugin(String idOrName) { + for (PluginRegistration reg : getRegisteredPlugins()) { + if (reg.id.equalsIgnoreCase(idOrName) || reg.name.equalsIgnoreCase(idOrName)) { + return reg; } } return null; } - - /** - * Installs the plugin from the plugin version. - * - * @param pv - * @throws IOException - * @return true if successful - */ @Override - public boolean installPlugin(PluginRelease pv) { - return installPlugin(pv.url); + public synchronized PluginRelease lookupRelease(String idOrName, String version) { + PluginRegistration reg = lookupPlugin(idOrName); + if (reg == null) { + return null; + } + + PluginRelease pv; + if (StringUtils.isEmpty(version)) { + pv = reg.getCurrentRelease(); + } else { + pv = reg.getRelease(version); + } + return pv; } /** - * Installs the plugin from the url. + * Downloads a file with optional checksum verification. * * @param url - * @return true if successful + * @param verifyChecksum + * @return + * @throws IOException */ - @Override - public boolean installPlugin(String url) { + protected File download(String url, boolean verifyChecksum) throws IOException { + File file = downloadFile(url); + + File sha1File = null; try { - if (!download(url)) { - return false; - } - // TODO stop, unload, load + sha1File = downloadFile(url + ".sha1"); + } catch (IOException e) { + } + + File md5File = null; + try { + md5File = downloadFile(url + ".md5"); } catch (IOException e) { - logger.error("Failed to install plugin from " + url, e); + } - return true; + + if (sha1File == null && md5File == null && verifyChecksum) { + throw new IOException("Missing SHA1 and MD5 checksums for " + url); + } + + String expected; + MessageDigest md = null; + if (sha1File != null && sha1File.exists()) { + // prefer SHA1 to MD5 + expected = FileUtils.readContent(sha1File, "\n").split(" ")[0].trim(); + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + logger.error(null, e); + } + } else { + expected = FileUtils.readContent(md5File, "\n").split(" ")[0].trim(); + try { + md = MessageDigest.getInstance("MD5"); + } catch (Exception e) { + logger.error(null, e); + } + } + + // calculate the checksum + FileInputStream is = null; + try { + is = new FileInputStream(file); + DigestInputStream dis = new DigestInputStream(is, md); + byte [] buffer = new byte[1024]; + while ((dis.read(buffer)) > -1) { + // read + } + dis.close(); + + byte [] digest = md.digest(); + String calculated = StringUtils.toHex(digest).trim(); + + if (!expected.equals(calculated)) { + String msg = String.format("Invalid checksum for %s\nAlgorithm: %s\nExpected: %s\nCalculated: %s", + file.getAbsolutePath(), + md.getAlgorithm(), + expected, + calculated); + file.delete(); + throw new IOException(msg); + } + } finally { + if (is != null) { + is.close(); + } + } + return file; } /** * Download a file to the plugins folder. * * @param url - * @return + * @return the downloaded file * @throws IOException */ - protected boolean download(String url) throws IOException { + protected File downloadFile(String url) throws IOException { File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); pFolder.mkdirs(); File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp"); @@ -257,9 +444,9 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis()); Files.copy(new InputSupplier<InputStream>() { - @Override + @Override public InputStream getInput() throws IOException { - return new BufferedInputStream(conn.getInputStream()); + return new BufferedInputStream(conn.getInputStream()); } }, tmpFile); @@ -270,7 +457,7 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage tmpFile.renameTo(destFile); destFile.setLastModified(lastModified); - return true; + return destFile; } protected URLConnection getConnection(URL url) throws IOException { |