Path: blob/master/src/java.base/share/classes/sun/nio/fs/PollingWatchService.java
41159 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// map of registrations64private final Map<Object, PollingWatchKey> map = new HashMap<>();6566// used to execute the periodic tasks that poll for changes67private final ScheduledExecutorService scheduledExecutor;6869PollingWatchService() {70// TBD: Make the number of threads configurable71scheduledExecutor = Executors72.newSingleThreadScheduledExecutor(new ThreadFactory() {73@Override74public Thread newThread(Runnable r) {75Thread t = new Thread(null, r, "FileSystemWatcher", 0, false);76t.setDaemon(true);77return t;78}});79}8081/**82* Register the given file with this watch service83*/84@SuppressWarnings("removal")85@Override86WatchKey register(final Path path,87WatchEvent.Kind<?>[] events,88WatchEvent.Modifier... modifiers)89throws IOException90{91// check events - CCE will be thrown if there are invalid elements92final Set<WatchEvent.Kind<?>> eventSet = new HashSet<>(events.length);93for (WatchEvent.Kind<?> event: events) {94// standard events95if (event == StandardWatchEventKinds.ENTRY_CREATE ||96event == StandardWatchEventKinds.ENTRY_MODIFY ||97event == StandardWatchEventKinds.ENTRY_DELETE)98{99eventSet.add(event);100continue;101}102103// OVERFLOW is ignored104if (event == StandardWatchEventKinds.OVERFLOW) {105continue;106}107108// null/unsupported109if (event == null)110throw new NullPointerException("An element in event set is 'null'");111throw new UnsupportedOperationException(event.name());112}113if (eventSet.isEmpty())114throw new IllegalArgumentException("No events to register");115116// Extended modifiers may be used to specify the sensitivity level117int sensitivity = 10;118if (modifiers.length > 0) {119for (WatchEvent.Modifier modifier: modifiers) {120if (modifier == null)121throw new NullPointerException();122123if (ExtendedOptions.SENSITIVITY_HIGH.matches(modifier)) {124sensitivity = ExtendedOptions.SENSITIVITY_HIGH.parameter();125} else if (ExtendedOptions.SENSITIVITY_MEDIUM.matches(modifier)) {126sensitivity = ExtendedOptions.SENSITIVITY_MEDIUM.parameter();127} else if (ExtendedOptions.SENSITIVITY_LOW.matches(modifier)) {128sensitivity = ExtendedOptions.SENSITIVITY_LOW.parameter();129} else {130throw new UnsupportedOperationException("Modifier not supported");131}132}133}134135// check if watch service is closed136if (!isOpen())137throw new ClosedWatchServiceException();138139// registration is done in privileged block as it requires the140// attributes of the entries in the directory.141try {142int value = sensitivity;143return AccessController.doPrivileged(144new PrivilegedExceptionAction<PollingWatchKey>() {145@Override146public PollingWatchKey run() throws IOException {147return doPrivilegedRegister(path, eventSet, value);148}149});150} catch (PrivilegedActionException pae) {151Throwable cause = pae.getCause();152if (cause instanceof IOException ioe)153throw ioe;154throw new AssertionError(pae);155}156}157158// registers directory returning a new key if not already registered or159// existing key if already registered160private PollingWatchKey doPrivilegedRegister(Path path,161Set<? extends WatchEvent.Kind<?>> events,162int sensitivityInSeconds)163throws IOException164{165// check file is a directory and get its file key if possible166BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);167if (!attrs.isDirectory()) {168throw new NotDirectoryException(path.toString());169}170Object fileKey = attrs.fileKey();171if (fileKey == null)172throw new AssertionError("File keys must be supported");173174// grab close lock to ensure that watch service cannot be closed175synchronized (closeLock()) {176if (!isOpen())177throw new ClosedWatchServiceException();178179PollingWatchKey watchKey;180synchronized (map) {181watchKey = map.get(fileKey);182if (watchKey == null) {183// new registration184watchKey = new PollingWatchKey(path, this, fileKey);185map.put(fileKey, watchKey);186} else {187// update to existing registration188watchKey.disable();189}190}191watchKey.enable(events, sensitivityInSeconds);192return watchKey;193}194195}196197@SuppressWarnings("removal")198@Override199void implClose() throws IOException {200synchronized (map) {201for (Map.Entry<Object, PollingWatchKey> entry: map.entrySet()) {202PollingWatchKey watchKey = entry.getValue();203watchKey.disable();204watchKey.invalidate();205}206map.clear();207}208AccessController.doPrivileged(new PrivilegedAction<Void>() {209@Override210public Void run() {211scheduledExecutor.shutdown();212return null;213}214});215}216217/**218* Entry in directory cache to record file last-modified-time and tick-count219*/220private static class CacheEntry {221private long lastModified;222private int lastTickCount;223224CacheEntry(long lastModified, int lastTickCount) {225this.lastModified = lastModified;226this.lastTickCount = lastTickCount;227}228229int lastTickCount() {230return lastTickCount;231}232233long lastModified() {234return lastModified;235}236237void update(long lastModified, int tickCount) {238this.lastModified = lastModified;239this.lastTickCount = tickCount;240}241}242243/**244* WatchKey implementation that encapsulates a map of the entries of the245* entries in the directory. Polling the key causes it to re-scan the246* directory and queue keys when entries are added, modified, or deleted.247*/248private class PollingWatchKey extends AbstractWatchKey {249private final Object fileKey;250251// current event set252private Set<? extends WatchEvent.Kind<?>> events;253254// the result of the periodic task that causes this key to be polled255private ScheduledFuture<?> poller;256257// indicates if the key is valid258private volatile boolean valid;259260// used to detect files that have been deleted261private int tickCount;262263// map of entries in directory264private Map<Path,CacheEntry> entries;265266PollingWatchKey(Path dir, PollingWatchService watcher, Object fileKey)267throws IOException268{269super(dir, watcher);270this.fileKey = fileKey;271this.valid = true;272this.tickCount = 0;273this.entries = new HashMap<Path,CacheEntry>();274275// get the initial entries in the directory276try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {277for (Path entry: stream) {278// don't follow links279long lastModified =280Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();281entries.put(entry.getFileName(), new CacheEntry(lastModified, tickCount));282}283} catch (DirectoryIteratorException e) {284throw e.getCause();285}286}287288Object fileKey() {289return fileKey;290}291292@Override293public boolean isValid() {294return valid;295}296297void invalidate() {298valid = false;299}300301// enables periodic polling302void enable(Set<? extends WatchEvent.Kind<?>> events, long period) {303synchronized (this) {304// update the events305this.events = events;306307// create the periodic task308Runnable thunk = new Runnable() { public void run() { poll(); }};309this.poller = scheduledExecutor310.scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS);311}312}313314// disables periodic polling315void disable() {316synchronized (this) {317if (poller != null)318poller.cancel(false);319}320}321322@Override323public void cancel() {324valid = false;325synchronized (map) {326map.remove(fileKey());327}328disable();329}330331/**332* Polls the directory to detect for new files, modified files, or333* deleted files.334*/335synchronized void poll() {336if (!valid) {337return;338}339340// update tick341tickCount++;342343// open directory344DirectoryStream<Path> stream = null;345try {346stream = Files.newDirectoryStream(watchable());347} catch (IOException x) {348// directory is no longer accessible so cancel key349cancel();350signal();351return;352}353354// iterate over all entries in directory355try {356for (Path entry: stream) {357long lastModified = 0L;358try {359lastModified =360Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();361} catch (IOException x) {362// unable to get attributes of entry. If file has just363// been deleted then we'll report it as deleted on the364// next poll365continue;366}367368// lookup cache369CacheEntry e = entries.get(entry.getFileName());370if (e == null) {371// new file found372entries.put(entry.getFileName(),373new CacheEntry(lastModified, tickCount));374375// queue ENTRY_CREATE if event enabled376if (events.contains(StandardWatchEventKinds.ENTRY_CREATE)) {377signalEvent(StandardWatchEventKinds.ENTRY_CREATE, entry.getFileName());378continue;379} else {380// if ENTRY_CREATE is not enabled and ENTRY_MODIFY is381// enabled then queue event to avoid missing out on382// modifications to the file immediately after it is383// created.384if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {385signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, entry.getFileName());386}387}388continue;389}390391// check if file has changed392if (e.lastModified != lastModified) {393if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {394signalEvent(StandardWatchEventKinds.ENTRY_MODIFY,395entry.getFileName());396}397}398// entry in cache so update poll time399e.update(lastModified, tickCount);400401}402} catch (DirectoryIteratorException e) {403// ignore for now; if the directory is no longer accessible404// then the key will be cancelled on the next poll405} finally {406407// close directory stream408try {409stream.close();410} catch (IOException x) {411// ignore412}413}414415// iterate over cache to detect entries that have been deleted416Iterator<Map.Entry<Path,CacheEntry>> i = entries.entrySet().iterator();417while (i.hasNext()) {418Map.Entry<Path,CacheEntry> mapEntry = i.next();419CacheEntry entry = mapEntry.getValue();420if (entry.lastTickCount() != tickCount) {421Path name = mapEntry.getKey();422// remove from map and queue delete event (if enabled)423i.remove();424if (events.contains(StandardWatchEventKinds.ENTRY_DELETE)) {425signalEvent(StandardWatchEventKinds.ENTRY_DELETE, name);426}427}428}429}430}431}432433434