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}