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