blob: bed8665058bdb93e3158c1965541c6be69a5ebf8 [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.sling.launchpad.base.impl;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import org.apache.felix.framework.Logger;
import org.apache.sling.launchpad.api.LaunchpadContentProvider;
import org.apache.sling.launchpad.api.StartupMode;
import org.apache.sling.launchpad.base.shared.SharedConstants;
import org.osgi.framework.Constants;
/**
* The <code>StartupManager</code> tries to detect the startup mode:
* It distinguishes between an initial startup (INSTALL), an update (UPDATE)
* and a restart without a change (RESTART).
* @since 2.4.0
*/
public class StartupManager {
/** The data file which works as a marker to detect the first startup. */
private static final String DATA_FILE = "launchpad-timestamp.txt";
/** The old data file. */
private static final String OLD_DATA_FILE = "bundle0" + File.separatorChar + "bootstrapinstaller.ser";
/** Name of the mode override property. */
private static final String OVERRIDE_PROP = "org.apache.sling.launchpad.startupmode";
/**
* The {@link Logger} use for logging messages during installation and
* startup.
*/
private final Logger logger;
private final StartupMode mode;
private final File startupDir;
private final File confDir;
private final long targetStartLevel;
private final boolean incrementalStartupEnabled;
StartupManager(final Map<String, String> properties,
final Logger logger) {
this.logger = logger;
this.startupDir = DirectoryUtil.getStartupDir(properties);
this.confDir = DirectoryUtil.getConfigDir(properties);
// check for override property
final String overrideMode = System.getProperty(OVERRIDE_PROP, properties.get(OVERRIDE_PROP));
if ( overrideMode != null ) {
this.mode = StartupMode.valueOf(overrideMode.toUpperCase());
this.logger.log(Logger.LOG_INFO, "Override property set. Starting in mode " + this.mode);
} else {
this.mode = detectMode(properties.get(Constants.FRAMEWORK_STORAGE));
this.logger.log(Logger.LOG_INFO, "Detected startup mode. Starting in mode " + this.mode);
}
// populate the sling target start level from the framework one, if not set,
// otherwise overwrite the framework one
if (null == properties.get(SharedConstants.SLING_INSTALL_TARGETSTARTLEVEL)) {
properties.put(SharedConstants.SLING_INSTALL_TARGETSTARTLEVEL, properties.get(Constants.FRAMEWORK_BEGINNING_STARTLEVEL));
} else {
properties.put(Constants.FRAMEWORK_BEGINNING_STARTLEVEL, properties.get(SharedConstants.SLING_INSTALL_TARGETSTARTLEVEL));
}
this.targetStartLevel = Long.valueOf(properties.get(Constants.FRAMEWORK_BEGINNING_STARTLEVEL));
this.incrementalStartupEnabled = Boolean.valueOf(properties.get(SharedConstants.SLING_INSTALL_INCREMENTAL_START));
// if this is not a restart, reduce start level
if ( this.mode != StartupMode.RESTART && this.incrementalStartupEnabled ) {
final String startLevel = properties.get(SharedConstants.SLING_INSTALL_STARTLEVEL);
properties.put(Constants.FRAMEWORK_BEGINNING_STARTLEVEL, startLevel != null ? startLevel : "10");
}
}
/**
* Return the startup mode
* @return The startup mode
*/
public StartupMode getMode() {
return this.mode;
}
/**
* Is the incremental startup enabled?
*/
public boolean isIncrementalStartupEnabled() {
return this.incrementalStartupEnabled;
}
/**
* Return the target start level.
* @return Target start level
*/
public long getTargetStartLevel() {
return this.targetStartLevel;
}
/**
* Detect the startup mode by comparing time stamps
*/
private StartupMode detectMode(final String osgiStorageDir) {
final File dataFile = new File(this.confDir, DATA_FILE);
if (dataFile.exists()) {
FileReader fis = null;
try {
final long selfStamp = this.getSelfTimestamp();
if (selfStamp > 0) {
fis = new FileReader(dataFile);
final char[] txt = new char[128];
final int len = fis.read(txt);
final String value = new String(txt, 0, len);
final long storedStamp = Long.parseLong(value);
logger.log(Logger.LOG_INFO, String.format("Stored startup timestamp: %s", storedStamp));
return (storedStamp >= selfStamp ? StartupMode.RESTART : StartupMode.UPDATE);
}
} catch (final NumberFormatException nfe) {
// probably still the old value, fallback to assume not
// installed
return StartupMode.RESTART;
} catch (final IOException ioe) {
logger.log(Logger.LOG_ERROR,
"IOException during reading of installed flag.", ioe);
} finally {
if (fis != null) {
try { fis.close(); } catch (IOException ignore) {}
}
}
} else {
// check for old data file
// this is a little bit hacky as we have to directly look into
// the Apache Felix bundle cache. However, as we know that older
// versions did use Felix this is fine.
final File felixDir = new File(osgiStorageDir);
final File oldFile = new File(felixDir, OLD_DATA_FILE);
if ( oldFile.exists() ) {
// this is an update - remove old file
oldFile.delete();
return StartupMode.UPDATE;
}
}
// not installed yet - fallback
return StartupMode.INSTALL;
}
/**
* Get the time stamp of a class through its url classloader (if possible)
*/
long getTimeStampOfClass(final Class<?> clazz, final long selfStamp) {
long timeStamp = selfStamp;
final ClassLoader loader = clazz.getClassLoader();
if (loader instanceof URLClassLoader) {
@SuppressWarnings("resource")
final URLClassLoader urlLoader = (URLClassLoader) loader;
final URL[] urls = urlLoader.getURLs();
if (urls.length > 0) {
final URL url = urls[0];
try {
final long stamp = urls[0].openConnection().getLastModified();
if ( stamp > selfStamp ) {
logger.log(Logger.LOG_INFO, String.format("Newer timestamp for %s from %s : %s", clazz.getName(), url, selfStamp));
timeStamp = stamp;
}
} catch (final IOException ignore) {}
}
}
return timeStamp;
}
/**
* Returns the time stamp of JAR file from which this class has been loaded
* or -1 if the timestamp cannot be resolved.
* <p>
* This method assumes that the ClassLoader of this class is an
* URLClassLoader and that the first URL entry of this class loader is the
* JAR providing this class. This is in fact true as the URLClassLoader has
* been created by the launcher from the launcher JAR file.
*
* @return The last modification time stamp of the launcher JAR file or -1
* if the class loader of this class is not an URLClassLoader or the
* class loader has no URL entries. Both situations are not really
* expected.
* @throws IOException If an error occurs reading accessing the last
* modification time stamp.
*/
private long getSelfTimestamp() {
// the time stamp of the launcher jar and the bootstrap jar
long selfStamp = this.getTimeStampOfClass(this.getClass(), -1);
selfStamp = this.getTimeStampOfClass(LaunchpadContentProvider.class, selfStamp);
// check whether any bundle is younger than the launcher jar
final File[] directories = this.startupDir.listFiles(DirectoryUtil.DIRECTORY_FILTER);
if ( directories != null ) {
for (final File levelDir : directories) {
// iterate through all files in the startlevel dir
final File[] jarFiles = levelDir.listFiles(DirectoryUtil.BUNDLE_FILE_FILTER);
if ( jarFiles != null ) {
for (final File bundleJar : jarFiles) {
if (bundleJar.lastModified() > selfStamp) {
selfStamp = bundleJar.lastModified();
logger.log(Logger.LOG_INFO, String.format("Newer timestamp from %s : %s", bundleJar, selfStamp));
}
}
}
}
}
logger.log(Logger.LOG_INFO, String.format("Final self timestamp: %s.", selfStamp));
// return the final stamp (may be -1 if launcher jar cannot be checked
// and there are no bundle jar files)
return selfStamp;
}
/**
* Set the finished installation marker.
*/
public void markInstalled() {
final File dataFile = new File(this.confDir, DATA_FILE);
try {
this.confDir.mkdirs();
final FileWriter fos = new FileWriter(dataFile);
try {
fos.write(String.valueOf(System.currentTimeMillis()));
} finally {
try { fos.close(); } catch (final IOException ignore) {}
}
} catch (final IOException ioe) {
logger.log(Logger.LOG_ERROR,
"IOException during writing of installed flag.", ioe);
}
}
}