/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy 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. * * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>. */ package icy.plugin; import icy.file.FileUtil; import icy.gui.dialog.ConfirmDialog; import icy.gui.frame.progress.CancelableProgressFrame; import icy.gui.frame.progress.DownloadFrame; import icy.gui.frame.progress.FailedAnnounceFrame; import icy.gui.frame.progress.SuccessfullAnnounceFrame; import icy.main.Icy; import icy.network.NetworkUtil; import icy.network.URLUtil; import icy.plugin.PluginDescriptor.PluginIdent; import icy.preferences.RepositoryPreferences.RepositoryInfo; import icy.system.IcyExceptionHandler; import icy.system.thread.ThreadUtil; import icy.update.Updater; import icy.util.StringUtil; import icy.util.XMLUtil; import icy.util.ZipUtil; import java.net.URL; import java.util.ArrayList; import java.util.EventListener; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.event.EventListenerList; /** * @author Stephane */ public class PluginInstaller implements Runnable { public static interface PluginInstallerListener extends EventListener { public void pluginInstalled(PluginDescriptor plugin, boolean success); public void pluginRemoved(PluginDescriptor plugin, boolean success); } private static class PluginInstallInfo { // final PluginRepositoryLoader loader; final PluginDescriptor plugin; final boolean showProgress; public PluginInstallInfo(PluginDescriptor plugin, boolean showProgress) { super(); this.plugin = plugin; this.showProgress = showProgress; } @Override public boolean equals(Object obj) { if (obj instanceof PluginInstallInfo) return ((PluginInstallInfo) obj).plugin.equals(plugin); return super.equals(obj); } @Override public int hashCode() { return plugin.hashCode(); } } private static final String ERROR_DOWNLOAD = "Error while downloading "; private static final String ERROR_SAVE = "Error while saving"; // private static final String INSTALL_CANCELED = "Plugin installation canceled by user."; /** * static class */ private static final PluginInstaller instance = new PluginInstaller(); /** * plugin(s) to install FIFO */ private final List<PluginInstallInfo> installFIFO; /** * plugin(s) to delete FIFO */ private final List<PluginInstallInfo> removeFIFO; /** * listeners */ private final EventListenerList listeners; /** * internals */ private final List<PluginDescriptor> installingPlugins; private final List<PluginDescriptor> desinstallingPlugin; /** * static class */ private PluginInstaller() { super(); installFIFO = new ArrayList<PluginInstallInfo>(); removeFIFO = new ArrayList<PluginInstallInfo>(); listeners = new EventListenerList(); installingPlugins = new ArrayList<PluginDescriptor>(); desinstallingPlugin = new ArrayList<PluginDescriptor>(); // launch installer thread new Thread(this, "Plugin installer").start(); } /** * Return true if install or desinstall is possible */ private static boolean isEnabled() { return !PluginLoader.isJCLDisabled(); } /** * Install a plugin (asynchronous) * * @param plugin * the plugin to install * @param showProgress * show a progress frame during process */ public static void install(PluginDescriptor plugin, boolean showProgress) { if ((plugin != null) && isEnabled()) { if (!NetworkUtil.hasInternetAccess()) { final String text = "Cannot install '" + plugin.getName() + "' plugin : you are not connected to Internet."; if (Icy.getMainInterface().isHeadLess()) System.err.println(text); else new FailedAnnounceFrame(text, 10); return; } synchronized (instance.installFIFO) { instance.installFIFO.add(new PluginInstallInfo(plugin, showProgress)); } } } /** * return true if PluginInstaller is processing */ public static boolean isProcessing() { return isInstalling() || isDesinstalling(); } /** * return a copy of the install FIFO */ public static ArrayList<PluginInstallInfo> getInstallFIFO() { synchronized (instance.installFIFO) { return new ArrayList<PluginInstaller.PluginInstallInfo>(instance.installFIFO); } } /** * Wait while installer is installing plugin. */ public static void waitInstall() { while (isInstalling()) ThreadUtil.sleep(100); } /** * return true if PluginInstaller is installing plugin(s) */ public static boolean isInstalling() { return !instance.installFIFO.isEmpty() || !instance.installingPlugins.isEmpty(); } /** * return true if 'plugin' is in the install FIFO */ public static boolean isWaitingForInstall(PluginDescriptor plugin) { synchronized (instance.installFIFO) { for (PluginInstallInfo info : instance.installFIFO) if (plugin == info.plugin) return true; } return false; } /** * return true if specified plugin is currently being installed or will be installed */ public static boolean isInstallingPlugin(PluginDescriptor plugin) { return (instance.installingPlugins.indexOf(plugin) != -1) || isWaitingForInstall(plugin); } /** * Uninstall a plugin (asynchronous) * * @param plugin * the plugin to uninstall * @param showConfirm * show a confirmation dialog * @param showProgress * show a progress frame during process */ public static void desinstall(PluginDescriptor plugin, boolean showConfirm, boolean showProgress) { if ((plugin != null) && isEnabled()) { if (showConfirm) { // get local plugins which depend from the plugin we want to delete final List<PluginDescriptor> dependants = getLocalDependenciesFrom(plugin); String message = "<html>"; if (!dependants.isEmpty()) { message = message + "The following plugin(s) won't work anymore :<br>"; for (PluginDescriptor depPlug : dependants) message = message + depPlug.getName() + " " + depPlug.getVersion() + "<br>"; message = message + "<br>"; } message = message + "Are you sure you want to remove '" + plugin.getName() + " " + plugin.getVersion() + "' ?</html>"; if (ConfirmDialog.confirm(message)) { synchronized (instance.removeFIFO) { instance.removeFIFO.add(new PluginInstallInfo(plugin, showConfirm)); } } } else { synchronized (instance.removeFIFO) { instance.removeFIFO.add(new PluginInstallInfo(plugin, showProgress)); } } } } /** * @deprecated Use {@link #desinstall(PluginDescriptor, boolean, boolean)} instead. */ @Deprecated public static void desinstall(PluginDescriptor plugin, boolean showConfirm) { desinstall(plugin, showConfirm, showConfirm); } /** * return a copy of the remove FIFO */ public static ArrayList<PluginInstallInfo> getRemoveFIFO() { synchronized (instance.removeFIFO) { return new ArrayList<PluginInstaller.PluginInstallInfo>(instance.removeFIFO); } } /** * Wait while installer is removing plugin. */ public static void waitDesinstall() { while (isDesinstalling()) ThreadUtil.sleep(100); } /** * return true if PluginInstaller is desinstalling plugin(s) */ public static boolean isDesinstalling() { return !instance.removeFIFO.isEmpty() || !instance.desinstallingPlugin.isEmpty(); } /** * return true if 'plugin' is in the remove FIFO */ public static boolean isWaitingForDesinstall(PluginDescriptor plugin) { synchronized (instance.removeFIFO) { for (PluginInstallInfo info : instance.removeFIFO) if (plugin == info.plugin) return true; } return false; } /** * return true if specified plugin is currently being desinstalled or will be desinstalled */ public static boolean isDesinstallingPlugin(PluginDescriptor plugin) { return (instance.desinstallingPlugin.indexOf(plugin) != -1) || isWaitingForDesinstall(plugin); } @Override public void run() { while (!Thread.interrupted()) { // process installations if (!installFIFO.isEmpty()) { // so list has sometime to fill-up ThreadUtil.sleep(200); do installInternal(); while (!installFIFO.isEmpty()); } // process deletions while (!removeFIFO.isEmpty()) { // so list has sometime to fill-up ThreadUtil.sleep(200); do desinstallInternal(); while (!removeFIFO.isEmpty()); } ThreadUtil.sleep(100); } } /** * Backup specified plugin if it already exists.<br> * Return an empty string if no error else return error message */ private static String backup(PluginDescriptor plugin) { boolean ok; // backup JAR, XML and image files ok = Updater.backup(plugin.getJarFilename()) && Updater.backup(plugin.getXMLFilename()) && Updater.backup(plugin.getIconFilename()) && Updater.backup(plugin.getImageFilename()); if (!ok) return "Can't backup plugin '" + plugin.getName() + "'"; return ""; } /** * Return an empty string if no error else return error message */ private static String downloadAndSavePlugin(PluginDescriptor plugin, DownloadFrame taskFrame) { String result; if (taskFrame != null) taskFrame.setMessage("Downloading " + plugin); // ensure descriptor is loaded plugin.loadDescriptor(); final RepositoryInfo repos = plugin.getRepository(); final String login; final String pass; // use authentication (repos should not be null at this point) if (repos.isAuthenticationEnabled()) { login = repos.getLogin(); pass = repos.getPassword(); } else { login = null; pass = null; } // try to build the final path using base repository address and plugin relative address // (useful for local repository) URL url; final String basePath = FileUtil.getDirectory(repos.getLocation()); // download and save JAR file url = URLUtil.buildURL(basePath, plugin.getJarUrl()); result = downloadAndSave(url, plugin.getJarFilename(), login, pass, true, taskFrame); if (!StringUtil.isEmpty(result)) return result; // verify JAR file is not corrupted if (!ZipUtil.isValid(plugin.getJarFilename(), false)) return "Downloaded JAR file '" + plugin.getJarFilename() + "' is corrupted !"; // download and save XML file url = URLUtil.buildURL(basePath, plugin.getUrl()); result = downloadAndSave(url, plugin.getXMLFilename(), login, pass, true, taskFrame); if (!StringUtil.isEmpty(result)) return result; // verify XML file is not corrupted if (XMLUtil.loadDocument(plugin.getXMLFilename()) == null) return "Downloaded XML file '" + plugin.getXMLFilename() + "' is corrupted !"; // download and save icon & image files if (!StringUtil.isEmpty(plugin.getIconUrl())) { url = URLUtil.buildURL(basePath, plugin.getIconUrl()); downloadAndSave(url, plugin.getIconFilename(), login, pass, false, taskFrame); } if (!StringUtil.isEmpty(plugin.getImageUrl())) { url = URLUtil.buildURL(basePath, plugin.getImageUrl()); downloadAndSave(url, plugin.getImageFilename(), login, pass, false, taskFrame); } return ""; } /** * Return an empty string if no error else return error message */ private static String downloadAndSave(URL downloadPath, String savePath, String login, String pass, boolean displayError, DownloadFrame downloadFrame) { if (downloadFrame != null) downloadFrame.setPath(FileUtil.getFileName(savePath)); // load data final byte[] data = NetworkUtil.download(downloadPath, login, pass, downloadFrame, displayError); if (data == null) return ERROR_DOWNLOAD + downloadPath.toString(); // save data if (!FileUtil.save(savePath, data, displayError)) { System.err.println("Can't write '" + savePath + "' !"); System.err.println("File may be locked or you don't own the rights to write files here."); return ERROR_SAVE + savePath; } return null; } private static boolean deletePlugin(PluginDescriptor plugin) { if (!FileUtil.delete(plugin.getJarFilename(), false)) { System.err.println("Can't delete '" + plugin.getJarFilename() + "' file !"); // fatal error return false; } if (FileUtil.exists(plugin.getXMLFilename())) if (!FileUtil.delete(plugin.getXMLFilename(), false)) System.err.println("Can't delete '" + plugin.getXMLFilename() + "' file !"); FileUtil.delete(plugin.getImageFilename(), false); FileUtil.delete(plugin.getIconFilename(), false); return true; } /** * Fill list with local dependencies (plugins) of specified plugin */ public static void getLocalDependenciesOf(List<PluginDescriptor> result, PluginDescriptor plugin) { // load plugin descriptor informations if not yet done plugin.loadDescriptor(); for (PluginIdent ident : plugin.getRequired()) { // already in our dependences ? --> pass to the next one if (PluginDescriptor.getPlugin(result, ident, true) != null) continue; // find local dependent plugin final PluginDescriptor dep = PluginLoader.getPlugin(ident, true); // dependence found ? if (dep != null) { // and add it to list PluginDescriptor.addToList(result, dep); // search its dependencies too getLocalDependenciesOf(result, dep); } } } /** * Return local plugins list which depend from the specified list of plugins. */ public static List<PluginDescriptor> getLocalDependenciesFrom(List<PluginDescriptor> plugins) { final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>(); for (PluginDescriptor plugin : plugins) getLocalDependenciesFrom(plugin, result); return result; } /** * Return local plugins list which depend from the specified plugin. */ public static List<PluginDescriptor> getLocalDependenciesFrom(PluginDescriptor plugin) { final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>(); getLocalDependenciesFrom(plugin, result); return result; } /** * Return local plugins list which depend from the specified plugin. */ private static void getLocalDependenciesFrom(PluginDescriptor plugin, List<PluginDescriptor> result) { for (PluginDescriptor curPlug : PluginLoader.getPlugins(false)) // require specified plugin ? if (curPlug.requires(plugin)) PluginDescriptor.addToList(result, curPlug); } /** * Fill list with 'sources' dependencies of specified plugin */ private static void getLocalDependenciesOf(List<PluginDescriptor> result, List<PluginDescriptor> sources, PluginDescriptor plugin) { // load plugin descriptor informations if not yet done plugin.loadDescriptor(); for (PluginIdent ident : plugin.getRequired()) { // already in our dependences ? --> pass to the next one if ((ident == null) || (PluginDescriptor.getPlugin(result, ident, true) != null)) continue; // find sources dependent plugin final PluginDescriptor dep = PluginDescriptor.getPlugin(sources, ident, true); // dependence found ? if (dep != null) { // and add it to list PluginDescriptor.addToList(result, dep); // search its dependencies too getLocalDependenciesOf(result, sources, dep); } } } /** * Reorder the list so needed dependencies comes first in list */ public static List<PluginDescriptor> orderDependencies(List<PluginDescriptor> plugins) { final List<PluginDescriptor> sources = new ArrayList<PluginDescriptor>(plugins); final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>(); while (sources.size() > 0) { final List<PluginDescriptor> deps = new ArrayList<PluginDescriptor>(); getLocalDependenciesOf(result, sources, sources.get(0)); // add last to first dep for (int i = deps.size() - 1; i >= 0; i--) PluginDescriptor.addToList(result, deps.get(i)); // then add plugin PluginDescriptor.addToList(result, sources.get(0)); // remove tested plugin and its dependencies from source sources.removeAll(result); } return result; } /** * Resolve dependencies for specified plugin * * @param taskFrame */ public static boolean getDependencies(PluginDescriptor plugin, List<PluginDescriptor> pluginsToInstall, CancelableProgressFrame taskFrame, boolean showError) { // load plugin descriptor informations if not yet done plugin.loadDescriptor(); // check dependencies for (PluginIdent ident : plugin.getRequired()) { if ((taskFrame != null) && taskFrame.isCancelRequested()) return false; // should not happen but... if (ident == null) continue; // already in our dependencies ? --> pass to the next one if (PluginDescriptor.getPlugin(pluginsToInstall, ident, true) != null) continue; final String className = ident.getClassName(); // get local & online plugin final PluginDescriptor localPlugin = PluginLoader.getPlugin(className); final PluginDescriptor onlinePlugin = PluginRepositoryLoader.getPlugin(className); // plugin not yet installed or outdated ? if ((localPlugin == null) || ident.getVersion().isGreater(localPlugin.getVersion())) { // online plugin not found ? if (onlinePlugin == null) { // error if (showError) { System.err.println("Can't resolve dependencies for plugin '" + plugin.getName() + "' :"); if (localPlugin == null) System.err.println("Plugin class '" + ident.getClassName() + " not found !"); else { System.err.println(localPlugin.getName() + " " + localPlugin.getVersion() + " installed"); System.err.println("but version " + ident.getVersion() + " or greater needed."); } } return false; } // online plugin version incorrect else if (ident.getVersion().isGreater(onlinePlugin.getVersion())) { // error if (showError) { System.err.println("Can't resolve dependencies for plugin '" + plugin.getName() + "' :"); System.err.println(onlinePlugin.getName() + " " + onlinePlugin.getVersion() + " found in repository"); System.err.println("but version " + ident.getVersion() + " or greater needed."); } return false; } // add to the install list PluginDescriptor.addToList(pluginsToInstall, onlinePlugin); // and check dependencies for this plugin if (!getDependencies(onlinePlugin, pluginsToInstall, taskFrame, showError)) return false; } else { // just check if we have update for dependency if ((onlinePlugin != null) && (localPlugin.getVersion().isLower(onlinePlugin.getVersion()))) { // as web site doesn't handle version dependency, we force the update // add to the install list PluginDescriptor.addToList(pluginsToInstall, onlinePlugin); // and check dependencies for this plugin if (!getDependencies(onlinePlugin, pluginsToInstall, taskFrame, showError)) return false; } } } return true; } private void installInternal() { DownloadFrame taskFrame = null; try { final List<PluginInstallInfo> infos; boolean showProgress; synchronized (installFIFO) { infos = new ArrayList<PluginInstaller.PluginInstallInfo>(installFIFO); showProgress = false; for (int i = infos.size() - 1; i >= 0; i--) { final PluginInstallInfo info = infos.get(i); PluginDescriptor.addToList(installingPlugins, info.plugin); showProgress |= info.showProgress; } installFIFO.clear(); } if (showProgress && !Icy.getMainInterface().isHeadLess()) { taskFrame = new DownloadFrame(); taskFrame.setMessage("Initializing..."); } List<PluginDescriptor> dependencies = new ArrayList<PluginDescriptor>(); final Set<PluginDescriptor> pluginsOk = new HashSet<PluginDescriptor>(); final Set<PluginDescriptor> pluginsNOk = new HashSet<PluginDescriptor>(); // get dependencies for (int i = installingPlugins.size() - 1; i >= 0; i--) { final PluginDescriptor plugin = installingPlugins.get(i); final String plugDesc = plugin.getName() + " " + plugin.getVersion(); if (taskFrame != null) { // cancel requested ? if (taskFrame.isCancelRequested()) return; taskFrame.setMessage("Checking dependencies for '" + plugDesc + "' ..."); } // check dependencies if (!getDependencies(plugin, dependencies, taskFrame, true)) { // can't resolve dependencies for this plugin pluginsNOk.add(plugin); installingPlugins.remove(i); } } // nothing to install if (installingPlugins.isEmpty()) return; // order dependencies dependencies = orderDependencies(dependencies); // add dependencies at the beginning of the installing list for (PluginDescriptor plugin : dependencies) PluginDescriptor.addToList(installingPlugins, plugin, 0); String error = ""; // clear backup folder FileUtil.delete(Updater.BACKUP_DIRECTORY, true); // now we can proceed the installation itself for (PluginDescriptor plugin : installingPlugins) { for (PluginIdent ident : plugin.getRequired()) { // one of the dependencies was not correctly installed ? if (PluginDescriptor.existInList(pluginsNOk, ident)) { // we can't install the plugin, continue with the next one pluginsNOk.add(plugin); continue; } } final String plugDesc = plugin.getName() + " " + plugin.getVersion(); if (taskFrame != null) { // cancel requested ? --> interrupt installation if (taskFrame.isCancelRequested()) break; taskFrame.setMessage("Installing " + plugDesc + "..."); } try { // backup plugin error = backup(plugin); // backup ok --> install plugin if (StringUtil.isEmpty(error)) { error = downloadAndSavePlugin(plugin, taskFrame); // an error occurred ? --> restore if (!StringUtil.isEmpty(error)) Updater.restore(); } } finally { // delete backup FileUtil.delete(Updater.BACKUP_DIRECTORY, true); } if (StringUtil.isEmpty(error)) pluginsOk.add(plugin); else { pluginsNOk.add(plugin); // print error System.err.println(error); } } // verify installed plugins if (taskFrame != null) taskFrame.setMessage("Verifying plugins..."); // reload plugin list PluginLoader.reload(); for (PluginDescriptor plugin : pluginsOk) { error = PluginLoader.verifyPlugin(plugin); // send report when we have verification error if (!StringUtil.isEmpty(error)) { IcyExceptionHandler.report(plugin, "An error occured while installing the plugin :\n" + error); // print error System.err.println(error); pluginsNOk.add(plugin); } } // remove all plugins which failed from OK list pluginsOk.removeAll(pluginsNOk); if (!pluginsNOk.isEmpty()) { System.err.println(); System.err.println("Installation of the following plugin(s) failed:"); for (PluginDescriptor plugin : pluginsNOk) { System.err.println(plugin.getName() + " " + plugin.getVersion()); // notify about installation fails fireInstalledEvent(plugin, false); } System.err.println(); } if (!pluginsOk.isEmpty()) { System.out.println(); System.out.println("The following plugin(s) has been correctly installed:"); for (PluginDescriptor plugin : pluginsOk) { System.out.println(plugin.getName() + " " + plugin.getVersion()); // notify about installation successes fireInstalledEvent(plugin, true); } System.out.println(); } if (showProgress && !Icy.getMainInterface().isHeadLess()) { if (pluginsNOk.isEmpty()) new SuccessfullAnnounceFrame("Plugin(s) installation was successful !"); else if (pluginsOk.isEmpty()) new FailedAnnounceFrame("Plugin(s) installation failed !"); else new FailedAnnounceFrame( "Some plugin(s) installation failed (looks at the output console for detail) !"); } } finally { // installation end installingPlugins.clear(); if (taskFrame != null) taskFrame.close(); } } private void desinstallInternal() { CancelableProgressFrame taskFrame = null; try { final List<PluginInstallInfo> infos; boolean showProgress; synchronized (removeFIFO) { infos = new ArrayList<PluginInstaller.PluginInstallInfo>(removeFIFO); // determine if we should display the progress bar showProgress = false; for (int i = infos.size() - 1; i >= 0; i--) { final PluginInstallInfo info = infos.get(i); desinstallingPlugin.add(info.plugin); showProgress |= info.showProgress; } removeFIFO.clear(); } if (showProgress && !Icy.getMainInterface().isHeadLess()) taskFrame = new CancelableProgressFrame("Initializing..."); // now we can proceed remove for (PluginDescriptor plugin : desinstallingPlugin) { final String plugDesc = plugin.getName() + " " + plugin.getVersion(); final boolean result; if (taskFrame != null) { // cancel requested ? if (taskFrame.isCancelRequested()) return; taskFrame.setMessage("Removing plugin '" + plugDesc + "'..."); } result = deletePlugin(plugin); // notify plugin deletion fireRemovedEvent(plugin, result); if (showProgress && !Icy.getMainInterface().isHeadLess()) { if (!result) new FailedAnnounceFrame("Plugin '" + plugDesc + "' delete operation failed !"); } if (result) System.out.println("Plugin '" + plugDesc + "' correctly removed."); else System.err.println("Plugin '" + plugDesc + "' delete operation failed !"); } } finally { if (taskFrame != null) taskFrame.close(); // removing end desinstallingPlugin.clear(); } // reload plugin list PluginLoader.reload(); } /** * Add a listener * * @param listener */ public static void addListener(PluginInstallerListener listener) { synchronized (instance.listeners) { instance.listeners.add(PluginInstallerListener.class, listener); } } /** * Remove a listener * * @param listener */ public static void removeListener(PluginInstallerListener listener) { synchronized (instance.listeners) { instance.listeners.remove(PluginInstallerListener.class, listener); } } /** * fire plugin installed event */ private void fireInstalledEvent(PluginDescriptor plugin, boolean success) { synchronized (listeners) { for (PluginInstallerListener listener : listeners.getListeners(PluginInstallerListener.class)) listener.pluginInstalled(plugin, success); } } /** * fire plugin removed event */ private void fireRemovedEvent(PluginDescriptor plugin, boolean success) { synchronized (listeners) { for (PluginInstallerListener listener : listeners.getListeners(PluginInstallerListener.class)) listener.pluginRemoved(plugin, success); } } }