Path: blob/master/src/java.base/share/classes/sun/nio/fs/PollingWatchService.java
67773 views
/*1* Copyright (c) 2008, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package sun.nio.fs;2627import java.nio.file.ClosedWatchServiceException;28import java.nio.file.DirectoryIteratorException;29import java.nio.file.DirectoryStream;30import java.nio.file.Files;31import java.nio.file.LinkOption;32import java.nio.file.NotDirectoryException;33import java.nio.file.Path;34import java.nio.file.StandardWatchEventKinds;35import java.nio.file.WatchEvent;36import java.nio.file.WatchKey;37import java.nio.file.attribute.BasicFileAttributes;38import java.security.AccessController;39import java.security.PrivilegedAction;40import java.security.PrivilegedExceptionAction;41import java.security.PrivilegedActionException;42import java.io.IOException;43import java.util.HashMap;44import java.util.HashSet;45import java.util.Iterator;46import java.util.Map;47import java.util.Set;48import java.util.concurrent.Executors;49import java.util.concurrent.ScheduledExecutorService;50import java.util.concurrent.ScheduledFuture;51import java.util.concurrent.ThreadFactory;52import java.util.concurrent.TimeUnit;5354/**55* Simple WatchService implementation that uses periodic tasks to poll56* registered directories for changes. This implementation is for use on57* operating systems that do not have native file change notification support.58*/5960class PollingWatchService61extends AbstractWatchService62{63// Wait between polling thread creation and first poll (seconds)64private static final int POLLING_INIT_DELAY = 1;65// Default time between polls (seconds)66private static final int DEFAULT_POLLING_INTERVAL = 2;6768// map of registrations69private final Map<Object, PollingWatchKey> map = new HashMap<>();7071// used to execute the periodic tasks that poll for changes72private final ScheduledExecutorService scheduledExecutor;7374PollingWatchService() {75// TBD: Make the number of threads configurable76scheduledExecutor = Executors77.newSingleThreadScheduledExecutor(new ThreadFactory() {78@Override79public Thread newThread(Runnable r) {80Thread t = new Thread(null, r, "FileSystemWatcher", 0, false);81t.setDaemon(true);82return t;83}});84}8586/**87* Register the given file with this watch service88*/89@SuppressWarnings("removal")90@Override91WatchKey register(final Path path,92WatchEvent.Kind<?>[] events,93WatchEvent.Modifier... modifiers)94throws IOException95{96// check events - CCE will be thrown if there are invalid elements97final Set<WatchEvent.Kind<?>> eventSet = new HashSet<>(events.length);98for (WatchEvent.Kind<?> event: events) {99// standard events100if (event == StandardWatchEventKinds.ENTRY_CREATE ||101event == StandardWatchEventKinds.ENTRY_MODIFY ||102event == StandardWatchEventKinds.ENTRY_DELETE)103{104eventSet.add(event);105continue;106}107108// OVERFLOW is ignored109if (event == StandardWatchEventKinds.OVERFLOW) {110continue;111}112113// null/unsupported114if (event == null)115throw new NullPointerException("An element in event set is 'null'");116throw new UnsupportedOperationException(event.name());117}118if (eventSet.isEmpty())119throw new IllegalArgumentException("No events to register");120121// Extended modifiers may be used to specify the sensitivity level122int sensitivity = DEFAULT_POLLING_INTERVAL;123if (modifiers.length > 0) {124for (WatchEvent.Modifier modifier: modifiers) {125if (modifier == null)126throw new NullPointerException();127128if (ExtendedOptions.SENSITIVITY_HIGH.matches(modifier)) {129sensitivity = ExtendedOptions.SENSITIVITY_HIGH.parameter();130} else if (ExtendedOptions.SENSITIVITY_MEDIUM.matches(modifier)) {131sensitivity = ExtendedOptions.SENSITIVITY_MEDIUM.parameter();132} else if (ExtendedOptions.SENSITIVITY_LOW.matches(modifier)) {133sensitivity = ExtendedOptions.SENSITIVITY_LOW.parameter();134} else {135throw new UnsupportedOperationException("Modifier not supported");136}137}138}139140// check if watch service is closed141if (!isOpen())142throw new ClosedWatchServiceException();143144// registration is done in privileged block as it requires the145// attributes of the entries in the directory.146try {147int value = sensitivity;148return AccessController.doPrivileged(149new PrivilegedExceptionAction<PollingWatchKey>() {150@Override151public PollingWatchKey run() throws IOException {152return doPrivilegedRegister(path, eventSet, value);153}154});155} catch (PrivilegedActionException pae) {156Throwable cause = pae.getCause();157if (cause instanceof IOException ioe)158throw ioe;159throw new AssertionError(pae);160}161}162163// registers directory returning a new key if not already registered or164// existing key if already registered165private PollingWatchKey doPrivilegedRegister(Path path,166Set<? extends WatchEvent.Kind<?>> events,167int sensitivityInSeconds)168throws IOException169{170// check file is a directory and get its file key if possible171BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);172if (!attrs.isDirectory()) {173throw new NotDirectoryException(path.toString());174}175Object fileKey = attrs.fileKey();176if (fileKey == null)177throw new AssertionError("File keys must be supported");178179// grab close lock to ensure that watch service cannot be closed180synchronized (closeLock()) {181if (!isOpen())182throw new ClosedWatchServiceException();183184PollingWatchKey watchKey;185synchronized (map) {186watchKey = map.get(fileKey);187if (watchKey == null) {188// new registration189watchKey = new PollingWatchKey(path, this, fileKey);190map.put(fileKey, watchKey);191} else {192// update to existing registration193watchKey.disable();194}195}196watchKey.enable(events, sensitivityInSeconds);197return watchKey;198}199200}201202@SuppressWarnings("removal")203@Override204void implClose() throws IOException {205synchronized (map) {206for (Map.Entry<Object, PollingWatchKey> entry: map.entrySet()) {207PollingWatchKey watchKey = entry.getValue();208watchKey.disable();209watchKey.invalidate();210}211map.clear();212}213AccessController.doPrivileged(new PrivilegedAction<Void>() {214@Override215public Void run() {216scheduledExecutor.shutdown();217return null;218}219});220}221222/**223* Entry in directory cache to record file last-modified-time and tick-count224*/225private static class CacheEntry {226private long lastModified;227private int lastTickCount;228229CacheEntry(long lastModified, int lastTickCount) {230this.lastModified = lastModified;231this.lastTickCount = lastTickCount;232}233234int lastTickCount() {235return lastTickCount;236}237238long lastModified() {239return lastModified;240}241242void update(long lastModified, int tickCount) {243this.lastModified = lastModified;244this.lastTickCount = tickCount;245}246}247248/**249* WatchKey implementation that encapsulates a map of the entries of the250* entries in the directory. Polling the key causes it to re-scan the251* directory and queue keys when entries are added, modified, or deleted.252*/253private class PollingWatchKey extends AbstractWatchKey {254255private final Object fileKey;256257// current event set258private Set<? extends WatchEvent.Kind<?>> events;259260// the result of the periodic task that causes this key to be polled261private ScheduledFuture<?> poller;262263// indicates if the key is valid264private volatile boolean valid;265266// used to detect files that have been deleted267private int tickCount;268269// map of entries in directory270private Map<Path,CacheEntry> entries;271272PollingWatchKey(Path dir, PollingWatchService watcher, Object fileKey)273throws IOException274{275super(dir, watcher);276this.fileKey = fileKey;277this.valid = true;278this.tickCount = 0;279this.entries = new HashMap<Path,CacheEntry>();280281// get the initial entries in the directory282try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {283for (Path entry: stream) {284// don't follow links285long lastModified =286Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();287entries.put(entry.getFileName(), new CacheEntry(lastModified, tickCount));288}289} catch (DirectoryIteratorException e) {290throw e.getCause();291}292}293294Object fileKey() {295return fileKey;296}297298@Override299public boolean isValid() {300return valid;301}302303void invalidate() {304valid = false;305}306307// enables periodic polling308void enable(Set<? extends WatchEvent.Kind<?>> events, long period) {309synchronized (this) {310// update the events311this.events = events;312313// create the periodic task with initialDelay set to the specified constant314Runnable thunk = new Runnable() { public void run() { poll(); }};315this.poller = scheduledExecutor316.scheduleAtFixedRate(thunk, POLLING_INIT_DELAY, period, TimeUnit.SECONDS);317}318}319320// disables periodic polling321void disable() {322synchronized (this) {323if (poller != null)324poller.cancel(false);325}326}327328@Override329public void cancel() {330valid = false;331synchronized (map) {332map.remove(fileKey());333}334disable();335}336337/**338* Polls the directory to detect for new files, modified files, or339* deleted files.340*/341synchronized void poll() {342if (!valid) {343return;344}345346// update tick347tickCount++;348349// open directory350DirectoryStream<Path> stream = null;351try {352stream = Files.newDirectoryStream(watchable());353} catch (IOException x) {354// directory is no longer accessible so cancel key355cancel();356signal();357return;358}359360// iterate over all entries in directory361try {362for (Path entry: stream) {363long lastModified = 0L;364try {365lastModified =366Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();367} catch (IOException x) {368// unable to get attributes of entry. If file has just369// been deleted then we'll report it as deleted on the370// next poll371continue;372}373374// lookup cache375CacheEntry e = entries.get(entry.getFileName());376if (e == null) {377// new file found378entries.put(entry.getFileName(),379new CacheEntry(lastModified, tickCount));380381// queue ENTRY_CREATE if event enabled382if (events.contains(StandardWatchEventKinds.ENTRY_CREATE)) {383signalEvent(StandardWatchEventKinds.ENTRY_CREATE, entry.getFileName());384continue;385} else {386// if ENTRY_CREATE is not enabled and ENTRY_MODIFY is387// enabled then queue event to avoid missing out on388// modifications to the file immediately after it is389// created.390if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {391signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, entry.getFileName());392}393}394continue;395}396397// check if file has changed398if (e.lastModified != lastModified) {399if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {400signalEvent(StandardWatchEventKinds.ENTRY_MODIFY,401entry.getFileName());402}403}404// entry in cache so update poll time405e.update(lastModified, tickCount);406407}408} catch (DirectoryIteratorException e) {409// ignore for now; if the directory is no longer accessible410// then the key will be cancelled on the next poll411} finally {412413// close directory stream414try {415stream.close();416} catch (IOException x) {417// ignore418}419}420421// iterate over cache to detect entries that have been deleted422Iterator<Map.Entry<Path,CacheEntry>> i = entries.entrySet().iterator();423while (i.hasNext()) {424Map.Entry<Path,CacheEntry> mapEntry = i.next();425CacheEntry entry = mapEntry.getValue();426if (entry.lastTickCount() != tickCount) {427Path name = mapEntry.getKey();428// remove from map and queue delete event (if enabled)429i.remove();430if (events.contains(StandardWatchEventKinds.ENTRY_DELETE)) {431signalEvent(StandardWatchEventKinds.ENTRY_DELETE, name);432}433}434}435}436}437}438439440