1 /*
   2  * Copyright (c) 2008, 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 sun.nio.fs;
  27 
  28 import java.nio.file.*;
  29 import java.nio.file.attribute.*;
  30 import java.security.AccessController;
  31 import java.security.PrivilegedAction;
  32 import java.security.PrivilegedExceptionAction;
  33 import java.security.PrivilegedActionException;
  34 import java.io.IOException;
  35 import java.util.*;
  36 import java.util.concurrent.*;
  37 import com.sun.nio.file.SensitivityWatchEventModifier;
  38 import sun.misc.ManagedLocalsThread;
  39 
  40 /**
  41  * Simple WatchService implementation that uses periodic tasks to poll
  42  * registered directories for changes.  This implementation is for use on
  43  * operating systems that do not have native file change notification support.
  44  */
  45 
  46 class PollingWatchService
  47     extends AbstractWatchService
  48 {
  49     // map of registrations
  50     private final Map<Object,PollingWatchKey> map =
  51         new HashMap<Object,PollingWatchKey>();
  52 
  53     // used to execute the periodic tasks that poll for changes
  54     private final ScheduledExecutorService scheduledExecutor;
  55 
  56     PollingWatchService() {
  57         // TBD: Make the number of threads configurable
  58         scheduledExecutor = Executors
  59             .newSingleThreadScheduledExecutor(new ThreadFactory() {
  60                  @Override
  61                  public Thread newThread(Runnable r) {
  62                      Thread t = new ManagedLocalsThread(r);
  63                      t.setDaemon(true);
  64                      return t;
  65                  }});
  66     }
  67 
  68     /**
  69      * Register the given file with this watch service
  70      */
  71     @Override
  72     WatchKey register(final Path path,
  73                       WatchEvent.Kind<?>[] events,
  74                       WatchEvent.Modifier... modifiers)
  75          throws IOException
  76     {
  77         // check events - CCE will be thrown if there are invalid elements
  78         final Set<WatchEvent.Kind<?>> eventSet =
  79             new HashSet<WatchEvent.Kind<?>>(events.length);
  80         for (WatchEvent.Kind<?> event: events) {
  81             // standard events
  82             if (event == StandardWatchEventKinds.ENTRY_CREATE ||
  83                 event == StandardWatchEventKinds.ENTRY_MODIFY ||
  84                 event == StandardWatchEventKinds.ENTRY_DELETE)
  85             {
  86                 eventSet.add(event);
  87                 continue;
  88             }
  89 
  90             // OVERFLOW is ignored
  91             if (event == StandardWatchEventKinds.OVERFLOW) {
  92                 continue;
  93             }
  94 
  95             // null/unsupported
  96             if (event == null)
  97                 throw new NullPointerException("An element in event set is 'null'");
  98             throw new UnsupportedOperationException(event.name());
  99         }
 100         if (eventSet.isEmpty())
 101             throw new IllegalArgumentException("No events to register");
 102 
 103         // A modifier may be used to specify the sensitivity level
 104         SensitivityWatchEventModifier sensivity = SensitivityWatchEventModifier.MEDIUM;
 105         if (modifiers.length > 0) {
 106             for (WatchEvent.Modifier modifier: modifiers) {
 107                 if (modifier == null)
 108                     throw new NullPointerException();
 109                 if (modifier instanceof SensitivityWatchEventModifier) {
 110                     sensivity = (SensitivityWatchEventModifier)modifier;
 111                     continue;
 112                 }
 113                 throw new UnsupportedOperationException("Modifier not supported");
 114             }
 115         }
 116 
 117         // check if watch service is closed
 118         if (!isOpen())
 119             throw new ClosedWatchServiceException();
 120 
 121         // registration is done in privileged block as it requires the
 122         // attributes of the entries in the directory.
 123         try {
 124             final SensitivityWatchEventModifier s = sensivity;
 125             return AccessController.doPrivileged(
 126                 new PrivilegedExceptionAction<PollingWatchKey>() {
 127                     @Override
 128                     public PollingWatchKey run() throws IOException {
 129                         return doPrivilegedRegister(path, eventSet, s);
 130                     }
 131                 });
 132         } catch (PrivilegedActionException pae) {
 133             Throwable cause = pae.getCause();
 134             if (cause != null && cause instanceof IOException)
 135                 throw (IOException)cause;
 136             throw new AssertionError(pae);
 137         }
 138     }
 139 
 140     // registers directory returning a new key if not already registered or
 141     // existing key if already registered
 142     private PollingWatchKey doPrivilegedRegister(Path path,
 143                                                  Set<? extends WatchEvent.Kind<?>> events,
 144                                                  SensitivityWatchEventModifier sensivity)
 145         throws IOException
 146     {
 147         // check file is a directory and get its file key if possible
 148         BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
 149         if (!attrs.isDirectory()) {
 150             throw new NotDirectoryException(path.toString());
 151         }
 152         Object fileKey = attrs.fileKey();
 153         if (fileKey == null)
 154             throw new AssertionError("File keys must be supported");
 155 
 156         // grab close lock to ensure that watch service cannot be closed
 157         synchronized (closeLock()) {
 158             if (!isOpen())
 159                 throw new ClosedWatchServiceException();
 160 
 161             PollingWatchKey watchKey;
 162             synchronized (map) {
 163                 watchKey = map.get(fileKey);
 164                 if (watchKey == null) {
 165                     // new registration
 166                     watchKey = new PollingWatchKey(path, this, fileKey);
 167                     map.put(fileKey, watchKey);
 168                 } else {
 169                     // update to existing registration
 170                     watchKey.disable();
 171                 }
 172             }
 173             watchKey.enable(events, sensivity.sensitivityValueInSeconds());
 174             return watchKey;
 175         }
 176 
 177     }
 178 
 179     @Override
 180     void implClose() throws IOException {
 181         synchronized (map) {
 182             for (Map.Entry<Object,PollingWatchKey> entry: map.entrySet()) {
 183                 PollingWatchKey watchKey = entry.getValue();
 184                 watchKey.disable();
 185                 watchKey.invalidate();
 186             }
 187             map.clear();
 188         }
 189         AccessController.doPrivileged(new PrivilegedAction<Void>() {
 190             @Override
 191             public Void run() {
 192                 scheduledExecutor.shutdown();
 193                 return null;
 194             }
 195          });
 196     }
 197 
 198     /**
 199      * Entry in directory cache to record file last-modified-time and tick-count
 200      */
 201     private static class CacheEntry {
 202         private long lastModified;
 203         private int lastTickCount;
 204 
 205         CacheEntry(long lastModified, int lastTickCount) {
 206             this.lastModified = lastModified;
 207             this.lastTickCount = lastTickCount;
 208         }
 209 
 210         int lastTickCount() {
 211             return lastTickCount;
 212         }
 213 
 214         long lastModified() {
 215             return lastModified;
 216         }
 217 
 218         void update(long lastModified, int tickCount) {
 219             this.lastModified = lastModified;
 220             this.lastTickCount = tickCount;
 221         }
 222     }
 223 
 224     /**
 225      * WatchKey implementation that encapsulates a map of the entries of the
 226      * entries in the directory. Polling the key causes it to re-scan the
 227      * directory and queue keys when entries are added, modified, or deleted.
 228      */
 229     private class PollingWatchKey extends AbstractWatchKey {
 230         private final Object fileKey;
 231 
 232         // current event set
 233         private Set<? extends WatchEvent.Kind<?>> events;
 234 
 235         // the result of the periodic task that causes this key to be polled
 236         private ScheduledFuture<?> poller;
 237 
 238         // indicates if the key is valid
 239         private volatile boolean valid;
 240 
 241         // used to detect files that have been deleted
 242         private int tickCount;
 243 
 244         // map of entries in directory
 245         private Map<Path,CacheEntry> entries;
 246 
 247         PollingWatchKey(Path dir, PollingWatchService watcher, Object fileKey)
 248             throws IOException
 249         {
 250             super(dir, watcher);
 251             this.fileKey = fileKey;
 252             this.valid = true;
 253             this.tickCount = 0;
 254             this.entries = new HashMap<Path,CacheEntry>();
 255 
 256             // get the initial entries in the directory
 257             try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
 258                 for (Path entry: stream) {
 259                     // don't follow links
 260                     long lastModified =
 261                         Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();
 262                     entries.put(entry.getFileName(), new CacheEntry(lastModified, tickCount));
 263                 }
 264             } catch (DirectoryIteratorException e) {
 265                 throw e.getCause();
 266             }
 267         }
 268 
 269         Object fileKey() {
 270             return fileKey;
 271         }
 272 
 273         @Override
 274         public boolean isValid() {
 275             return valid;
 276         }
 277 
 278         void invalidate() {
 279             valid = false;
 280         }
 281 
 282         // enables periodic polling
 283         void enable(Set<? extends WatchEvent.Kind<?>> events, long period) {
 284             synchronized (this) {
 285                 // update the events
 286                 this.events = events;
 287 
 288                 // create the periodic task
 289                 Runnable thunk = new Runnable() { public void run() { poll(); }};
 290                 this.poller = scheduledExecutor
 291                     .scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS);
 292             }
 293         }
 294 
 295         // disables periodic polling
 296         void disable() {
 297             synchronized (this) {
 298                 if (poller != null)
 299                     poller.cancel(false);
 300             }
 301         }
 302 
 303         @Override
 304         public void cancel() {
 305             valid = false;
 306             synchronized (map) {
 307                 map.remove(fileKey());
 308             }
 309             disable();
 310         }
 311 
 312         /**
 313          * Polls the directory to detect for new files, modified files, or
 314          * deleted files.
 315          */
 316         synchronized void poll() {
 317             if (!valid) {
 318                 return;
 319             }
 320 
 321             // update tick
 322             tickCount++;
 323 
 324             // open directory
 325             DirectoryStream<Path> stream = null;
 326             try {
 327                 stream = Files.newDirectoryStream(watchable());
 328             } catch (IOException x) {
 329                 // directory is no longer accessible so cancel key
 330                 cancel();
 331                 signal();
 332                 return;
 333             }
 334 
 335             // iterate over all entries in directory
 336             try {
 337                 for (Path entry: stream) {
 338                     long lastModified = 0L;
 339                     try {
 340                         lastModified =
 341                             Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();
 342                     } catch (IOException x) {
 343                         // unable to get attributes of entry. If file has just
 344                         // been deleted then we'll report it as deleted on the
 345                         // next poll
 346                         continue;
 347                     }
 348 
 349                     // lookup cache
 350                     CacheEntry e = entries.get(entry.getFileName());
 351                     if (e == null) {
 352                         // new file found
 353                         entries.put(entry.getFileName(),
 354                                      new CacheEntry(lastModified, tickCount));
 355 
 356                         // queue ENTRY_CREATE if event enabled
 357                         if (events.contains(StandardWatchEventKinds.ENTRY_CREATE)) {
 358                             signalEvent(StandardWatchEventKinds.ENTRY_CREATE, entry.getFileName());
 359                             continue;
 360                         } else {
 361                             // if ENTRY_CREATE is not enabled and ENTRY_MODIFY is
 362                             // enabled then queue event to avoid missing out on
 363                             // modifications to the file immediately after it is
 364                             // created.
 365                             if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {
 366                                 signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, entry.getFileName());
 367                             }
 368                         }
 369                         continue;
 370                     }
 371 
 372                     // check if file has changed
 373                     if (e.lastModified != lastModified) {
 374                         if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {
 375                             signalEvent(StandardWatchEventKinds.ENTRY_MODIFY,
 376                                         entry.getFileName());
 377                         }
 378                     }
 379                     // entry in cache so update poll time
 380                     e.update(lastModified, tickCount);
 381 
 382                 }
 383             } catch (DirectoryIteratorException e) {
 384                 // ignore for now; if the directory is no longer accessible
 385                 // then the key will be cancelled on the next poll
 386             } finally {
 387 
 388                 // close directory stream
 389                 try {
 390                     stream.close();
 391                 } catch (IOException x) {
 392                     // ignore
 393                 }
 394             }
 395 
 396             // iterate over cache to detect entries that have been deleted
 397             Iterator<Map.Entry<Path,CacheEntry>> i = entries.entrySet().iterator();
 398             while (i.hasNext()) {
 399                 Map.Entry<Path,CacheEntry> mapEntry = i.next();
 400                 CacheEntry entry = mapEntry.getValue();
 401                 if (entry.lastTickCount() != tickCount) {
 402                     Path name = mapEntry.getKey();
 403                     // remove from map and queue delete event (if enabled)
 404                     i.remove();
 405                     if (events.contains(StandardWatchEventKinds.ENTRY_DELETE)) {
 406                         signalEvent(StandardWatchEventKinds.ENTRY_DELETE, name);
 407                     }
 408                 }
 409             }
 410         }
 411     }
 412 }