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