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