blob: c9ff19f87710d34ad0f9eb0b596bbd863b5587e6 [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.netbeans.core.startup;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.DuplicateException;
import org.netbeans.Events;
import org.netbeans.JarClassLoader;
import org.netbeans.JaveleonModule;
import org.netbeans.Module;
import org.netbeans.ModuleManager;
import org.netbeans.Stamps;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.modules.ModuleInfo;
import org.openide.modules.OnStop;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;
/** Controller of the IDE's whole module system.
* Contains higher-level convenience methods to
* access the basic functionality and procedural
* stages of the module system.
* Main should hold a reference to one instance.
* Methods are thread-safe.
* @author Jesse Glick
*/
public final class ModuleSystem {
private static final Logger LOG = Logger.getLogger(ModuleSystem.class.getName());
private final ModuleManager mgr;
private final NbInstaller installer;
private ModuleList list;
private final Events ev;
/** Initialize module system.
* The system file system is needed as that holds the Modules/ folder.
* Note if the systemFileSystem is read-only, no module list will be created,
* so it is forbidden to call readList, scanForNewAndRestore, or installNew.
*/
public ModuleSystem(FileSystem systemFileSystem) throws IOException {
this();
init(systemFileSystem);
}
ModuleSystem() {
ev = Boolean.getBoolean("netbeans.modules.quiet") ? (Events) new QuietEvents() : new NbEvents();
installer = new NbInstaller(ev);
mgr = new ModuleManager(installer, ev);
}
final void init(FileSystem systemFileSystem) throws IOException {
if (Boolean.getBoolean("org.netbeans.core.startup.ModuleSystem.CULPRIT")) Thread.dumpStack(); // NOI18N
PropertyChangeListener l = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
if (ModuleManager.PROP_CLASS_LOADER.equals(ev.getPropertyName())) {
org.netbeans.core.startup.MainLookup.systemClassLoaderChanged(mgr.getClassLoader());
}
}
};
mgr.addPropertyChangeListener(l);
// now initialize to core/* classloader, later we reassign to all modules
org.netbeans.core.startup.MainLookup.systemClassLoaderChanged(installer.getClass ().getClassLoader ());
// #28465: initialize module lookup early
org.netbeans.core.startup.MainLookup.moduleLookupReady(mgr.getModuleLookup());
if (systemFileSystem.isReadOnly()) {
list = null;
} else {
FileObject root = systemFileSystem.getRoot();
FileObject modulesFolder = root.getFileObject("Modules"); // NOI18N
if (modulesFolder == null) {
modulesFolder = root.createFolder("Modules"); // NOI18N
}
list = new ModuleList(mgr, modulesFolder, ev);
installer.registerList(list);
installer.registerManager(mgr);
}
ev.log(Events.CREATED_MODULE_SYSTEM);
}
/** Get the raw module manager.
* Useful for pieces of the UI needing to directly affect the set of installed modules.
* For example, the Modules node in the Options window may use this.
*/
public ModuleManager getManager() {
return mgr;
}
/** Get the event-logging handler.
*/
public Events getEvents() {
return ev;
}
/** Produce a list of JAR files including all installed modules,
* their extensions, and enabled locale variants of both.
* Will be returned in a classpath-like order.
* Intended for use by the execution engine (though sort of deprecated).
* @return list of module-related JARs/ZIPs
*/
public List<File> getModuleJars () {
mgr.mutexPrivileged().enterReadAccess();
try {
List<File> l = new ArrayList<File>();
for (Module m: mgr.getEnabledModules()) {
l.addAll(m.getAllJars());
}
return l;
} finally {
mgr.mutexPrivileged().exitReadAccess();
}
}
/** We just make the modules now, restore them later
* to optimize the layer merge.
*/
private Set<Module> bootModules = null;
/** Load modules found in the classpath.
* Note that they might not satisfy all their dependencies, in which
* case oh well...
*/
public void loadBootModules() {
// Keep a list of manifest URL bases which we know we do not need to
// parse. Some of these manifests might be signed, and if so, we do not
// want to touch them, as it slows down startup quite a bit.
Set<File> ignoredJars = new HashSet<File>();
String javaHome = System.getProperty("java.home"); // NOI18N
if (javaHome != null) {
File lib = new File(new File(javaHome).getParentFile(), "lib"); // NOI18N
ignoredJars.add(new File(lib, "tools.jar")); // NOI18N
ignoredJars.add(new File(lib, "dt.jar")); // NOI18N
}
for (String entry : System.getProperty("sun.boot.class.path", "").split(File.pathSeparator)) { // NOI18N
ignoredJars.add(new File(entry));
}
LOG.log(Level.FINE, "Ignored JARs: {0}", ignoredJars);
mgr.mutexPrivileged().enterWriteAccess();
ev.log(Events.START_LOAD_BOOT_MODULES);
try {
bootModules = new HashSet<Module>(10);
ClassLoader upperLoader = ModuleSystem.class.getClassLoader();
// wrap alien loader, so it can be used among parent loaders of module (instanceof ProxyClassLoader)
ClassLoader loader = new JarClassLoader(Collections.<File>emptyList(), new ClassLoader[] { Module.class.getClassLoader() });
Enumeration<URL> e = loader.getResources("META-INF/MANIFEST.MF"); // NOI18N
Enumeration<URL> upperE = upperLoader.getResources("META-INF/MANIFEST.MF"); // NOI18N
ev.log(Events.PERF_TICK, "got all manifests"); // NOI18N
// There will be duplicates: cf. #32576.
Set<URL> checkedManifests = new HashSet<URL>();
// process libs in 2 passes; first, process bootstrap libraries in platform/libs, creating
// FixedModules with a classloader that only loads from those libs.
// 2nd pass will process the remaining libraries, using this classloader.
createBootModules(e, checkedManifests, ignoredJars, loader);
createBootModules(upperE, checkedManifests, ignoredJars, upperLoader);
if (list == null) {
// Plain calling us, we have to install now.
// Do it the simple way.
mgr.enable(bootModules);
}
ev.log(Events.PERF_TICK, "added all classpath modules"); // NOI18N
} catch (IOException ioe) {
// Note: includes also InvalidException's for malformed this and that.
// Probably if a bootstrap module is corrupt we are in pretty bad shape
// anyway, so don't bother trying to be fancy and install just some of
// them etc.
LOG.log(Level.WARNING, null, ioe);
} catch (DuplicateException de) {
LOG.log(Level.WARNING, null, de);
} finally {
// Not 100% accurate in this case:
ev.log(Events.FINISH_LOAD_BOOT_MODULES);
mgr.mutexPrivileged().exitWriteAccess();
}
}
/**
* Creates bootstrap (fixed) modules for the given manifests.
*
* @param e manifest URLs.
* @param checkedManifests manifests already processed earlier
* @param ignoredJars JARs which should be ignored
* @param loader module classloader for the created modules
*
* @throws IOException
* @throws DuplicateException
*/
private void createBootModules(Enumeration<URL> e, Set<URL> checkedManifests, Set<File> ignoredJars, ClassLoader loader) throws IOException, DuplicateException {
while (e.hasMoreElements()) {
URL manifestUrl = e.nextElement();
if (!checkedManifests.add(manifestUrl)) {
// Already seen, ignore.
continue;
}
URL jarURL = FileUtil.getArchiveFile(manifestUrl);
if (jarURL != null && jarURL.getProtocol().equals("file") &&
/* #121777 */ jarURL.getPath().startsWith("/")) {
LOG.log(Level.FINE, "Considering JAR: {0}", jarURL);
try {
if (ignoredJars.contains(BaseUtilities.toFile(jarURL.toURI()))) {
LOG.log(Level.FINE, "ignoring JDK/JRE manifest: {0}", manifestUrl);
continue;
}
} catch (URISyntaxException x) {
Exceptions.printStackTrace(x);
}
}
LOG.log(Level.FINE, "Checking boot manifest: {0}", manifestUrl);
InputStream is;
try {
is = manifestUrl.openStream();
} catch (IOException ioe) {
// Debugging for e.g. #32493 - which JAR was guilty?
throw Exceptions.attachMessage(ioe, "URL: " + manifestUrl); // NOI18N
}
try {
Manifest mani = new Manifest(is);
Attributes attr = mani.getMainAttributes();
if (attr.getValue("OpenIDE-Module") == null) { // NOI18N
// Not a module.
continue;
}
bootModules.add(mgr.createFixed(mani, manifestUrl, loader));
} finally {
is.close();
}
}
}
/** Refreshes the list of modules. Used from autoupdate.services.
* @since 1.28
*/
public final void refresh() {
list.moduleListChanged();
}
/** Read disk settings and determine what the known modules are.
*/
public void readList() {
ev.log(Events.PERF_START, "ModuleSystem.readList"); // NOI18N
mgr.mutexPrivileged().enterWriteAccess();
try {
list.readInitial();
} finally {
mgr.mutexPrivileged().exitWriteAccess();
}
ev.log(Events.PERF_END, "ModuleSystem.readList"); // NOI18N
}
/** Install read modules.
*/
public void restore() {
ev.log(Events.PERF_START, "ModuleSystem.restore"); // NOI18N
mgr.mutexPrivileged().enterWriteAccess();
try {
Set<Module> toTrigger = new HashSet<Module>(bootModules/*Collections.EMPTY_SET*/);
list.trigger(toTrigger);
mgr.releaseModuleManifests();
} finally {
mgr.mutexPrivileged().exitWriteAccess();
}
installer.preloadCache(mgr.getModules());
ev.log(Events.PERF_END, "ModuleSystem.restore"); // NOI18N
ev.log(Events.PERF_START, "ModuleSystem.waitOnStart"); // NOI18N
installer.waitOnStart();
ev.log(Events.PERF_END, "ModuleSystem.waitOnStart"); // NOI18N
}
/** Shut down the system: ask modules to shut down.
* Some of them may refuse.
*/
public boolean shutDown(final Runnable midHook) {
try {
return shutDownAsync(midHook).get();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
} catch (ExecutionException ex) {
Exceptions.printStackTrace(ex);
}
return false;
}
/** Initializes the shutdown, asks modules to confirm shut down, if some
* of the refure, returns <code>false</code> immediately. If they
* agree, run midHook code and asks modules to really shut down.
* Returns even {@link OnStop} code may not have finished yet. One
* can wait for the returned future till all post clean up hooks are
* finished.
*
* @since 1.44
* @param midHook the code to run when the shutdown is approved
* @return future for tracking final progress of shutdown and obtaining
* the final value
*/
public Future<Boolean> shutDownAsync(final Runnable midHook) {
mgr.mutexPrivileged().enterWriteAccess();
Runnable both = new Runnable() {
@Override
public void run() {
midHook.run();
Stamps.getModulesJARs().shutdown();
}
};
Future<Boolean> res;
try {
res = mgr.shutDownAsync(both);
} finally {
mgr.mutexPrivileged().exitWriteAccess();
}
return res;
}
/** Load a module in test (reloadable) mode.
* If there is an existing module with a different JAR, get
* rid of it and load this one instead.
* If it is already installed, disable it and reenable it
* to reload its contents.
* If other modules depend on it, disable them first and
* then (try to) enable them again later.
*/
final void deployTestModule(File jar) throws IOException {
if (! jar.isAbsolute()) throw new IOException("Absolute paths only please"); // NOI18N
// Check to see if Javeleon is enabled. If so,
// let Javeleon handle the module reloading.
if (JaveleonModule.isJaveleonPresent && JaveleonModuleReloader.getDefault().reloadJaveleonModule(jar, mgr, installer, ev)) {
return;
}
mgr.mutexPrivileged().enterWriteAccess();
ev.log(Events.START_DEPLOY_TEST_MODULE, jar);
// For now, just print to stderr directly; could also go thru Events.
// No need for I18N, module developers are expected to know English
// well enough.
System.err.println("Deploying test module " + jar + "..."); // NOI18N
try {
// The test module:
Module tm = null;
// Anything that needs to be turned back on later:
Set<Module> toReenable = new HashSet<Module>();
// First see if this refers to an existing module.
// (If so, make sure it is reloadable.)
for (Module m : mgr.getModules()) {
if (m.getJarFile() != null) {
if (jar.equals(m.getJarFile())) {
if (m.isAutoload() || m.isEager()) {
System.err.println("Reloading autoload and eager modules is not supported, sorry! (Try Javeleon.)");
return;
}
// Hah, found it.
if (! m.isReloadable()) {
m.setReloadable(true);
}
turnOffModule(m, toReenable);
mgr.reload(m);
tm = m;
break;
}
}
}
if (tm == null) {
// This JAR not encountered before. Try to load it. If it is
// a duplicate of an existing module in a different location,
// kill the existing one and replace it with this one.
try {
tm = mgr.create(jar, new ModuleHistory(jar.getAbsolutePath()), true, false, false);
} catch (DuplicateException dupe) {
Module old = dupe.getOldModule();
System.err.println("Replacing old module in " + old); // NOI18N
turnOffModule(old, toReenable);
mgr.delete(old);
try {
tm = mgr.create(jar, new ModuleHistory(jar.getAbsolutePath()), true, false, false);
} catch (DuplicateException dupe2) {
// Should not happen.
throw (IOException) new IOException(dupe2.toString()).initCause(dupe2);
}
}
}
// Try to turn on the test module. It might throw InvalidExc < IOExc.
System.err.println("Enabling " + tm + "..."); // NOI18N
if (!mgr.simulateEnable(Collections.singleton(tm)).contains(tm)) {
throw new IOException("Cannot enable " + tm + "; problems: " + tm.getProblems());
}
mgr.enable(tm);
// OK, so far so good; also try to turn on any other modules if
// we can that were on before. Just try to turn them all on.
// Don't get fancy; if some of them could not be turned on, the
// developer will be told and can clean up the situation as needed.
// Also any of them marked as reloadable, reload them now.
if (! toReenable.isEmpty()) {
System.err.println("Also re-enabling:"); // NOI18N
for (Module m : toReenable) {
System.err.println("\t" + m.getDisplayName()); // NOI18N
if (m.isReloadable()) {
m.reload();
}
}
try {
mgr.enable(toReenable);
} catch (IllegalArgumentException iae) {
// Strange new dependencies, etc.
throw new IOException(iae.toString());
}
}
System.err.println("Done."); // NOI18N
} finally {
ev.log(Events.FINISH_DEPLOY_TEST_MODULE, jar);
mgr.mutexPrivileged().exitWriteAccess();
}
}
/** Make sure some module is disabled.
* If there were any other non-autoload modules enabled
* which depended on it, make note of them.
*/
private void turnOffModule(Module m, Set<Module> toReenable) {
if (! m.isEnabled()) {
// Already done.
return;
}
for (Module m2: mgr.simulateDisable(Collections.<Module>singleton(m))) {
if (!m2.isAutoload() && !m2.isEager()) {
toReenable.add(m2);
}
}
try {
System.err.println("Disabling " + m + "..."); // NOI18N
// Don't mention the others, they will be mentioned later anyway.
mgr.disable(toReenable);
} finally {
toReenable.remove(m);
}
}
/** Get the effective "classpath" used by a module.
* <p>This is a somewhat stretched notion, but should give something that looks
* as much like a classpath as possible, i.e. a list of directories or JARs
* separated by the standard separator, which roughly represents what resources
* are visible to the module's classloader. May use special syntax to represent
* situations in which only certain packages are available from a particular
* "classpath" entry.
* <p>Disabled modules have no classpath (empty string).
* <p>Call within a mutex.
* @param m the module to build a classpath for
* @return an approximation of that module's classpath
* @see "#22466"
* @since org.netbeans.core/1 > 1.5
*/
public String getEffectiveClasspath(Module m) {
return installer.getEffectiveClasspath(m);
}
/** Checks whether the provided module will be visible in autoupdate client.
* Seeks for AutoUpdate-Show-In-Client attribute in manifest. The module
* is visible if the attribute is missing or if it is set to <code>true</code>.
* Uses caches to remember the value between restarts.
*
* @since 1.38
* @param mi the module to test
* @return <code>true</code> if the module is supposed to be visible in
* autoupdate client
*/
public boolean isShowInAutoUpdateClient(ModuleInfo mi) {
return this.installer.isShowInAutoUpdateClient(mi);
}
/** Creates files that instruct the native launcher to perform restart as
* soon as the Java process finishes.
*
* @since 1.45
* @throws UnsupportedOperationException some environments (like WebStart)
* do not support restart and may throw an exception to indicate that
*/
public static void markForRestart() throws UnsupportedOperationException {
ModuleLifecycleManager.markReadyForRestart();
}
/** Dummy event handler that does not print anything.
* Useful for test scripts where you do not really want to see
* everything going by.
*/
private static final class QuietEvents extends Events {
QuietEvents() {}
protected void logged(String message, Object[] args) {}
}
}