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 
PollingWatchService()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
register(final Path path, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)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