| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * 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; |
| |
| // Android-changed: @hide. |
| /** |
| * 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 |
| * |
| * @hide |
| */ |
| public class FileSystemPreferences extends AbstractPreferences { |
| /** |
| * 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<Void>() { |
| 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<Void>() { |
| 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<String, String> 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<Change> 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<n; i++) |
| changeLog.get(i).replay(); |
| } |
| |
| static { |
| // Add shutdown hook to flush cached prefs on normal termination |
| Runtime.getRuntime().addShutdownHook(new Thread() { |
| public void run() { |
| syncWorld(); |
| } |
| }); |
| } |
| |
| 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"); |
| } |
| |
| /** @hide for unit testing only */ |
| // Android-added constructor for testing. |
| public FileSystemPreferences(String path, File lockFile, boolean isUserNode) { |
| super(null, ""); |
| this.isUserNode = isUserNode; |
| this.dir = new File(path); |
| prefsFile = new File(dir, "prefs.xml"); |
| tmpFile = new File(dir, "prefs.tmp"); |
| newNode = !dir.exists(); |
| if (newNode) { |
| // These 2 things guarantee node will get wrtten at next flush/sync |
| prefsCache = new TreeMap<>(); |
| nodeCreate = new NodeCreate(); |
| changeLog.add(nodeCreate); |
| } |
| |
| if (isUserNode) { |
| userLockFile = lockFile; |
| userRootModFile = new File(lockFile.getParentFile(), lockFile.getName() + ".rootmod"); |
| } else { |
| systemLockFile = lockFile; |
| systemRootModFile = new File(lockFile.getParentFile(), lockFile.getName() + ".rootmod"); |
| } |
| } |
| |
| /** |
| * 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<Void>() { |
| 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 { |
| Map<String, String> 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 { |
| // Android-added: This exception may be ignored by some callers, |
| // added a logger entry to prevent omitting it completely. |
| getLogger().warning("Exception while reading cache: " |
| + e.getMessage()); |
| throw new BackingStoreException(e); |
| } |
| } |
| // Attempt succeeded; update state |
| prefsCache = m; |
| lastSyncTime = newLastSyncTime; |
| } |
| |
| /** |
| * 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<Void>() { |
| 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<String[]>() { |
| public String[] run() { |
| List<String> 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<Void>() { |
| 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<junk.length; i++) |
| junk[i].delete(); |
| } |
| if (!dir.delete()) |
| throw new BackingStoreException("Couldn't delete dir: " |
| + dir); |
| return null; |
| } |
| }); |
| } catch (PrivilegedActionException e) { |
| throw (BackingStoreException) e.getException(); |
| } |
| } |
| |
| public synchronized void sync() throws BackingStoreException { |
| boolean userNode = isUserNode(); |
| boolean shared; |
| |
| if (userNode) { |
| shared = false; /* use exclusive lock for user prefs */ |
| } else { |
| /* if can write to system root, use exclusive lock. |
| otherwise use shared lock. */ |
| shared = !isSystemRootWritable; |
| } |
| synchronized (isUserNode()? userLockFile:systemLockFile) { |
| if (!lockFile(shared)) |
| throw(new BackingStoreException("Couldn't get file lock.")); |
| final Long newModTime = |
| AccessController.doPrivileged( |
| new PrivilegedAction<Long>() { |
| 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<Void>() { |
| 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 { |
| syncSpiPrivileged(); |
| } |
| |
| 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<len; i++) { |
| char c = s.charAt(i); |
| result[j++] = (byte) (c>>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); |
| // Android-changed: don't lose the interrupt unless we throw. |
| Thread.currentThread().interrupt(); |
| 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; |
| } |
| } |
| } |