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