001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.operations.watch; 019 020import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; 021import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; 022import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; 023import static java.nio.file.StandardWatchEventKinds.OVERFLOW; 024 025import java.io.IOException; 026import java.nio.file.FileSystems; 027import java.nio.file.FileVisitResult; 028import java.nio.file.FileVisitor; 029import java.nio.file.Files; 030import java.nio.file.LinkOption; 031import java.nio.file.Path; 032import java.nio.file.WatchKey; 033import java.nio.file.WatchService; 034import java.nio.file.attribute.BasicFileAttributes; 035import java.util.HashMap; 036import java.util.HashSet; 037import java.util.List; 038import java.util.Map; 039import java.util.Set; 040import java.util.logging.Level; 041 042/** 043 * The default recursive file watcher monitors a folder (and its sub-folders) 044 * by registering a watch on each of the sub-folders. This class is used on 045 * Linux/Unix-based operating systems and uses the Java 7 {@link WatchService}. 046 * 047 * <p>The class walks through the file tree and registers to a watch to every sub-folder. 048 * For new folders, a new watch is registered, and stale watches are removed. 049 * 050 * <p>When a file event occurs, a timer is started to wait for the file operations 051 * to settle. It is reset whenever a new event occurs. When the timer times out, 052 * an event is thrown through the {@link WatchListener}. 053 * 054 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 055 */ 056public class DefaultRecursiveWatcher extends RecursiveWatcher { 057 private WatchService watchService; 058 private Map<Path, WatchKey> watchPathKeyMap; 059 060 public DefaultRecursiveWatcher(Path root, List<Path> ignorePaths, int settleDelay, WatchListener listener) { 061 super(root, ignorePaths, settleDelay, listener); 062 063 this.watchService = null; 064 this.watchPathKeyMap = new HashMap<Path, WatchKey>(); 065 } 066 067 @Override 068 public void beforeStart() throws Exception { 069 watchService = FileSystems.getDefault().newWatchService(); 070 } 071 072 @Override 073 protected void beforePollEventLoop() { 074 walkTreeAndSetWatches(); 075 } 076 077 @Override 078 protected boolean pollEvents() throws InterruptedException { 079 // Take events, but don't care what they are! 080 WatchKey watchKey = watchService.take(); 081 082 watchKey.pollEvents(); 083 watchKey.reset(); 084 085 // Events are always relevant; ignored paths are not monitored 086 return true; 087 } 088 089 @Override 090 protected void watchEventsOccurred() { 091 walkTreeAndSetWatches(); 092 unregisterStaleWatches(); 093 } 094 095 @Override 096 public void afterStop() throws IOException { 097 watchService.close(); 098 } 099 100 private synchronized void walkTreeAndSetWatches() { 101 logger.log(Level.INFO, "Registering new folders at watch service ..."); 102 103 try { 104 Files.walkFileTree(root, new FileVisitor<Path>() { 105 @Override 106 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 107 if (ignorePaths.contains(dir)) { 108 return FileVisitResult.SKIP_SUBTREE; 109 } 110 else { 111 registerWatch(dir); 112 return FileVisitResult.CONTINUE; 113 } 114 } 115 116 @Override 117 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 118 return FileVisitResult.CONTINUE; 119 } 120 121 @Override 122 public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { 123 return FileVisitResult.CONTINUE; 124 } 125 126 @Override 127 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 128 return FileVisitResult.CONTINUE; 129 } 130 }); 131 } 132 catch (IOException e) { 133 logger.log(Level.FINE, "IO failed", e); 134 } 135 } 136 137 private synchronized void unregisterStaleWatches() { 138 Set<Path> paths = new HashSet<Path>(watchPathKeyMap.keySet()); 139 Set<Path> stalePaths = new HashSet<Path>(); 140 141 for (Path path : paths) { 142 if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { 143 stalePaths.add(path); 144 } 145 } 146 147 if (stalePaths.size() > 0) { 148 logger.log(Level.INFO, "Cancelling stale path watches ..."); 149 150 for (Path stalePath : stalePaths) { 151 unregisterWatch(stalePath); 152 } 153 } 154 } 155 156 private synchronized void registerWatch(Path dir) { 157 if (!watchPathKeyMap.containsKey(dir)) { 158 logger.log(Level.INFO, "- Registering " + dir); 159 160 try { 161 WatchKey watchKey = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW); 162 watchPathKeyMap.put(dir, watchKey); 163 } 164 catch (IOException e) { 165 logger.log(Level.FINE, "IO Failed", e); 166 } 167 } 168 } 169 170 private synchronized void unregisterWatch(Path dir) { 171 WatchKey watchKey = watchPathKeyMap.get(dir); 172 173 if (watchKey != null) { 174 logger.log(Level.INFO, "- Cancelling " + dir); 175 176 watchKey.cancel(); 177 watchPathKeyMap.remove(dir); 178 } 179 } 180}