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