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.daemon;
019
020import java.io.File;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Map;
024import java.util.TreeMap;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.syncany.config.Config;
029import org.syncany.config.ConfigException;
030import org.syncany.config.ConfigHelper;
031import org.syncany.config.DaemonConfigHelper;
032import org.syncany.config.LocalEventBus;
033import org.syncany.config.to.DaemonConfigTO;
034import org.syncany.config.to.FolderTO;
035import org.syncany.operations.daemon.Watch.SyncStatus;
036import org.syncany.operations.daemon.messages.AddWatchManagementRequest;
037import org.syncany.operations.daemon.messages.AddWatchManagementResponse;
038import org.syncany.operations.daemon.messages.BadRequestResponse;
039import org.syncany.operations.daemon.messages.DaemonReloadedExternalEvent;
040import org.syncany.operations.daemon.messages.ListWatchesManagementRequest;
041import org.syncany.operations.daemon.messages.ListWatchesManagementResponse;
042import org.syncany.operations.daemon.messages.RemoveWatchManagementRequest;
043import org.syncany.operations.daemon.messages.RemoveWatchManagementResponse;
044import org.syncany.operations.daemon.messages.api.FolderRequest;
045import org.syncany.operations.daemon.messages.api.ManagementRequest;
046import org.syncany.operations.daemon.messages.api.ManagementRequestHandler;
047import org.syncany.operations.daemon.messages.api.Response;
048import org.syncany.operations.watch.WatchOperation;
049import org.syncany.operations.watch.WatchOperationOptions;
050
051import com.google.common.collect.Maps;
052import com.google.common.eventbus.Subscribe;
053
054/**
055 * The watch server can manage many different {@link WatchOperation}s. When started
056 * with {@link #start(DaemonConfigTO)} or {@link #reload(DaemonConfigTO)}, it first reads the daemon configuration file
057 * and then runs new threads for each configured Syncany folder. Invalid or non-existing folders
058 * are ignored.
059 *
060 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
061 */
062public class WatchServer {
063        private static final Logger logger = Logger.getLogger(WatchServer.class.getSimpleName());
064
065        private DaemonConfigTO daemonConfig;
066        private Map<File, WatchRunner> watchOperations;
067        private LocalEventBus eventBus;
068
069        public WatchServer() {
070                this.daemonConfig = null;
071                this.watchOperations = new TreeMap<File, WatchRunner>();
072
073                this.eventBus = LocalEventBus.getInstance();
074                this.eventBus.register(this);
075        }
076
077        public void start(DaemonConfigTO daemonConfigTO) {
078                reload(daemonConfigTO);
079        }
080
081        public void reload(DaemonConfigTO daemonConfigTO) {
082                logger.log(Level.INFO, "Starting/reloading watch server ... ");
083
084                // Update config
085                daemonConfig = daemonConfigTO;
086
087                // Restart threads
088                try {
089                        Map<File, FolderTO> watchedFolders = getFolderMap(daemonConfigTO.getFolders());
090
091                        stopAllWatchOperations();
092                        startWatchOperations(watchedFolders);
093
094                        fireDaemonReloadedEvent();
095                }
096                catch (Exception e) {
097                        logger.log(Level.WARNING, "Cannot (re-)load config. Exception thrown.", e);
098                }
099        }
100
101        public void stop() {
102                logger.log(Level.INFO, "Stopping watch server ...  ");
103                Map<File, WatchRunner> copyOfWatchOperations = Maps.newHashMap(watchOperations);
104
105                for (Map.Entry<File, WatchRunner> folderEntry : copyOfWatchOperations.entrySet()) {
106                        File localDir = folderEntry.getKey();
107                        WatchRunner watchOperationThread = folderEntry.getValue();
108
109                        logger.log(Level.INFO, "- Stopping watch operation at " + localDir + " ...");
110                        watchOperationThread.stop();
111
112                        watchOperations.remove(localDir);
113                }
114        }
115
116        private void startWatchOperations(Map<File, FolderTO> newWatchedFolderTOs) throws ConfigException, ServiceAlreadyStartedException {
117                for (Map.Entry<File, FolderTO> folderEntry : newWatchedFolderTOs.entrySet()) {
118                        File localDir = folderEntry.getKey();
119
120                        try {
121                                Config watchConfig = ConfigHelper.loadConfig(localDir);
122
123                                if (watchConfig != null) {
124                                        logger.log(Level.INFO, "- Starting watch operation at " + localDir + " ...");
125
126                                        WatchOperationOptions watchOptions = folderEntry.getValue().getWatchOptions();
127
128                                        if (watchOptions == null) {
129                                                watchOptions = new WatchOperationOptions();
130                                        }
131
132                                        WatchRunner watchRunner = new WatchRunner(watchConfig, watchOptions, daemonConfig.getPortTO());
133                                        watchRunner.start();
134
135                                        watchOperations.put(localDir, watchRunner);
136                                }
137                                else {
138                                        logger.log(Level.INFO, "- CANNOT start watch, because no config found at " + localDir + " ...");
139                                }
140                        }
141                        catch (Exception e) {
142                                logger.log(Level.SEVERE, "  + Cannot start watch operation at " + localDir + ". IGNORING.", e);
143                        }
144                }
145        }
146
147        /**
148         * Stops all watchOperations and verifies if
149         * they actually have stopped.
150         */
151        private void stopAllWatchOperations() {
152                for (File localDir : watchOperations.keySet()) {
153                        WatchRunner watchOperationThread = watchOperations.get(localDir);
154
155                        logger.log(Level.INFO, "- Stopping watch operation at " + localDir + " ...");
156                        watchOperationThread.stop();
157                }
158
159                // Check if watch operations actually have stopped.
160                while (watchOperations.keySet().size() > 0) {
161                        Map<File, WatchRunner> watchOperationsCopy = new TreeMap<File, WatchRunner>(watchOperations);
162
163                        for (File localDir : watchOperationsCopy.keySet()) {
164                                WatchRunner watchOperationThread = watchOperationsCopy.get(localDir);
165
166                                if (watchOperationThread.hasStopped()) {
167                                        logger.log(Level.INFO, "- Watch operation at " + localDir + " has stopped");
168                                        watchOperations.remove(localDir);
169                                }
170                        }
171                }
172        }
173
174        private Map<File, FolderTO> getFolderMap(List<FolderTO> watchedFolders) {
175                Map<File, FolderTO> watchedFolderTOs = new TreeMap<File, FolderTO>();
176
177                for (FolderTO folderTO : watchedFolders) {
178                        if (folderTO.isEnabled()) {
179                                watchedFolderTOs.put(new File(folderTO.getPath()), folderTO);
180                        }
181                }
182
183                return watchedFolderTOs;
184        }
185
186        private void fireDaemonReloadedEvent() {
187                logger.log(Level.INFO, "Firing daemon-reloaded event ...");
188                eventBus.post(new DaemonReloadedExternalEvent());
189        }
190
191        @Subscribe
192        public void onFolderRequestReceived(FolderRequest folderRequest) {
193                File rootFolder = new File(folderRequest.getRoot());
194
195                if (!watchOperations.containsKey(rootFolder)) {
196                        eventBus.post(new BadRequestResponse(folderRequest.getId(), "Unknown root folder."));
197                }
198        }
199        
200        @Subscribe
201        public void onManagementRequestReceived(ManagementRequest managementRequest) {
202                logger.log(Level.INFO, "Received " + managementRequest);
203
204                try {
205                        ManagementRequestHandler handler = ManagementRequestHandler.createManagementRequestHandler(managementRequest);
206                        Response response = handler.handleRequest(managementRequest);
207
208                        if (response != null) {
209                                eventBus.post(response);
210                        }
211                }
212                catch (ClassNotFoundException e) {
213                        logger.log(Level.FINE, "No handler found for management request class " + managementRequest.getClass() + ". Ignoring."); // Not logging 'e'!
214                }
215                catch (Exception e) {
216                        logger.log(Level.FINE, "Failed to process request", e);
217                        eventBus.post(new BadRequestResponse(managementRequest.getId(), "Invalid request."));
218                }
219        }
220
221        @Subscribe
222        public void onListWatchesRequestReceived(ListWatchesManagementRequest request) {
223                List<Watch> watchList = new ArrayList<Watch>();
224
225                for (File watchFolder : watchOperations.keySet()) {
226                        boolean syncRunning = watchOperations.get(watchFolder).isSyncRunning();
227                        SyncStatus syncStatus = (syncRunning) ? SyncStatus.SYNCING : SyncStatus.IN_SYNC;
228
229                        watchList.add(new Watch(watchFolder, syncStatus));
230                }
231
232                eventBus.post(new ListWatchesManagementResponse(request.getId(), watchList));
233        }
234
235        @Subscribe
236        public void onAddWatchRequestReceived(AddWatchManagementRequest request) {
237                File rootFolder = request.getWatch();
238
239                if (watchOperations.containsKey(rootFolder)) {
240                        eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_ALREADY_EXISTS, request.getId(), "Watch already exists."));
241                }
242                else {
243                        try {
244                                boolean folderAdded = DaemonConfigHelper.addFolder(rootFolder);
245
246                                if (folderAdded) {
247                                        eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.OKAY, request.getId(), "Successfully added."));
248                                }
249                                else {
250                                        eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_ALREADY_EXISTS, request.getId(),
251                                                        "Watch already exists (inactive/disabled)."));
252                                }
253                        }
254                        catch (ConfigException e) {
255                                logger.log(Level.WARNING, "Error adding watch to daemon config.", e);
256                                eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_OTHER, request.getId(), "Error adding to config: "
257                                                + e.getMessage()));
258                        }
259                }
260        }
261        
262        @Subscribe
263        public void onRemoveWatchRequestReceived(RemoveWatchManagementRequest request) {
264                File rootFolder = request.getWatch();
265
266                if (!watchOperations.containsKey(rootFolder)) {
267                        eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_DOES_NOT_EXIST, request.getId(), "Watch does not exist."));
268                }
269                else {
270                        try {
271                                boolean folderRemoved = DaemonConfigHelper.removeFolder(rootFolder);
272
273                                if (folderRemoved) {
274                                        eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.OKAY, request.getId(), "Successfully removed."));
275                                }
276                                else {
277                                        eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_DOES_NOT_EXIST, request.getId(),
278                                                        "Watch does not exist (inactive/disabled)."));
279                                }
280                        }
281                        catch (ConfigException e) {
282                                logger.log(Level.WARNING, "Error removing watch from daemon config.", e);
283                                eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_OTHER, request.getId(), "Error removing to config: "
284                                                + e.getMessage()));
285                        }
286                }
287        }
288}