1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved. 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * This code is free software; you can redistribute it and/or modify it 7 * under the terms of the GNU General Public License version 2 only, as 8 * published by the Free Software Foundation. Oracle designates this 9 * particular file as subject to the "Classpath" exception as provided 10 * by Oracle in the LICENSE file that accompanied this code. 11 * 12 * This code is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 15 * version 2 for more details (a copy is included in the LICENSE file that 16 * accompanied this code). 17 * 18 * You should have received a copy of the GNU General Public License version 19 * 2 along with this work; if not, write to the Free Software Foundation, 20 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 21 * 22 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 23 * or visit www.oracle.com if you need additional information or have any 24 * questions. 25 */ 26 27 package java.util.prefs; 28 import java.util.*; 29 import java.io.*; 30 import java.security.AccessController; 31 import java.security.PrivilegedAction; 32 import java.security.PrivilegedExceptionAction; 33 import java.security.PrivilegedActionException; 34 35 import sun.util.logging.PlatformLogger; 36 37 /** 38 * Preferences implementation for Unix. Preferences are stored in the file 39 * system, with one directory per preferences node. All of the preferences 40 * at each node are stored in a single file. Atomic file system operations 41 * (e.g. File.renameTo) are used to ensure integrity. An in-memory cache of 42 * the "explored" portion of the tree is maintained for performance, and 43 * written back to the disk periodically. File-locking is used to ensure 44 * reasonable behavior when multiple VMs are running at the same time. 45 * (The file lock is obtained only for sync(), flush() and removeNode().) 46 * 47 * @author Josh Bloch 48 * @see Preferences 49 * @since 1.4 50 * 51 * @hide 52 */ 53 // Android changed: @hide. 54 public class FileSystemPreferences extends AbstractPreferences { 55 /** 56 * Returns logger for error messages. Backing store exceptions are logged at 57 * WARNING level. 58 */ 59 private static PlatformLogger getLogger() { 60 return PlatformLogger.getLogger("java.util.prefs"); 61 } 62 63 /** 64 * Directory for system preferences. 65 */ 66 private static File systemRootDir; 67 68 /* 69 * Flag, indicating whether systemRoot directory is writable 70 */ 71 private static boolean isSystemRootWritable; 72 73 /** 74 * Directory for user preferences. 75 */ 76 private static File userRootDir; 77 78 /* 79 * Flag, indicating whether userRoot directory is writable 80 */ 81 private static boolean isUserRootWritable; 82 83 /** 84 * The user root. 85 */ 86 static Preferences userRoot = null; 87 88 static synchronized Preferences getUserRoot() { 89 if (userRoot == null) { 90 setupUserRoot(); 91 userRoot = new FileSystemPreferences(true); 92 } 93 return userRoot; 94 } 95 96 private static void setupUserRoot() { 97 AccessController.doPrivileged(new PrivilegedAction<Void>() { 98 public Void run() { 99 userRootDir = 100 new File(System.getProperty("java.util.prefs.userRoot", 101 System.getProperty("user.home")), ".java/.userPrefs"); 102 // Attempt to create root dir if it does not yet exist. 103 if (!userRootDir.exists()) { 104 if (userRootDir.mkdirs()) { 105 try { 106 chmod(userRootDir.getCanonicalPath(), USER_RWX); 107 } catch (IOException e) { 108 getLogger().warning("Could not change permissions" + 109 " on userRoot directory. "); 110 } 111 getLogger().info("Created user preferences directory."); 112 } 113 else 114 getLogger().warning("Couldn't create user preferences" + 115 " directory. User preferences are unusable."); 116 } 117 isUserRootWritable = userRootDir.canWrite(); 118 String USER_NAME = System.getProperty("user.name"); 119 userLockFile = new File (userRootDir,".user.lock." + USER_NAME); 120 userRootModFile = new File (userRootDir, 121 ".userRootModFile." + USER_NAME); 122 if (!userRootModFile.exists()) 123 try { 124 // create if does not exist. 125 userRootModFile.createNewFile(); 126 // Only user can read/write userRootModFile. 127 int result = chmod(userRootModFile.getCanonicalPath(), 128 USER_READ_WRITE); 129 if (result !=0) 130 getLogger().warning("Problem creating userRoot " + 131 "mod file. Chmod failed on " + 132 userRootModFile.getCanonicalPath() + 133 " Unix error code " + result); 134 } catch (IOException e) { 135 getLogger().warning(e.toString()); 136 } 137 userRootModTime = userRootModFile.lastModified(); 138 return null; 139 } 140 }); 141 } 142 143 144 /** 145 * The system root. 146 */ 147 static Preferences systemRoot; 148 149 static synchronized Preferences getSystemRoot() { 150 if (systemRoot == null) { 151 setupSystemRoot(); 152 systemRoot = new FileSystemPreferences(false); 153 } 154 return systemRoot; 155 } 156 157 private static void setupSystemRoot() { 158 AccessController.doPrivileged(new PrivilegedAction<Void>() { 159 public Void run() { 160 String systemPrefsDirName = 161 System.getProperty("java.util.prefs.systemRoot","/etc/.java"); 162 systemRootDir = 163 new File(systemPrefsDirName, ".systemPrefs"); 164 // Attempt to create root dir if it does not yet exist. 165 if (!systemRootDir.exists()) { 166 // system root does not exist in /etc/.java 167 // Switching to java.home 168 systemRootDir = 169 new File(System.getProperty("java.home"), 170 ".systemPrefs"); 171 if (!systemRootDir.exists()) { 172 if (systemRootDir.mkdirs()) { 173 getLogger().info( 174 "Created system preferences directory " 175 + "in java.home."); 176 try { 177 chmod(systemRootDir.getCanonicalPath(), 178 USER_RWX_ALL_RX); 179 } catch (IOException e) { 180 } 181 } else { 182 getLogger().warning("Could not create " 183 + "system preferences directory. System " 184 + "preferences are unusable."); 185 } 186 } 187 } 188 isSystemRootWritable = systemRootDir.canWrite(); 189 systemLockFile = new File(systemRootDir, ".system.lock"); 190 systemRootModFile = 191 new File (systemRootDir,".systemRootModFile"); 192 if (!systemRootModFile.exists() && isSystemRootWritable) 193 try { 194 // create if does not exist. 195 systemRootModFile.createNewFile(); 196 int result = chmod(systemRootModFile.getCanonicalPath(), 197 USER_RW_ALL_READ); 198 if (result !=0) 199 getLogger().warning("Chmod failed on " + 200 systemRootModFile.getCanonicalPath() + 201 " Unix error code " + result); 202 } catch (IOException e) { getLogger().warning(e.toString()); 203 } 204 systemRootModTime = systemRootModFile.lastModified(); 205 return null; 206 } 207 }); 208 } 209 210 211 /** 212 * Unix user write/read permission 213 */ 214 private static final int USER_READ_WRITE = 0600; 215 216 private static final int USER_RW_ALL_READ = 0644; 217 218 219 private static final int USER_RWX_ALL_RX = 0755; 220 221 private static final int USER_RWX = 0700; 222 223 /** 224 * The lock file for the user tree. 225 */ 226 static File userLockFile; 227 228 229 230 /** 231 * The lock file for the system tree. 232 */ 233 static File systemLockFile; 234 235 /** 236 * Unix lock handle for userRoot. 237 * Zero, if unlocked. 238 */ 239 240 private static int userRootLockHandle = 0; 241 242 /** 243 * Unix lock handle for systemRoot. 244 * Zero, if unlocked. 245 */ 246 247 private static int systemRootLockHandle = 0; 248 249 /** 250 * The directory representing this preference node. There is no guarantee 251 * that this directory exits, as another VM can delete it at any time 252 * that it (the other VM) holds the file-lock. While the root node cannot 253 * be deleted, it may not yet have been created, or the underlying 254 * directory could have been deleted accidentally. 255 */ 256 private final File dir; 257 258 /** 259 * The file representing this preference node's preferences. 260 * The file format is undocumented, and subject to change 261 * from release to release, but I'm sure that you can figure 262 * it out if you try real hard. 263 */ 264 private final File prefsFile; 265 266 /** 267 * A temporary file used for saving changes to preferences. As part of 268 * the sync operation, changes are first saved into this file, and then 269 * atomically renamed to prefsFile. This results in an atomic state 270 * change from one valid set of preferences to another. The 271 * the file-lock is held for the duration of this transformation. 272 */ 273 private final File tmpFile; 274 275 /** 276 * File, which keeps track of global modifications of userRoot. 277 */ 278 private static File userRootModFile; 279 280 /** 281 * Flag, which indicated whether userRoot was modified by another VM 282 */ 283 private static boolean isUserRootModified = false; 284 285 /** 286 * Keeps track of userRoot modification time. This time is reset to 287 * zero after UNIX reboot, and is increased by 1 second each time 288 * userRoot is modified. 289 */ 290 private static long userRootModTime; 291 292 293 /* 294 * File, which keeps track of global modifications of systemRoot 295 */ 296 private static File systemRootModFile; 297 /* 298 * Flag, which indicates whether systemRoot was modified by another VM 299 */ 300 private static boolean isSystemRootModified = false; 301 302 /** 303 * Keeps track of systemRoot modification time. This time is reset to 304 * zero after system reboot, and is increased by 1 second each time 305 * systemRoot is modified. 306 */ 307 private static long systemRootModTime; 308 309 /** 310 * Locally cached preferences for this node (includes uncommitted 311 * changes). This map is initialized with from disk when the first get or 312 * put operation occurs on this node. It is synchronized with the 313 * corresponding disk file (prefsFile) by the sync operation. The initial 314 * value is read *without* acquiring the file-lock. 315 */ 316 private Map<String, String> prefsCache = null; 317 318 /** 319 * The last modification time of the file backing this node at the time 320 * that prefCache was last synchronized (or initially read). This 321 * value is set *before* reading the file, so it's conservative; the 322 * actual timestamp could be (slightly) higher. A value of zero indicates 323 * that we were unable to initialize prefsCache from the disk, or 324 * have not yet attempted to do so. (If prefsCache is non-null, it 325 * indicates the former; if it's null, the latter.) 326 */ 327 private long lastSyncTime = 0; 328 329 /** 330 * Unix error code for locked file. 331 */ 332 private static final int EAGAIN = 11; 333 334 /** 335 * Unix error code for denied access. 336 */ 337 private static final int EACCES = 13; 338 339 /* Used to interpret results of native functions */ 340 private static final int LOCK_HANDLE = 0; 341 private static final int ERROR_CODE = 1; 342 343 /** 344 * A list of all uncommitted preference changes. The elements in this 345 * list are of type PrefChange. If this node is concurrently modified on 346 * disk by another VM, the two sets of changes are merged when this node 347 * is sync'ed by overwriting our prefsCache with the preference map last 348 * written out to disk (by the other VM), and then replaying this change 349 * log against that map. The resulting map is then written back 350 * to the disk. 351 */ 352 final List<Change> changeLog = new ArrayList<>(); 353 354 /** 355 * Represents a change to a preference. 356 */ 357 private abstract class Change { 358 /** 359 * Reapplies the change to prefsCache. 360 */ 361 abstract void replay(); 362 }; 363 364 /** 365 * Represents a preference put. 366 */ 367 private class Put extends Change { 368 String key, value; 369 370 Put(String key, String value) { 371 this.key = key; 372 this.value = value; 373 } 374 375 void replay() { 376 prefsCache.put(key, value); 377 } 378 } 379 380 /** 381 * Represents a preference remove. 382 */ 383 private class Remove extends Change { 384 String key; 385 386 Remove(String key) { 387 this.key = key; 388 } 389 390 void replay() { 391 prefsCache.remove(key); 392 } 393 } 394 395 /** 396 * Represents the creation of this node. 397 */ 398 private class NodeCreate extends Change { 399 /** 400 * Performs no action, but the presence of this object in changeLog 401 * will force the node and its ancestors to be made permanent at the 402 * next sync. 403 */ 404 void replay() { 405 } 406 } 407 408 /** 409 * NodeCreate object for this node. 410 */ 411 NodeCreate nodeCreate = null; 412 413 /** 414 * Replay changeLog against prefsCache. 415 */ 416 private void replayChanges() { 417 for (int i = 0, n = changeLog.size(); i<n; i++) 418 changeLog.get(i).replay(); 419 } 420 421 static { 422 // Add shutdown hook to flush cached prefs on normal termination 423 Runtime.getRuntime().addShutdownHook(new Thread() { 424 public void run() { 425 syncWorld(); 426 } 427 }); 428 } 429 430 private static void syncWorld() { 431 /* 432 * Synchronization necessary because userRoot and systemRoot are 433 * lazily initialized. 434 */ 435 Preferences userRt; 436 Preferences systemRt; 437 synchronized(FileSystemPreferences.class) { 438 userRt = userRoot; 439 systemRt = systemRoot; 440 } 441 442 try { 443 if (userRt != null) 444 userRt.flush(); 445 } catch(BackingStoreException e) { 446 getLogger().warning("Couldn't flush user prefs: " + e); 447 } 448 449 try { 450 if (systemRt != null) 451 systemRt.flush(); 452 } catch(BackingStoreException e) { 453 getLogger().warning("Couldn't flush system prefs: " + e); 454 } 455 } 456 457 private final boolean isUserNode; 458 459 /** 460 * Special constructor for roots (both user and system). This constructor 461 * will only be called twice, by the static initializer. 462 */ 463 private FileSystemPreferences(boolean user) { 464 super(null, ""); 465 isUserNode = user; 466 dir = (user ? userRootDir: systemRootDir); 467 prefsFile = new File(dir, "prefs.xml"); 468 tmpFile = new File(dir, "prefs.tmp"); 469 } 470 471 /** @hide for unit testing only */ 472 // Android added constructor for testing. 473 public FileSystemPreferences(String path, File lockFile, boolean isUserNode) { 474 super(null, ""); 475 this.isUserNode = isUserNode; 476 this.dir = new File(path); 477 prefsFile = new File(dir, "prefs.xml"); 478 tmpFile = new File(dir, "prefs.tmp"); 479 newNode = !dir.exists(); 480 if (newNode) { 481 // These 2 things guarantee node will get wrtten at next flush/sync 482 prefsCache = new TreeMap<>(); 483 nodeCreate = new NodeCreate(); 484 changeLog.add(nodeCreate); 485 } 486 487 if (isUserNode) { 488 userLockFile = lockFile; 489 userRootModFile = new File(lockFile.getParentFile(), lockFile.getName() + ".rootmod"); 490 } else { 491 systemLockFile = lockFile; 492 systemRootModFile = new File(lockFile.getParentFile(), lockFile.getName() + ".rootmod"); 493 } 494 } 495 496 /** 497 * Construct a new FileSystemPreferences instance with the specified 498 * parent node and name. This constructor, called from childSpi, 499 * is used to make every node except for the two //roots. 500 */ 501 private FileSystemPreferences(FileSystemPreferences parent, String name) { 502 super(parent, name); 503 isUserNode = parent.isUserNode; 504 dir = new File(parent.dir, dirName(name)); 505 prefsFile = new File(dir, "prefs.xml"); 506 tmpFile = new File(dir, "prefs.tmp"); 507 AccessController.doPrivileged(new PrivilegedAction<Void>() { 508 public Void run() { 509 newNode = !dir.exists(); 510 return null; 511 } 512 }); 513 if (newNode) { 514 // These 2 things guarantee node will get wrtten at next flush/sync 515 prefsCache = new TreeMap<>(); 516 nodeCreate = new NodeCreate(); 517 changeLog.add(nodeCreate); 518 } 519 } 520 521 public boolean isUserNode() { 522 return isUserNode; 523 } 524 525 protected void putSpi(String key, String value) { 526 initCacheIfNecessary(); 527 changeLog.add(new Put(key, value)); 528 prefsCache.put(key, value); 529 } 530 531 protected String getSpi(String key) { 532 initCacheIfNecessary(); 533 return prefsCache.get(key); 534 } 535 536 protected void removeSpi(String key) { 537 initCacheIfNecessary(); 538 changeLog.add(new Remove(key)); 539 prefsCache.remove(key); 540 } 541 542 /** 543 * Initialize prefsCache if it has yet to be initialized. When this method 544 * returns, prefsCache will be non-null. If the data was successfully 545 * read from the file, lastSyncTime will be updated. If prefsCache was 546 * null, but it was impossible to read the file (because it didn't 547 * exist or for any other reason) prefsCache will be initialized to an 548 * empty, modifiable Map, and lastSyncTime remain zero. 549 */ 550 private void initCacheIfNecessary() { 551 if (prefsCache != null) 552 return; 553 554 try { 555 loadCache(); 556 } catch(Exception e) { 557 // assert lastSyncTime == 0; 558 prefsCache = new TreeMap<>(); 559 } 560 } 561 562 /** 563 * Attempt to load prefsCache from the backing store. If the attempt 564 * succeeds, lastSyncTime will be updated (the new value will typically 565 * correspond to the data loaded into the map, but it may be less, 566 * if another VM is updating this node concurrently). If the attempt 567 * fails, a BackingStoreException is thrown and both prefsCache and 568 * lastSyncTime are unaffected by the call. 569 */ 570 private void loadCache() throws BackingStoreException { 571 Map<String, String> m = new TreeMap<>(); 572 long newLastSyncTime = 0; 573 try { 574 newLastSyncTime = prefsFile.lastModified(); 575 try (FileInputStream fis = new FileInputStream(prefsFile)) { 576 XmlSupport.importMap(fis, m); 577 } 578 } catch(Exception e) { 579 if (e instanceof InvalidPreferencesFormatException) { 580 getLogger().warning("Invalid preferences format in " 581 + prefsFile.getPath()); 582 prefsFile.renameTo( new File( 583 prefsFile.getParentFile(), 584 "IncorrectFormatPrefs.xml")); 585 m = new TreeMap<>(); 586 } else if (e instanceof FileNotFoundException) { 587 getLogger().warning("Prefs file removed in background " 588 + prefsFile.getPath()); 589 } else { 590 // Android added: This exception may be ignored by some callers, 591 // added a logger entry to prevent omitting it completely. 592 getLogger().warning("Exception while reading cache: " 593 + e.getMessage()); 594 throw new BackingStoreException(e); 595 } 596 } 597 // Attempt succeeded; update state 598 prefsCache = m; 599 lastSyncTime = newLastSyncTime; 600 } 601 602 /** 603 * Attempt to write back prefsCache to the backing store. If the attempt 604 * succeeds, lastSyncTime will be updated (the new value will correspond 605 * exactly to the data thust written back, as we hold the file lock, which 606 * prevents a concurrent write. If the attempt fails, a 607 * BackingStoreException is thrown and both the backing store (prefsFile) 608 * and lastSyncTime will be unaffected by this call. This call will 609 * NEVER leave prefsFile in a corrupt state. 610 */ 611 private void writeBackCache() throws BackingStoreException { 612 try { 613 AccessController.doPrivileged( 614 new PrivilegedExceptionAction<Void>() { 615 public Void run() throws BackingStoreException { 616 try { 617 if (!dir.exists() && !dir.mkdirs()) 618 throw new BackingStoreException(dir + 619 " create failed."); 620 try (FileOutputStream fos = new FileOutputStream(tmpFile)) { 621 XmlSupport.exportMap(fos, prefsCache); 622 } 623 if (!tmpFile.renameTo(prefsFile)) 624 throw new BackingStoreException("Can't rename " + 625 tmpFile + " to " + prefsFile); 626 } catch(Exception e) { 627 if (e instanceof BackingStoreException) 628 throw (BackingStoreException)e; 629 throw new BackingStoreException(e); 630 } 631 return null; 632 } 633 }); 634 } catch (PrivilegedActionException e) { 635 throw (BackingStoreException) e.getException(); 636 } 637 } 638 639 protected String[] keysSpi() { 640 initCacheIfNecessary(); 641 return prefsCache.keySet().toArray(new String[prefsCache.size()]); 642 } 643 644 protected String[] childrenNamesSpi() { 645 return AccessController.doPrivileged( 646 new PrivilegedAction<String[]>() { 647 public String[] run() { 648 List<String> result = new ArrayList<>(); 649 File[] dirContents = dir.listFiles(); 650 if (dirContents != null) { 651 for (int i = 0; i < dirContents.length; i++) 652 if (dirContents[i].isDirectory()) 653 result.add(nodeName(dirContents[i].getName())); 654 } 655 return result.toArray(EMPTY_STRING_ARRAY); 656 } 657 }); 658 } 659 660 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 661 662 protected AbstractPreferences childSpi(String name) { 663 return new FileSystemPreferences(this, name); 664 } 665 666 public void removeNode() throws BackingStoreException { 667 synchronized (isUserNode()? userLockFile: systemLockFile) { 668 // to remove a node we need an exclusive lock 669 if (!lockFile(false)) 670 throw(new BackingStoreException("Couldn't get file lock.")); 671 try { 672 super.removeNode(); 673 } finally { 674 unlockFile(); 675 } 676 } 677 } 678 679 /** 680 * Called with file lock held (in addition to node locks). 681 */ 682 protected void removeNodeSpi() throws BackingStoreException { 683 try { 684 AccessController.doPrivileged( 685 new PrivilegedExceptionAction<Void>() { 686 public Void run() throws BackingStoreException { 687 if (changeLog.contains(nodeCreate)) { 688 changeLog.remove(nodeCreate); 689 nodeCreate = null; 690 return null; 691 } 692 if (!dir.exists()) 693 return null; 694 prefsFile.delete(); 695 tmpFile.delete(); 696 // dir should be empty now. If it's not, empty it 697 File[] junk = dir.listFiles(); 698 if (junk.length != 0) { 699 getLogger().warning( 700 "Found extraneous files when removing node: " 701 + Arrays.asList(junk)); 702 for (int i=0; i<junk.length; i++) 703 junk[i].delete(); 704 } 705 if (!dir.delete()) 706 throw new BackingStoreException("Couldn't delete dir: " 707 + dir); 708 return null; 709 } 710 }); 711 } catch (PrivilegedActionException e) { 712 throw (BackingStoreException) e.getException(); 713 } 714 } 715 716 public synchronized void sync() throws BackingStoreException { 717 boolean userNode = isUserNode(); 718 boolean shared; 719 720 if (userNode) { 721 shared = false; /* use exclusive lock for user prefs */ 722 } else { 723 /* if can write to system root, use exclusive lock. 724 otherwise use shared lock. */ 725 shared = !isSystemRootWritable; 726 } 727 synchronized (isUserNode()? userLockFile:systemLockFile) { 728 if (!lockFile(shared)) 729 throw(new BackingStoreException("Couldn't get file lock.")); 730 final Long newModTime = 731 AccessController.doPrivileged( 732 new PrivilegedAction<Long>() { 733 public Long run() { 734 long nmt; 735 if (isUserNode()) { 736 nmt = userRootModFile.lastModified(); 737 isUserRootModified = userRootModTime == nmt; 738 } else { 739 nmt = systemRootModFile.lastModified(); 740 isSystemRootModified = systemRootModTime == nmt; 741 } 742 return new Long(nmt); 743 } 744 }); 745 try { 746 super.sync(); 747 AccessController.doPrivileged(new PrivilegedAction<Void>() { 748 public Void run() { 749 if (isUserNode()) { 750 userRootModTime = newModTime.longValue() + 1000; 751 userRootModFile.setLastModified(userRootModTime); 752 } else { 753 systemRootModTime = newModTime.longValue() + 1000; 754 systemRootModFile.setLastModified(systemRootModTime); 755 } 756 return null; 757 } 758 }); 759 } finally { 760 unlockFile(); 761 } 762 } 763 } 764 765 protected void syncSpi() throws BackingStoreException { 766 syncSpiPrivileged(); 767 } 768 769 private void syncSpiPrivileged() throws BackingStoreException { 770 if (isRemoved()) 771 throw new IllegalStateException("Node has been removed"); 772 if (prefsCache == null) 773 return; // We've never been used, don't bother syncing 774 long lastModifiedTime; 775 if ((isUserNode() ? isUserRootModified : isSystemRootModified)) { 776 lastModifiedTime = prefsFile.lastModified(); 777 if (lastModifiedTime != lastSyncTime) { 778 // Prefs at this node were externally modified; read in node and 779 // playback any local mods since last sync 780 loadCache(); 781 replayChanges(); 782 lastSyncTime = lastModifiedTime; 783 } 784 } else if (lastSyncTime != 0 && !dir.exists()) { 785 // This node was removed in the background. Playback any changes 786 // against a virgin (empty) Map. 787 prefsCache = new TreeMap<>(); 788 replayChanges(); 789 } 790 if (!changeLog.isEmpty()) { 791 writeBackCache(); // Creates directory & file if necessary 792 /* 793 * Attempt succeeded; it's barely possible that the call to 794 * lastModified might fail (i.e., return 0), but this would not 795 * be a disaster, as lastSyncTime is allowed to lag. 796 */ 797 lastModifiedTime = prefsFile.lastModified(); 798 /* If lastSyncTime did not change, or went back 799 * increment by 1 second. Since we hold the lock 800 * lastSyncTime always monotonically encreases in the 801 * atomic sense. 802 */ 803 if (lastSyncTime <= lastModifiedTime) { 804 lastSyncTime = lastModifiedTime + 1000; 805 prefsFile.setLastModified(lastSyncTime); 806 } 807 changeLog.clear(); 808 } 809 } 810 811 public void flush() throws BackingStoreException { 812 if (isRemoved()) 813 return; 814 sync(); 815 } 816 817 protected void flushSpi() throws BackingStoreException { 818 // assert false; 819 } 820 821 /** 822 * Returns true if the specified character is appropriate for use in 823 * Unix directory names. A character is appropriate if it's a printable 824 * ASCII character (> 0x1f && < 0x7f) and unequal to slash ('/', 0x2f), 825 * dot ('.', 0x2e), or underscore ('_', 0x5f). 826 */ 827 private static boolean isDirChar(char ch) { 828 return ch > 0x1f && ch < 0x7f && ch != '/' && ch != '.' && ch != '_'; 829 } 830 831 /** 832 * Returns the directory name corresponding to the specified node name. 833 * Generally, this is just the node name. If the node name includes 834 * inappropriate characters (as per isDirChar) it is translated to Base64. 835 * with the underscore character ('_', 0x5f) prepended. 836 */ 837 private static String dirName(String nodeName) { 838 for (int i=0, n=nodeName.length(); i < n; i++) 839 if (!isDirChar(nodeName.charAt(i))) 840 return "_" + Base64.byteArrayToAltBase64(byteArray(nodeName)); 841 return nodeName; 842 } 843 844 /** 845 * Translate a string into a byte array by translating each character 846 * into two bytes, high-byte first ("big-endian"). 847 */ 848 private static byte[] byteArray(String s) { 849 int len = s.length(); 850 byte[] result = new byte[2*len]; 851 for (int i=0, j=0; i<len; i++) { 852 char c = s.charAt(i); 853 result[j++] = (byte) (c>>8); 854 result[j++] = (byte) c; 855 } 856 return result; 857 } 858 859 /** 860 * Returns the node name corresponding to the specified directory name. 861 * (Inverts the transformation of dirName(String). 862 */ 863 private static String nodeName(String dirName) { 864 if (dirName.charAt(0) != '_') 865 return dirName; 866 byte a[] = Base64.altBase64ToByteArray(dirName.substring(1)); 867 StringBuffer result = new StringBuffer(a.length/2); 868 for (int i = 0; i < a.length; ) { 869 int highByte = a[i++] & 0xff; 870 int lowByte = a[i++] & 0xff; 871 result.append((char) ((highByte << 8) | lowByte)); 872 } 873 return result.toString(); 874 } 875 876 /** 877 * Try to acquire the appropriate file lock (user or system). If 878 * the initial attempt fails, several more attempts are made using 879 * an exponential backoff strategy. If all attempts fail, this method 880 * returns false. 881 * @throws SecurityException if file access denied. 882 */ 883 private boolean lockFile(boolean shared) throws SecurityException{ 884 boolean usernode = isUserNode(); 885 int[] result; 886 int errorCode = 0; 887 File lockFile = (usernode ? userLockFile : systemLockFile); 888 long sleepTime = INIT_SLEEP_TIME; 889 for (int i = 0; i < MAX_ATTEMPTS; i++) { 890 try { 891 int perm = (usernode? USER_READ_WRITE: USER_RW_ALL_READ); 892 result = lockFile0(lockFile.getCanonicalPath(), perm, shared); 893 894 errorCode = result[ERROR_CODE]; 895 if (result[LOCK_HANDLE] != 0) { 896 if (usernode) { 897 userRootLockHandle = result[LOCK_HANDLE]; 898 } else { 899 systemRootLockHandle = result[LOCK_HANDLE]; 900 } 901 return true; 902 } 903 } catch(IOException e) { 904 // // If at first, you don't succeed... 905 } 906 907 try { 908 Thread.sleep(sleepTime); 909 } catch(InterruptedException e) { 910 checkLockFile0ErrorCode(errorCode); 911 return false; 912 } 913 sleepTime *= 2; 914 } 915 checkLockFile0ErrorCode(errorCode); 916 return false; 917 } 918 919 /** 920 * Checks if unlockFile0() returned an error. Throws a SecurityException, 921 * if access denied. Logs a warning otherwise. 922 */ 923 private void checkLockFile0ErrorCode (int errorCode) 924 throws SecurityException { 925 if (errorCode == EACCES) 926 throw new SecurityException("Could not lock " + 927 (isUserNode()? "User prefs." : "System prefs.") + 928 " Lock file access denied."); 929 if (errorCode != EAGAIN) 930 getLogger().warning("Could not lock " + 931 (isUserNode()? "User prefs. " : "System prefs.") + 932 " Unix error code " + errorCode + "."); 933 } 934 935 /** 936 * Locks file using UNIX file locking. 937 * @param fileName Absolute file name of the lock file. 938 * @return Returns a lock handle, used to unlock the file. 939 */ 940 private static native int[] 941 lockFile0(String fileName, int permission, boolean shared); 942 943 /** 944 * Unlocks file previously locked by lockFile0(). 945 * @param lockHandle Handle to the file lock. 946 * @return Returns zero if OK, UNIX error code if failure. 947 */ 948 private static native int unlockFile0(int lockHandle); 949 950 /** 951 * Changes UNIX file permissions. 952 */ 953 private static native int chmod(String fileName, int permission); 954 955 /** 956 * Initial time between lock attempts, in ms. The time is doubled 957 * after each failing attempt (except the first). 958 */ 959 private static int INIT_SLEEP_TIME = 50; 960 961 /** 962 * Maximum number of lock attempts. 963 */ 964 private static int MAX_ATTEMPTS = 5; 965 966 /** 967 * Release the the appropriate file lock (user or system). 968 * @throws SecurityException if file access denied. 969 */ 970 private void unlockFile() { 971 int result; 972 boolean usernode = isUserNode(); 973 File lockFile = (usernode ? userLockFile : systemLockFile); 974 int lockHandle = ( usernode ? userRootLockHandle:systemRootLockHandle); 975 if (lockHandle == 0) { 976 getLogger().warning("Unlock: zero lockHandle for " + 977 (usernode ? "user":"system") + " preferences.)"); 978 return; 979 } 980 result = unlockFile0(lockHandle); 981 if (result != 0) { 982 getLogger().warning("Could not drop file-lock on " + 983 (isUserNode() ? "user" : "system") + " preferences." + 984 " Unix error code " + result + "."); 985 if (result == EACCES) 986 throw new SecurityException("Could not unlock" + 987 (isUserNode()? "User prefs." : "System prefs.") + 988 " Lock file access denied."); 989 } 990 if (isUserNode()) { 991 userRootLockHandle = 0; 992 } else { 993 systemRootLockHandle = 0; 994 } 995 } 996 } 997