View Javadoc

1   package cz.cuni.amis.pogamut.ut2004.tournament.botexecution;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.util.ArrayList;
6   import java.util.List;
7   import java.util.Map.Entry;
8   import java.util.logging.Level;
9   import java.util.logging.Logger;
10  
11  import cz.cuni.amis.pogamut.ut2004.utils.PogamutUT2004Property;
12  import cz.cuni.amis.utils.StreamSink;
13  import cz.cuni.amis.utils.StringIdifier;
14  import cz.cuni.amis.utils.exception.PogamutException;
15  import cz.cuni.amis.utils.exception.PogamutIOException;
16  import cz.cuni.amis.utils.flag.Flag;
17  import cz.cuni.amis.utils.flag.FlagListener;
18  import cz.cuni.amis.utils.flag.ImmutableFlag;
19  
20  /**
21   * Class that wrapps the execution of the bot from the jar defined by {@link UT2004BotExecutionConfig}.
22   * <p><p>
23   * The class is not suitable for inheritance ... better copy-paste and adjust for yourself.
24   * 
25   * @author Jimmy
26   */
27  public class UT2004BotExecution {
28  	
29  	protected UT2004BotExecutionConfig config;
30  	
31  	/**
32  	 * Flag whether the bot is running, we're synchronizing access to this class on this flag.
33  	 * <p><p>
34  	 * Set to TRUE ONLY IN {@link UT2004BotExecution#start()} and back to FALSE ONLY IN {@link UT2004BotExecution#shutdown(boolean)}.
35  	 * Do not alter the flag from anywhere else!
36  	 */
37  	protected Flag<Boolean> running = new Flag<Boolean>(false);
38  	
39  	/**
40  	 * When {@link UT2004BotExecution#start()}ed it contains the bot process.
41  	 */
42  	protected Process botProcess = null;
43  	
44  	/**
45  	 * Sink for the STDOUT of the bot, it may be redirected to {@link UT2004BotExecution#log}.
46  	 */
47  	protected StreamSink streamSinkOutput = null;
48  	
49  	/**
50  	 * Sink for the STDERR of the bot, it may be redirected to {@link UT2004BotExecution#log}.
51  	 */
52  	protected StreamSink streamSinkError = null;
53  
54  	/**
55  	 * Log used by the object, specified during the construction.
56  	 */
57  	protected Logger log;
58  
59  	/**
60  	 * @param config bot execution configuration (contains path to jar to run)
61  	 * @param log log to be used (stdout/stderr of the bot will be redirected there as well with {@link Level#INFO})
62  	 */
63  	public UT2004BotExecution(UT2004BotExecutionConfig config, Logger log) {
64  		this.log = log;
65  		this.config = config;
66  	}
67  		
68  	/**
69  	 * Task that will kill the BOT process when user forgets to do so before the JVM is killed.
70       */
71      protected Runnable shutDownHook = new Runnable(){
72  
73          @Override
74          public void run() {
75              if (botProcess != null) botProcess.destroy();
76          }
77      };
78      
79      protected Thread shutDownHookThread;
80      
81      /**
82       * Task that is waiting for the end of the bot process to switch {@link UT2004BotExecution#running} back to false
83       * and shutdown all other utility threads (like sink).
84       */
85      protected Runnable waitForEnd = new Runnable() {
86  
87  		@Override
88  		public void run() {
89  			try {
90  				botProcess.waitFor();
91  				// BOT PROCESS HAS STOPPED WHEN THE JVM REACHES HERE
92  			} catch (InterruptedException e) {
93  				// we've been interrupted!
94  				synchronized(running) {
95  					if (!running.getFlag()) {
96  						// okey we're not running, screw it...
97  						return;
98  					}
99  				}
100 				// hups, we're still running, warn the user!
101 				if (log != null && log.isLoggable(Level.WARNING)) {
102 					log.warning("Interrupted while waiting for the botProcess(" + config.getBotId().getToken() + ") to end!");
103 				}
104 				
105 			} finally {
106 				// perform clean-up
107 				synchronized(running) {
108 					if (!running.getFlag()) {
109 						// okey we're not running, somebody has already shutdown, screw it...
110 						return;
111 					}
112 					if (waitForEndThread == Thread.currentThread()) {
113 						// we're still in the middle of the same execution... (shut down only if it is the same bot execution)
114 						shutdown(true);
115 					}
116 				}
117 			}
118 		}
119     };
120     
121     protected Thread waitForEndThread;
122 	
123     /**
124      * Start the bot process. Throws {@link PogamutIOException} if it fails to start the JVM.
125      * <p><p>
126      * It is wise to observe the state of the {@link UT2004BotExecution#getRunning()} flag to obtain the state of the process
127      * (whether the process is running or is dead). Usage of {@link FlagListener} is advised, see {@link ImmutableFlag#addListener(FlagListener)}.
128      * 
129      * @param host GB2004 host
130      * @param port GB2004 bot port
131      */
132 	public void start(String host, int port) throws PogamutIOException {
133 		synchronized(running) {
134 			if (running.getFlag()) {
135 				throw new PogamutException("Could not start the bot again, it is already running! stop() it first!", log, this);
136 			}
137 			
138 			if (log != null && log.isLoggable(Level.WARNING)) {
139 				log.warning("Starting bot: " + config);
140 			}
141 			
142 			if (!config.isBotJarExist()) {
143 				throw new PogamutException("Could not start the bot according to config " + config + " as the bot jar does not exist at specified place " + config.getJarFile().getAbsolutePath() + "!", this);
144 			}
145 			
146 			String javaHome = System.getProperty("JAVA_HOME");
147 			
148 			boolean linux = System.getProperty("os.name").toLowerCase().contains("linux");
149 			boolean mac   = System.getProperty("os.name").contains("Mac");
150 			
151 				
152 			
153 			String command = 
154 					(javaHome == null 
155 						? (linux || mac ? "java" : "java.exe")
156 						: javaHome + (linux || mac ? "/bin/java" : "\\bin\\java.exe"));
157 			
158 			Object origHost = config.addParameter(PogamutUT2004Property.POGAMUT_UT2004_BOT_HOST.getKey(), host);
159 			if (origHost != null) {
160 				log.warning("Reconfiguring Bot[id=" + config.getBotId().getToken() + "] parameter " + PogamutUT2004Property.POGAMUT_UT2004_BOT_HOST.getKey() + " from value '" + String.valueOf(origHost) + "' to value '" + host + "'.");
161 			}
162 			Object origPort = config.addParameter(PogamutUT2004Property.POGAMUT_UT2004_BOT_PORT.getKey(), port);
163 			if (origPort != null) {
164 				log.warning("Reconfiguring Bot[id=" + config.getBotId().getToken() + "] parameter " + PogamutUT2004Property.POGAMUT_UT2004_BOT_HOST.getKey() + " from value '" + String.valueOf(origPort) + "' to value '" + port + "'.");
165 			}
166 			
167 			List<String> commandForProcessBuilder = new ArrayList<String>(3 + config.getParameters().size());
168 			
169 			commandForProcessBuilder.add(command);
170 			
171 			StringBuffer javaParameters = new StringBuffer();
172 			
173 			for (Entry<String, Object> parameter : config.getParameters().entrySet()) {
174 				String parameterKey = StringIdifier.idify(parameter.getKey(), ".-_", "_");
175 				String parameterValue = String.valueOf(parameter.getValue());
176 				if (parameterValue.contains("\"")) {
177 					throw new PogamutException("Could not start the bot according to config " + config + " as it contains parameter containing '\"': " + parameterKey + " = " + parameterValue, this);
178 				}
179 				String param = "-D" + parameterKey + "=" + parameterValue;
180 				
181 				javaParameters.append(" \"" + param + "\"");
182 				
183 				commandForProcessBuilder.add(param);
184 			}
185 						
186 			commandForProcessBuilder.add("-jar");
187 			commandForProcessBuilder.add(config.getJarFile().getAbsolutePath());
188 			
189 			String fullCommand = command + javaParameters.toString() + " -jar \"" + config.getJarFile().getAbsolutePath() + "\"";
190 			
191 			if (log != null && log.isLoggable(Level.INFO)) {
192 				log.info("Executing command: " + fullCommand);
193 			}
194 			
195 			ProcessBuilder procBuilder = 
196 				new ProcessBuilder(
197 						commandForProcessBuilder.toArray(new String[commandForProcessBuilder.size()])											
198 				);		
199 			
200 			procBuilder.directory(new File(config.getJarFile().getParent()));
201 	        
202 	        try {
203 	        	botProcess = procBuilder.start();
204 	        } catch (IOException e) {
205 	        	// failed to start the process
206 	        	if (log != null && log.isLoggable(Level.SEVERE)) {
207 	        		log.severe("Could not start the bot: " + e.getMessage());
208 	        	}
209 	        	botProcess = null;
210 	        	throw new PogamutIOException("Failed to start the botProcess(" + config.getBotId().getToken() + "). IOException: " + e.getMessage(), e, this);
211 	        }
212 	        
213 	        // INITIALIZE ALL THREADS
214 	        if (config.isRedirectStdErr()) {
215 	        	streamSinkError = new StreamSink(config.getBotId().getToken()+"-StdErrSink", botProcess.getErrorStream(), log, config.getBotId().getToken()+"-StdErr");
216 	        } else {
217 	        	streamSinkError = new StreamSink(config.getBotId().getToken()+"-StdErrSink", botProcess.getErrorStream());
218 	        }
219 	        streamSinkError.start();
220 	        if (config.isRedirectStdOut()) {
221 	        	streamSinkOutput = new StreamSink(config.getBotId().getToken()+"-StdOutSink", botProcess.getInputStream(), log, config.getBotId().getToken()+"-StdOut");
222 	        } else {
223 	        	streamSinkOutput = new StreamSink(config.getBotId().getToken()+"-StdOutSink", botProcess.getInputStream());
224 	        }
225 	        streamSinkOutput.start();
226 	        shutDownHookThread = new Thread(shutDownHook, config.getBotId().getToken()+"-JVMShutdownHook");
227 	        Runtime.getRuntime().addShutdownHook(shutDownHookThread);
228 	        waitForEndThread = new Thread(waitForEnd, config.getBotId().getToken()+"-WaitForProcessEnd");
229 	        running.setFlag(true);    // we're ONLINE!
230 	        waitForEndThread.start(); // execute the thread that will wait for the end of the process to switch 'running' back to false
231 		}
232 	}
233 	
234 	/**
235 	 * Shutdowns the process and cleans-up everything, preparing the object to be {@link UT2004BotExecution#start()}ed again.
236 	 * @param waitForEndThread whether we're being called from the {@link UT2004BotExecution#waitForEnd} thread.
237 	 */
238 	protected void shutdown(boolean waitForEndThread) {
239 		synchronized(running) {
240 			if (!running.getFlag()) {
241 				// we're not running, screw it!
242 				return;
243 			}
244 			
245 			if (log != null && log.isLoggable(Level.WARNING)) {
246 				log.warning("Shutting down botProcess(" + config.getBotId().getToken() + ")!");
247 			}
248 
249 			if (log != null && log.isLoggable(Level.WARNING)) {
250 				log.warning("... destroying botProcess(" + config.getBotId().getToken() + ").");
251 			}
252 			if (botProcess != null) {
253 				try {
254 					botProcess.destroy();
255 				} catch (Exception e) {
256 				}
257 			}
258 			botProcess = null;
259 			
260 			if (log != null && log.isLoggable(Level.WARNING)) {
261 				log.warning("... destroying streamSinkError(" + config.getBotId().getToken() + ").");
262 			}
263 			try {
264 				if (streamSinkError != null) streamSinkError.interrupt();
265 			} catch (Exception e) {
266 			}
267 			streamSinkError = null;
268 			
269 			if (log != null && log.isLoggable(Level.WARNING)) {
270 				log.warning("... destroying streamSinkOutput(" + config.getBotId().getToken() + ").");
271 			}
272 			try {
273 				if (streamSinkOutput != null) streamSinkOutput.interrupt();
274 			} catch (Exception e) {
275 			}
276 			streamSinkOutput = null;
277 			
278 			if (log != null && log.isLoggable(Level.WARNING)) {
279 				log.warning("... destroying waitForEnd(" + config.getBotId().getToken() + ").");
280 			}
281 			if (!waitForEndThread) {
282 				try {
283 					if (this.waitForEndThread != null) this.waitForEndThread.interrupt();
284 				} catch (Exception e) {
285 				}
286 			}
287 			this.waitForEndThread = null;
288 			
289 			if (log != null && log.isLoggable(Level.WARNING)) {
290 				log.warning("... removing shutDownHook(" + config.getBotId().getToken() + ").");
291 			}
292 			if (shutDownHookThread != null) {
293 				try {
294 					Runtime.getRuntime().removeShutdownHook(shutDownHookThread);
295 				} catch (Exception e) {
296 				}	
297 			}
298 			shutDownHookThread = null;
299 			
300 			if (log != null && log.isLoggable(Level.WARNING)) {
301 				log.warning("... setting running-flag(" + config.getBotId().getToken() + ") to FALSE.");
302 			}
303 			running.setFlag(false);
304 			
305 			if (log != null && log.isLoggable(Level.WARNING)) {
306 				log.warning("Shutdown(" + config.getBotId().getToken() + ") finished.");
307 			}
308 		}
309 	}
310 	
311 	/**
312 	 * Shuts down the bot process (if it is running, otherwise does nothing).
313 	 */
314 	public void stop() {
315 		shutdown(false);
316 	}
317     
318     /**
319      * Process of the bot (non-null if started).
320      * @return
321      */
322     public Process getBotProcess() {
323         return botProcess;
324     }
325 	
326     /**
327      * Flag of the state of the bot process. True == process is running (has been started using {@link UT2004BotExecution#start()}), 
328      * False == process was not started or has been already terminated, either by itself or via {@link UT2004BotExecution#stop()}.
329      * @return
330      */
331 	public ImmutableFlag<Boolean> getRunning() {
332 		return running.getImmutable();
333 	}
334 	
335 	/**
336 	 * Flag of the state of the bot process. True == process is running (has been started using {@link UT2004BotExecution#start()}), 
337      * False == process was not started or has been already terminated, either by itself or via {@link UT2004BotExecution#stop()}.
338 	 * @return
339 	 */
340 	public boolean isRunning() {
341 		return running.getFlag();
342 	}
343 	
344 }