From a502d96a860456ec5e8c96761db70f7cabb74751 Mon Sep 17 00:00:00 2001 From: Paul Martin <paul@paulsputer.com> Date: Sat, 30 Apr 2016 04:19:14 -0400 Subject: [PATCH] Merge pull request #1073 from gitblit/1062-DocEditorUpdates --- src/main/java/com/gitblit/manager/PluginManager.java | 493 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 files changed, 406 insertions(+), 87 deletions(-) diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java index 47154b8..b3936e5 100644 --- a/src/main/java/com/gitblit/manager/PluginManager.java +++ b/src/main/java/com/gitblit/manager/PluginManager.java @@ -15,16 +15,23 @@ */ package com.gitblit.manager; -import java.io.BufferedInputStream; import java.io.File; import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.InetSocketAddress; 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; @@ -32,82 +39,293 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ro.fortsoft.pf4j.DefaultPluginFactory; import ro.fortsoft.pf4j.DefaultPluginManager; -import ro.fortsoft.pf4j.PluginVersion; +import ro.fortsoft.pf4j.ExtensionFactory; +import ro.fortsoft.pf4j.Plugin; +import ro.fortsoft.pf4j.PluginClassLoader; +import ro.fortsoft.pf4j.PluginFactory; +import ro.fortsoft.pf4j.PluginState; +import ro.fortsoft.pf4j.PluginStateEvent; +import ro.fortsoft.pf4j.PluginStateListener; import ro.fortsoft.pf4j.PluginWrapper; +import ro.fortsoft.pf4j.Version; +import com.gitblit.Constants; import com.gitblit.Keys; +import com.gitblit.extensions.GitblitPlugin; 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; import com.gitblit.utils.FileUtils; import com.gitblit.utils.JsonUtils; import com.gitblit.utils.StringUtils; -import com.google.common.io.Files; -import com.google.common.io.InputSupplier; +import com.google.common.io.ByteStreams; +import com.google.inject.Inject; +import com.google.inject.Singleton; /** * 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 + * @author James Moger * */ -public class PluginManager extends DefaultPluginManager implements IPluginManager { +@Singleton +public class PluginManager implements IPluginManager, PluginStateListener { private final Logger logger = LoggerFactory.getLogger(getClass()); private final IRuntimeManager runtimeManager; + + private DefaultPluginManager pf4j; // timeout defaults of Maven 3.0.4 in seconds private int connectTimeout = 20; private int readTimeout = 12800; + @Inject public PluginManager(IRuntimeManager runtimeManager) { - super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins")); this.runtimeManager = runtimeManager; } @Override + public Version getSystemVersion() { + return pf4j.getSystemVersion(); + } + + @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(); + File dir = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + dir.mkdirs(); + + pf4j = new DefaultPluginManager(dir) { + + @Override + protected PluginFactory createPluginFactory() { + return new GuicePluginFactory(); + } + + @Override + protected ExtensionFactory createExtensionFactory() { + return new GuiceExtensionFactory(); + } + }; + + try { + Version systemVersion = Version.createVersion(Constants.getVersion()); + pf4j.setSystemVersion(systemVersion); + } catch (Exception e) { + logger.error(null, e); + } + 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); + + // allow the plugin to prepare for operation after installation + PluginWrapper pluginWrapper = pf4j.getPlugin(pluginId); + if (pluginWrapper.getPlugin() instanceof GitblitPlugin) { + ((GitblitPlugin) pluginWrapper.getPlugin()).onInstall(); } - return true; + + PluginState state = pf4j.startPlugin(pluginId); + return PluginState.STARTED.equals(state); } @Override - public boolean refreshRegistry() { + public synchronized boolean upgradePlugin(String pluginId, String url, boolean verifyChecksum) throws IOException { + // ensure we can download the update BEFORE we remove the existing one + File file = download(url, verifyChecksum); + if (file == null || !file.exists()) { + logger.error("Failed to download plugin {}", url); + return false; + } + + Version oldVersion = pf4j.getPlugin(pluginId).getDescriptor().getVersion(); + if (removePlugin(pluginId, false)) { + String newPluginId = pf4j.loadPlugin(file); + if (StringUtils.isEmpty(newPluginId)) { + logger.error("Failed to load plugin {}", file); + return false; + } + + // the plugin to handle an upgrade + PluginWrapper pluginWrapper = pf4j.getPlugin(newPluginId); + if (pluginWrapper.getPlugin() instanceof GitblitPlugin) { + ((GitblitPlugin) pluginWrapper.getPlugin()).onUpgrade(oldVersion); + } + + PluginState state = pf4j.startPlugin(newPluginId); + return PluginState.STARTED.equals(state); + } else { + logger.error("Failed to delete plugin {}", pluginId); + } + return false; + } + + @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 false; + } + + @Override + public synchronized boolean uninstallPlugin(String pluginId) { + return removePlugin(pluginId, true); + } + + protected boolean removePlugin(String pluginId, boolean isUninstall) { + PluginWrapper pluginWrapper = getPlugin(pluginId); + final String name = pluginWrapper.getPluginPath().substring(1); + + if (isUninstall) { + // allow the plugin to prepare for uninstallation + if (pluginWrapper.getPlugin() instanceof GitblitPlugin) { + ((GitblitPlugin) pluginWrapper.getPlugin()).onUninstall(); + } + } + + 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 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(boolean verifyChecksum) { 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, verifyChecksum); + 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,10 +342,10 @@ } }; - 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(); + refreshRegistry(true); files = folder.listFiles(jsonFilter); } @@ -140,6 +358,7 @@ 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,20 +370,20 @@ } @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(); + Version pv = pw.getDescriptor().getVersion(); PluginRegistration reg = map.get(id); if (reg != null) { reg.installedRelease = pv.toString(); @@ -174,10 +393,21 @@ } @Override - public PluginRegistration lookupPlugin(String idOrName) { - for (PluginRegistry registry : getRegistries()) { - PluginRegistration reg = registry.lookup(idOrName); - if (reg != null) { + public synchronized List<PluginRegistration> getRegisteredPlugins(InstallState state) { + List<PluginRegistration> list = getRegisteredPlugins(); + Iterator<PluginRegistration> itr = list.iterator(); + while (itr.hasNext()) { + if (state != itr.next().getInstallState(getSystemVersion())) { + itr.remove(); + } + } + return list; + } + + @Override + public synchronized PluginRegistration lookupPlugin(String pluginId) { + for (PluginRegistration reg : getRegisteredPlugins()) { + if (reg.id.equalsIgnoreCase(pluginId)) { return reg; } } @@ -185,64 +415,111 @@ } @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 PluginRelease lookupRelease(String pluginId, String version) { + PluginRegistration reg = lookupPlugin(pluginId); + if (reg == null) { + return null; } - 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); + PluginRelease pv; + if (StringUtils.isEmpty(version)) { + pv = reg.getCurrentRelease(getSystemVersion()); + } 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) { - try { - if (!download(url)) { - return false; - } - // TODO stop, unload, load - } catch (IOException e) { - logger.error("Failed to install plugin from " + url, e); + protected File download(String url, boolean verifyChecksum) throws IOException { + File file = downloadFile(url); + + if (!verifyChecksum) { + return file; } - return true; + + File sha1File = null; + try { + sha1File = downloadFile(url + ".sha1"); + } catch (IOException e) { + } + + File md5File = null; + try { + md5File = downloadFile(url + ".md5"); + } catch (IOException e) { + + } + + if (sha1File == null && md5File == null) { + 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"); @@ -256,12 +533,9 @@ // try to get the server-specified last-modified date of this artifact long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis()); - Files.copy(new InputSupplier<InputStream>() { - @Override - public InputStream getInput() throws IOException { - return new BufferedInputStream(conn.getInputStream()); - } - }, tmpFile); + try (InputStream is = conn.getInputStream(); OutputStream os = new FileOutputStream(tmpFile);) { + ByteStreams.copy(is, os); + } File destFile = new File(pFolder, StringUtils.getLastPathElement(u.getPath())); if (destFile.exists()) { @@ -270,7 +544,7 @@ tmpFile.renameTo(destFile); destFile.setLastModified(lastModified); - return true; + return destFile; } protected URLConnection getConnection(URL url) throws IOException { @@ -310,10 +584,55 @@ } protected Proxy getProxy(URL url) { - return java.net.Proxy.NO_PROXY; + String proxyHost = runtimeManager.getSettings().getString(Keys.plugins.httpProxyHost, ""); + String proxyPort = runtimeManager.getSettings().getString(Keys.plugins.httpProxyPort, ""); + + if (!StringUtils.isEmpty(proxyHost) && !StringUtils.isEmpty(proxyPort)) { + return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, Integer.parseInt(proxyPort))); + } else { + return java.net.Proxy.NO_PROXY; + } } protected String getProxyAuthorization(URL url) { - return ""; + String proxyAuth = runtimeManager.getSettings().getString(Keys.plugins.httpProxyAuthorization, ""); + return proxyAuth; + } + + /** + * Instantiates a plugin using pf4j but injects member fields + * with Guice. + */ + private class GuicePluginFactory extends DefaultPluginFactory { + + @Override + public Plugin create(PluginWrapper pluginWrapper) { + // use pf4j to create the plugin + Plugin plugin = super.create(pluginWrapper); + + if (plugin != null) { + // allow Guice to inject member fields + runtimeManager.getInjector().injectMembers(plugin); + } + + return plugin; + } + } + + /** + * Instantiates an extension using Guice. + */ + private class GuiceExtensionFactory implements ExtensionFactory { + @Override + public Object create(Class<?> extensionClass) { + // instantiate && inject the extension + logger.debug("Create instance for extension '{}'", extensionClass.getName()); + try { + return runtimeManager.getInjector().getInstance(extensionClass); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return null; + } } } -- Gitblit v1.9.1