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 java.io.IOException; 021import java.nio.file.ClosedWatchServiceException; 022import java.nio.file.Path; 023import java.util.List; 024import java.util.Timer; 025import java.util.TimerTask; 026import java.util.concurrent.atomic.AtomicBoolean; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import org.syncany.util.EnvironmentUtil; 031 032/** 033 * The recursive file watcher monitors a folder (and its sub-folders). 034 * 035 * <p>When a file event occurs, a timer is started to wait for the file operations 036 * to settle. It is reset whenever a new event occurs. When the timer times out, 037 * an event is thrown through the {@link WatchListener}. 038 * 039 * <p>This is an abstract class, using several template methods that are called 040 * in different lifecycle states: {@link #beforeStart()}, {@link #beforePollEventLoop()}, 041 * {@link #pollEvents()}, and {@link #afterStop()}. 042 * 043 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 044 */ 045public abstract class RecursiveWatcher { 046 protected static final Logger logger = Logger.getLogger(RecursiveWatcher.class.getSimpleName()); 047 048 protected Path root; 049 protected List<Path> ignorePaths; 050 private int settleDelay; 051 private WatchListener listener; 052 053 private AtomicBoolean running; 054 055 private Thread watchThread; 056 private Timer timer; 057 058 public RecursiveWatcher(Path root, List<Path> ignorePaths, int settleDelay, WatchListener listener) { 059 this.root = root; 060 this.ignorePaths = ignorePaths; 061 this.settleDelay = settleDelay; 062 this.listener = listener; 063 064 this.running = new AtomicBoolean(false); 065 } 066 067 /** 068 * Creates a recursive watcher for the given root path. The returned watcher 069 * will ignore the ignore paths and fire an event through the {@link WatchListener} 070 * as soon as the settle delay (in ms) has passed. 071 * 072 * <p>The method returns a platform-specific recursive watcher: {@link WindowsRecursiveWatcher} 073 * for Windows and {@link DefaultRecursiveWatcher} for other operating systems. 074 */ 075 public static RecursiveWatcher createRecursiveWatcher(Path root, List<Path> ignorePaths, int settleDelay, WatchListener listener) { 076 if (EnvironmentUtil.isWindows()) { 077 return new WindowsRecursiveWatcher(root, ignorePaths, settleDelay, listener); 078 } 079 else { 080 return new DefaultRecursiveWatcher(root, ignorePaths, settleDelay, listener); 081 } 082 } 083 084 /** 085 * Starts the watcher service and registers watches in all of the sub-folders of 086 * the given root folder. 087 * 088 * <p>This method calls the {@link #beforeStart()} method before everything else. 089 * Subclasses may execute their own commands there. Before the watch thread is started, 090 * {@link #beforePollEventLoop()} is called. And in the watch thread loop, 091 * {@link #pollEvents()} is called. 092 * 093 * <p><b>Important:</b> This method returns immediately, even though the watches 094 * might not be in place yet. For large file trees, it might take several seconds 095 * until all directories are being monitored. For normal cases (1-100 folders), this 096 * should not take longer than a few milliseconds. 097 */ 098 public void start() throws Exception { 099 // Call before-start hook 100 beforeStart(); 101 102 // Start watcher thread 103 watchThread = new Thread(new Runnable() { 104 @Override 105 public void run() { 106 running.set(true); 107 beforePollEventLoop(); // Call before-loop hook 108 109 while (running.get()) { 110 try { 111 boolean relevantEvents = pollEvents(); 112 113 if (relevantEvents) { 114 restartWaitSettlementTimer(); 115 } 116 } 117 catch (InterruptedException e) { 118 logger.log(Level.FINE, "Could not poll the events. EXITING watcher.", e); 119 running.set(false); 120 } 121 catch (ClosedWatchServiceException e) { 122 logger.log(Level.FINE, "Watch closed or polling failed. EXITING watcher.", e); 123 running.set(false); 124 } 125 } 126 } 127 }, "Watcher/" + root.toFile().getName()); 128 129 watchThread.start(); 130 } 131 132 /** 133 * Stops the watch thread by interrupting it and subsequently 134 * calls the {@link #afterStop()} template method (to be implemented 135 * by subclasses. 136 */ 137 public synchronized void stop() { 138 if (watchThread != null) { 139 try { 140 running.set(false); 141 watchThread.interrupt(); 142 143 // Call after-stop hook 144 afterStop(); 145 } 146 catch (IOException e) { 147 logger.log(Level.FINE, "Could not close watcher", e); 148 } 149 } 150 } 151 152 private synchronized void restartWaitSettlementTimer() { 153 logger.log(Level.FINE, "File system events registered. Waiting " + settleDelay + "ms for settlement ...."); 154 155 if (timer != null) { 156 timer.cancel(); 157 timer = null; 158 } 159 160 timer = new Timer("FsSettleTim/" + root.toFile().getName()); 161 timer.schedule(new TimerTask() { 162 @Override 163 public void run() { 164 logger.log(Level.INFO, "File system actions (on watched folders) settled. Updating watches ..."); 165 166 watchEventsOccurred(); 167 fireListenerEvents(); 168 } 169 }, settleDelay); 170 } 171 172 private synchronized void fireListenerEvents() { 173 if (listener != null) { 174 logger.log(Level.INFO, "- Firing watch event (watchEventsOccurred) ..."); 175 listener.watchEventsOccurred(); 176 } 177 } 178 179 /** 180 * Called before the {@link #start()} method. This method is 181 * only called once. 182 */ 183 protected abstract void beforeStart() throws Exception; 184 185 /** 186 * Called in the watch service polling thread, right 187 * before the {@link #pollEvents()} loop. This method is 188 * only called once. 189 */ 190 protected abstract void beforePollEventLoop(); 191 192 /** 193 * Called in the watch service polling thread, inside 194 * of the {@link #pollEvents()} loop. This method is called 195 * multiple times. 196 */ 197 protected abstract boolean pollEvents() throws InterruptedException; 198 199 /** 200 * Called in the watch service polling thread, whenever 201 * a file system event occurs. This may be used by subclasses 202 * to (re-)set watches on folders. This method is called 203 * multiple times. 204 */ 205 protected abstract void watchEventsOccurred(); 206 207 /** 208 * Called after the {@link #stop()} method. This method is 209 * only called once. 210 */ 211 protected abstract void afterStop() throws IOException; 212 213 public interface WatchListener { 214 public void watchEventsOccurred(); 215 } 216}