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}