FileSystemPreferences.java revision 13541:7f5b7acebffd
1/* 2 * Copyright (c) 2000, 2016, 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 26package java.util.prefs; 27import java.util.*; 28import java.io.*; 29import java.security.AccessController; 30import java.security.PrivilegedAction; 31import java.security.PrivilegedExceptionAction; 32import java.security.PrivilegedActionException; 33import sun.util.logging.PlatformLogger; 34 35/** 36 * Preferences implementation for Unix. Preferences are stored in the file 37 * system, with one directory per preferences node. All of the preferences 38 * at each node are stored in a single file. Atomic file system operations 39 * (e.g. File.renameTo) are used to ensure integrity. An in-memory cache of 40 * the "explored" portion of the tree is maintained for performance, and 41 * written back to the disk periodically. File-locking is used to ensure 42 * reasonable behavior when multiple VMs are running at the same time. 43 * (The file lock is obtained only for sync(), flush() and removeNode().) 44 * 45 * @author Josh Bloch 46 * @see Preferences 47 * @since 1.4 48 */ 49class FileSystemPreferences extends AbstractPreferences { 50 51 static { 52 PrivilegedAction<Void> load = () -> { 53 System.loadLibrary("prefs"); 54 return null; 55 }; 56 AccessController.doPrivileged(load); 57 } 58 59 /** 60 * Sync interval in seconds. 61 */ 62 private static final int SYNC_INTERVAL = Math.max(1, 63 AccessController.doPrivileged((PrivilegedAction<Integer>) () -> 64 Integer.getInteger("java.util.prefs.syncInterval", 30))); 65 66 /** 67 * Returns logger for error messages. Backing store exceptions are logged at 68 * WARNING level. 69 */ 70 private static PlatformLogger getLogger() { 71 return PlatformLogger.getLogger("java.util.prefs"); 72 } 73 74 /** 75 * Directory for system preferences. 76 */ 77 private static File systemRootDir; 78 79 /* 80 * Flag, indicating whether systemRoot directory is writable 81 */ 82 private static boolean isSystemRootWritable; 83 84 /** 85 * Directory for user preferences. 86 */ 87 private static File userRootDir; 88 89 /* 90 * Flag, indicating whether userRoot directory is writable 91 */ 92 private static boolean isUserRootWritable; 93 94 /** 95 * The user root. 96 */ 97 static Preferences userRoot = null; 98 99 static synchronized Preferences getUserRoot() { 100 if (userRoot == null) { 101 setupUserRoot(); 102 userRoot = new FileSystemPreferences(true); 103 } 104 return userRoot; 105 } 106 107 private static void setupUserRoot() { 108 AccessController.doPrivileged(new PrivilegedAction<Void>() { 109 public Void run() { 110 userRootDir = 111 new File(System.getProperty("java.util.prefs.userRoot", 112 System.getProperty("user.home")), ".java/.userPrefs"); 113 // Attempt to create root dir if it does not yet exist. 114 if (!userRootDir.exists()) { 115 if (userRootDir.mkdirs()) { 116 try { 117 chmod(userRootDir.getCanonicalPath(), USER_RWX); 118 } catch (IOException e) { 119 getLogger().warning("Could not change permissions" + 120 " on userRoot directory. "); 121 } 122 getLogger().info("Created user preferences directory."); 123 } 124 else 125 getLogger().warning("Couldn't create user preferences" + 126 " directory. User preferences are unusable."); 127 } 128 isUserRootWritable = userRootDir.canWrite(); 129 String USER_NAME = System.getProperty("user.name"); 130 userLockFile = new File (userRootDir,".user.lock." + USER_NAME); 131 userRootModFile = new File (userRootDir, 132 ".userRootModFile." + USER_NAME); 133 if (!userRootModFile.exists()) 134 try { 135 // create if does not exist. 136 userRootModFile.createNewFile(); 137 // Only user can read/write userRootModFile. 138 int result = chmod(userRootModFile.getCanonicalPath(), 139 USER_READ_WRITE); 140 if (result !=0) 141 getLogger().warning("Problem creating userRoot " + 142 "mod file. Chmod failed on " + 143 userRootModFile.getCanonicalPath() + 144 " Unix error code " + result); 145 } catch (IOException e) { 146 getLogger().warning(e.toString()); 147 } 148 userRootModTime = userRootModFile.lastModified(); 149 return null; 150 } 151 }); 152 } 153 154 155 /** 156 * The system root. 157 */ 158 static Preferences systemRoot; 159 160 static synchronized Preferences getSystemRoot() { 161 if (systemRoot == null) { 162 setupSystemRoot(); 163 systemRoot = new FileSystemPreferences(false); 164 } 165 return systemRoot; 166 } 167 168 private static void setupSystemRoot() { 169 AccessController.doPrivileged(new PrivilegedAction<Void>() { 170 public Void run() { 171 String systemPrefsDirName = 172 System.getProperty("java.util.prefs.systemRoot","/etc/.java"); 173 systemRootDir = 174 new File(systemPrefsDirName, ".systemPrefs"); 175 // Attempt to create root dir if it does not yet exist. 176 if (!systemRootDir.exists()) { 177 // system root does not exist in /etc/.java 178 // Switching to java.home 179 systemRootDir = 180 new File(System.getProperty("java.home"), 181 ".systemPrefs"); 182 if (!systemRootDir.exists()) { 183 if (systemRootDir.mkdirs()) { 184 getLogger().info( 185 "Created system preferences directory " 186 + "in java.home."); 187 try { 188 chmod(systemRootDir.getCanonicalPath(), 189 USER_RWX_ALL_RX); 190 } catch (IOException e) { 191 } 192 } else { 193 getLogger().warning("Could not create " 194 + "system preferences directory. System " 195 + "preferences are unusable."); 196 } 197 } 198 } 199 isSystemRootWritable = systemRootDir.canWrite(); 200 systemLockFile = new File(systemRootDir, ".system.lock"); 201 systemRootModFile = 202 new File (systemRootDir,".systemRootModFile"); 203 if (!systemRootModFile.exists() && isSystemRootWritable) 204 try { 205 // create if does not exist. 206 systemRootModFile.createNewFile(); 207 int result = chmod(systemRootModFile.getCanonicalPath(), 208 USER_RW_ALL_READ); 209 if (result !=0) 210 getLogger().warning("Chmod failed on " + 211 systemRootModFile.getCanonicalPath() + 212 " Unix error code " + result); 213 } catch (IOException e) { getLogger().warning(e.toString()); 214 } 215 systemRootModTime = systemRootModFile.lastModified(); 216 return null; 217 } 218 }); 219 } 220 221 222 /** 223 * Unix user write/read permission 224 */ 225 private static final int USER_READ_WRITE = 0600; 226 227 private static final int USER_RW_ALL_READ = 0644; 228 229 230 private static final int USER_RWX_ALL_RX = 0755; 231 232 private static final int USER_RWX = 0700; 233 234 /** 235 * The lock file for the user tree. 236 */ 237 static File userLockFile; 238 239 240 241 /** 242 * The lock file for the system tree. 243 */ 244 static File systemLockFile; 245 246 /** 247 * Unix lock handle for userRoot. 248 * Zero, if unlocked. 249 */ 250 251 private static int userRootLockHandle = 0; 252 253 /** 254 * Unix lock handle for systemRoot. 255 * Zero, if unlocked. 256 */ 257 258 private static int systemRootLockHandle = 0; 259 260 /** 261 * The directory representing this preference node. There is no guarantee 262 * that this directory exits, as another VM can delete it at any time 263 * that it (the other VM) holds the file-lock. While the root node cannot 264 * be deleted, it may not yet have been created, or the underlying 265 * directory could have been deleted accidentally. 266 */ 267 private final File dir; 268 269 /** 270 * The file representing this preference node's preferences. 271 * The file format is undocumented, and subject to change 272 * from release to release, but I'm sure that you can figure 273 * it out if you try real hard. 274 */ 275 private final File prefsFile; 276 277 /** 278 * A temporary file used for saving changes to preferences. As part of 279 * the sync operation, changes are first saved into this file, and then 280 * atomically renamed to prefsFile. This results in an atomic state 281 * change from one valid set of preferences to another. The 282 * the file-lock is held for the duration of this transformation. 283 */ 284 private final File tmpFile; 285 286 /** 287 * File, which keeps track of global modifications of userRoot. 288 */ 289 private static File userRootModFile; 290 291 /** 292 * Flag, which indicated whether userRoot was modified by another VM 293 */ 294 private static boolean isUserRootModified = false; 295 296 /** 297 * Keeps track of userRoot modification time. This time is reset to 298 * zero after UNIX reboot, and is increased by 1 second each time 299 * userRoot is modified. 300 */ 301 private static long userRootModTime; 302 303 304 /* 305 * File, which keeps track of global modifications of systemRoot 306 */ 307 private static File systemRootModFile; 308 /* 309 * Flag, which indicates whether systemRoot was modified by another VM 310 */ 311 private static boolean isSystemRootModified = false; 312 313 /** 314 * Keeps track of systemRoot modification time. This time is reset to 315 * zero after system reboot, and is increased by 1 second each time 316 * systemRoot is modified. 317 */ 318 private static long systemRootModTime; 319 320 /** 321 * Locally cached preferences for this node (includes uncommitted 322 * changes). This map is initialized with from disk when the first get or 323 * put operation occurs on this node. It is synchronized with the 324 * corresponding disk file (prefsFile) by the sync operation. The initial 325 * value is read *without* acquiring the file-lock. 326 */ 327 private Map<String, String> prefsCache = null; 328 329 /** 330 * The last modification time of the file backing this node at the time 331 * that prefCache was last synchronized (or initially read). This 332 * value is set *before* reading the file, so it's conservative; the 333 * actual timestamp could be (slightly) higher. A value of zero indicates 334 * that we were unable to initialize prefsCache from the disk, or 335 * have not yet attempted to do so. (If prefsCache is non-null, it 336 * indicates the former; if it's null, the latter.) 337 */ 338 private long lastSyncTime = 0; 339 340 /** 341 * Unix error code for locked file. 342 */ 343 private static final int EAGAIN = 11; 344 345 /** 346 * Unix error code for denied access. 347 */ 348 private static final int EACCES = 13; 349 350 /* Used to interpret results of native functions */ 351 private static final int LOCK_HANDLE = 0; 352 private static final int ERROR_CODE = 1; 353 354 /** 355 * A list of all uncommitted preference changes. The elements in this 356 * list are of type PrefChange. If this node is concurrently modified on 357 * disk by another VM, the two sets of changes are merged when this node 358 * is sync'ed by overwriting our prefsCache with the preference map last 359 * written out to disk (by the other VM), and then replaying this change 360 * log against that map. The resulting map is then written back 361 * to the disk. 362 */ 363 final List<Change> changeLog = new ArrayList<>(); 364 365 /** 366 * Represents a change to a preference. 367 */ 368 private abstract class Change { 369 /** 370 * Reapplies the change to prefsCache. 371 */ 372 abstract void replay(); 373 }; 374 375 /** 376 * Represents a preference put. 377 */ 378 private class Put extends Change { 379 String key, value; 380 381 Put(String key, String value) { 382 this.key = key; 383 this.value = value; 384 } 385 386 void replay() { 387 prefsCache.put(key, value); 388 } 389 } 390 391 /** 392 * Represents a preference remove. 393 */ 394 private class Remove extends Change { 395 String key; 396 397 Remove(String key) { 398 this.key = key; 399 } 400 401 void replay() { 402 prefsCache.remove(key); 403 } 404 } 405 406 /** 407 * Represents the creation of this node. 408 */ 409 private class NodeCreate extends Change { 410 /** 411 * Performs no action, but the presence of this object in changeLog 412 * will force the node and its ancestors to be made permanent at the 413 * next sync. 414 */ 415 void replay() { 416 } 417 } 418 419 /** 420 * NodeCreate object for this node. 421 */ 422 NodeCreate nodeCreate = null; 423 424 /** 425 * Replay changeLog against prefsCache. 426 */ 427 private void replayChanges() { 428 for (int i = 0, n = changeLog.size(); i<n; i++) 429 changeLog.get(i).replay(); 430 } 431 432 private static Timer syncTimer = new Timer(true); // Daemon Thread 433 434 static { 435 // Add periodic timer task to periodically sync cached prefs 436 syncTimer.schedule(new TimerTask() { 437 public void run() { 438 syncWorld(); 439 } 440 }, SYNC_INTERVAL*1000, SYNC_INTERVAL*1000); 441 442 // Add shutdown hook to flush cached prefs on normal termination 443 AccessController.doPrivileged(new PrivilegedAction<Void>() { 444 public Void run() { 445 Runtime.getRuntime().addShutdownHook( 446 new Thread(null, null, "Sync Timer Thread", 0, false) { 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} 1014