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.logging.Level;
023import java.util.logging.Logger;
024
025import org.apache.commons.io.input.Tailer;
026import org.apache.commons.io.input.TailerListener;
027import org.syncany.config.LocalEventBus;
028import org.syncany.config.UserConfig;
029
030/**
031 * The control server watches the daemon control file for changes and 
032 * reacts on certain commands. 
033 * 
034 * <p>Although it is not a real socket, it can be seen as a cross-platform
035 * unix-like socket. Due to the nature of the commands in the control 
036 * file (shutdown/reload), a normal TCP socket is not possible.
037 * 
038 * <p>The central method is {@link #enterLoop()}: This method tails file changes
039 * in the daemon control file in the currently active thread. It does not
040 * fork a new thread. It <b>blocks</b> and waits for commands until 
041 * <b>shutdown</b> is received.
042 * 
043 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
044 */
045public class ControlServer implements TailerListener {  
046        private static final Logger logger = Logger.getLogger(ControlServer.class.getSimpleName());
047        private static final String CONTROL_FILE = "daemon.ctrl";
048
049        public enum ControlCommand {
050                SHUTDOWN, RELOAD
051        }
052        
053        private File controlFile;
054        private Tailer controlFileTailer;
055        private LocalEventBus eventBus;
056
057        public ControlServer() {
058                this.controlFile = new File(UserConfig.getUserConfigDir(), CONTROL_FILE);
059                this.controlFileTailer = new Tailer(controlFile, this, 1000, true);
060                this.eventBus = LocalEventBus.getInstance();            
061        }
062        
063        /**
064         * Constructor required for unit testing, as you can inject mocks in this way.
065         */
066        @Deprecated
067        public ControlServer(File ctrlFile, Tailer ctrTailer, LocalEventBus eventBus) {
068                this.controlFile = ctrlFile;
069                this.controlFileTailer = ctrTailer;
070                this.eventBus = eventBus;
071        }
072
073        public void enterLoop() throws IOException, ServiceAlreadyStartedException {
074                File userAppDir = UserConfig.getUserConfigDir();
075                userAppDir.mkdirs();
076                
077                controlFile.delete();
078                controlFile.createNewFile();            
079                controlFile.deleteOnExit();     
080                
081                logger.log(Level.INFO, "Monitoring control file for commands at " + controlFile + " ...");
082                logger.log(Level.INFO, "   (Note: This is a blocking operation. The 'main' thread is now blocked until '" + ControlCommand.SHUTDOWN + "' is received.)");
083                
084                controlFileTailer.run(); // This blocks!
085        }       
086
087        /**
088         * Functions that handle tailing the control file.
089         */
090        @Override
091        public void fileNotFound() {
092                logger.log(Level.SEVERE, "Control file not found. FATAL. EXITING.");
093                throw new RuntimeException("Control file not found. FATAL. EXITING.");
094        }
095        
096        @Override
097        public void handle(String command) {
098                try {
099                        ControlCommand controlCommand = ControlCommand.valueOf(command.trim().toUpperCase());
100                        
101                        switch (controlCommand) {
102                        case SHUTDOWN:
103                                logger.log(Level.INFO, "Control file: Received shutdown command. Shutting down.");
104
105                                eventBus.post(controlCommand);
106                                controlFileTailer.stop();
107                                break;
108                                
109                        case RELOAD:
110                                logger.log(Level.INFO, "Control file: Received reload command. Reloading config ...");
111
112                                eventBus.post(controlCommand);
113                                break;
114                                
115                        default:
116                                throw new RuntimeException("This command should have been handled.");
117                        }
118                }
119                catch (Exception e) {
120                        logger.log(Level.WARNING, "Control file: Ignoring unknown command: " + command, e);
121                }
122        }
123
124        @Override
125        public void handle(Exception e) {
126                logger.log(Level.SEVERE, "Control file tailer exception received. FATAL. EXITING.", e);
127                throw new RuntimeException("Control file tailer exception received. FATAL. EXITING.", e);
128        }
129        
130        @Override
131        public void init(Tailer tailer) {
132                // Don't care
133        }
134        
135        @Override
136        public void fileRotated() {
137                // Don't care
138        }
139}