/* ================================================================== * OBRProvisionTask.java - Apr 24, 2014 8:09:12 PM * * Copyright 2007-2014 SolarNetwork.net Dev Team * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== */ package net.solarnetwork.node.setup.obr; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.codec.digest.DigestUtils; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; import org.osgi.framework.Version; import org.osgi.framework.VersionRange; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.FrameworkWiring; import org.osgi.service.obr.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import net.solarnetwork.node.backup.Backup; import net.solarnetwork.node.backup.BackupManager; import net.solarnetwork.node.setup.BundlePlugin; import net.solarnetwork.node.setup.Plugin; /** * Task to install plugins. * * @author matt * @version 1.1 */ public class OBRProvisionTask implements Callable<OBRPluginProvisionStatus> { private static final Logger LOG = LoggerFactory.getLogger(OBRProvisionTask.class); private final BundleContext bundleContext; private final OBRPluginProvisionStatus status; private Future<OBRPluginProvisionStatus> future; private final File directory; private final BackupManager backupManager; /** * Construct with a status. * * @param bundleContext * the BundleContext to manipulate bundles with * @param status * the status, which defines the plugins to install * @param directory * the directory to download plugins to * @param backupManager * if provided, then a backup will be performed before provisioning * any bundles */ public OBRProvisionTask(BundleContext bundleContext, OBRPluginProvisionStatus status, File directory, BackupManager backupManager) { super(); this.bundleContext = bundleContext; this.status = status; this.directory = directory; this.backupManager = backupManager; this.status.setBackupComplete(backupManager == null); } @Override public OBRPluginProvisionStatus call() throws Exception { try { status.setStatusMessage("Starting provisioning operation."); handleBackupBeforeProvisioningOperation(); if ( status.getPluginsToInstall() != null && status.getPluginsToInstall().size() > 0 ) { downloadPlugins(status.getPluginsToInstall()); } if ( status.getPluginsToRemove() != null && status.getPluginsToRemove().size() > 0 ) { removePlugins(status.getPluginsToRemove()); } status.setStatusMessage("Provisioning operation complete."); return status; } catch ( Exception e ) { LOG.warn("Error in provision task: {}", e.getMessage(), e); status.setStatusMessage("Error in provisioning operation: " + e.getMessage()); throw e; } } private void handleBackupBeforeProvisioningOperation() { // if we are actually going to provision something, let's make a backup if ( backupManager != null && status.getOverallProgress() < 1 ) { status.setStatusMessage("Creating backup before provisioning operation."); LOG.info("Creating backup before provisioning operation."); try { Backup backup = backupManager.createBackup(); if ( backup != null ) { LOG.info("Created backup {} (size {})", backup.getKey(), backup.getSize()); status.setStatusMessage("Backup complete."); status.setBackupComplete(Boolean.TRUE); } } catch ( RuntimeException e ) { status.setBackupComplete(Boolean.FALSE); LOG.warn("Error creating backup for provisioning operation {}", status.getProvisionID(), e); } } } /** * Find all installed bundles for a specific ID that are less than or equal * to a specific version. * * @param symbolicName * The bundle ID to look for. * @param maxVersion * The maximum version to include in the result. * @return All found bundles whose symbolic name matches and has a version * less than {@code maxVersion}, in largest to smallest order, or * <em>null</em> if none found. */ private List<Bundle> findBundlesOlderThanVersion(String symbolicName, Version maxVersion) { List<Bundle> olderBundles = null; Bundle[] bundles = bundleContext.getBundles(); for ( Bundle b : bundles ) { if ( b.getSymbolicName().equals(symbolicName) && b.getVersion().compareTo(maxVersion) < 1 ) { if ( olderBundles == null ) { olderBundles = new ArrayList<Bundle>(2); } olderBundles.add(b); } } if ( olderBundles != null ) { Collections.sort(olderBundles, new Comparator<Bundle>() { @Override public int compare(Bundle o1, Bundle o2) { return o2.getVersion().compareTo(o1.getVersion()); } }); } return olderBundles; } private void downloadPlugins(List<Plugin> plugins) throws InterruptedException { assert plugins != null; LOG.debug("Starting install of {} plugins", plugins.size()); if ( !directory.exists() && !directory.mkdirs() ) { throw new RuntimeException("Unable to create plugin directory: " + directory.toString()); } // This method will manually download the bundle for each resolved plugin, // then install it and start it in the running OSGi platform. We don't // make use of the OBR RepositoryAdmin to do this because on SolarNode // the bundle's runtime area is held only in RAM (not persisted to disk) // but we want these downloaded bundles to be persisted to disk. Thus we // just do a bit of work here to download and start the bundles ourselves. List<Bundle> installedBundles = new ArrayList<Bundle>(plugins.size()); // iterate backwards, to work our way up through deps to requested plugin for ( ListIterator<Plugin> itr = plugins.listIterator(plugins.size()); itr.hasPrevious(); ) { Plugin plugin = itr.previous(); assert plugin instanceof OBRResourcePlugin; LOG.debug("Starting install of plugin: {}", plugin.getUID()); status.setStatusMessage("Starting install of plugin " + plugin.getUID()); OBRResourcePlugin obrPlugin = (OBRResourcePlugin) plugin; Resource resource = obrPlugin.getResource(); URL resourceURL = resource.getURL(); String pluginFileName = StringUtils.getFilename(resourceURL.getPath()); File outputFile = new File(directory, pluginFileName); String bundleSymbolicName = resource.getSymbolicName(); // download to tmp file first, then we'll rename File tmpOutputFile = new File(directory, "." + pluginFileName); LOG.debug("Downloading plugin {} => {}", resourceURL, tmpOutputFile); try { FileCopyUtils.copy(resourceURL.openStream(), new FileOutputStream(tmpOutputFile)); } catch ( IOException e ) { throw new RuntimeException("Unable to download plugin " + bundleSymbolicName, e); } moveTemporaryDownloadedPluginFile(resource, outputFile, tmpOutputFile); installDownloadedPlugin(resource, outputFile, installedBundles); LOG.debug("Installed plugin: {}", plugin.getUID()); status.markPluginInstalled(plugin); } if ( !installedBundles.isEmpty() ) { Set<Bundle> toRefresh = findFragmentHostsForBundles(installedBundles); toRefresh.addAll(installedBundles); status.setStatusMessage("Refreshing OSGi framework."); FrameworkWiring fw = bundleContext.getBundle(0).adapt(FrameworkWiring.class); fw.refreshBundles(toRefresh); for ( ListIterator<Bundle> itr = installedBundles.listIterator(); itr.hasNext(); ) { Bundle b = itr.next(); boolean fragment = isFragment(b); status.setStatusMessage("Starting plugin: " + b.getSymbolicName()); try { if ( !fragment && !(b.getState() == Bundle.ACTIVE || b.getState() == Bundle.STARTING) ) { b.start(); } // bundles are in reverse order of plugins Plugin p = plugins.get(plugins.size() - itr.nextIndex()); status.markPluginStarted(p); } catch ( BundleException e ) { throw new RuntimeException("Unable to start plugin " + b.getSymbolicName() + " version " + b.getVersion(), e); } } } LOG.debug("Install of {} plugins complete", plugins.size()); status.setStatusMessage("Install of " + plugins.size() + " plugins complete"); } private boolean isFragment(Bundle b) { BundleRevision r = b.adapt(BundleRevision.class); return (r != null && (r.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0); } private static final Pattern BUNDLE_VERSION_PATTERN = Pattern .compile("bundle-version\\s*=\\s*\"([^\"]+)", Pattern.CASE_INSENSITIVE); private Set<Bundle> findFragmentHostsForBundles(Collection<Bundle> toRefresh) { Set<Bundle> fragmentHosts = new HashSet<Bundle>(); Bundle[] bundles = bundleContext.getBundles(); for ( Bundle b : toRefresh ) { if ( b.getState() == Bundle.UNINSTALLED ) { continue; } String hostHeader = b.getHeaders().get(Constants.FRAGMENT_HOST); if ( hostHeader == null ) { continue; } String[] clauses = StringUtils.delimitedListToStringArray(hostHeader, ";"); if ( clauses == null || clauses.length < 1 ) { continue; } String hostSymbolicName = clauses[0]; for ( Bundle hostBundle : bundles ) { if ( hostBundle.getSymbolicName() != null && hostBundle.getSymbolicName().equals(hostSymbolicName) ) { VersionRange hostVersionRange = null; if ( clauses.length > 1 ) { for ( String clause : clauses ) { Matcher m = BUNDLE_VERSION_PATTERN.matcher(clause); if ( m.find() ) { String ver = m.group(1); try { hostVersionRange = new org.osgi.framework.VersionRange(ver); } catch ( IllegalArgumentException e ) { LOG.warn( "Ignoring fragment bundle {} version range syntax error: {}", hostSymbolicName, e.getMessage()); } break; } } } if ( hostVersionRange == null || hostVersionRange.includes(hostBundle.getVersion()) ) { LOG.debug("Found fragment {} host {} to refresh", b, hostBundle); fragmentHosts.add(hostBundle); } continue; } } } return fragmentHosts; } private void moveTemporaryDownloadedPluginFile(Resource resource, File outputFile, File tmpOutputFile) { if ( outputFile.exists() ) { // if the file has not changed, just delete tmp file InputStream outputFileInputStream = null; InputStream tmpOutputFileInputStream = null; try { outputFileInputStream = new FileInputStream(outputFile); tmpOutputFileInputStream = new FileInputStream(tmpOutputFile); String outputFileHash = DigestUtils.sha1Hex(outputFileInputStream); String tmpOutputFileHash = DigestUtils.sha1Hex(tmpOutputFileInputStream); if ( tmpOutputFileHash.equals(outputFileHash) ) { // file unchanged, so just delete tmp file tmpOutputFile.delete(); } else { LOG.debug("Bundle {} version {} content updated", resource.getSymbolicName(), resource.getVersion()); outputFile.delete(); tmpOutputFile.renameTo(outputFile); } } catch ( IOException e ) { throw new RuntimeException("Error downloading plugin " + resource.getSymbolicName(), e); } finally { if ( outputFileInputStream != null ) { try { outputFileInputStream.close(); } catch ( IOException e ) { // ignore; } } if ( tmpOutputFileInputStream != null ) { try { tmpOutputFileInputStream.close(); } catch ( IOException e ) { // ignore } } } } else { // rename tmp file tmpOutputFile.renameTo(outputFile); } } private boolean installDownloadedPlugin(Resource resource, File outputFile, List<Bundle> installedBundles) { final String bundleSymbolicName = resource.getSymbolicName(); boolean refreshNeeded = false; try { URL newBundleURL = outputFile.toURI().toURL(); List<Bundle> oldBundles = findBundlesOlderThanVersion(bundleSymbolicName, resource.getVersion()); Bundle oldBundle = (oldBundles != null && oldBundles.size() > 0 ? oldBundles.get(0) : null); Version oldVersion = (oldBundle != null ? oldBundle.getVersion() : null); if ( oldVersion != null && oldVersion.compareTo(resource.getVersion()) >= 0 ) { LOG.debug("Skipping install of plugin {} as version is unchanged at {}", bundleSymbolicName, oldVersion); } else if ( oldVersion != null ) { LOG.debug("Upgrading plugin {} from {} to {}", bundleSymbolicName, oldVersion, resource.getVersion()); InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(outputFile)); oldBundle.update(in); // try to delete the old version File oldJar = new File(directory, bundleSymbolicName + "-" + oldVersion + ".jar"); if ( !oldJar.delete() ) { LOG.warn("Error deleting old plugin " + oldJar.getName()); } installedBundles.add(oldBundle); LOG.info("Upgraded plugin {} from version {} to {}", bundleSymbolicName, oldVersion, resource.getVersion()); refreshNeeded = true; } catch ( BundleException e ) { throw new RuntimeException("Unable to upgrade plugin " + bundleSymbolicName, e); } catch ( FileNotFoundException e ) { throw new RuntimeException("Unable to upgrade plugin " + bundleSymbolicName, e); } finally { if ( in != null ) { try { in.close(); } catch ( IOException e ) { // ignore } } } } else { LOG.debug("Installing plugin {} version {}", newBundleURL, resource.getVersion()); Bundle newBundle = bundleContext.installBundle(newBundleURL.toString()); LOG.info("Installed plugin {} version {}", newBundle.getSymbolicName(), newBundle.getVersion()); installedBundles.add(newBundle); } } catch ( BundleException e ) { throw new RuntimeException("Unable to install plugin " + bundleSymbolicName, e); } catch ( MalformedURLException e ) { throw new RuntimeException("Unable to install plugin " + bundleSymbolicName, e); } return refreshNeeded; } private void removePlugins(List<Plugin> plugins) { assert plugins != null; LOG.debug("Starting removal of {} plugins", plugins.size()); boolean refreshNeeded = false; for ( Plugin plugin : plugins ) { assert plugin instanceof BundlePlugin; LOG.debug("Starting removal of plugin: {}", plugin.getUID()); status.setStatusMessage("Starting removal of plugin " + plugin.getUID()); BundlePlugin bundlePlugin = (BundlePlugin) plugin; Bundle oldBundle = bundlePlugin.getBundle(); if ( oldBundle != null ) { Version oldVersion = oldBundle.getVersion(); LOG.debug("Removing plugin {} version {}", oldBundle.getSymbolicName(), oldVersion); try { oldBundle.uninstall(); refreshNeeded = true; } catch ( BundleException e ) { throw new RuntimeException( "Unable to uninstall plugin " + oldBundle.getSymbolicName(), e); } File oldJar = new File(directory, oldBundle.getSymbolicName() + "-" + oldVersion + ".jar"); if ( !oldJar.delete() ) { LOG.warn("Error deleting plugin JAR " + oldJar.getName()); } } LOG.debug("Removed plugin: {}", plugin.getUID()); status.setStatusMessage("Removed plugin " + plugin.getUID()); status.markPluginRemoved(plugin); } if ( refreshNeeded ) { status.setStatusMessage("Refreshing OSGi framework."); FrameworkWiring fw = bundleContext.getBundle(0).adapt(FrameworkWiring.class); fw.refreshBundles(null); } LOG.debug("Removal of {} plugins complete", plugins.size()); } public OBRPluginProvisionStatus getStatus() { return status; } Future<OBRPluginProvisionStatus> getFuture() { return future; } void setFuture(Future<OBRPluginProvisionStatus> future) { this.future = future; } public File getDirectory() { return directory; } }