/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.configuration.bootstrap; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import java.nio.file.Files; /** * Helper class that chooses the profile directory and related paths to use, based on command-line or .ini file parameters. * * @author Robert Mischke * @author Oliver Seebach * @author Tobias Rodehutskors */ public final class BootstrapConfiguration { /** * System property for exit code 1 on locked profile. */ public static final String DRCE_LAUNCH_EXIT_ON_LOCKED_PROFILE = "rce.launch.exitOnLockedProfile"; /** * A system property that can be used to override the parent directory for profiles defined by a relative path or id (default "~/.rce"). */ public static final String SYSTEM_PROPERTY_PROFILES_PARENT_DIRECTORY_OVERRIDE = "rce.profiles.parentDir"; /** * A system property that can be used to override the default profile id/path (default id: "default"). It is only applied if no "-p" * launch parameter is set, and can be either a relative or absolute path. */ public static final String SYSTEM_PROPERTY_DEFAULT_PROFILE_ID_OR_PATH = "rce.profile.default"; /** * The relative path where the shutdown information (*not* the temporary shutdown *profile*!) of a profile is stored. Made public to * allow sending shutdown signals to external instances. */ public static final String PROFILE_SHUTDOWN_DATA_SUBDIR = "internal"; /** * The current profile's version number. Needs to be updated manually whenever changed in the profile require an update. */ public static final Integer PROFILE_VERSION_NUMBER = 1; /** The name of the file containing the profile version. */ public static final String PROFILE_VERSION_FILE_NAME = "profile.version"; /** * The name of the lock file to signal that the containing profile is in use. */ public static final String PROFILE_DIR_LOCK_FILE_NAME = "instance.lock"; private static final String SYSTEM_PROPERTY_USER_HOME = "user.home"; private static final String SYSTEM_PROPERTY_SYSTEM_TEMP_DIR = "java.io.tmpdir"; /** * Standard OSGi "osgi.configuration.area" property. * * This is the directory where OSGi stores all runtime data which is not stored inside the workspace (the "instance area" in OSGi * terms). */ private static final String SYSTEM_PROPERTY_OSGI_CONFIGURATION_AREA = "osgi.configuration.area"; private static final String PROFILE_INTERNAL_DATA_SUBDIR = "internal"; private static final String PROFILE_RELATIVE_OSGI_STORAGE_PATH = PROFILE_INTERNAL_DATA_SUBDIR + "/osgi"; private static final String PROFILE_LOGFILES_PATH_PROPERTY = "profile.logfiles.path"; // set by this class private static final String PROFILE_LOGFILES_PREFIX_PROPERTY = "profile.logfiles.prefix"; // set by this class private static final String PROFILE_OPTION_HINT = " (use -p/--profile <id or path> to override)"; // note: not using the singleton pattern so it can be reset by unit tests - misc_ro private static volatile BootstrapConfiguration instance; private static String introText; private final File originalProfileDirectory; private final boolean intendedProfileDirectoryLocked; private final boolean hasIntendedProfileDirectoryValidVersion; private final File finalProfileDirectory; private final File internalDataDirectory; private final String finalProfileDirectoryPath; // the temporary/stub profile location for the process sending the shutdown signal private final File shutdownProfileDirectory; // the shutdown.dat location of the process which should be terminated private final File targetShutdownDataDirectory; private final boolean shutdownRequested; private final File profilesRootDirectory; private final boolean fallbackProfileDisabled; /** * True, if either of the options -p or --profile were used. */ private boolean profileOptionUsed = false; /** * Performs the bootstrap profile initialization. * * @throws IOException on bootstrap profile path errors */ private BootstrapConfiguration() throws IOException { PrintStream stdErr = System.err; LaunchParameters launchParameters = LaunchParameters.getInstance(); profilesRootDirectory = determineProfilesParentDirectory(); originalProfileDirectory = determineOriginalProfileDir(launchParameters); File preliminaryProfileDir = originalProfileDirectory; shutdownRequested = launchParameters.containsToken("--shutdown"); // For headless mode, fallback profile is automatically disabled. fallbackProfileDisabled = System.getProperties().containsKey(DRCE_LAUNCH_EXIT_ON_LOCKED_PROFILE) || launchParameters.containsToken("--headless") || launchParameters.containsToken("--batch"); boolean isProfileAccessible = true; boolean hasIntendedProfileDirectoryValidVersionTemp; try { // check profile version number. // if the preliminary profile is not read and/or not writable this method will throw an IOException hasIntendedProfileDirectoryValidVersionTemp = validateProfileDirectoryVersionNumber(preliminaryProfileDir, stdErr); } catch (IOException e) { isProfileAccessible = false; hasIntendedProfileDirectoryValidVersionTemp = false; } // using a temporary local variable since the member variable should be final hasIntendedProfileDirectoryValidVersion = hasIntendedProfileDirectoryValidVersionTemp; // In case of error either start in fallback profile or don't start if (!isProfileAccessible || !hasIntendedProfileDirectoryValidVersion) { // fail if fallback profile disabled if (fallbackProfileDisabled) { String errorMessage; if (!isProfileAccessible) { errorMessage = "The specified profile folder " + preliminaryProfileDir.getAbsolutePath() + " is either nor readable and/or not writeable. " + " Choose another profile directory. (See the user guide for more information about the profile directory.)"; } else { // !hasIntendedProfileDirectoryValidVersion errorMessage = "The required version of the profile directory is " + BootstrapConfiguration.PROFILE_VERSION_NUMBER + " but the profile directory's current version is newer. Most likely, this is the case " + " because it has been used with a newer RCE version before. As downgrading of profiles is not supported," + " the configured profile directory cannot be used with this RCE version." + " Choose another profile directory. (See the user guide for more information about the profile directory.)"; } stdErr.println(errorMessage + " Fallback profile is disabled, shutting down."); System.exit(1); } else { // else go on in the process with the fallback profile; instance validator will inform the user and force shutdown preliminaryProfileDir = determineFallbackProfileDirectory(originalProfileDirectory); } } // the temporary/stub profile location for the process sending the shutdown signal shutdownProfileDirectory = new File(originalProfileDirectory, PROFILE_INTERNAL_DATA_SUBDIR + "/shutdown"); targetShutdownDataDirectory = new File(originalProfileDirectory, PROFILE_INTERNAL_DATA_SUBDIR); if (shutdownRequested) { // if used as a shutdown trigger, use the shutdown data sub-directory as profile directory preliminaryProfileDir = shutdownProfileDirectory; introText = "Using shutdown profile directory"; } intendedProfileDirectoryLocked = attemptToLockProfileDirectory(preliminaryProfileDir); if (intendedProfileDirectoryLocked) { finalProfileDirectory = preliminaryProfileDir; introText = "Using profile directory"; } else { stdErr.println("Failed to lock profile directory " + preliminaryProfileDir + " - most likely, another instance is already using it"); // If the "--disable-profile-fallback" option is set, shut down, else try to create a fallback profile directory if (fallbackProfileDisabled) { stdErr.println("Fallback profile is disabled, shutting down."); System.exit(1); } preliminaryProfileDir = determineFallbackProfileDirectory(originalProfileDirectory); if (attemptToLockProfileDirectory(preliminaryProfileDir)) { finalProfileDirectory = preliminaryProfileDir; introText = "Using fallback profile directory"; } else { throw new IOException("Could not acquire a lock on the fallback profile directory " + preliminaryProfileDir + " either - giving up"); } } finalProfileDirectoryPath = finalProfileDirectory.getAbsolutePath(); internalDataDirectory = new File(finalProfileDirectory, PROFILE_INTERNAL_DATA_SUBDIR); // create internal data directory only if it was not already created by profile version checking procedure if (!internalDataDirectory.exists()) { internalDataDirectory.mkdirs(); if (!internalDataDirectory.isDirectory()) { throw new IOException("Failed to initialize internal data directory " + internalDataDirectory.getAbsolutePath()); } } String profileOptionHintToPrint = PROFILE_OPTION_HINT; // if the user specified profile directory is used, print a modified profile option hint if (profileOptionUsed && finalProfileDirectory.getCanonicalPath().equals(originalProfileDirectory.getCanonicalPath())) { profileOptionHintToPrint = "(as specified by the -p/--profile option)"; } // circumvent CheckStyle rule to generate basic output before the log system is initialized PrintStream stdout = System.out; stdout.println(String.format("%s %s%s", introText, finalProfileDirectoryPath, profileOptionHintToPrint)); setLoggingParameters(); // TODO/NOTE: this does not take full effect; apparently, the setting has already been read and applied // setOsgiStorageLocation(); } /** * Initializes the singleton instance from system properties and launch parameters. * * @throws IOException on bootstrap profile path errors */ public static void initialize() throws IOException { instance = new BootstrapConfiguration(); } /** * @return the singleton instance */ public static BootstrapConfiguration getInstance() { if (instance == null) { throw new IllegalStateException("No " + BootstrapConfiguration.class.getSimpleName() + " instance available - most likely, its containing bundle has not been properly initialized"); } return instance; } public File getProfileDirectory() { return finalProfileDirectory; } /** * Low-level access to the storage path for internal data. This method is intended for classes that need to remain independent of the * configuration service. Other classes should fetch the path from there. * * @return the location for internal data files; default: "<profile dir>/internal" */ public File getInternalDataDirectory() { return internalDataDirectory; } public File getOriginalProfileDirectory() { return originalProfileDirectory; } public boolean isShutdownRequested() { return shutdownRequested; } // the shutdown.dat location for the process sending the shutdown signal is within its own profile directory public File getOwnShutdownDataDirectory() { return internalDataDirectory; } public File getTargetShutdownDataDirectory() { return targetShutdownDataDirectory; } public boolean isIntendedProfileDirectorySuccessfullyLocked() { return intendedProfileDirectoryLocked; } /** * @return <code>true</code> if profile directory has valid version (<= current one) */ public boolean hasIntendedProfileDirectoryValidVersion() { return hasIntendedProfileDirectoryValidVersion; } public File getProfilesRootDirectory() { return profilesRootDirectory; } /** * Ensures that the current profiles root directory exists and is a directory. * * @throws IOException if the conditions are not met */ public void initializeProfilesRootDirectory() throws IOException { profilesRootDirectory.mkdirs(); if (!profilesRootDirectory.isDirectory()) { throw new IOException(String.format( "Failed to create the default profile root directory \"%s\"", profilesRootDirectory.getAbsolutePath())); } } private File determineProfilesParentDirectory() throws IOException { String parentPathOverride = System.getProperty(SYSTEM_PROPERTY_PROFILES_PARENT_DIRECTORY_OVERRIDE); File profilesRootDir; if (parentPathOverride != null) { profilesRootDir = new File(parentPathOverride); if (!profilesRootDir.isDirectory()) { throw new IOException(String.format( "The configured profile parent directory \"%s\" does not exist; please check your launch settings", profilesRootDir.getAbsolutePath())); } } else { String userHome = System.getProperty(SYSTEM_PROPERTY_USER_HOME); profilesRootDir = new File(userHome, ".rce").getAbsoluteFile(); // do not create yet; the specified profile directory may be absolute - misc_ro } return profilesRootDir; } private File determineOriginalProfileDir(LaunchParameters launchParams) throws IOException { String profilePathShortOption = launchParams.getNamedParameter("-p"); String profilePathLongOption = launchParams.getNamedParameter("--profile"); String profilePath; if (profilePathShortOption != null) { profilePath = profilePathShortOption; // sanity check: forbid "rce -p path1 --profile path2" if (profilePathLongOption != null) { // TODO use more appropriate exception type? throw new IOException("Invalid combination of command-line parameters: cannot specify -p and --profile at the same time"); } } else { profilePath = profilePathLongOption; // can still be null if none of the options is used } if (profilePath == null) { String explicitDefault = System.getProperty(SYSTEM_PROPERTY_DEFAULT_PROFILE_ID_OR_PATH); if (explicitDefault != null) { profilePath = explicitDefault; } else { profilePath = "default"; } } else if (profilePath.equals("common")) { throw new IOException("Error: The profile \"common\" can not be used as it is reserved for cross-profile settings"); } else if (profilePath != null) { profileOptionUsed = true; } File configuredPath = new File(profilePath); File profileDir; if (configuredPath.isAbsolute()) { profileDir = configuredPath; } else { initializeProfilesRootDirectory(); profileDir = new File(profilesRootDirectory, profilePath).getAbsoluteFile(); } if (profileDir.exists() && !profileDir.isDirectory()) { throw new IOException(String.format( "The configured profile directory \"%s\" points to a file, it must either point to an existing profile directory " + "or must be a path pointing to a not yet existing directory; please check your launch settings", profileDir.getAbsolutePath())); } return profileDir; } private File determineFallbackProfileDirectory(File originalProfileDir) { String fallbackProfileName = "rce-fallback-profile-" + System.currentTimeMillis(); return new File(System.getProperty(SYSTEM_PROPERTY_SYSTEM_TEMP_DIR), fallbackProfileName); } private void setLoggingParameters() { // make the profile path available to log4j/pax-logging System.setProperty(PROFILE_LOGFILES_PATH_PROPERTY, finalProfileDirectory.getAbsolutePath()); if (shutdownRequested) { System.setProperty(PROFILE_LOGFILES_PREFIX_PROPERTY, "shutdown-"); } else { System.setProperty(PROFILE_LOGFILES_PREFIX_PROPERTY, ""); } } private void setOsgiStorageLocation() { File location = new File(finalProfileDirectory, PROFILE_RELATIVE_OSGI_STORAGE_PATH); location.mkdirs(); System.setProperty(SYSTEM_PROPERTY_OSGI_CONFIGURATION_AREA, location.getAbsolutePath()); } /** * Attempts to acquire an exclusive lock on the given file. Note that this is not an OS-level lock, but only protects against locks made * by other JVM applications; see {@link FileChannel#tryLock(long, long, boolean)} for details. * * As a side effect of locking, this method also verifies that the profile directory exists and is actually a directory. * * @param profileDir the profile directory to lock * @return true if the lock was acquired, false if the lock is already held by another JVM application * @throws IOException on unusual errors; should not occur on a simple failure to acquire the lock */ // note: technically, this method produces a resource leak, but this is irrelevant as the lock must be held anyway // made this method public to be able to use it during testing ~ rode_to public static boolean attemptToLockProfileDirectory(File profileDir) throws IOException { profileDir.mkdirs(); if (!profileDir.isDirectory()) { throw new IOException("Profile directory " + profileDir.getAbsolutePath() + " can not be created or is not a directory"); } File lockfile = new File(profileDir, PROFILE_DIR_LOCK_FILE_NAME); FileLock lock = null; // create lock file if it does not exist lockfile.createNewFile(); // try to get a lock on this file try { lock = new RandomAccessFile(lockfile, "rw").getChannel().tryLock(); } catch (IOException | OverlappingFileLockException e) { throw new IOException("Unexpected error when trying to acquire a file lock on " + lockfile, e); } // NOTE: It is not necessary to release the lock on the file, this is automatically done // by the Java VM or in case of an abnormal end by the operating system return lock != null; } /** * Validates profile directory version number. * * @param profileFolder the profile folder * @param stdErr the std err * @return true, if successful * @throws IOException Signals that an I/O exception has occurred. */ private boolean validateProfileDirectoryVersionNumber(File profileFolder, PrintStream stdErr) throws IOException { File versionFile = new File(new File(profileFolder, PROFILE_INTERNAL_DATA_SUBDIR), PROFILE_VERSION_FILE_NAME); if (versionFile.isFile() && versionFile.exists()) { try { String content = new String(Files.readAllBytes(versionFile.toPath())); int currentProfilesVersionNumber = Integer.parseInt(content); if (currentProfilesVersionNumber > PROFILE_VERSION_NUMBER) { // if RCE started although the profile version was higher, something when wrong return false; } else if (currentProfilesVersionNumber < PROFILE_VERSION_NUMBER) { // else update version number writeProfileVersionNumberToProfile(versionFile, PROFILE_VERSION_NUMBER); } } catch (NumberFormatException e) { stdErr.println("Failed to read version of profile directory; considered as invalid: " + e.getMessage()); // if profile could not be read, return false return false; } } else { // if version number file does not exist: create it writeProfileVersionNumberToProfile(versionFile, PROFILE_VERSION_NUMBER); } return true; } private void writeProfileVersionNumberToProfile(File versionFile, int versionNumber) throws IOException { // if version file's parent folder does not exist, create it if (!versionFile.getParentFile().exists()) { versionFile.getParentFile().mkdirs(); } // if file does not exist, create it if (!versionFile.exists()) { versionFile.createNewFile(); } // don't append but overwrite file's content FileWriter fw = new FileWriter(versionFile, false); BufferedWriter bw = new BufferedWriter(fw); bw.write(String.valueOf(versionNumber)); bw.close(); } }