/* * Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.util.prefs; import java.util.*; import java.io.*; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; import java.security.PrivilegedActionException; import sun.util.logging.PlatformLogger; /** * Preferences implementation for Unix. Preferences are stored in the file * system, with one directory per preferences node. All of the preferences * at each node are stored in a single file. Atomic file system operations * (e.g. File.renameTo) are used to ensure integrity. An in-memory cache of * the "explored" portion of the tree is maintained for performance, and * written back to the disk periodically. File-locking is used to ensure * reasonable behavior when multiple VMs are running at the same time. * (The file lock is obtained only for sync(), flush() and removeNode().) * * @author Josh Bloch * @see Preferences * @since 1.4 */ class FileSystemPreferences extends AbstractPreferences { /** * Sync interval in seconds. */ private static final int SYNC_INTERVAL = Math.max(1, Integer.parseInt( AccessController.doPrivileged( new sun.security.action.GetPropertyAction( "java.util.prefs.syncInterval", "30")))); /** * Returns logger for error messages. Backing store exceptions are logged at * WARNING level. */ private static PlatformLogger getLogger() { return PlatformLogger.getLogger("java.util.prefs"); } /** * Directory for system preferences. */ private static File systemRootDir; /* * Flag, indicating whether systemRoot directory is writable */ private static boolean isSystemRootWritable; /** * Directory for user preferences. */ private static File userRootDir; /* * Flag, indicating whether userRoot directory is writable */ private static boolean isUserRootWritable; /** * The user root. */ static Preferences userRoot = null; static synchronized Preferences getUserRoot() { if (userRoot == null) { setupUserRoot(); userRoot = new FileSystemPreferences(true); } return userRoot; } private static void setupUserRoot() { AccessController.doPrivileged(new PrivilegedAction() { public Void run() { userRootDir = new File(System.getProperty("java.util.prefs.userRoot", System.getProperty("user.home")), ".java/.userPrefs"); // Attempt to create root dir if it does not yet exist. if (!userRootDir.exists()) { if (userRootDir.mkdirs()) { try { chmod(userRootDir.getCanonicalPath(), USER_RWX); } catch (IOException e) { getLogger().warning("Could not change permissions" + " on userRoot directory. "); } getLogger().info("Created user preferences directory."); } else getLogger().warning("Couldn't create user preferences" + " directory. User preferences are unusable."); } isUserRootWritable = userRootDir.canWrite(); String USER_NAME = System.getProperty("user.name"); userLockFile = new File (userRootDir,".user.lock." + USER_NAME); userRootModFile = new File (userRootDir, ".userRootModFile." + USER_NAME); if (!userRootModFile.exists()) try { // create if does not exist. userRootModFile.createNewFile(); // Only user can read/write userRootModFile. int result = chmod(userRootModFile.getCanonicalPath(), USER_READ_WRITE); if (result !=0) getLogger().warning("Problem creating userRoot " + "mod file. Chmod failed on " + userRootModFile.getCanonicalPath() + " Unix error code " + result); } catch (IOException e) { getLogger().warning(e.toString()); } userRootModTime = userRootModFile.lastModified(); return null; } }); } /** * The system root. */ static Preferences systemRoot; static synchronized Preferences getSystemRoot() { if (systemRoot == null) { setupSystemRoot(); systemRoot = new FileSystemPreferences(false); } return systemRoot; } private static void setupSystemRoot() { AccessController.doPrivileged(new PrivilegedAction() { public Void run() { String systemPrefsDirName = System.getProperty("java.util.prefs.systemRoot","/etc/.java"); systemRootDir = new File(systemPrefsDirName, ".systemPrefs"); // Attempt to create root dir if it does not yet exist. if (!systemRootDir.exists()) { // system root does not exist in /etc/.java // Switching to java.home systemRootDir = new File(System.getProperty("java.home"), ".systemPrefs"); if (!systemRootDir.exists()) { if (systemRootDir.mkdirs()) { getLogger().info( "Created system preferences directory " + "in java.home."); try { chmod(systemRootDir.getCanonicalPath(), USER_RWX_ALL_RX); } catch (IOException e) { } } else { getLogger().warning("Could not create " + "system preferences directory. System " + "preferences are unusable."); } } } isSystemRootWritable = systemRootDir.canWrite(); systemLockFile = new File(systemRootDir, ".system.lock"); systemRootModFile = new File (systemRootDir,".systemRootModFile"); if (!systemRootModFile.exists() && isSystemRootWritable) try { // create if does not exist. systemRootModFile.createNewFile(); int result = chmod(systemRootModFile.getCanonicalPath(), USER_RW_ALL_READ); if (result !=0) getLogger().warning("Chmod failed on " + systemRootModFile.getCanonicalPath() + " Unix error code " + result); } catch (IOException e) { getLogger().warning(e.toString()); } systemRootModTime = systemRootModFile.lastModified(); return null; } }); } /** * Unix user write/read permission */ private static final int USER_READ_WRITE = 0600; private static final int USER_RW_ALL_READ = 0644; private static final int USER_RWX_ALL_RX = 0755; private static final int USER_RWX = 0700; /** * The lock file for the user tree. */ static File userLockFile; /** * The lock file for the system tree. */ static File systemLockFile; /** * Unix lock handle for userRoot. * Zero, if unlocked. */ private static int userRootLockHandle = 0; /** * Unix lock handle for systemRoot. * Zero, if unlocked. */ private static int systemRootLockHandle = 0; /** * The directory representing this preference node. There is no guarantee * that this directory exits, as another VM can delete it at any time * that it (the other VM) holds the file-lock. While the root node cannot * be deleted, it may not yet have been created, or the underlying * directory could have been deleted accidentally. */ private final File dir; /** * The file representing this preference node's preferences. * The file format is undocumented, and subject to change * from release to release, but I'm sure that you can figure * it out if you try real hard. */ private final File prefsFile; /** * A temporary file used for saving changes to preferences. As part of * the sync operation, changes are first saved into this file, and then * atomically renamed to prefsFile. This results in an atomic state * change from one valid set of preferences to another. The * the file-lock is held for the duration of this transformation. */ private final File tmpFile; /** * File, which keeps track of global modifications of userRoot. */ private static File userRootModFile; /** * Flag, which indicated whether userRoot was modified by another VM */ private static boolean isUserRootModified = false; /** * Keeps track of userRoot modification time. This time is reset to * zero after UNIX reboot, and is increased by 1 second each time * userRoot is modified. */ private static long userRootModTime; /* * File, which keeps track of global modifications of systemRoot */ private static File systemRootModFile; /* * Flag, which indicates whether systemRoot was modified by another VM */ private static boolean isSystemRootModified = false; /** * Keeps track of systemRoot modification time. This time is reset to * zero after system reboot, and is increased by 1 second each time * systemRoot is modified. */ private static long systemRootModTime; /** * Locally cached preferences for this node (includes uncommitted * changes). This map is initialized with from disk when the first get or * put operation occurs on this node. It is synchronized with the * corresponding disk file (prefsFile) by the sync operation. The initial * value is read *without* acquiring the file-lock. */ private Map prefsCache = null; /** * The last modification time of the file backing this node at the time * that prefCache was last synchronized (or initially read). This * value is set *before* reading the file, so it's conservative; the * actual timestamp could be (slightly) higher. A value of zero indicates * that we were unable to initialize prefsCache from the disk, or * have not yet attempted to do so. (If prefsCache is non-null, it * indicates the former; if it's null, the latter.) */ private long lastSyncTime = 0; /** * Unix error code for locked file. */ private static final int EAGAIN = 11; /** * Unix error code for denied access. */ private static final int EACCES = 13; /* Used to interpret results of native functions */ private static final int LOCK_HANDLE = 0; private static final int ERROR_CODE = 1; /** * A list of all uncommitted preference changes. The elements in this * list are of type PrefChange. If this node is concurrently modified on * disk by another VM, the two sets of changes are merged when this node * is sync'ed by overwriting our prefsCache with the preference map last * written out to disk (by the other VM), and then replaying this change * log against that map. The resulting map is then written back * to the disk. */ final List changeLog = new ArrayList<>(); /** * Represents a change to a preference. */ private abstract class Change { /** * Reapplies the change to prefsCache. */ abstract void replay(); }; /** * Represents a preference put. */ private class Put extends Change { String key, value; Put(String key, String value) { this.key = key; this.value = value; } void replay() { prefsCache.put(key, value); } } /** * Represents a preference remove. */ private class Remove extends Change { String key; Remove(String key) { this.key = key; } void replay() { prefsCache.remove(key); } } /** * Represents the creation of this node. */ private class NodeCreate extends Change { /** * Performs no action, but the presence of this object in changeLog * will force the node and its ancestors to be made permanent at the * next sync. */ void replay() { } } /** * NodeCreate object for this node. */ NodeCreate nodeCreate = null; /** * Replay changeLog against prefsCache. */ private void replayChanges() { for (int i = 0, n = changeLog.size(); i() { public Void run() { Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { syncTimer.cancel(); syncWorld(); } }); return null; } }); } private static void syncWorld() { /* * Synchronization necessary because userRoot and systemRoot are * lazily initialized. */ Preferences userRt; Preferences systemRt; synchronized(FileSystemPreferences.class) { userRt = userRoot; systemRt = systemRoot; } try { if (userRt != null) userRt.flush(); } catch(BackingStoreException e) { getLogger().warning("Couldn't flush user prefs: " + e); } try { if (systemRt != null) systemRt.flush(); } catch(BackingStoreException e) { getLogger().warning("Couldn't flush system prefs: " + e); } } private final boolean isUserNode; /** * Special constructor for roots (both user and system). This constructor * will only be called twice, by the static initializer. */ private FileSystemPreferences(boolean user) { super(null, ""); isUserNode = user; dir = (user ? userRootDir: systemRootDir); prefsFile = new File(dir, "prefs.xml"); tmpFile = new File(dir, "prefs.tmp"); } /** * Construct a new FileSystemPreferences instance with the specified * parent node and name. This constructor, called from childSpi, * is used to make every node except for the two //roots. */ private FileSystemPreferences(FileSystemPreferences parent, String name) { super(parent, name); isUserNode = parent.isUserNode; dir = new File(parent.dir, dirName(name)); prefsFile = new File(dir, "prefs.xml"); tmpFile = new File(dir, "prefs.tmp"); AccessController.doPrivileged(new PrivilegedAction() { public Void run() { newNode = !dir.exists(); return null; } }); if (newNode) { // These 2 things guarantee node will get wrtten at next flush/sync prefsCache = new TreeMap<>(); nodeCreate = new NodeCreate(); changeLog.add(nodeCreate); } } public boolean isUserNode() { return isUserNode; } protected void putSpi(String key, String value) { initCacheIfNecessary(); changeLog.add(new Put(key, value)); prefsCache.put(key, value); } protected String getSpi(String key) { initCacheIfNecessary(); return prefsCache.get(key); } protected void removeSpi(String key) { initCacheIfNecessary(); changeLog.add(new Remove(key)); prefsCache.remove(key); } /** * Initialize prefsCache if it has yet to be initialized. When this method * returns, prefsCache will be non-null. If the data was successfully * read from the file, lastSyncTime will be updated. If prefsCache was * null, but it was impossible to read the file (because it didn't * exist or for any other reason) prefsCache will be initialized to an * empty, modifiable Map, and lastSyncTime remain zero. */ private void initCacheIfNecessary() { if (prefsCache != null) return; try { loadCache(); } catch(Exception e) { // assert lastSyncTime == 0; prefsCache = new TreeMap<>(); } } /** * Attempt to load prefsCache from the backing store. If the attempt * succeeds, lastSyncTime will be updated (the new value will typically * correspond to the data loaded into the map, but it may be less, * if another VM is updating this node concurrently). If the attempt * fails, a BackingStoreException is thrown and both prefsCache and * lastSyncTime are unaffected by the call. */ private void loadCache() throws BackingStoreException { try { AccessController.doPrivileged( new PrivilegedExceptionAction() { public Void run() throws BackingStoreException { Map m = new TreeMap<>(); long newLastSyncTime = 0; try { newLastSyncTime = prefsFile.lastModified(); try (FileInputStream fis = new FileInputStream(prefsFile)) { XmlSupport.importMap(fis, m); } } catch(Exception e) { if (e instanceof InvalidPreferencesFormatException) { getLogger().warning("Invalid preferences format in " + prefsFile.getPath()); prefsFile.renameTo( new File( prefsFile.getParentFile(), "IncorrectFormatPrefs.xml")); m = new TreeMap<>(); } else if (e instanceof FileNotFoundException) { getLogger().warning("Prefs file removed in background " + prefsFile.getPath()); } else { throw new BackingStoreException(e); } } // Attempt succeeded; update state prefsCache = m; lastSyncTime = newLastSyncTime; return null; } }); } catch (PrivilegedActionException e) { throw (BackingStoreException) e.getException(); } } /** * Attempt to write back prefsCache to the backing store. If the attempt * succeeds, lastSyncTime will be updated (the new value will correspond * exactly to the data thust written back, as we hold the file lock, which * prevents a concurrent write. If the attempt fails, a * BackingStoreException is thrown and both the backing store (prefsFile) * and lastSyncTime will be unaffected by this call. This call will * NEVER leave prefsFile in a corrupt state. */ private void writeBackCache() throws BackingStoreException { try { AccessController.doPrivileged( new PrivilegedExceptionAction() { public Void run() throws BackingStoreException { try { if (!dir.exists() && !dir.mkdirs()) throw new BackingStoreException(dir + " create failed."); try (FileOutputStream fos = new FileOutputStream(tmpFile)) { XmlSupport.exportMap(fos, prefsCache); } if (!tmpFile.renameTo(prefsFile)) throw new BackingStoreException("Can't rename " + tmpFile + " to " + prefsFile); } catch(Exception e) { if (e instanceof BackingStoreException) throw (BackingStoreException)e; throw new BackingStoreException(e); } return null; } }); } catch (PrivilegedActionException e) { throw (BackingStoreException) e.getException(); } } protected String[] keysSpi() { initCacheIfNecessary(); return prefsCache.keySet().toArray(new String[prefsCache.size()]); } protected String[] childrenNamesSpi() { return AccessController.doPrivileged( new PrivilegedAction() { public String[] run() { List result = new ArrayList<>(); File[] dirContents = dir.listFiles(); if (dirContents != null) { for (int i = 0; i < dirContents.length; i++) if (dirContents[i].isDirectory()) result.add(nodeName(dirContents[i].getName())); } return result.toArray(EMPTY_STRING_ARRAY); } }); } private static final String[] EMPTY_STRING_ARRAY = new String[0]; protected AbstractPreferences childSpi(String name) { return new FileSystemPreferences(this, name); } public void removeNode() throws BackingStoreException { synchronized (isUserNode()? userLockFile: systemLockFile) { // to remove a node we need an exclusive lock if (!lockFile(false)) throw(new BackingStoreException("Couldn't get file lock.")); try { super.removeNode(); } finally { unlockFile(); } } } /** * Called with file lock held (in addition to node locks). */ protected void removeNodeSpi() throws BackingStoreException { try { AccessController.doPrivileged( new PrivilegedExceptionAction() { public Void run() throws BackingStoreException { if (changeLog.contains(nodeCreate)) { changeLog.remove(nodeCreate); nodeCreate = null; return null; } if (!dir.exists()) return null; prefsFile.delete(); tmpFile.delete(); // dir should be empty now. If it's not, empty it File[] junk = dir.listFiles(); if (junk.length != 0) { getLogger().warning( "Found extraneous files when removing node: " + Arrays.asList(junk)); for (int i=0; i() { public Long run() { long nmt; if (isUserNode()) { nmt = userRootModFile.lastModified(); isUserRootModified = userRootModTime == nmt; } else { nmt = systemRootModFile.lastModified(); isSystemRootModified = systemRootModTime == nmt; } return new Long(nmt); } }); try { super.sync(); AccessController.doPrivileged(new PrivilegedAction() { public Void run() { if (isUserNode()) { userRootModTime = newModTime.longValue() + 1000; userRootModFile.setLastModified(userRootModTime); } else { systemRootModTime = newModTime.longValue() + 1000; systemRootModFile.setLastModified(systemRootModTime); } return null; } }); } finally { unlockFile(); } } } protected void syncSpi() throws BackingStoreException { try { AccessController.doPrivileged( new PrivilegedExceptionAction() { public Void run() throws BackingStoreException { syncSpiPrivileged(); return null; } }); } catch (PrivilegedActionException e) { throw (BackingStoreException) e.getException(); } } private void syncSpiPrivileged() throws BackingStoreException { if (isRemoved()) throw new IllegalStateException("Node has been removed"); if (prefsCache == null) return; // We've never been used, don't bother syncing long lastModifiedTime; if ((isUserNode() ? isUserRootModified : isSystemRootModified)) { lastModifiedTime = prefsFile.lastModified(); if (lastModifiedTime != lastSyncTime) { // Prefs at this node were externally modified; read in node and // playback any local mods since last sync loadCache(); replayChanges(); lastSyncTime = lastModifiedTime; } } else if (lastSyncTime != 0 && !dir.exists()) { // This node was removed in the background. Playback any changes // against a virgin (empty) Map. prefsCache = new TreeMap<>(); replayChanges(); } if (!changeLog.isEmpty()) { writeBackCache(); // Creates directory & file if necessary /* * Attempt succeeded; it's barely possible that the call to * lastModified might fail (i.e., return 0), but this would not * be a disaster, as lastSyncTime is allowed to lag. */ lastModifiedTime = prefsFile.lastModified(); /* If lastSyncTime did not change, or went back * increment by 1 second. Since we hold the lock * lastSyncTime always monotonically encreases in the * atomic sense. */ if (lastSyncTime <= lastModifiedTime) { lastSyncTime = lastModifiedTime + 1000; prefsFile.setLastModified(lastSyncTime); } changeLog.clear(); } } public void flush() throws BackingStoreException { if (isRemoved()) return; sync(); } protected void flushSpi() throws BackingStoreException { // assert false; } /** * Returns true if the specified character is appropriate for use in * Unix directory names. A character is appropriate if it's a printable * ASCII character (> 0x1f && < 0x7f) and unequal to slash ('/', 0x2f), * dot ('.', 0x2e), or underscore ('_', 0x5f). */ private static boolean isDirChar(char ch) { return ch > 0x1f && ch < 0x7f && ch != '/' && ch != '.' && ch != '_'; } /** * Returns the directory name corresponding to the specified node name. * Generally, this is just the node name. If the node name includes * inappropriate characters (as per isDirChar) it is translated to Base64. * with the underscore character ('_', 0x5f) prepended. */ private static String dirName(String nodeName) { for (int i=0, n=nodeName.length(); i < n; i++) if (!isDirChar(nodeName.charAt(i))) return "_" + Base64.byteArrayToAltBase64(byteArray(nodeName)); return nodeName; } /** * Translate a string into a byte array by translating each character * into two bytes, high-byte first ("big-endian"). */ private static byte[] byteArray(String s) { int len = s.length(); byte[] result = new byte[2*len]; for (int i=0, j=0; i>8); result[j++] = (byte) c; } return result; } /** * Returns the node name corresponding to the specified directory name. * (Inverts the transformation of dirName(String). */ private static String nodeName(String dirName) { if (dirName.charAt(0) != '_') return dirName; byte a[] = Base64.altBase64ToByteArray(dirName.substring(1)); StringBuffer result = new StringBuffer(a.length/2); for (int i = 0; i < a.length; ) { int highByte = a[i++] & 0xff; int lowByte = a[i++] & 0xff; result.append((char) ((highByte << 8) | lowByte)); } return result.toString(); } /** * Try to acquire the appropriate file lock (user or system). If * the initial attempt fails, several more attempts are made using * an exponential backoff strategy. If all attempts fail, this method * returns false. * @throws SecurityException if file access denied. */ private boolean lockFile(boolean shared) throws SecurityException{ boolean usernode = isUserNode(); int[] result; int errorCode = 0; File lockFile = (usernode ? userLockFile : systemLockFile); long sleepTime = INIT_SLEEP_TIME; for (int i = 0; i < MAX_ATTEMPTS; i++) { try { int perm = (usernode? USER_READ_WRITE: USER_RW_ALL_READ); result = lockFile0(lockFile.getCanonicalPath(), perm, shared); errorCode = result[ERROR_CODE]; if (result[LOCK_HANDLE] != 0) { if (usernode) { userRootLockHandle = result[LOCK_HANDLE]; } else { systemRootLockHandle = result[LOCK_HANDLE]; } return true; } } catch(IOException e) { // // If at first, you don't succeed... } try { Thread.sleep(sleepTime); } catch(InterruptedException e) { checkLockFile0ErrorCode(errorCode); return false; } sleepTime *= 2; } checkLockFile0ErrorCode(errorCode); return false; } /** * Checks if unlockFile0() returned an error. Throws a SecurityException, * if access denied. Logs a warning otherwise. */ private void checkLockFile0ErrorCode (int errorCode) throws SecurityException { if (errorCode == EACCES) throw new SecurityException("Could not lock " + (isUserNode()? "User prefs." : "System prefs.") + " Lock file access denied."); if (errorCode != EAGAIN) getLogger().warning("Could not lock " + (isUserNode()? "User prefs. " : "System prefs.") + " Unix error code " + errorCode + "."); } /** * Locks file using UNIX file locking. * @param fileName Absolute file name of the lock file. * @return Returns a lock handle, used to unlock the file. */ private static native int[] lockFile0(String fileName, int permission, boolean shared); /** * Unlocks file previously locked by lockFile0(). * @param lockHandle Handle to the file lock. * @return Returns zero if OK, UNIX error code if failure. */ private static native int unlockFile0(int lockHandle); /** * Changes UNIX file permissions. */ private static native int chmod(String fileName, int permission); /** * Initial time between lock attempts, in ms. The time is doubled * after each failing attempt (except the first). */ private static int INIT_SLEEP_TIME = 50; /** * Maximum number of lock attempts. */ private static int MAX_ATTEMPTS = 5; /** * Release the the appropriate file lock (user or system). * @throws SecurityException if file access denied. */ private void unlockFile() { int result; boolean usernode = isUserNode(); File lockFile = (usernode ? userLockFile : systemLockFile); int lockHandle = ( usernode ? userRootLockHandle:systemRootLockHandle); if (lockHandle == 0) { getLogger().warning("Unlock: zero lockHandle for " + (usernode ? "user":"system") + " preferences.)"); return; } result = unlockFile0(lockHandle); if (result != 0) { getLogger().warning("Could not drop file-lock on " + (isUserNode() ? "user" : "system") + " preferences." + " Unix error code " + result + "."); if (result == EACCES) throw new SecurityException("Could not unlock" + (isUserNode()? "User prefs." : "System prefs.") + " Lock file access denied."); } if (isUserNode()) { userRootLockHandle = 0; } else { systemRootLockHandle = 0; } } }