Applied patch to improve error handling of managed bundles (FELIX-937), to
account for explicitly stopped bundles (FELIX-938), and to try to optimize
processing (FELIX-939).
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@749325 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
index 7c164cf..3eb6219 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/DirectoryWatcher.java
@@ -18,20 +18,40 @@
*/
package org.apache.felix.fileinstall;
-/**
- * -DirectoryWatcher-
- *
- * This class runs a background task that checks a directory for new files or
- * removed files. These files can be configuration files or jars.
- */
import java.io.*;
import java.util.*;
+import java.net.URISyntaxException;
+import java.net.URI;
import org.osgi.framework.*;
import org.osgi.service.cm.*;
import org.osgi.service.log.*;
import org.osgi.service.packageadmin.*;
+/**
+ * -DirectoryWatcher-
+ *
+ * This class runs a background task that checks a directory for new files or
+ * removed files. These files can be configuration files or jars.
+ * For jar files, its behavior is defined below:
+ * - If there are new jar files, it installs them and optionally starts them.
+ * - If it fails to install a jar, it does not try to install it again until
+ * the jar has been modified.
+ * - If it fail to start a bundle, it attempts to start it in following
+ * iterations until it succeeds or the corresponding jar is uninstalled.
+ * - If some jar files have been deleted, it uninstalls them.
+ * - If some jar files have been updated, it updates them.
+ * - If it fails to update a bundle, it tries to update it in following
+ * iterations until it is successful.
+ * - If any bundle gets updated or uninstalled, it refreshes the framework
+ * for the changes to take effect.
+ * - If it detects any new installations, uninstallations or updations,
+ * it tries to start all the managed bundle unless it has been configured
+ * to only install bundles.
+ *
+ * @author Peter Kriens
+ * @author Sanjeeb Sahoo
+ */
public class DirectoryWatcher extends Thread
{
final static String ALIAS_KEY = "_alias_factory_pid";
@@ -46,6 +66,13 @@
boolean startBundles = true; // by default, we start bundles.
BundleContext context;
boolean reported;
+ Map/* <String, Jar> */ currentManagedBundles = new HashMap();
+
+ // Represents jars that could not be installed
+ Map/* <String, Jar> */ installationFailures = new HashMap();
+
+ // Represents jars that could not be installed
+ Set/* <Bundle> */ startupFailures = new HashSet();
public DirectoryWatcher(Dictionary properties, BundleContext context)
{
@@ -79,17 +106,16 @@
log(DIR + " " + watchedDirectory.getAbsolutePath(), null);
log(DEBUG + " " + debug, null);
log(START_NEW_BUNDLES + " " + startBundles, null);
- Map currentManagedBundles = new HashMap(); // location -> Long(time)
+ initializeCurrentManagedBundles();
Map currentManagedConfigs = new HashMap(); // location -> Long(time)
-
while (!interrupted())
{
try
{
- Set/* <String> */ installed = new HashSet();
+ Map/* <String, Jar> */ installed = new HashMap();
Set/* <String> */ configs = new HashSet();
traverse(installed, configs, watchedDirectory);
- doInstalled(currentManagedBundles, installed);
+ doInstalled(installed);
doConfigs(currentManagedConfigs, configs);
Thread.sleep(poll);
}
@@ -276,138 +302,55 @@
}
/**
- * Install bundles that were discovered and uninstall bundles that are gone
- * from the current state.
+ * This is the core of this class.
+ * Install bundles that were discovered, uninstall bundles that are gone
+ * from the current state and update the ones that have been changed.
+ * Keep {@link #currentManagedBundles} up-to-date.
*
- * @param current
- * A map location -> path that holds the current state
* @param discovered
- * A set of paths that represent the just found bundles
+ * A map of path to {@link Jar} that holds the discovered state
*/
- void doInstalled(Map current, Set discovered)
+ void doInstalled(Map discovered)
{
- boolean refresh = false;
- Bundle bundles[] = context.getBundles();
- for (int i = 0; i < bundles.length; i++)
+ // Find out all the new, deleted and common bundles.
+ // new = discovered - current,
+ Set newBundles = new HashSet(discovered.values());
+ newBundles.removeAll(currentManagedBundles.values());
+
+ // deleted = current - discovered
+ Set deletedBundles = new HashSet(currentManagedBundles.values());
+ deletedBundles.removeAll(discovered.values());
+
+ // existing = intersection of current & discovered
+ Set existingBundles = new HashSet(discovered.values());
+ existingBundles.retainAll(currentManagedBundles.values());
+
+ // We do the operations in the following order:
+ // uninstall, update, install, refresh & start.
+ Collection uninstalledBundles = uninstall(deletedBundles);
+ Collection updatedBundles = update(existingBundles);
+ Collection installedBundles = install(newBundles);
+ if (uninstalledBundles.size() > 0 || updatedBundles.size() > 0)
{
- Bundle bundle = bundles[i];
- String location = bundle.getLocation();
- if (discovered.contains(location))
- {
- // We have a bundle that is already installed
- // so we know it
- discovered.remove(location);
-
- File file = new File(location);
-
- // Modified date does not work on the Nokia
- // for some reason, so we take size into account
- // as well.
- long newSize = file.length();
- Long oldSizeObj = (Long) current.get(location);
- long oldSize = oldSizeObj == null ? 0 : oldSizeObj.longValue();
-
- if (file.lastModified() > bundle.getLastModified() + 4000 && oldSize != newSize)
- {
- try
- {
- // We treat this as an update, it is modified,,
- // different size, and it is present in the dir
- // as well as in the list of bundles.
- current.put(location, new Long(newSize));
- InputStream in = new FileInputStream(file);
- bundle.update(in);
- refresh = true;
- in.close();
- log("Updated " + location, null);
- }
- catch (Exception e)
- {
- log("Failed to update bundle ", e);
- }
- }
-
- // Fragments can not be started. All other
- // bundles are started only if user has asked us to
- // start bundles. No need to check status of bundles
- // before starting, because OSGi treats this
- // as a noop when the bundle is already started
- if (startBundles && !isFragment(bundle))
- {
- try
- {
- bundle.start();
- }
- catch (Exception e)
- {
- log("Fail to start bundle " + location, e);
- }
- }
- }
- else
- {
- // Hmm. We found a bundle that looks like it came from our
- // watched directory but we did not find it this round.
- // Just remove it.
- if (bundle.getLocation().startsWith(watchedDirectory.getAbsolutePath()))
- {
- try
- {
- bundle.uninstall();
- refresh = true;
- log("Uninstalled " + location, null);
- }
- catch (Exception e)
- {
- log("failed to uninstall bundle: ", e);
- }
- }
- }
- }
-
- List starters = new ArrayList();
- for (Iterator it = discovered.iterator(); it.hasNext();)
- {
- try
- {
- String path = (String) it.next();
- File file = new File(path);
- InputStream in = new FileInputStream(file);
- Bundle bundle = context.installBundle(path, in);
- in.close();
- if (startBundles)
- {
- // We do not start this bundle yet. We wait after
- // refresh because this will minimize the disruption
- // as well as temporary unresolved errors.
- starters.add(bundle);
- }
- log("Installed " + file.getAbsolutePath(), null);
- }
- catch (Exception e)
- {
- log("failed to install/start bundle: ", e);
- }
- }
-
- if (refresh || starters.size() != 0)
- {
+ // Refresh if any bundle got uninstalled or updated.
+ // This can lead to restart of recently updated bundles, but
+ // don't worry about that at this point of time.
refresh();
- for (Iterator b = starters.iterator(); b.hasNext();)
- {
- Bundle bundle = (Bundle) b.next();
- if (!isFragment(bundle))
- {
- try
- {
- bundle.start();
- }
- catch (BundleException e)
- {
- log("Error while starting a newly installed bundle", e);
- }
- }
- }
+ }
+
+ // Try to start all the bundles that we could not start last time.
+ // Make a copy, because start() changes the underlying collection
+ start(new HashSet(startupFailures));
+
+ if (startBundles
+ &&
+ (uninstalledBundles.size() > 0
+ || updatedBundles.size() > 0
+ || installedBundles.size() > 0))
+ {
+ // Something has changed in the system, so
+ // try to start all the bundles.
+ startAllBundles();
}
}
@@ -461,17 +404,17 @@
}
/**
- * Traverse the directory and fill the map with the found jars and
- * configurations keyed by the abs file path.
+ * Traverse the directory and fill the set with the found jars and
+ * configurations.
*
* @param jars
- * Returns the abspath -> file for found jars
+ * Returns path -> {@link Jar} map for found jars
* @param configs
* Returns the abspath -> file for found configurations
* @param jardir
* The directory to traverse
*/
- void traverse(Set jars, Set configs, File jardir)
+ void traverse(Map/* <String, Jar> */ jars, Set configs, File jardir)
{
String list[] = jardir.list();
for (int i = 0; i < list.length; i++)
@@ -479,7 +422,8 @@
File file = new File(jardir, list[i]);
if (list[i].endsWith(".jar"))
{
- jars.add(file.getAbsolutePath());
+ Jar jar = new Jar(file);
+ jars.put(jar.getPath(), jar);
}
else if (list[i].endsWith(".cfg"))
{
@@ -570,4 +514,260 @@
// Ignore
}
}
-}
+
+ /**
+ * This method goes through all the currently installed bundles
+ * and returns information about those bundles whose location
+ * refers to a file in our {@link #watchedDirectory}.
+ */
+ private void initializeCurrentManagedBundles()
+ {
+ Bundle[] bundles = this.context.getBundles();
+ String watchedDirPath = watchedDirectory.toURI().normalize().getPath();
+ for (int i = 0; i < bundles.length; ++i)
+ {
+ try
+ {
+ final URI uri = new URI(bundles[i].getLocation());
+ if (uri.isOpaque())
+ {
+ // We can't do any meaningful processing of Opaque URI.
+ // e.g. Path component of an opaque URI is null
+ continue;
+ }
+ String location = uri.normalize().getPath();
+ final int index = location.lastIndexOf('/');
+ if (index != -1 && location.substring(0, index + 1).equals(watchedDirPath))
+ {
+ // This bundle's location matches our watched dir path
+ Jar jar = new Jar(bundles[i]);
+ currentManagedBundles.put(jar.getPath(), jar);
+ }
+ }
+ catch (URISyntaxException e)
+ {
+ // Ignore and continue.
+ // This can never happen for bundles that have been installed
+ // by FileInstall, as we always use proper filepath as location.
+ }
+ }
+ }
+
+ /**
+ * This method installs a collection of jar files.
+ * @param jars Collection of {@link Jar} to be installed
+ * @return List of Bundles just installed
+ */
+ private Collection/* <Bundle> */ install(Collection jars)
+ {
+ List bundles = new ArrayList();
+ for (Iterator iter = jars.iterator(); iter.hasNext();)
+ {
+ Jar jar = (Jar) iter.next();
+
+ Bundle bundle = install(jar);
+ if (bundle != null)
+ {
+ bundles.add(bundle);
+ }
+ }
+ return bundles;
+ }
+
+ /**
+ * @param jars Collection of {@link Jar} to be uninstalled
+ * @return Collection of Bundles that got uninstalled
+ */
+ private Collection/* <Bundle> */ uninstall(Collection jars)
+ {
+ List bundles = new ArrayList();
+ for (Iterator iter = jars.iterator(); iter.hasNext();)
+ {
+ final Jar jar = (Jar) iter.next();
+ Bundle b = uninstall(jar);
+ if (b != null)
+ {
+ bundles.add(b);
+ }
+ }
+ return bundles;
+ }
+
+ private void start(Collection bundles)
+ {
+ for (Iterator b = bundles.iterator(); b.hasNext();)
+ {
+ start((Bundle) b.next());
+ }
+ }
+
+ /**
+ * Update the bundles if the underlying files have changed.
+ * This method reads the information about jars to be updated,
+ * compares them with information available in {@link #currentManagedBundles}.
+ * If the file is newer, it updates the bundle.
+ *
+ * @param jars Collection of {@link Jar}s representing state of files.
+ * @return Collection of bundles that got updated
+ */
+ private Collection/* <Bundle> */ update(Collection jars)
+ {
+ List bundles = new ArrayList();
+ for (Iterator iter = jars.iterator(); iter.hasNext();)
+ {
+ Jar e = (Jar) iter.next();
+ Jar c = (Jar) currentManagedBundles.get(e.getPath());
+ if (e.isNewer(c))
+ {
+ Bundle b = update(c);
+ if (b != null)
+ {
+ bundles.add(b);
+ }
+ }
+ }
+ return bundles;
+ }
+
+ /**
+ * Install a jar and return the bundle object.
+ * It uses {@link org.apache.felix.fileinstall.Jar#getPath()} as location
+ * of the new bundle. Before installing a file,
+ * it sees if the file has been identified as a bad file in
+ * earlier run. If yes, then it compares to see if the file has changed
+ * since then. It installs the file if the file has changed.
+ * If the file has not been identified as a bad file in earlier run,
+ * then it always installs it.
+ *
+ * @param jar the jar to be installed
+ * @return Bundle object that was installed
+ */
+ private Bundle install(Jar jar)
+ {
+ Bundle bundle = null;
+ try
+ {
+ String path = jar.getPath();
+ Jar badJar = (Jar) installationFailures.get(jar.getPath());
+ if (badJar != null && badJar.getLastModified() == jar.getLastModified())
+ {
+ return null; // Don't attempt to install it; nothing has changed.
+ }
+ File file = new File(path);
+ InputStream in = new FileInputStream(file);
+ try
+ {
+ bundle = context.installBundle(path, in);
+ }
+ finally
+ {
+ in.close();
+ }
+ installationFailures.remove(path);
+ currentManagedBundles.put(path, new Jar(bundle));
+ log("Installed " + file.getAbsolutePath(), null);
+ }
+ catch (Exception e)
+ {
+ log("Failed to install bundle: " + jar.getPath(), e);
+
+ // Add it our bad jars list, so that we don't
+ // attempt to install it again and again until the underlying
+ // jar has been modified.
+ installationFailures.put(jar.getPath(), jar);
+ }
+ return bundle;
+ }
+
+ /**
+ * Uninstall a jar file.
+ */
+ private Bundle uninstall(Jar jar)
+ {
+ try
+ {
+ Jar old = (Jar) currentManagedBundles.remove(jar.getPath());
+
+ // old can't be null because of the way we calculate deleted list.
+ Bundle bundle = context.getBundle(old.getBundleId());
+ bundle.uninstall();
+ startupFailures.remove(bundle);
+ log("Uninstalled " + jar.getPath(), null);
+ return bundle;
+ }
+ catch (Exception e)
+ {
+ log("Failed to uninstall bundle: " + jar.getPath(), e);
+ }
+ return null;
+ }
+
+ private Bundle update(Jar jar)
+ {
+ InputStream in = null;
+ try
+ {
+ File file = new File(jar.getPath());
+ in = new FileInputStream(file);
+ Bundle bundle = context.getBundle(jar.getBundleId());
+ bundle.update(in);
+ startupFailures.remove(bundle);
+ jar.setLastModified(bundle.getLastModified());
+ jar.setLength(file.length());
+ log("Updated " + jar.getPath(), null);
+ return bundle;
+ }
+ catch (Exception e)
+ {
+ log("Failed to update bundle " + jar.getPath(), e);
+ }
+ finally
+ {
+ if (in != null)
+ {
+ try
+ {
+ in.close();
+ }
+ catch (IOException e)
+ {
+ }
+ }
+ }
+ return null;
+ }
+
+ private void start(Bundle bundle)
+ {
+ // Fragments can not be started.
+ // No need to check status of bundles
+ // before starting, because OSGi treats this
+ // as a noop when the bundle is already started
+ if (!isFragment(bundle))
+ {
+ try
+ {
+ bundle.start();
+ startupFailures.remove(bundle);
+ }
+ catch (BundleException e)
+ {
+ log("Error while starting bundle: " + bundle.getLocation(), e);
+ startupFailures.add(bundle);
+ }
+ }
+ }
+
+ /**
+ * Start all bundles that we are currently managing.
+ */
+ private void startAllBundles()
+ {
+ for (Iterator jars = currentManagedBundles.values().iterator(); jars.hasNext();)
+ {
+ Jar jar = (Jar) jars.next();
+ Bundle bundle = context.getBundle(jar.getBundleId());
+ start(bundle);
+ }
+ }
+}
\ No newline at end of file
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/Jar.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/Jar.java
new file mode 100644
index 0000000..bca2019
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/Jar.java
@@ -0,0 +1,114 @@
+/*
+ * 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.fileinstall;
+
+import org.osgi.framework.Bundle;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * This class is used to cache vital information of a jar file
+ * that is used during later processing. It also overrides hashCode and
+ * equals methods so that it can be used in various Set operations.
+ * It uses file's path as the primary key. Before
+ *
+ * @author Sanjeeb.Sahoo@Sun.COM
+ */
+class Jar
+{
+ private String path;
+ private long length = -1;
+ private long lastModified = -1;
+ private long bundleId = -1;
+
+ Jar(File file)
+ {
+ // Convert to a URI because the location of a bundle
+ // is typically a URI. At least, that's the case for
+ // autostart bundles.
+ // Normalisation is needed to ensure that we don't treat (e.g.)
+ // /tmp/foo and /tmp//foo differently.
+ path = file.toURI().normalize().getPath();
+ lastModified = file.lastModified();
+ length = file.length();
+ }
+
+ Jar(Bundle b) throws URISyntaxException
+ {
+ // Normalisation is needed to ensure that we don't treat (e.g.)
+ // /tmp/foo and /tmp//foo differently.
+ URI uri = new URI(b.getLocation()).normalize();
+ path = uri.getPath();
+ lastModified = b.getLastModified();
+ bundleId = b.getBundleId();
+ }
+
+ public String getPath()
+ {
+ return path;
+ }
+
+ public long getLastModified()
+ {
+ return lastModified;
+ }
+
+ public void setLastModified(long lastModified)
+ {
+ this.lastModified = lastModified;
+ }
+
+ public long getLength()
+ {
+ return length;
+ }
+
+ public void setLength(long length)
+ {
+ this.length = length;
+ }
+
+ public long getBundleId()
+ {
+ return bundleId;
+ }
+
+ public boolean isNewer(Jar other)
+ {
+ return (getLastModified() > other.getLastModified());
+ }
+
+ // Override hashCode and equals as this object is used in Set
+ public int hashCode()
+ {
+ return path.hashCode();
+ }
+
+ public boolean equals(Object obj)
+ {
+ if (obj instanceof Jar)
+ {
+ return this.path.equals(((Jar) obj).path);
+ }
+ return false;
+ }
+}
\ No newline at end of file