blob: b7bab7cd1e5b59780c5fe1793f8dc211deb466c6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.felix.configurator.impl;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.felix.configurator.impl.json.BinUtil;
import org.apache.felix.configurator.impl.json.BinaryManager;
import org.apache.felix.configurator.impl.json.JSONUtil;
import org.apache.felix.configurator.impl.logger.SystemLogger;
import org.apache.felix.configurator.impl.model.BundleState;
import org.apache.felix.configurator.impl.model.Config;
import org.apache.felix.configurator.impl.model.ConfigList;
import org.apache.felix.configurator.impl.model.ConfigPolicy;
import org.apache.felix.configurator.impl.model.ConfigState;
import org.apache.felix.configurator.impl.model.ConfigurationFile;
import org.apache.felix.configurator.impl.model.State;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServicePermission;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.configurator.ConfiguratorConstants;
import org.osgi.util.tracker.BundleTrackerCustomizer;
/**
* The main class of the configurator.
*
*/
public class Configurator {
private final BundleContext bundleContext;
private final State state;
private final org.osgi.util.tracker.BundleTracker<Bundle> tracker;
private volatile boolean active = true;
private volatile Object coordinator;
private final WorkerQueue queue;
private final List<ServiceReference<ConfigurationAdmin>> configAdminReferences;
/**
* Create a new configurator and start it
*
* @param bc The bundle context
* @param configAdminReferences Dynamic list of references to the configuration admin service visible to the configurator
*/
public Configurator(final BundleContext bc, final List<ServiceReference<ConfigurationAdmin>> configAdminReferences) {
this.queue = new WorkerQueue();
this.bundleContext = bc;
this.configAdminReferences = configAdminReferences;
State s = null;
try {
s = State.createOrReadState(bundleContext.getDataFile(State.FILE_NAME));
} catch ( final ClassNotFoundException | IOException e ) {
SystemLogger.error("Unable to read persisted state from " + State.FILE_NAME, e);
s = new State();
}
this.state = s;
this.tracker = new org.osgi.util.tracker.BundleTracker<>(this.bundleContext,
Bundle.ACTIVE|Bundle.STARTING|Bundle.STOPPING|Bundle.RESOLVED|Bundle.INSTALLED,
new BundleTrackerCustomizer<Bundle>() {
@Override
public Bundle addingBundle(final Bundle bundle, final BundleEvent event) {
final int state = bundle.getState();
if ( active &&
(state == Bundle.ACTIVE || state == Bundle.STARTING) ) {
SystemLogger.debug("Adding bundle " + getBundleIdentity(bundle) + " : " + getBundleState(state));
queue.enqueue(new Runnable() {
@Override
public void run() {
if ( processAddBundle(bundle) ) {
process();
}
}
});
}
return bundle;
}
@Override
public void modifiedBundle(final Bundle bundle, final BundleEvent event, final Bundle object) {
this.addingBundle(bundle, event);
}
@Override
public void removedBundle(final Bundle bundle, final BundleEvent event, final Bundle object) {
final int state = bundle.getState();
if ( active && state == Bundle.UNINSTALLED ) {
SystemLogger.debug("Removing bundle " + getBundleIdentity(bundle) + " : " + getBundleState(state));
queue.enqueue(new Runnable() {
@Override
public void run() {
try {
if ( processRemoveBundle(bundle.getBundleId()) ) {
process();
}
} catch ( final IllegalStateException ise) {
SystemLogger.error("Error processing bundle " + getBundleIdentity(bundle), ise);
}
}
});
}
}
});
}
public void configAdminAdded() {
queue.enqueue(new Runnable() {
@Override
public void run() {
process();
}
});
}
private String getBundleIdentity(final Bundle bundle) {
if ( bundle.getSymbolicName() == null ) {
return bundle.getBundleId() + " (" + bundle.getLocation() + ")";
} else {
return bundle.getSymbolicName() + ":" + bundle.getVersion() + " (" + bundle.getBundleId() + ")";
}
}
private String getBundleState(int state) {
switch ( state ) {
case Bundle.ACTIVE : return "active";
case Bundle.INSTALLED : return "installed";
case Bundle.RESOLVED : return "resolved";
case Bundle.STARTING : return "starting";
case Bundle.STOPPING : return "stopping";
case Bundle.UNINSTALLED : return "uninstalled";
}
return String.valueOf(state);
}
/**
* Shut down the configurator
*/
public void shutdown() {
this.active = false;
this.queue.stop();
this.tracker.close();
}
/**
* Start the configurator.
*/
public void start() {
// get the directory for storing binaries
String dirPath = this.bundleContext.getProperty(ConfiguratorConstants.CONFIGURATOR_BINARIES);
if ( dirPath != null ) {
final File dir = new File(dirPath);
if ( dir.exists() && dir.isDirectory() ) {
BinUtil.binDirectory = dir;
} else if ( dir.exists() ) {
SystemLogger.error("Directory property is pointing at a file not a dir: " + dirPath + ". Using default path.");
} else {
try {
if ( dir.mkdirs() ) {
BinUtil.binDirectory = dir;
}
} catch ( final SecurityException se ) {
// ignore
}
if ( BinUtil.binDirectory == null ) {
SystemLogger.error("Unable to create a directory at: " + dirPath + ". Using default path.");
}
}
}
if ( BinUtil.binDirectory == null ) {
BinUtil.binDirectory = this.bundleContext.getDataFile("binaries" + File.separatorChar + ".check");
BinUtil.binDirectory = BinUtil.binDirectory.getParentFile();
BinUtil.binDirectory.mkdirs();
}
// before we start the tracker we process all available bundles and initial configuration
final String initial = this.bundleContext.getProperty(ConfiguratorConstants.CONFIGURATOR_INITIAL);
if ( initial == null ) {
this.processRemoveBundle(-1);
} else {
// JSON or URLs ?
final Set<String> hashes = new HashSet<>();
final Map<String, String> files = new TreeMap<>();
if ( !initial.trim().startsWith("{") ) {
// URLs
final String[] urls = initial.trim().split(",");
for(final String urlString : urls) {
URL url = null;
try {
url = new URL(urlString);
} catch (final MalformedURLException e) {
}
if ( url != null ) {
try {
final String contents = JSONUtil.getResource(urlString, url);
files.put(urlString, contents);
hashes.add(Util.getSHA256(contents.trim()));
} catch ( final IOException ioe ) {
SystemLogger.error("Unable to read " + urlString, ioe);
}
}
}
} else {
// JSON
hashes.add(Util.getSHA256(initial.trim()));
files.put(ConfiguratorConstants.CONFIGURATOR_INITIAL, initial);
}
if ( state.getInitialHashes() == null || !state.getInitialHashes().equals(hashes)) {
if ( state.getInitialHashes() != null ) {
processRemoveBundle(-1);
}
final JSONUtil.Report report = new JSONUtil.Report();
final BinaryManager converter = new BinaryManager(null, report);
final List<ConfigurationFile> allFiles = new ArrayList<>();
for(final Map.Entry<String, String> entry : files.entrySet()) {
final ConfigurationFile file = JSONUtil.readJSON(converter, entry.getKey(), null, -1, entry.getValue(), report);
if ( file != null ) {
allFiles.add(file);
}
}
for(final String w : report.warnings) {
SystemLogger.warning(w);
}
for(final String e : report.errors) {
SystemLogger.error(e);
}
final BundleState bState = new BundleState();
bState.addFiles(allFiles);
for(final String pid : bState.getPids()) {
state.addAll(pid, bState.getConfigurations(pid));
}
state.setInitialHashes(hashes);
}
}
final Bundle[] bundles = this.bundleContext.getBundles();
final Set<Long> ids = new HashSet<>();
for(final Bundle b : bundles) {
ids.add(b.getBundleId());
final int state = b.getState();
if ( state == Bundle.ACTIVE || state == Bundle.STARTING ) {
processAddBundle(b);
}
}
for(final long id : state.getKnownBundleIds()) {
if ( !ids.contains(id) ) {
processRemoveBundle(id);
}
}
this.process();
this.tracker.open();
}
public boolean processAddBundle(final Bundle bundle) {
final long bundleId = bundle.getBundleId();
final long bundleLastModified = bundle.getLastModified();
final Long lastModified = state.getLastModified(bundleId);
if ( lastModified != null && lastModified.longValue() == bundleLastModified ) {
// no changes, nothing to do
return false;
}
BundleState config = null;
try {
final Set<String> paths = Util.isConfigurerBundle(bundle, this.bundleContext.getBundle().getBundleId());
if ( paths != null ) {
final JSONUtil.Report report = new JSONUtil.Report();
config = JSONUtil.readConfigurationsFromBundle(new BinUtil.ResourceProvider() {
@Override
public String getIdentifier() {
return bundle.toString();
}
@Override
public URL getEntry(String path) {
return bundle.getEntry(path);
}
@Override
public long getBundleId() {
return bundle.getBundleId();
}
@Override
public Enumeration<URL> findEntries(String path, String filePattern) {
return bundle.findEntries(path, filePattern, false);
}
}, paths, report);
for(final String w : report.warnings) {
SystemLogger.warning(w);
}
for(final String e : report.errors) {
SystemLogger.error(e);
}
}
} catch ( final IllegalStateException ise) {
SystemLogger.error("Error processing bundle " + getBundleIdentity(bundle), ise);
}
if ( lastModified != null ) {
processRemoveBundle(bundleId);
}
if ( config != null ) {
for(final String pid : config.getPids()) {
state.addAll(pid, config.getConfigurations(pid));
}
state.setLastModified(bundleId, bundleLastModified);
return true;
}
return lastModified != null;
}
public boolean processRemoveBundle(final long bundleId) {
if ( state.getLastModified(bundleId) != null ) {
state.removeLastModified(bundleId);
for(final String pid : state.getPids()) {
final ConfigList configList = state.getConfigurations(pid);
configList.uninstall(bundleId);
}
return true;
}
return false;
}
/**
* Set or unset the coordinator service
* @param coordinator The coordinator service or {@code null}
*/
public void setCoordinator(final Object coordinator) {
this.coordinator = coordinator;
}
/**
* Process the state to activate/deactivate configurations
*/
public void process() {
final Object localCoordinator = this.coordinator;
Object coordination = null;
if ( localCoordinator != null ) {
coordination = CoordinatorUtil.getCoordination(localCoordinator);
}
boolean retry = false;
try {
for(final String pid : state.getPids()) {
final ConfigList configList = state.getConfigurations(pid);
if ( configList.hasChanges() ) {
if ( process(configList) ) {
try {
State.writeState(this.bundleContext.getDataFile(State.FILE_NAME), state);
} catch ( final IOException ioe) {
SystemLogger.error("Unable to persist state to " + State.FILE_NAME, ioe);
}
} else {
retry = true;
}
}
}
} finally {
if ( coordination != null ) {
CoordinatorUtil.endCoordination(coordination);
}
}
if ( !retry ) {
// check whether there is a stale config admin bundle id
boolean changed = false;
for(final Long bundleId : this.state.getBundleIdsUsingConfigAdmin()) {
if ( this.state.getLastModified(bundleId) == null ) {
this.state.removeConfigAdminBundleId(bundleId);
changed = true;
}
}
if ( changed ) {
try {
State.writeState(this.bundleContext.getDataFile(State.FILE_NAME), state);
} catch ( final IOException ioe) {
SystemLogger.error("Unable to persist state to " + State.FILE_NAME, ioe);
}
}
}
}
/**
* Process changes to a pid.
* @param configList The config list
* @return {@code true} if the change has been processed, {@code false} if a retry is required
*/
public boolean process(final ConfigList configList) {
Config toActivate = null;
Config toDeactivate = null;
for(final Config cfg : configList) {
switch ( cfg.getState() ) {
case INSTALL : // activate if first found
if ( toActivate == null ) {
toActivate = cfg;
}
break;
case IGNORED : // same as installed
case INSTALLED : // check if we have to uninstall
if ( toActivate == null ) {
toActivate = cfg;
} else {
cfg.setState(ConfigState.INSTALL);
}
break;
case UNINSTALL : // deactivate if first found (we should only find one anyway)
if ( toDeactivate == null ) {
toDeactivate = cfg;
}
break;
case UNINSTALLED : // nothing to do
break;
}
}
// if there is a configuration to activate, we can directly activate it
// without deactivating (reducing the changes of the configuration from two
// to one)
boolean noRetryNeeded = true;
if ( toActivate != null && toActivate.getState() == ConfigState.INSTALL ) {
noRetryNeeded = activate(configList, toActivate);
}
if ( toActivate == null && toDeactivate != null ) {
noRetryNeeded = deactivate(configList, toDeactivate);
}
if ( noRetryNeeded ) {
// remove all uninstall(ed) configurations
final Iterator<Config> iter = configList.iterator();
boolean foundInstalled = false;
while ( iter.hasNext() ) {
final Config cfg = iter.next();
if ( cfg.getState() == ConfigState.UNINSTALL || cfg.getState() == ConfigState.UNINSTALLED ) {
if ( cfg.getFiles() != null ) {
for(final File f : cfg.getFiles()) {
f.delete();
}
}
iter.remove();
} else if ( cfg.getState() == ConfigState.INSTALLED ) {
if ( foundInstalled ) {
cfg.setState(ConfigState.INSTALL);
} else {
foundInstalled = true;
}
}
}
// mark as processed
configList.setHasChanges(false);
}
return noRetryNeeded;
}
private ConfigurationAdmin getConfigurationAdmin(final long configAdminServiceBundleId) {
ServiceReference<ConfigurationAdmin> ref = null;
synchronized ( this.configAdminReferences ) {
for(final ServiceReference<ConfigurationAdmin> r : this.configAdminReferences ) {
final Bundle bundle = r.getBundle();
if ( bundle != null && bundle.getBundleId() == configAdminServiceBundleId) {
ref = r;
break;
}
}
}
if ( ref != null ) {
return this.bundleContext.getService(ref);
}
return null;
}
/**
* Try to activate a configuration
* Check policy and change count
* @param configList The configuration list
* @param cfg The configuration to activate
* @return {@code true} if activation was successful
*/
public boolean activate(final ConfigList configList, final Config cfg) {
// check for configuration admin
Long configAdminServiceBundleId = this.state.getConfigAdminBundleId(cfg.getBundleId());
if ( configAdminServiceBundleId == null ) {
final Bundle configBundle = cfg.getBundleId() == -1 ? this.bundleContext.getBundle() : this.bundleContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).getBundleContext().getBundle(cfg.getBundleId());
// we check the state again, just to be sure (to avoid race conditions)
if ( configBundle != null
&& (configBundle.getState() == Bundle.STARTING || configBundle.getState() == Bundle.ACTIVE)) {
if ( System.getSecurityManager() == null
|| configBundle.hasPermission( new ServicePermission(ConfigurationAdmin.class.getName(), ServicePermission.GET)) ) {
try {
final BundleContext ctx = configBundle.getBundleContext();
if ( ctx != null ) {
final Collection<ServiceReference<ConfigurationAdmin>> refs = ctx.getServiceReferences(ConfigurationAdmin.class, null);
final List<ServiceReference<ConfigurationAdmin>> sortedRefs = new ArrayList<>(refs);
Collections.sort(sortedRefs);
for(int i=sortedRefs.size();i>0;i--) {
final ServiceReference<ConfigurationAdmin> r = sortedRefs.get(i-1);
synchronized ( this.configAdminReferences ) {
if ( this.configAdminReferences.contains(r) ) {
configAdminServiceBundleId = r.getBundle().getBundleId();
break;
}
}
}
}
} catch ( final IllegalStateException e) {
// this might happen if the config admin bundle gets deactivated while we use it
// we can ignore this and retry later on
} catch (final InvalidSyntaxException e) {
// this can never happen as we pass {@code null} as the filter
}
}
}
}
if ( configAdminServiceBundleId == null ) {
// no configuration admin found, we have to retry
return false;
}
final ConfigurationAdmin configAdmin = this.getConfigurationAdmin(configAdminServiceBundleId);
if ( configAdmin == null ) {
// getting configuration admin failed, we have to retry
return false;
}
this.state.setConfigAdminBundleId(cfg.getBundleId(), configAdminServiceBundleId);
boolean ignore = false;
try {
// get existing configuration - if any
boolean update = false;
Configuration configuration = ConfigUtil.getOrCreateConfiguration(configAdmin, cfg.getPid(), false);
if ( configuration == null ) {
// new configuration
configuration = ConfigUtil.getOrCreateConfiguration(configAdmin, cfg.getPid(), true);
update = true;
} else {
if ( cfg.getPolicy() == ConfigPolicy.FORCE ) {
update = true;
} else {
if ( configList.getLastInstalled() == null
|| configList.getChangeCount() != configuration.getChangeCount() ) {
ignore = true;
} else {
update = true;
}
}
}
if ( update ) {
configuration.updateIfDifferent(cfg.getProperties());
cfg.setState(ConfigState.INSTALLED);
configList.setChangeCount(configuration.getChangeCount());
configList.setLastInstalled(cfg);
}
} catch (final InvalidSyntaxException | IOException e) {
SystemLogger.error("Unable to update configuration " + cfg.getPid() + " : " + e.getMessage(), e);
ignore = true;
}
if ( ignore ) {
cfg.setState(ConfigState.IGNORED);
configList.setChangeCount(-1);
configList.setLastInstalled(null);
}
return true;
}
/**
* Try to deactivate a configuration
* Check policy and change count
* @param cfg The configuration
*/
public boolean deactivate(final ConfigList configList, final Config cfg) {
final Long configAdminServiceBundleId = this.state.getConfigAdminBundleId(cfg.getBundleId());
// check if configuration admin bundle is still available
// if not or if we didn't record anything, we consider the configuration uninstalled
final Bundle configBundle = configAdminServiceBundleId == null ? null : this.bundleContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).getBundleContext().getBundle(configAdminServiceBundleId);
if ( configBundle != null ) {
final ConfigurationAdmin configAdmin = this.getConfigurationAdmin(configAdminServiceBundleId);
if ( configAdmin == null ) {
// getting configuration admin failed, we have to retry
return false;
}
try {
final Configuration c = ConfigUtil.getOrCreateConfiguration(configAdmin, cfg.getPid(), false);
if ( c != null ) {
if ( cfg.getPolicy() == ConfigPolicy.FORCE
|| configList.getChangeCount() == c.getChangeCount() ) {
c.delete();
}
}
} catch (final InvalidSyntaxException | IOException e) {
SystemLogger.error("Unable to remove configuration " + cfg.getPid() + " : " + e.getMessage(), e);
}
}
cfg.setState(ConfigState.UNINSTALLED);
configList.setChangeCount(-1);
configList.setLastInstalled(null);
return true;
}
}