Home | History | Annotate | Download | only in fs
      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         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 }
    412