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