/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 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.io.IOException;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.config.Config;
import org.syncany.config.ConfigException;
import org.syncany.config.DaemonConfigHelper;
import org.syncany.config.LocalEventBus;
import org.syncany.config.UserConfig;
import org.syncany.config.to.DaemonConfigTO;
import org.syncany.config.to.FolderTO;
import org.syncany.config.to.PortTO;
import org.syncany.config.to.UserTO;
import org.syncany.crypto.CipherUtil;
import org.syncany.operations.Operation;
import org.syncany.operations.daemon.ControlServer.ControlCommand;
import org.syncany.operations.daemon.DaemonOperationOptions.DaemonAction;
import org.syncany.operations.daemon.DaemonOperationResult.DaemonResultCode;
import org.syncany.operations.daemon.messages.ControlManagementRequest;
import org.syncany.operations.daemon.messages.ControlManagementResponse;
import org.syncany.operations.watch.WatchOperation;
import org.syncany.util.PidFileUtil;
import com.google.common.collect.Ordering;
import com.google.common.eventbus.Subscribe;
/**
* This operation is the central part of the daemon. It can manage many different
* {@link WatchOperation}s and exposes a web socket server to control and query the
* daemon. It furthermore offers a file-based control server to stop and reload the
* daemon.
*
* <p>When started via {@link #execute()}, the operation starts the following core
* components:
*
* <ul>
* <li>The {@link WatchServer} starts a {@link WatchOperation} for every
* folder registered in the <tt>daemon.xml</tt> file. It can be reloaded via
* the <tt>syd reload</tt> command.</li>
* <li>The {@link WebServer} starts a websocket and allows clients
* (e.g. GUI, Web) to control the daemon (if authenticated).
* TODO [medium] This is not yet implemented!</li>
* <li>The {@link ControlServer} creates and watches the daemon control file
* which allows the <tt>syd</tt> shell/batch script to write reload/shutdown
* commands.</li>
* </ul>
*
* @author Vincent Wiencek <vwiencek@gmail.com>
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
* @author Pim Otte
*/
public class DaemonOperation extends Operation {
private static final Logger logger = Logger.getLogger(DaemonOperation.class.getSimpleName());
public static final String PID_FILE = "daemon.pid";
private DaemonOperationOptions options;
private File pidFile;
private WebServer webServer;
private WatchServer watchServer;
private ControlServer controlServer;
private LocalEventBus eventBus;
private DaemonConfigTO daemonConfig;
private PortTO portTO;
public DaemonOperation() {
this(new DaemonOperationOptions(DaemonAction.RUN));
}
public DaemonOperation(DaemonOperationOptions options) {
super(null);
this.options = options;
this.pidFile = new File(UserConfig.getUserConfigDir(), PID_FILE);
}
@Override
public DaemonOperationResult execute() throws Exception {
logger.log(Level.INFO, "Starting daemon operation with action " + options.getAction() + " ...");
switch (options.getAction()) {
case LIST:
return executeList();
case ADD:
return executeAdd();
case REMOVE:
return executeRemove();
case RUN:
return executeRun();
default:
throw new Exception("Unknown action: " + options.getAction());
}
}
private DaemonOperationResult executeList() {
logger.log(Level.INFO, "Listing daemon-managed folders ...");
loadOrCreateConfig();
return new DaemonOperationResult(DaemonResultCode.OK, daemonConfig.getFolders());
}
private DaemonOperationResult executeAdd() throws Exception {
// Check all folders
for (String watchRoot : options.getWatchRoots()) {
File watchRootFolder = new File(watchRoot);
File watchRootAppFolder = new File(watchRootFolder, Config.DIR_APPLICATION);
if (!watchRootFolder.isDirectory() || !watchRootAppFolder.isDirectory()) {
throw new Exception("Given argument is not an existing folder, or a valid Syncany folder: " + watchRoot);
}
}
// Add them
for (String watchRoot : options.getWatchRoots()) {
DaemonConfigHelper.addFolder(new File(watchRoot));
}
// Determine return code
loadOrCreateConfig();
int watchedMatchingFoldersCount = countWatchedMatchingFolders();
if (watchedMatchingFoldersCount == options.getWatchRoots().size()) {
return new DaemonOperationResult(DaemonResultCode.OK, daemonConfig.getFolders());
}
else if (watchedMatchingFoldersCount > 0) {
return new DaemonOperationResult(DaemonResultCode.OK_PARTIAL, daemonConfig.getFolders());
}
else {
return new DaemonOperationResult(DaemonResultCode.NOK, daemonConfig.getFolders());
}
}
private DaemonOperationResult executeRemove() throws ConfigException {
// Sort
Collections.sort(options.getWatchRoots(), Ordering.natural().reverse());
// Remove all folders
for (String watchRoot : options.getWatchRoots()) {
logger.log(Level.INFO, "- Removing folder from daemon config: " + watchRoot + " ...");
DaemonConfigHelper.removeFolder(watchRoot);
}
// Check if folders were removed
loadOrCreateConfig();
int watchedMatchingFoldersCount = countWatchedMatchingFolders();
if (watchedMatchingFoldersCount == options.getWatchRoots().size()) {
return new DaemonOperationResult(DaemonResultCode.NOK, daemonConfig.getFolders());
}
else if (watchedMatchingFoldersCount > 0) {
return new DaemonOperationResult(DaemonResultCode.NOK_PARTIAL, daemonConfig.getFolders());
}
else {
return new DaemonOperationResult(DaemonResultCode.OK, daemonConfig.getFolders());
}
}
private int countWatchedMatchingFolders() {
int watchedMatchingFoldersCount = 0;
for (FolderTO folderTO : daemonConfig.getFolders()) {
if (options.getWatchRoots().contains(folderTO.getPath())) {
watchedMatchingFoldersCount++;
}
}
return watchedMatchingFoldersCount;
}
private DaemonOperationResult executeRun() throws Exception {
if (PidFileUtil.isProcessRunning(pidFile)) {
throw new ServiceAlreadyStartedException("Syncany daemon already running.");
}
PidFileUtil.createPidFile(pidFile);
initEventBus();
loadOrCreateConfig();
startWebServer();
startWatchServer();
enterControlLoop(); // This blocks until SHUTDOWN is received!
return new DaemonOperationResult(DaemonResultCode.OK);
}
@Subscribe
public void onControlCommand(ControlCommand controlCommand) {
switch (controlCommand) {
case SHUTDOWN:
logger.log(Level.INFO, "SHUTDOWN requested.");
stopOperation();
break;
case RELOAD:
logger.log(Level.INFO, "RELOAD requested.");
reloadOperation();
break;
}
}
@Subscribe
public void onControlManagementRequest(ControlManagementRequest controlRequest) {
onControlCommand(controlRequest.getControlCommand());
eventBus.post(new ControlManagementResponse(200, controlRequest.getId(), "Command executed."));
}
// General initialization functions. These create the EventBus and control loop.
private void initEventBus() {
eventBus = LocalEventBus.getInstance();
eventBus.register(this);
}
private void enterControlLoop() throws IOException, ServiceAlreadyStartedException {
logger.log(Level.INFO, "Starting daemon control server ...");
controlServer = new ControlServer();
controlServer.enterLoop(); // This blocks!
}
// General stopping and reloading functions
private void stopOperation() {
stopWebServer();
stopWatchServer();
}
private void reloadOperation() {
loadOrCreateConfig();
watchServer.reload(daemonConfig);
}
// Config related functions. Used on starting and reloading.
private void loadOrCreateConfig() {
try {
File daemonConfigFile = new File(UserConfig.getUserConfigDir(), UserConfig.DAEMON_FILE);
File daemonConfigFileExample = new File(UserConfig.getUserConfigDir(), UserConfig.DAEMON_EXAMPLE_FILE);
if (daemonConfigFile.exists()) {
logger.log(Level.INFO, "Loading daemon config file from " + daemonConfigFile + " ...");
daemonConfig = DaemonConfigTO.load(daemonConfigFile);
}
else {
logger.log(Level.INFO, "Daemon config file does not exist.");
logger.log(Level.INFO, "- Writing example config file to " + daemonConfigFileExample + " ...");
DaemonConfigHelper.createAndWriteExampleDaemonConfig(daemonConfigFileExample);
logger.log(Level.INFO, "- Creating at " + daemonConfigFile + " ...");
daemonConfig = DaemonConfigHelper.createAndWriteDefaultDaemonConfig(daemonConfigFile);
}
// Add user and password for access from the CLI
if (daemonConfig.getPortTO() == null && portTO == null) {
// Access info has not been created yet, generate new user-password pair
String accessToken = CipherUtil.createRandomAlphabeticString(20);
UserTO cliUser = new UserTO();
cliUser.setUsername(UserConfig.USER_CLI);
cliUser.setPassword(accessToken);
portTO = new PortTO();
portTO.setPort(daemonConfig.getWebServer().getBindPort());
portTO.setUser(cliUser);
daemonConfig.setPortTO(portTO);
}
else if (daemonConfig.getPortTO() == null) {
// Access info is not included in the daemon config, but exists. Happens when reloading.
// We reload the information about the port, but keep the access token the same.
portTO.setPort(daemonConfig.getWebServer().getBindPort());
daemonConfig.setPortTO(portTO);
}
}
catch (Exception e) {
logger.log(Level.WARNING, "Cannot (re-)load config. Exception thrown.", e);
}
}
// Web server starting and stopping functions
private void startWebServer() throws Exception {
if (daemonConfig.getWebServer().isEnabled()) {
logger.log(Level.INFO, "Starting web server ...");
webServer = new WebServer(daemonConfig);
webServer.start();
}
else {
logger.log(Level.INFO, "Not starting web server (disabled in confi)");
}
}
private void stopWebServer() {
if (webServer != null) {
logger.log(Level.INFO, "Stopping web server ...");
webServer.stop();
}
else {
logger.log(Level.INFO, "Not stopping web server (not running)");
}
}
// Watch server starting and stopping functions
private void startWatchServer() throws ConfigException {
logger.log(Level.INFO, "Starting watch server ...");
watchServer = new WatchServer();
watchServer.start(daemonConfig);
}
private void stopWatchServer() {
logger.log(Level.INFO, "Stopping watch server ...");
watchServer.stop();
}
}