/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.modular;
import static com.google_voltpatches.common.base.Preconditions.checkNotNull;
import static com.google_voltpatches.common.base.Predicates.equalTo;
import static com.google_voltpatches.common.base.Predicates.not;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import org.voltcore.logging.VoltLogger;
import com.google_voltpatches.common.base.Function;
import com.google_voltpatches.common.base.Joiner;
import com.google_voltpatches.common.base.Predicate;
import com.google_voltpatches.common.collect.FluentIterable;
import com.google_voltpatches.common.collect.ImmutableList;
import com.google_voltpatches.common.collect.ImmutableMap;
import com.google_voltpatches.common.collect.ImmutableSortedMap;
import com.google_voltpatches.common.collect.Maps;
/**
* Singleton Wrapper around OSGi module loading and unloading operations.
*/
public class ModuleManager {
/*
* Note for developers: please keep list in alpha-numerical order. Exclude ; and use only package names.
*/
final static List<String> SYSTEM_PACKAGES = ImmutableList.<String>builder()
.add("com.google_voltpatches.common.base;")
.add("com.google_voltpatches.common.collect;")
.add("com.google_voltpatches.common.io;")
.add("com.google_voltpatches.common.net;")
.add("com.google_voltpatches.common.util.concurrent;")
.add("com.yammer.metrics;")
.add("com.yammer.metrics.core;")
.add("com.yammer.metrics.reporting;")
.add("com.yammer.metrics.stats;")
.add("com.yammer.metrics.util;")
.add("jsr166y;")
.add("org.apache.log4j;")
.add("org.slf4j;")
.add("org.voltcore.network;")
.add("org.voltcore.logging;")
.add("org.voltcore.utils;")
.add("org.voltdb;include:=\"VoltType\",")
.add("org.voltdb.client;")
.add("org.voltdb.common;exclude=\"Permission\",")
.add("org.voltdb.importer;")
.add("org.voltdb.importer.formatter;")
.add("org.voltdb.types;")
.build();
private static final VoltLogger LOG = new VoltLogger("HOST");
private final static Joiner COMMA_JOINER = Joiner.on(",").skipNulls();
private final static AtomicReference<File> CACHE_ROOT = new AtomicReference<>();
private static ModuleManager m_self = null;
public static void initializeCacheRoot(File cacheRoot) {
if (CACHE_ROOT.compareAndSet(null, checkNotNull(cacheRoot))) {
if (!cacheRoot.exists() && !cacheRoot.mkdirs()) {
throw new SetUpException("Failed to create required OSGI cache directory: " + cacheRoot.getAbsolutePath());
}
if ( !cacheRoot.isDirectory()
|| !cacheRoot.canRead()
|| !cacheRoot.canWrite()
|| !cacheRoot.canExecute())
{
throw new SetUpException("Cannot access OSGI cache directory: " + cacheRoot.getAbsolutePath());
}
m_self = new ModuleManager(cacheRoot);
}
}
public static void resetCacheRoot() {
File cacheRoot = CACHE_ROOT.get();
if (cacheRoot != null && CACHE_ROOT.compareAndSet(cacheRoot, null)) {
try {
m_self.m_framework.stop();
} catch (BundleException bex) {
//Ignore
}
m_self = null;
}
}
private final static Function<String,String> appendVersion = new Function<String, String>() {
@Override
public String apply(String input) {
return input + "version=1.0.0";
}
};
public static ModuleManager instance() {
return m_self;
}
static ModularException loggedModularException(Throwable e, String msg, Object...args) {
ModularException.isCauseFor(e).map(me -> { throw me; });
LOG.error(String.format(msg, args), e);
return new ModularException(msg, e, args);
}
public static URI bundleURI(File fl) {
return fl.toPath().toUri();
}
private final Framework m_framework;
private final BundleRef m_bundles;
private ModuleManager(File cacheRoot) {
String systemPackagesSpec = FluentIterable
.from(SYSTEM_PACKAGES)
.transform(appendVersion)
.join(COMMA_JOINER);
Map<String, String> frameworkProps = ImmutableMap.<String,String>builder()
.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, systemPackagesSpec)
.put("org.osgi.framework.storage.clean", "onFirstInit")
.put("felix.cache.rootdir", cacheRoot.getAbsolutePath())
.put("felix.cache.locking", Boolean.FALSE.toString())
.build();
LOG.info("Framework properties are: " + frameworkProps);
FrameworkFactory frameworkFactory = ServiceLoader
.load(FrameworkFactory.class)
.iterator()
.next();
m_framework = frameworkFactory.newFramework(frameworkProps);
try {
m_framework.start();
} catch (BundleException e) {
LOG.error("Failed to start the felix OSGi framework", e);
throw new SetUpException("Failed to start the felix OSGi framework", e);
}
m_bundles = new BundleRef(m_framework);
}
/**
* Gets the service from the given bundle jar uri. Loads and starts the bundle
* if it isn't yet loaded
*
* @param bundleURI bundle jar URI
* @param svcClazz the service class exposed by the bundle jar
* @return a reference to an instance of the service class
*/
public <T> T getService(URI bundleURI, Class<T> svcClazz) {
return m_bundles.getService(bundleURI, svcClazz);
}
public void unload(URI bundleURI) {
m_bundles.stopBundle(bundleURI);
}
public void unload(Set<URI> uris) {
m_bundles.stopBundles(uris);
}
public void uninstall(URI bundleURI) {
m_bundles.uninstallBundle(bundleURI);
}
public void uninstall(Set<URI> uris) {
m_bundles.uninstallBundles(uris);
}
public static class SetUpException extends RuntimeException {
private static final long serialVersionUID = 8197183357774453653L;
public SetUpException() {
}
public SetUpException(String message, Throwable cause) {
super(message, cause);
}
public SetUpException(String message) {
super(message);
}
public SetUpException(Throwable cause) {
super(cause);
}
}
static class BundleRef extends AtomicReference<NavigableMap<URI,Bundle>> {
private static final long serialVersionUID = -3691039780541403034L;
static NavigableMap<URI,Bundle> EMPTY_MAP = ImmutableSortedMap.of();
final Framework m_framework;
public BundleRef(Framework framework, NavigableMap<URI,Bundle> initialRef) {
super(initialRef);
m_framework = framework;
}
public BundleRef(Framework framework) {
this(framework, EMPTY_MAP);
}
private Bundle startBundle(URI bundleURI) {
NavigableMap<URI,Bundle> expect, update;
Bundle bundle = null;
do {
expect = get();
if (expect.containsKey(bundleURI)) break;
BundleContext ctx = m_framework.getBundleContext();
bundle = ctx.getBundle(bundleURI.toASCIIString());
if (bundle != null) {
try {
bundle.update();
} catch (BundleException e) {
String msg = e.getMessage();
throw loggedModularException(e, "Unable to update bundle %s. %s", bundleURI, msg);
} catch (Throwable t) {
throw loggedModularException(t, "Unable to update bundle %s", bundleURI);
}
} else {
try {
bundle = ctx.installBundle(bundleURI.toASCIIString());
} catch (BundleException e) {
String msg = e.getMessage();
throw loggedModularException(e, "Unable to install bundle %s. %s", bundleURI, msg);
} catch (Throwable t) {
throw loggedModularException(t, "Unable to instal bundle %s", bundleURI);
}
}
try {
bundle.start();
} catch (BundleException e) {
String msg = e.getMessage();
throw loggedModularException(e, "Unable to start bundle %s. %s", bundleURI, msg);
} catch (Throwable t) {
throw loggedModularException(t, "Unable to start bundle %s", bundleURI);
}
update = ImmutableSortedMap.<URI,Bundle>naturalOrder()
.putAll(expect)
.put(bundleURI, bundle)
.build();
} while (!compareAndSet(expect, update));
return get().get(bundleURI);
}
<T> T getService(URI bundleURI, Class<T> svcClazz) {
Bundle bundle = get().get(bundleURI);
if (bundle == null) {
synchronized(this) {
bundle = startBundle(bundleURI);
}
}
BundleContext ctx = bundle.getBundleContext();
for (ServiceReference<?> ref: bundle.getRegisteredServices()) {
if (ref.isAssignableTo(bundle, svcClazz.getName())) {
return svcClazz.cast(ctx.getService(ref));
}
}
return null;
}
Optional<Bundle> stopBundle(URI bundleURI) {
NavigableMap<URI,Bundle> expect, update;
do {
expect = get();
update = ImmutableSortedMap.<URI,Bundle>naturalOrder()
.putAll(Maps.filterKeys(expect, not(equalTo(bundleURI))))
.build();
} while (expect.containsKey(bundleURI) && !compareAndSet(expect, update));
Bundle bundle = expect.get(bundleURI);
if (bundle != null) {
try {
bundle.stop();
} catch (BundleException e) {
throw loggedModularException(e, "Failed to stop bundle %s", bundleURI);
}
}
return Optional.ofNullable(bundle);
}
void uninstallBundle(URI bundleURI) {
stopBundle(bundleURI).ifPresent( (Bundle b) -> {
try {
b.uninstall();
} catch (Throwable t) {
throw loggedModularException(t, "Failed to uninstall %s", b.getLocation());
}
});
}
NavigableMap<URI, Bundle> stopBundles(Set<URI> bundles) {
NavigableMap<URI,Bundle> expect, update;
do {
expect = get();
update = ImmutableSortedMap.<URI,Bundle>naturalOrder()
.putAll(Maps.filterKeys(expect, not(in(bundles))))
.build();
} while (!compareAndSet(expect, update));
List<URI> couldNotStop = new ArrayList<>();
NavigableMap<URI,Bundle> stopped = Maps.filterKeys(expect,in(bundles));
for (Map.Entry<URI,Bundle> e: stopped.entrySet()) {
URI bundleURI = e.getKey();
Bundle bundle = e.getValue();
try {
bundle.stop();
} catch (BundleException exc) {
LOG.error("Failed to stop bundle " + bundleURI, exc);
couldNotStop.add(bundleURI);
}
}
if (!couldNotStop.isEmpty()) {
throw new ModularException("Failed to stop bundles %s", couldNotStop);
}
return stopped;
}
void uninstallBundles(Set<URI> bundles) {
List<URI> couldNotUninstall = new ArrayList<>();
for (Map.Entry<URI,Bundle> e: stopBundles(bundles).entrySet()) {
URI bundleURI = e.getKey();
Bundle bundle = e.getValue();
try {
bundle.uninstall();
} catch (BundleException exc) {
LOG.error("Failed to uninstall bundle " + bundleURI, exc);
couldNotUninstall.add(bundleURI);
}
if (!couldNotUninstall.isEmpty()) {
throw new ModularException("Failed to uninstall bundles %s", couldNotUninstall);
}
}
}
}
public final static <T> Predicate<T> in(final Set<T> set) {
return new Predicate<T>() {
@Override
public boolean apply(T m) {
return set.contains(m);
}
};
}
}