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}