/* * Syncany, www.syncany.org * Copyright (C) 2011-2016 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.daemon; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.syncany.config.Config; import org.syncany.config.ConfigException; import org.syncany.config.ConfigHelper; import org.syncany.config.DaemonConfigHelper; import org.syncany.config.LocalEventBus; import org.syncany.config.to.DaemonConfigTO; import org.syncany.config.to.FolderTO; import org.syncany.operations.daemon.Watch.SyncStatus; import org.syncany.operations.daemon.messages.AddWatchManagementRequest; import org.syncany.operations.daemon.messages.AddWatchManagementResponse; import org.syncany.operations.daemon.messages.BadRequestResponse; import org.syncany.operations.daemon.messages.DaemonReloadedExternalEvent; import org.syncany.operations.daemon.messages.ListWatchesManagementRequest; import org.syncany.operations.daemon.messages.ListWatchesManagementResponse; import org.syncany.operations.daemon.messages.RemoveWatchManagementRequest; import org.syncany.operations.daemon.messages.RemoveWatchManagementResponse; import org.syncany.operations.daemon.messages.api.FolderRequest; import org.syncany.operations.daemon.messages.api.ManagementRequest; import org.syncany.operations.daemon.messages.api.ManagementRequestHandler; import org.syncany.operations.daemon.messages.api.Response; import org.syncany.operations.watch.WatchOperation; import org.syncany.operations.watch.WatchOperationOptions; import com.google.common.collect.Maps; import com.google.common.eventbus.Subscribe; /** * The watch server can manage many different {@link WatchOperation}s. When started * with {@link #start()} or {@link #reload()}, it first reads the daemon configuration file * and then runs new threads for each configured Syncany folder. Invalid or non-existing folders * are ignored. * * @author Philipp C. Heckel <philipp.heckel@gmail.com> */ public class WatchServer { private static final Logger logger = Logger.getLogger(WatchServer.class.getSimpleName()); private DaemonConfigTO daemonConfig; private Map<File, WatchRunner> watchOperations; private LocalEventBus eventBus; public WatchServer() { this.daemonConfig = null; this.watchOperations = new TreeMap<File, WatchRunner>(); this.eventBus = LocalEventBus.getInstance(); this.eventBus.register(this); } public void start(DaemonConfigTO daemonConfigTO) { reload(daemonConfigTO); } public void reload(DaemonConfigTO daemonConfigTO) { logger.log(Level.INFO, "Starting/reloading watch server ... "); // Update config daemonConfig = daemonConfigTO; // Restart threads try { Map<File, FolderTO> watchedFolders = getFolderMap(daemonConfigTO.getFolders()); stopAllWatchOperations(); startWatchOperations(watchedFolders); fireDaemonReloadedEvent(); } catch (Exception e) { logger.log(Level.WARNING, "Cannot (re-)load config. Exception thrown.", e); } } public void stop() { logger.log(Level.INFO, "Stopping watch server ... "); Map<File, WatchRunner> copyOfWatchOperations = Maps.newHashMap(watchOperations); for (Map.Entry<File, WatchRunner> folderEntry : copyOfWatchOperations.entrySet()) { File localDir = folderEntry.getKey(); WatchRunner watchOperationThread = folderEntry.getValue(); logger.log(Level.INFO, "- Stopping watch operation at " + localDir + " ..."); watchOperationThread.stop(); watchOperations.remove(localDir); } } private void startWatchOperations(Map<File, FolderTO> newWatchedFolderTOs) throws ConfigException, ServiceAlreadyStartedException { for (Map.Entry<File, FolderTO> folderEntry : newWatchedFolderTOs.entrySet()) { File localDir = folderEntry.getKey(); try { Config watchConfig = ConfigHelper.loadConfig(localDir); if (watchConfig != null) { logger.log(Level.INFO, "- Starting watch operation at " + localDir + " ..."); WatchOperationOptions watchOptions = folderEntry.getValue().getWatchOptions(); if (watchOptions == null) { watchOptions = new WatchOperationOptions(); } WatchRunner watchRunner = new WatchRunner(watchConfig, watchOptions, daemonConfig.getPortTO()); watchRunner.start(); watchOperations.put(localDir, watchRunner); } else { logger.log(Level.INFO, "- CANNOT start watch, because no config found at " + localDir + " ..."); } } catch (Exception e) { logger.log(Level.SEVERE, " + Cannot start watch operation at " + localDir + ". IGNORING.", e); } } } /** * Stops all watchOperations and verifies if * they actually have stopped. */ private void stopAllWatchOperations() { for (File localDir : watchOperations.keySet()) { WatchRunner watchOperationThread = watchOperations.get(localDir); logger.log(Level.INFO, "- Stopping watch operation at " + localDir + " ..."); watchOperationThread.stop(); } // Check if watch operations actually have stopped. while (watchOperations.keySet().size() > 0) { Map<File, WatchRunner> watchOperationsCopy = new TreeMap<File, WatchRunner>(watchOperations); for (File localDir : watchOperationsCopy.keySet()) { WatchRunner watchOperationThread = watchOperationsCopy.get(localDir); if (watchOperationThread.hasStopped()) { logger.log(Level.INFO, "- Watch operation at " + localDir + " has stopped"); watchOperations.remove(localDir); } } } } private Map<File, FolderTO> getFolderMap(List<FolderTO> watchedFolders) { Map<File, FolderTO> watchedFolderTOs = new TreeMap<File, FolderTO>(); for (FolderTO folderTO : watchedFolders) { if (folderTO.isEnabled()) { watchedFolderTOs.put(new File(folderTO.getPath()), folderTO); } } return watchedFolderTOs; } private void fireDaemonReloadedEvent() { logger.log(Level.INFO, "Firing daemon-reloaded event ..."); eventBus.post(new DaemonReloadedExternalEvent()); } @Subscribe public void onFolderRequestReceived(FolderRequest folderRequest) { File rootFolder = new File(folderRequest.getRoot()); if (!watchOperations.containsKey(rootFolder)) { eventBus.post(new BadRequestResponse(folderRequest.getId(), "Unknown root folder.")); } } @Subscribe public void onManagementRequestReceived(ManagementRequest managementRequest) { logger.log(Level.INFO, "Received " + managementRequest); try { ManagementRequestHandler handler = ManagementRequestHandler.createManagementRequestHandler(managementRequest); Response response = handler.handleRequest(managementRequest); if (response != null) { eventBus.post(response); } } catch (ClassNotFoundException e) { logger.log(Level.FINE, "No handler found for management request class " + managementRequest.getClass() + ". Ignoring."); // Not logging 'e'! } catch (Exception e) { logger.log(Level.FINE, "Failed to process request", e); eventBus.post(new BadRequestResponse(managementRequest.getId(), "Invalid request.")); } } @Subscribe public void onListWatchesRequestReceived(ListWatchesManagementRequest request) { List<Watch> watchList = new ArrayList<Watch>(); for (File watchFolder : watchOperations.keySet()) { boolean syncRunning = watchOperations.get(watchFolder).isSyncRunning(); SyncStatus syncStatus = (syncRunning) ? SyncStatus.SYNCING : SyncStatus.IN_SYNC; watchList.add(new Watch(watchFolder, syncStatus)); } eventBus.post(new ListWatchesManagementResponse(request.getId(), watchList)); } @Subscribe public void onAddWatchRequestReceived(AddWatchManagementRequest request) { File rootFolder = request.getWatch(); if (watchOperations.containsKey(rootFolder)) { eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_ALREADY_EXISTS, request.getId(), "Watch already exists.")); } else { try { boolean folderAdded = DaemonConfigHelper.addFolder(rootFolder); if (folderAdded) { eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.OKAY, request.getId(), "Successfully added.")); } else { eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_ALREADY_EXISTS, request.getId(), "Watch already exists (inactive/disabled).")); } } catch (ConfigException e) { logger.log(Level.WARNING, "Error adding watch to daemon config.", e); eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_OTHER, request.getId(), "Error adding to config: " + e.getMessage())); } } } @Subscribe public void onRemoveWatchRequestReceived(RemoveWatchManagementRequest request) { File rootFolder = request.getWatch(); if (!watchOperations.containsKey(rootFolder)) { eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_DOES_NOT_EXIST, request.getId(), "Watch does not exist.")); } else { try { boolean folderRemoved = DaemonConfigHelper.removeFolder(rootFolder); if (folderRemoved) { eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.OKAY, request.getId(), "Successfully removed.")); } else { eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_DOES_NOT_EXIST, request.getId(), "Watch does not exist (inactive/disabled).")); } } catch (ConfigException e) { logger.log(Level.WARNING, "Error removing watch from daemon config.", e); eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_OTHER, request.getId(), "Error removing to config: " + e.getMessage())); } } } }