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}