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(r); 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 if (events.length == 0) 78 throw new IllegalArgumentException("No events to register"); 79 final Set<WatchEvent.Kind<?>> eventSet = 80 new HashSet<WatchEvent.Kind<?>>(events.length); 81 for (WatchEvent.Kind<?> event: events) { 82 // standard events 83 if (event == StandardWatchEventKind.ENTRY_CREATE || 84 event == StandardWatchEventKind.ENTRY_MODIFY || 85 event == StandardWatchEventKind.ENTRY_DELETE) 86 { 87 eventSet.add(event); 88 continue; 89 } 90 91 // OVERFLOW is ignored 92 if (event == StandardWatchEventKind.OVERFLOW) { 93 if (events.length == 1) 94 throw new IllegalArgumentException("No events to register"); 95 continue; 96 } 97 98 // null/unsupported 99 if (event == null) 100 throw new NullPointerException("An element in event set is 'null'"); 101 throw new UnsupportedOperationException(event.name()); 102 } 103 104 // A modifier may be used to specify the sensitivity level 105 SensitivityWatchEventModifier sensivity = SensitivityWatchEventModifier.MEDIUM; 106 if (modifiers.length > 0) { 107 for (WatchEvent.Modifier modifier: modifiers) { 108 if (modifier == null) 109 throw new NullPointerException(); 110 if (modifier instanceof SensitivityWatchEventModifier) { 111 sensivity = (SensitivityWatchEventModifier)modifier; 112 continue; 113 } 114 throw new UnsupportedOperationException("Modifier not supported"); 115 } 116 } 117 118 // check if watch service is closed 119 if (!isOpen()) 120 throw new ClosedWatchServiceException(); 121 122 // registration is done in privileged block as it requires the 123 // attributes of the entries in the directory. 124 try { 125 final SensitivityWatchEventModifier s = sensivity; 126 return AccessController.doPrivileged( 127 new PrivilegedExceptionAction<PollingWatchKey>() { 128 @Override 129 public PollingWatchKey run() throws IOException { 130 return doPrivilegedRegister(path, eventSet, s); 131 } 132 }); 133 } catch (PrivilegedActionException pae) { 134 Throwable cause = pae.getCause(); 135 if (cause != null && cause instanceof IOException) 136 throw (IOException)cause; 137 throw new AssertionError(pae); 138 } 139 } 140 141 // registers directory returning a new key if not already registered or 142 // existing key if already registered 143 private PollingWatchKey doPrivilegedRegister(Path path, 144 Set<? extends WatchEvent.Kind<?>> events, 145 SensitivityWatchEventModifier sensivity) 146 throws IOException 147 { 148 // check file is a directory and get its file key if possible 149 BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); 150 if (!attrs.isDirectory()) { 151 throw new NotDirectoryException(path.toString()); 152 } 153 Object fileKey = attrs.fileKey(); 154 if (fileKey == null) 155 throw new AssertionError("File keys must be supported"); 156 157 // grab close lock to ensure that watch service cannot be closed 158 synchronized (closeLock()) { 159 if (!isOpen()) 160 throw new ClosedWatchServiceException(); 161 162 PollingWatchKey watchKey; 163 synchronized (map) { 164 watchKey = map.get(fileKey); 165 if (watchKey == null) { 166 // new registration 167 watchKey = new PollingWatchKey(path, this, fileKey); 168 map.put(fileKey, watchKey); 169 } else { 170 // update to existing registration 171 watchKey.disable(); 172 } 173 } 174 watchKey.enable(events, sensivity.sensitivityValueInSeconds()); 175 return watchKey; 176 } 177 178 } 179 180 @Override 181 void implClose() throws IOException { 182 synchronized (map) { 183 for (Map.Entry<Object,PollingWatchKey> entry: map.entrySet()) { 184 PollingWatchKey watchKey = entry.getValue(); 185 watchKey.disable(); 186 watchKey.invalidate(); 187 } 188 map.clear(); 189 } 190 AccessController.doPrivileged(new PrivilegedAction<Void>() { 191 @Override 192 public Void run() { 193 scheduledExecutor.shutdown(); 194 return null; 195 } 196 }); 197 } 198 199 /** 200 * Entry in directory cache to record file last-modified-time and tick-count 201 */ 202 private static class CacheEntry { 203 private long lastModified; 204 private int lastTickCount; 205 206 CacheEntry(long lastModified, int lastTickCount) { 207 this.lastModified = lastModified; 208 this.lastTickCount = lastTickCount; 209 } 210 211 int lastTickCount() { 212 return lastTickCount; 213 } 214 215 long lastModified() { 216 return lastModified; 217 } 218 219 void update(long lastModified, int tickCount) { 220 this.lastModified = lastModified; 221 this.lastTickCount = tickCount; 222 } 223 } 224 225 /** 226 * WatchKey implementation that encapsulates a map of the entries of the 227 * entries in the directory. Polling the key causes it to re-scan the 228 * directory and queue keys when entries are added, modified, or deleted. 229 */ 230 private class PollingWatchKey extends AbstractWatchKey { 231 private final Object fileKey; 232 233 // current event set 234 private Set<? extends WatchEvent.Kind<?>> events; 235 236 // the result of the periodic task that causes this key to be polled 237 private ScheduledFuture<?> poller; 238 239 // indicates if the key is valid 240 private volatile boolean valid; 241 242 // used to detect files that have been deleted 243 private int tickCount; 244 245 // map of entries in directory 246 private Map<Path,CacheEntry> entries; 247 248 PollingWatchKey(Path dir, PollingWatchService watcher, Object fileKey) 249 throws IOException 250 { 251 super(dir, watcher); 252 this.fileKey = fileKey; 253 this.valid = true; 254 this.tickCount = 0; 255 this.entries = new HashMap<Path,CacheEntry>(); 256 257 // get the initial entries in the directory 258 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { 259 for (Path entry: stream) { 260 // don't follow links 261 long lastModified = 262 Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis(); 263 entries.put(entry.getFileName(), new CacheEntry(lastModified, tickCount)); 264 } 265 } catch (DirectoryIteratorException e) { 266 throw e.getCause(); 267 } 268 } 269 270 Object fileKey() { 271 return fileKey; 272 } 273 274 @Override 275 public boolean isValid() { 276 return valid; 277 } 278 279 void invalidate() { 280 valid = false; 281 } 282 283 // enables periodic polling 284 void enable(Set<? extends WatchEvent.Kind<?>> events, long period) { 285 synchronized (this) { 286 // update the events 287 this.events = events; 288 289 // create the periodic task 290 Runnable thunk = new Runnable() { public void run() { poll(); }}; 291 this.poller = scheduledExecutor 292 .scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS); 293 } 294 } 295 296 // disables periodic polling 297 void disable() { 298 synchronized (this) { 299 if (poller != null) 300 poller.cancel(false); 301 } 302 } 303 304 @Override 305 public void cancel() { 306 valid = false; 307 synchronized (map) { 308 map.remove(fileKey()); 309 } 310 disable(); 311 } 312 313 /** 314 * Polls the directory to detect for new files, modified files, or 315 * deleted files. 316 */ 317 synchronized void poll() { 318 if (!valid) { 319 return; 320 } 321 322 // update tick 323 tickCount++; 324 325 // open directory 326 DirectoryStream<Path> stream = null; 327 try { 328 stream = Files.newDirectoryStream(watchable()); 329 } catch (IOException x) { 330 // directory is no longer accessible so cancel key 331 cancel(); 332 signal(); 333 return; 334 } 335 336 // iterate over all entries in directory 337 try { 338 for (Path entry: stream) { 339 long lastModified = 0L; 340 try { 341 lastModified = 342 Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis(); 343 } catch (IOException x) { 344 // unable to get attributes of entry. If file has just 345 // been deleted then we'll report it as deleted on the 346 // next poll 347 continue; 348 } 349 350 // lookup cache 351 CacheEntry e = entries.get(entry.getFileName()); 352 if (e == null) { 353 // new file found 354 entries.put(entry.getFileName(), 355 new CacheEntry(lastModified, tickCount)); 356 357 // queue ENTRY_CREATE if event enabled 358 if (events.contains(StandardWatchEventKind.ENTRY_CREATE)) { 359 signalEvent(StandardWatchEventKind.ENTRY_CREATE, entry.getFileName()); 360 continue; 361 } else { 362 // if ENTRY_CREATE is not enabled and ENTRY_MODIFY is 363 // enabled then queue event to avoid missing out on 364 // modifications to the file immediately after it is 365 // created. 366 if (events.contains(StandardWatchEventKind.ENTRY_MODIFY)) { 367 signalEvent(StandardWatchEventKind.ENTRY_MODIFY, entry.getFileName()); 368 } 369 } 370 continue; 371 } 372 373 // check if file has changed 374 if (e.lastModified != lastModified) { 375 if (events.contains(StandardWatchEventKind.ENTRY_MODIFY)) { 376 signalEvent(StandardWatchEventKind.ENTRY_MODIFY, 377 entry.getFileName()); 378 } 379 } 380 // entry in cache so update poll time 381 e.update(lastModified, tickCount); 382 383 } 384 } catch (DirectoryIteratorException e) { 385 // ignore for now; if the directory is no longer accessible 386 // then the key will be cancelled on the next poll 387 } finally { 388 389 // close directory stream 390 try { 391 stream.close(); 392 } catch (IOException x) { 393 // ignore 394 } 395 } 396 397 // iterate over cache to detect entries that have been deleted 398 Iterator<Map.Entry<Path,CacheEntry>> i = entries.entrySet().iterator(); 399 while (i.hasNext()) { 400 Map.Entry<Path,CacheEntry> mapEntry = i.next(); 401 CacheEntry entry = mapEntry.getValue(); 402 if (entry.lastTickCount() != tickCount) { 403 Path name = mapEntry.getKey(); 404 // remove from map and queue delete event (if enabled) 405 i.remove(); 406 if (events.contains(StandardWatchEventKind.ENTRY_DELETE)) { 407 signalEvent(StandardWatchEventKind.ENTRY_DELETE, name); 408 } 409 } 410 } 411 } 412 } 413 }