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