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
26package sun.nio.fs;
27
28import java.nio.file.ClosedWatchServiceException;
29import java.nio.file.DirectoryIteratorException;
30import java.nio.file.DirectoryStream;
31import java.nio.file.Files;
32import java.nio.file.LinkOption;
33import java.nio.file.NotDirectoryException;
34import java.nio.file.Path;
35import java.nio.file.StandardWatchEventKinds;
36import java.nio.file.WatchEvent;
37import java.nio.file.WatchKey;
38import java.nio.file.attribute.BasicFileAttributes;
39import java.security.AccessController;
40import java.security.PrivilegedAction;
41import java.security.PrivilegedExceptionAction;
42import java.security.PrivilegedActionException;
43import java.io.IOException;
44import java.util.HashMap;
45import java.util.HashSet;
46import java.util.Iterator;
47import java.util.Map;
48import java.util.Set;
49import java.util.concurrent.Executors;
50import java.util.concurrent.ScheduledExecutorService;
51import java.util.concurrent.ScheduledFuture;
52import java.util.concurrent.ThreadFactory;
53import 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
61class 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
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
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