/* * Syncany, www.syncany.org * Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.syncany.operations.watch; import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import org.syncany.util.EnvironmentUtil; /** * The recursive file watcher monitors a folder (and its sub-folders). * * <p>When a file event occurs, a timer is started to wait for the file operations * to settle. It is reset whenever a new event occurs. When the timer times out, * an event is thrown through the {@link WatchListener}. * * <p>This is an abstract class, using several template methods that are called * in different lifecycle states: {@link #beforeStart()}, {@link #beforePollEventLoop()}, * {@link #pollEvents()}, and {@link #afterStop()}. * * @author Philipp C. Heckel <philipp.heckel@gmail.com> */ public abstract class RecursiveWatcher { protected static final Logger logger = Logger.getLogger(RecursiveWatcher.class.getSimpleName()); protected Path root; protected List<Path> ignorePaths; private int settleDelay; private WatchListener listener; private AtomicBoolean running; private Thread watchThread; private Timer timer; public RecursiveWatcher(Path root, List<Path> ignorePaths, int settleDelay, WatchListener listener) { this.root = root; this.ignorePaths = ignorePaths; this.settleDelay = settleDelay; this.listener = listener; this.running = new AtomicBoolean(false); } /** * Creates a recursive watcher for the given root path. The returned watcher * will ignore the ignore paths and fire an event through the {@link WatchListener} * as soon as the settle delay (in ms) has passed. * * <p>The method returns a platform-specific recursive watcher: {@link WindowsRecursiveWatcher} * for Windows and {@link DefaultRecursiveWatcher} for other operating systems. */ public static RecursiveWatcher createRecursiveWatcher(Path root, List<Path> ignorePaths, int settleDelay, WatchListener listener) { if (EnvironmentUtil.isWindows()) { return new WindowsRecursiveWatcher(root, ignorePaths, settleDelay, listener); } else { return new DefaultRecursiveWatcher(root, ignorePaths, settleDelay, listener); } } /** * Starts the watcher service and registers watches in all of the sub-folders of * the given root folder. * * <p>This method calls the {@link #beforeStart()} method before everything else. * Subclasses may execute their own commands there. Before the watch thread is started, * {@link #beforePollEventLoop()} is called. And in the watch thread loop, * {@link #pollEvents()} is called. * * <p><b>Important:</b> This method returns immediately, even though the watches * might not be in place yet. For large file trees, it might take several seconds * until all directories are being monitored. For normal cases (1-100 folders), this * should not take longer than a few milliseconds. */ public void start() throws Exception { // Call before-start hook beforeStart(); // Start watcher thread watchThread = new Thread(new Runnable() { @Override public void run() { running.set(true); beforePollEventLoop(); // Call before-loop hook while (running.get()) { try { boolean relevantEvents = pollEvents(); if (relevantEvents) { restartWaitSettlementTimer(); } } catch (InterruptedException e) { logger.log(Level.FINE, "Could not poll the events", e); running.set(false); } } } }, "Watcher/" + root.toFile().getName()); watchThread.start(); } /** * Stops the watch thread by interrupting it and subsequently * calls the {@link #afterStop()} template method (to be implemented * by subclasses. */ public synchronized void stop() { if (watchThread != null) { try { running.set(false); watchThread.interrupt(); // Call after-stop hook afterStop(); } catch (IOException e) { logger.log(Level.FINE, "Could not close watcher", e); } } } private synchronized void restartWaitSettlementTimer() { logger.log(Level.FINE, "File system events registered. Waiting " + settleDelay + "ms for settlement ...."); if (timer != null) { timer.cancel(); timer = null; } timer = new Timer("FsSettleTim/" + root.toFile().getName()); timer.schedule(new TimerTask() { @Override public void run() { logger.log(Level.INFO, "File system actions (on watched folders) settled. Updating watches ..."); watchEventsOccurred(); fireListenerEvents(); } }, settleDelay); } private synchronized void fireListenerEvents() { if (listener != null) { logger.log(Level.INFO, "- Firing watch event (watchEventsOccurred) ..."); listener.watchEventsOccurred(); } } /** * Called before the {@link #start()} method. This method is * only called once. */ protected abstract void beforeStart() throws Exception; /** * Called in the watch service polling thread, right * before the {@link #pollEvents()} loop. This method is * only called once. */ protected abstract void beforePollEventLoop(); /** * Called in the watch service polling thread, inside * of the {@link #pollEvents()} loop. This method is called * multiple times. */ protected abstract boolean pollEvents() throws InterruptedException; /** * Called in the watch service polling thread, whenever * a file system event occurs. This may be used by subclasses * to (re-)set watches on folders. This method is called * multiple times. */ protected abstract void watchEventsOccurred(); /** * Called after the {@link #stop()} method. This method is * only called once. */ protected abstract void afterStop() throws IOException; public interface WatchListener { public void watchEventsOccurred(); } }