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 }
--- EOF ---