The OSGi specification states that the framework must cache bundles and their run-time state, but it does not explicitly define how this should be done. As a result, each OSGi framework implementation is likely to cache bundles differently. This document describes in detail how Oscar handles bundle caching by default and also illustrates the mechanisms Oscar provides to modify this default behavior.
Oscar creates a local cache directory, called .oscar, in the home directory of the user; the location of the user's home directory varies depending on the operating system. Oscar does not directly cache bundles into the local cache directory, since this would only allow one set of bundles to be installed for each user. Instead, Oscar introduces the notion of a profile, which is an arbitrarily name given to a set of installed bundles. When starting Oscar from the command-line, the user is prompted for a profile name. Oscar creates a sub-directory in .oscar named after the profile. All bundle information is then cached inside of the profile directory. The benefit of this approach is that the user is able to have different profiles for different purposes, such as ones for debugging, testing, or experimenting.
The structure of a profile directory is reasonably simple, it contains a directory for each bundle, where the directory name corresponds to the bundle identifier number. Each bundle directory contains a file for the bundle's location, start level, state, and a directory containing the bundle bundle JAR file and any extracted embedded JAR files or native libraries if any exist. As an example, the profile directory for the simple.jar example bundle looks like this:
~/.oscar/ example/ bundle4/ bundle.location bundle.startlevel bundle.state data/ version0.0/ bundle.jar embedded/ embedded.jar lib/ libfoo.so
The above directory structure indicates that the simple.jar bundle is in the profile named "example" and that "4" is its bundle identifier. Additionally, besides the bundle JAR file, the bundle has one embedded JAR file and one native library. The naming convention is rather straightforward and consistent. All bundle directories will follow the naming pattern displayed above, except for the names of the embedded JAR files and native libraries. Embedded JAR files and native libraries use the names specified in the bundle manifest and may also include sub-directories. The naming convention for the directory containing the bundle JAR file, version0.0, requires further explanation.
The bundle JAR directory uses a numbering scheme to keep track of the number of times a bundle is updated and the number of times it is refreshed; the name of the directory maps like this: version<refresh-count>.<revision-count>. The reasoning behind this is tricky. It is necessary to keep track of the revision count because the OSGi specification requires when a bundle is updated that the update takes effect immediately. However, it also requires that old packages from older revisions of the updated bundle are kept available until a refresh of the framework is performed. As a result, it is possible for multiple revisions of a bundle JAR to be providing packages at a given time. For example, if a bundle provides package foo and it is updated and now provides packages foo and bar, then after the update foo will be supplied from the older revision and bar will be supplied from the newer revision. This is possible for any number of updates, thus the bundle JAR directory must keep around each revision until a refresh is performed. Such "revision directories" are generally only run-time directories and are removed when the framework is shutdown or refreshed, they are not intended to exist for multiple sessions of execution. To illustrate, upon initial installation, the bundle JAR file is placed into a revision directory named revision0.0. When an update is performed, the updated bundle JAR file is placed in a directory named revision0.1. If another update is performed without a refresh of the framework, the newer revision will be placed into a directory named revision0.2 and so on.
When the framework is refreshed or shutdown, all revision directories are purged from the bundle cache and only the most recent revision directory is maintained. Simply purging the old revision directories may appear adequate for refreshing the framework, but it is not due to how the JVM handles native libraries. When a native library is loaded it is associated with a specific class loader; no other class loader can load the same native library. The uniqueness of a native library is determined by its absolute path in the file system. Consequently, when the framework is refreshed, it is necessary to recreate the class loaders for all refreshed bundles. If a refreshed bundle has a native library, then this would result in an exception since the native library is still associated with the prior class loader; the refresh counter remedies this situation. After purging all old revision directories, the current revision directory is renamed based on the current refresh count. By renaming the directory, it is possible to re-load the native library since its path in the file system has changed. The old class loaders and native libraries will eventually be garbage collected. The current refresh count is stored in a file, called refresh.counter, in the bundle's directory. To illustrate, if a bundle was updated and has two revision directories, revision0.0 and revision0.1, after a refresh the older revision directory will be deleted and the newest revision directory will be renamed to revision1.0. If the bundle is refresh again, its revision directory will become revision2.0 and so on. Note: Bundles may be refreshed when they are updated or when they depend on other bundles that have been updated; either way it is necessary to increment the refresh count and rename the revision directory.
It is possible to modify the default behavior of Oscar's bundle cache by setting certain system properties; see the usage document for information on how to set system properties for Oscar. The following properties are related to bundle caching in Oscar:
Older verions of Oscar (prior to version 1.0.0) used a different bundle cache structure. For the most part, the new structure just re-arranges the old structure, but there was one change that is not simply a rearrangement. Previous versions of Oscar saved some data as binary, but current versions of Oscar saves data as plaing text, so that it is more easily editable. In particular, the bundle location, state, start level, and refresh counter are all now saved as plain text. Unforunately, this makes editing by hand more difficult, but not impossible. The following Java class will convert from pre-1.0.0 Oscar profiles to post-1.0.0 Oscar profiles (definitely make a back-up of your cache directory before running this program):
package converter; import java.io.*; import org.osgi.framework.Bundle; public class Convert { public static void main(String[] argv) throws Exception { // This assumes that we are converting the .oscar directory. String home = System.getProperty("user.home"); File cacheDir = new File(home, ".oscar"); // Get all of the profile directories and convert each one. File[] children = cacheDir.listFiles(); for (int i = 0; i < children.length; i++) { System.out.println("Converting: " + children[i].getName()); convertProfileDirectory(children[i]); } } public static void convertProfileDirectory(File profileDir) throws IOException { // There is no longer a "bundles" directory, so we want // to remove it, but first convert all bundle directories // and then move them up into the profile directory. File bundlesDir = new File(profileDir, "bundles"); File[] children = bundlesDir.listFiles(); for (int i = 0; i < children.length; i++) { convertBundleDirectory(children[i]); children[i].renameTo(new File(profileDir, children[i].getName())); } // Now that all bundle directories are moved, // delete the bundles directory. bundlesDir.delete(); } public static void convertBundleDirectory(File bundleDir) throws IOException { File file = null; // Convert location file to plain text. file = new File(bundleDir, "bundle.location"); if (file.exists()) { String s = readString(file); writeString(file, s); } // Convert state file to plain text. file = new File(bundleDir, "bundle.state"); if (file.exists()) { int i = readInteger(file); writeString(file, (i == Bundle.ACTIVE) ? "active" : "installed"); } // Convert start level file to plain text. file = new File(bundleDir, "bundle.startlevel"); if (file.exists()) { int i = readInteger(file); writeString(file, Integer.toString(i)); } // Delete old update counter file; it is not necessary to // recreate this since it will be done automatically the // next time the bundle is updated. file = new File(bundleDir, "update.counter"); if (file.exists()) { file.delete(); } // Create a revision directory for the current bundle JAR file. File revisionDir = new File(bundleDir, "version0.0"); revisionDir.mkdir(); // Move bundle JAR file into revision directory. file = new File(bundleDir, "bundle.jar"); file.renameTo(new File(revisionDir, "bundle.jar")); // Move embedded JAR directory into revision directory. file = new File(bundleDir, "embedded"); file.renameTo(new File(revisionDir, "embedded")); // Find the native library directory and move it to the // revision directory and rename it. File[] children = bundleDir.listFiles(); for (int i = 0; i < children.length; i++) { if (children[i].getName().startsWith("lib")) { children[i].renameTo(new File(revisionDir, "lib")); break; } } } public static void writeString(File file, String s) throws IOException { FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(s, 0, s.length()); } finally { if (bw != null) bw.close(); if (fw != null) fw.close(); } } public static String readString(File file) throws IOException { FileInputStream fis = null; DataInputStream dis = null; try { fis = new FileInputStream(file); dis = new DataInputStream(fis); return dis.readUTF(); } finally { if (fis != null) fis.close(); if (dis != null) fis.close(); } } public static int readInteger(File file) throws IOException { FileInputStream fis = null; DataInputStream dis = null; try { fis = new FileInputStream(file); dis = new DataInputStream(fis); return dis.readInt(); } finally { if (fis != null) fis.close(); if (dis != null) fis.close(); } } public static long readLong(File file) throws IOException { FileInputStream fis = null; DataInputStream dis = null; try { fis = new FileInputStream(file); dis = new DataInputStream(fis); return dis.readLong(); } finally { if (fis != null) fis.close(); if (dis != null) fis.close(); } } }
This program requires the osgi.jar to compile and run. This program was not extensively tested, but it did work under Linux on my own bundle cache directory.
If you have comments or suggestions, feel free to contact me at heavy@ungoverned.org
Richard S. Hall