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.io.IOException; 022import java.util.Collections; 023import java.util.logging.Level; 024import java.util.logging.Logger; 025 026import org.syncany.config.Config; 027import org.syncany.config.ConfigException; 028import org.syncany.config.DaemonConfigHelper; 029import org.syncany.config.LocalEventBus; 030import org.syncany.config.UserConfig; 031import org.syncany.config.to.DaemonConfigTO; 032import org.syncany.config.to.FolderTO; 033import org.syncany.config.to.PortTO; 034import org.syncany.config.to.UserTO; 035import org.syncany.crypto.CipherUtil; 036import org.syncany.operations.Operation; 037import org.syncany.operations.daemon.ControlServer.ControlCommand; 038import org.syncany.operations.daemon.DaemonOperationOptions.DaemonAction; 039import org.syncany.operations.daemon.DaemonOperationResult.DaemonResultCode; 040import org.syncany.operations.daemon.messages.ControlManagementRequest; 041import org.syncany.operations.daemon.messages.ControlManagementResponse; 042import org.syncany.operations.watch.WatchOperation; 043import org.syncany.util.PidFileUtil; 044 045import com.google.common.collect.Ordering; 046import com.google.common.eventbus.Subscribe; 047 048/** 049 * This operation is the central part of the daemon. It can manage many different 050 * {@link WatchOperation}s and exposes a web socket server to control and query the 051 * daemon. It furthermore offers a file-based control server to stop and reload the 052 * daemon. 053 * 054 * <p>When started via {@link #execute()}, the operation starts the following core 055 * components: 056 * 057 * <ul> 058 * <li>The {@link WatchServer} starts a {@link WatchOperation} for every 059 * folder registered in the <code>daemon.xml</code> file. It can be reloaded via 060 * the <code>syd reload</code> command.</li> 061 * <li>The {@link WebServer} starts a websocket and allows clients 062 * (e.g. GUI, Web) to control the daemon (if authenticated). 063 * TODO [medium] This is not yet implemented!</li> 064 * <li>The {@link ControlServer} creates and watches the daemon control file 065 * which allows the <code>syd</code> shell/batch script to write reload/shutdown 066 * commands.</li> 067 * </ul> 068 * 069 * @author Vincent Wiencek (vwiencek@gmail.com) 070 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 071 * @author Pim Otte 072 */ 073public class DaemonOperation extends Operation { 074 private static final Logger logger = Logger.getLogger(DaemonOperation.class.getSimpleName()); 075 public static final String PID_FILE = "daemon.pid"; 076 077 private DaemonOperationOptions options; 078 private File pidFile; 079 080 private WebServer webServer; 081 private WatchServer watchServer; 082 private ControlServer controlServer; 083 private LocalEventBus eventBus; 084 private DaemonConfigTO daemonConfig; 085 private PortTO portTO; 086 087 public DaemonOperation() { 088 this(new DaemonOperationOptions(DaemonAction.RUN)); 089 } 090 091 public DaemonOperation(DaemonOperationOptions options) { 092 super(null); 093 094 this.options = options; 095 this.pidFile = new File(UserConfig.getUserConfigDir(), PID_FILE); 096 } 097 098 @Override 099 public DaemonOperationResult execute() throws Exception { 100 logger.log(Level.INFO, "Starting daemon operation with action " + options.getAction() + " ..."); 101 102 switch (options.getAction()) { 103 case LIST: 104 return executeList(); 105 106 case ADD: 107 return executeAdd(); 108 109 case REMOVE: 110 return executeRemove(); 111 112 case RUN: 113 return executeRun(); 114 115 default: 116 throw new Exception("Unknown action: " + options.getAction()); 117 } 118 } 119 120 private DaemonOperationResult executeList() { 121 logger.log(Level.INFO, "Listing daemon-managed folders ..."); 122 123 loadOrCreateConfig(); 124 return new DaemonOperationResult(DaemonResultCode.OK, daemonConfig.getFolders()); 125 } 126 127 private DaemonOperationResult executeAdd() throws Exception { 128 // Check all folders 129 for (String watchRoot : options.getWatchRoots()) { 130 File watchRootFolder = new File(watchRoot); 131 File watchRootAppFolder = new File(watchRootFolder, Config.DIR_APPLICATION); 132 133 if (!watchRootFolder.isDirectory() || !watchRootAppFolder.isDirectory()) { 134 throw new Exception("Given argument is not an existing folder, or a valid Syncany folder: " + watchRoot); 135 } 136 } 137 138 // Add them 139 for (String watchRoot : options.getWatchRoots()) { 140 DaemonConfigHelper.addFolder(new File(watchRoot)); 141 } 142 143 // Determine return code 144 loadOrCreateConfig(); 145 int watchedMatchingFoldersCount = countWatchedMatchingFolders(); 146 147 if (watchedMatchingFoldersCount == options.getWatchRoots().size()) { 148 return new DaemonOperationResult(DaemonResultCode.OK, daemonConfig.getFolders()); 149 } 150 else if (watchedMatchingFoldersCount > 0) { 151 return new DaemonOperationResult(DaemonResultCode.OK_PARTIAL, daemonConfig.getFolders()); 152 } 153 else { 154 return new DaemonOperationResult(DaemonResultCode.NOK, daemonConfig.getFolders()); 155 } 156 } 157 158 private DaemonOperationResult executeRemove() throws ConfigException { 159 // Sort 160 Collections.sort(options.getWatchRoots(), Ordering.natural().reverse()); 161 162 // Remove all folders 163 for (String watchRoot : options.getWatchRoots()) { 164 logger.log(Level.INFO, "- Removing folder from daemon config: " + watchRoot + " ..."); 165 DaemonConfigHelper.removeFolder(watchRoot); 166 } 167 168 // Check if folders were removed 169 loadOrCreateConfig(); 170 int watchedMatchingFoldersCount = countWatchedMatchingFolders(); 171 172 if (watchedMatchingFoldersCount == options.getWatchRoots().size()) { 173 return new DaemonOperationResult(DaemonResultCode.NOK, daemonConfig.getFolders()); 174 } 175 else if (watchedMatchingFoldersCount > 0) { 176 return new DaemonOperationResult(DaemonResultCode.NOK_PARTIAL, daemonConfig.getFolders()); 177 } 178 else { 179 return new DaemonOperationResult(DaemonResultCode.OK, daemonConfig.getFolders()); 180 } 181 } 182 183 private int countWatchedMatchingFolders() { 184 int watchedMatchingFoldersCount = 0; 185 186 for (FolderTO folderTO : daemonConfig.getFolders()) { 187 if (options.getWatchRoots().contains(folderTO.getPath())) { 188 watchedMatchingFoldersCount++; 189 } 190 } 191 192 return watchedMatchingFoldersCount; 193 } 194 195 private DaemonOperationResult executeRun() throws Exception { 196 if (PidFileUtil.isProcessRunning(pidFile)) { 197 throw new ServiceAlreadyStartedException("Syncany daemon already running."); 198 } 199 200 PidFileUtil.createPidFile(pidFile); 201 202 initEventBus(); 203 loadOrCreateConfig(); 204 205 startWebServer(); 206 startWatchServer(); 207 208 enterControlLoop(); // This blocks until SHUTDOWN is received! 209 210 return new DaemonOperationResult(DaemonResultCode.OK); 211 } 212 213 @Subscribe 214 public void onControlCommand(ControlCommand controlCommand) { 215 switch (controlCommand) { 216 case SHUTDOWN: 217 logger.log(Level.INFO, "SHUTDOWN requested."); 218 stopOperation(); 219 break; 220 221 case RELOAD: 222 logger.log(Level.INFO, "RELOAD requested."); 223 reloadOperation(); 224 break; 225 } 226 } 227 228 @Subscribe 229 public void onControlManagementRequest(ControlManagementRequest controlRequest) { 230 onControlCommand(controlRequest.getControlCommand()); 231 eventBus.post(new ControlManagementResponse(200, controlRequest.getId(), "Command executed.")); 232 } 233 234 // General initialization functions. These create the EventBus and control loop. 235 236 private void initEventBus() { 237 eventBus = LocalEventBus.getInstance(); 238 eventBus.register(this); 239 } 240 241 private void enterControlLoop() throws IOException, ServiceAlreadyStartedException { 242 logger.log(Level.INFO, "Starting daemon control server ..."); 243 244 controlServer = new ControlServer(); 245 controlServer.enterLoop(); // This blocks! 246 } 247 248 // General stopping and reloading functions 249 250 private void stopOperation() { 251 stopWebServer(); 252 stopWatchServer(); 253 } 254 255 private void reloadOperation() { 256 loadOrCreateConfig(); 257 watchServer.reload(daemonConfig); 258 } 259 260 // Config related functions. Used on starting and reloading. 261 262 private void loadOrCreateConfig() { 263 try { 264 File daemonConfigFile = new File(UserConfig.getUserConfigDir(), UserConfig.DAEMON_FILE); 265 File daemonConfigFileExample = new File(UserConfig.getUserConfigDir(), UserConfig.DAEMON_EXAMPLE_FILE); 266 267 if (daemonConfigFile.exists()) { 268 logger.log(Level.INFO, "Loading daemon config file from " + daemonConfigFile + " ..."); 269 daemonConfig = DaemonConfigTO.load(daemonConfigFile); 270 } 271 else { 272 logger.log(Level.INFO, "Daemon config file does not exist."); 273 logger.log(Level.INFO, "- Writing example config file to " + daemonConfigFileExample + " ..."); 274 DaemonConfigHelper.createAndWriteExampleDaemonConfig(daemonConfigFileExample); 275 276 logger.log(Level.INFO, "- Creating at " + daemonConfigFile + " ..."); 277 daemonConfig = DaemonConfigHelper.createAndWriteDefaultDaemonConfig(daemonConfigFile); 278 } 279 280 // Add user and password for access from the CLI 281 if (daemonConfig.getPortTO() == null && portTO == null) { 282 // Access info has not been created yet, generate new user-password pair 283 String accessToken = CipherUtil.createRandomAlphabeticString(20); 284 285 UserTO cliUser = new UserTO(); 286 cliUser.setUsername(UserConfig.USER_CLI); 287 cliUser.setPassword(accessToken); 288 289 portTO = new PortTO(); 290 291 portTO.setPort(daemonConfig.getWebServer().getBindPort()); 292 portTO.setUser(cliUser); 293 294 daemonConfig.setPortTO(portTO); 295 } 296 else if (daemonConfig.getPortTO() == null) { 297 // Access info is not included in the daemon config, but exists. Happens when reloading. 298 // We reload the information about the port, but keep the access token the same. 299 300 portTO.setPort(daemonConfig.getWebServer().getBindPort()); 301 daemonConfig.setPortTO(portTO); 302 } 303 } 304 catch (Exception e) { 305 logger.log(Level.WARNING, "Cannot (re-)load config. Exception thrown.", e); 306 } 307 } 308 309 // Web server starting and stopping functions 310 311 private void startWebServer() throws Exception { 312 if (daemonConfig.getWebServer().isEnabled()) { 313 logger.log(Level.INFO, "Starting web server ..."); 314 315 webServer = new WebServer(daemonConfig); 316 webServer.start(); 317 } 318 else { 319 logger.log(Level.INFO, "Not starting web server (disabled in confi)"); 320 } 321 } 322 323 private void stopWebServer() { 324 if (webServer != null) { 325 logger.log(Level.INFO, "Stopping web server ..."); 326 webServer.stop(); 327 } 328 else { 329 logger.log(Level.INFO, "Not stopping web server (not running)"); 330 } 331 } 332 333 // Watch server starting and stopping functions 334 335 private void startWatchServer() throws ConfigException { 336 logger.log(Level.INFO, "Starting watch server ..."); 337 338 watchServer = new WatchServer(); 339 watchServer.start(daemonConfig); 340 } 341 342 private void stopWatchServer() { 343 logger.log(Level.INFO, "Stopping watch server ..."); 344 watchServer.stop(); 345 } 346}