View Javadoc

1   /*
2    * UCCWrapper.java
3    *
4    */
5   package cz.cuni.amis.pogamut.udk.utils;
6   
7   import java.io.BufferedReader;
8   import java.io.File;
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.io.InputStreamReader;
12  import java.io.Serializable;
13  import java.util.Timer;
14  import java.util.TimerTask;
15  import java.util.concurrent.CountDownLatch;
16  import java.util.logging.Level;
17  import java.util.logging.Logger;
18  import java.util.regex.Matcher;
19  import java.util.regex.Pattern;
20  
21  import cz.cuni.amis.pogamut.base.agent.impl.AgentId;
22  import cz.cuni.amis.pogamut.base.communication.connection.impl.socket.SocketConnectionAddress;
23  import cz.cuni.amis.pogamut.base.utils.Pogamut;
24  import cz.cuni.amis.pogamut.base.utils.logging.LogCategory;
25  import cz.cuni.amis.pogamut.base.utils.logging.LogPublisher;
26  import cz.cuni.amis.pogamut.udk.factory.direct.remoteagent.UDKServerFactory;
27  import cz.cuni.amis.pogamut.udk.server.IUDKServer;
28  import cz.cuni.amis.pogamut.udk.server.exception.UCCStartException;
29  import cz.cuni.amis.utils.exception.PogamutException;
30  
31  /**
32   * Wrapper of running instance of UDK server. Implements pooling of instances.
33   * Usage scenario is:
34   * <code>
35   * UCCWrapper ucc = UCCWrapper.create();
36   * ...
37   * ucc.release();
38   * </code>
39   * The location of UDK executabe will be determined by an environment variable
40   * pogamut.udk.home (e.g. c:\Games\UDK). The property cam be set via <i>java ...
41   * -Dpogamut.udk.home=c:\Games\UDK</i>. Another posibility is to set it
42   * by code <code>System.setProperty("pogamut.udk.home", "c:\\udks\\UDK-2011-05");</code>.
43   * 
44   * <p>
45   * As of now, the server does not respond properly to process.destroy (it spawns another process, which
46   * is out of our control), so killing the server is done with system calls <code>taskkill /f /im udk.*</code>
47   * on Windows and
48   * <code>killall UDK.exe</code> on UNIX
49   * - so it is not possible to run more than instance of the server on one machine with UCCWrapper.
50   * </p>
51   * <h3>UNIX usage</h3>
52   * <p>
53   *  It is possible to use UCCWrapper under UNIX systems. First you need to setup UDK
54   * as described at our WIKI: <a href="http://pogamut.cuni.cz/pogamut-devel/doku.php?id=guidelines:udk_on_linux">
55   * pogamut.cuni.cz/pogamut-devel/doku.php?id=guidelines:udk_on_linux</a>. Then UCCWrapper must have access to Wine 
56   * in order to this, UCCWrapper uses property called WINE (might be an environment variable or it might be a Java property - see above).
57   * Property value defaults to "wine" which should be just fine if you have wine regularly installed. Make sure that WINEPREFIX environment
58   * variable is set in the shell that runs your program, otherwise Wine will not have access to correct wineprefix.
59   * </p>
60   * 
61   * @author Ik
62   */
63  public class UCCWrapper {
64  
65      /**
66       * Configuration object of the UCC wrapper instance.
67       */
68      public static class UCCWrapperConf implements Serializable {
69  
70          String mapName = "DM-Deck";
71          String gameBotsPack = "GameBotsUDK";
72          String gameType = "BotDeathMatch";
73          String mutators = "";
74          String options = "";
75          boolean startOnUnusedPort = true;
76          transient Logger log = null;
77  
78          /**
79           * Forces UCC to find free port and start on it, otherwise it will start on ports 3000 + 3001.
80           * @param startOnUnusedPort
81           */
82          public UCCWrapperConf setStartOnUnusedPort(boolean startOnUnusedPort) {
83              this.startOnUnusedPort = startOnUnusedPort;
84              return this;
85          }
86  
87          /**
88           * Eg. GameBots2004, GBSceanrio etc.
89           * @param gameBotsPack
90           */
91          public UCCWrapperConf setGameBotsPack(String gameBotsPack) {
92              this.gameBotsPack = gameBotsPack;
93              return this;
94          }
95  
96          public UCCWrapperConf setMapName(String mapName) {
97              this.mapName = mapName;
98              return this;
99          }
100 
101         /**
102          * Eg. BotDeathMatch, BotCTFGame etc. Consult GameBots documentation for
103          * complete list available game types.
104          */
105         public UCCWrapperConf setGameType(String gameType) {
106             this.gameType = gameType;
107             return this;
108         }
109 
110         /**
111          * Can be used for setting mutators etc.
112          * @param options
113          */
114         public UCCWrapperConf setOptions(String options) {
115             this.options = options;
116             return this;
117         }
118 
119         /**
120          * Logger used by the UCC.
121          * @param log
122          */
123         public UCCWrapperConf setLogger(Logger log) {
124             this.log = log;
125             return this;
126         }
127     }
128     
129     
130     public static final long DEFAULT_START_TIMEOUT = 2 * 60 * 1000;
131     
132     
133     /** Loger containing all output from running instance of UCC. */
134     protected LogCategory uccLog;
135     protected static int fileCounter = 0;
136     Process uccProcess = null;
137     /** Port for bots. */
138     protected int gbPort = -1;
139     /** Port for server connection. */
140     protected int controlPort = -1;
141     /** Port for observer connection. */
142     protected int observerPort = -1;
143     protected IUDKServer utServer = null;
144     /** First port assigned to a ucc instance. */
145     protected static final int basePort = 39782;
146     protected static Integer nextUccWrapperUID = 0;
147     /** ID of the wrapper object. Useful for debuging. */
148     protected int uccWrapperUID = 0;
149     protected String unrealHome = null;
150     protected UCCWrapperConf configuration = null;
151     
152     protected long startingTimeout;
153     //protected String mapToLoad
154 
155     /**
156      * @return Log with output of UCC. If you want to listen also for messages 
157      * from the startup sequence then use UCCWrapper.create(Logger parent). Set
158      * Parent logger of this log and register listeners before creating this
159      * instance of UCCWrapper.  
160      */
161     public Logger getLogger() {
162         return uccLog;
163     }
164 
165     /**
166      * @return Server connected to this UCC instance.
167      */
168     public IUDKServer getUTServer() {
169         stopCheck();
170         if (utServer == null) {
171             UDKServerFactory factory = new UDKServerFactory();
172             UDKServerRunner serverRunner = new UDKServerRunner(factory, "NBUTServer", "localhost", controlPort);
173             utServer = serverRunner.startAgent();
174         }
175         return utServer;
176     }
177 
178     protected String getUnrealHome() {
179         if (unrealHome == null) {
180             return Pogamut.getPlatform().getProperty(PogamutUDKProperty.POGAMUT_UNREAL_HOME.getKey());
181         } else {
182             return unrealHome;
183         }
184     }
185 
186     public UCCWrapper(UCCWrapperConf configuration){
187         this(configuration, DEFAULT_START_TIMEOUT);
188     }
189     
190     public UCCWrapper(UCCWrapperConf configuration, long startingTimeout) throws UCCStartException {
191         this.startingTimeout = startingTimeout;
192     	uccLog = new LogCategory("Wrapper");
193     	uccLog.addHandler(new LogPublisher.ConsolePublisher(new AgentId("UCC")));
194     	if (configuration.log != null) {
195             uccLog.setParent(configuration.log);
196         }
197         this.configuration = configuration;
198         uccWrapperUID = nextUccWrapperUID++;
199         initUCCWrapper();
200         Runtime.getRuntime().addShutdownHook(shutDownHook);
201     }
202     /**
203      * Task that will kill the UCC process when user forgets to do so.
204      */
205     Thread shutDownHook = new Thread("UCC wrapper finalizer") {
206 
207         @Override
208         public void run() {
209             UCCWrapper.this.stopNoWaiting();
210         }
211     };
212 
213     private boolean isWindows() {
214         return System.getProperty("os.name").contains("Windows");
215     }
216 
217     /**
218      * Reads content of the stream and discards it.
219      */
220     protected class StreamSink extends Thread {
221 
222         protected InputStream os = null;
223 
224         public StreamSink(InputStream os) {
225             setName("UCC Stream handler");
226             this.os = os;
227         }
228 
229         protected void handleInput(String str) {
230             if (uccLog.isLoggable(Level.INFO)) uccLog.info("ID" + uccWrapperUID + " " + str);
231         }
232 
233         @Override
234         public void run() {
235             BufferedReader stdInput = new BufferedReader(new InputStreamReader(os));
236 
237             String s = null;
238             try {
239                 while ((s = stdInput.readLine()) != null) {
240                     handleInput(s);
241                 }
242                 os.close();
243             } catch (IOException ex) {
244                 // the process has been closed so reading the line has failed, 
245                 // don't worry about it
246                 //ex.printStackTrace();
247             }
248         }
249     }
250 
251     /**
252      * Scanns the output of UCC for some specific srings (Ports bounded. START MATCH). 
253      */
254     public class ScannerSink extends StreamSink {
255         
256         /** Exception that ended the startig. Should be checked after the latch is raised. */
257         public UCCStartException exception = null;
258 
259         public ScannerSink(InputStream is) {
260             super(is);
261             timer.schedule(task = new TimerTask() {
262 
263                 @Override
264                 public void run() {
265                     exception = new UCCStartException("Starting timed out. Ports weren't bound in the required time (" + startingTimeout + " ms).", this);
266                     timer.cancel();
267                     serverStartedLatch.countDown();
268                 }
269             }, startingTimeout);            
270         }
271         public CountDownLatch serverStartedLatch = new CountDownLatch(1);
272         public int controlPort = -1;
273         public int botsPort = -1;
274         /**
275          * Thread that kills ucc process after specified time if the ports aren't 
276          * read from the console. This prevents freezing the ScannerSink when ucc
277          * fails to start.
278          */
279         Timer timer = new Timer("UDK start timeout");
280         TimerTask task = null;
281 //        private final String defaultPatternStart = "\\[[0-9]*\\.[0-9]*\\] [^:]*: ";
282         private final String defaultPatternStart = "";
283         private final Pattern portPattern = Pattern.compile(defaultPatternStart + "BotServerPort:(\\d*) ControlServerPort:(\\d*)");
284         private final Pattern commandletNotFoundPattern = Pattern.compile(defaultPatternStart + "Commandlet server not found");
285         private final Pattern mapNotFoundPattern = Pattern.compile(defaultPatternStart + "No maplist entries found matching the current command line.*");
286         private final Pattern matchStartedPattern = Pattern.compile(defaultPatternStart + "START MATCH");
287 
288         @Override
289         protected void handleInput(String str) {
290             super.handleInput(str);
291             if (serverStartedLatch.getCount() != 0) {
292                 // server has not yet started
293 
294                 Matcher matcher = portPattern.matcher(str);
295                 if (matcher.find()) {
296                 	botsPort = Integer.parseInt(matcher.group(1));
297                 	controlPort = Integer.parseInt(matcher.group(2));
298                     //raiseLatch();
299                 }
300 
301                 matcher = commandletNotFoundPattern.matcher(str);
302                 if (matcher.find()) {
303                     exception = new UCCStartException("UDK failed to start due to: Commandlet server not found.", this);
304                     raiseLatch();
305                 }
306 
307                 matcher = mapNotFoundPattern.matcher(str);
308                 if (matcher.find()) {
309                     exception = new UCCStartException("UDK failed to start due to: Map not found.", this);
310                     raiseLatch();
311                 }
312 
313                 matcher = matchStartedPattern.matcher(str);
314                 if (matcher.find()) {
315                     // The match has started, raise the latch
316                     raiseLatch();
317                 }
318             }
319 
320         }
321 
322         protected void raiseLatch() {
323             timer.cancel();
324             task.cancel();
325             serverStartedLatch.countDown();
326         }
327     }
328     public static long stamp = System.currentTimeMillis();
329 
330     private void cleanupAfterException(){
331         if(uccProcess != null){
332             uccProcess.destroy();
333         }
334     }
335     
336     protected void initUCCWrapper() throws UCCStartException {
337         try {
338             // start new ucc instance
339             String id = System.currentTimeMillis() + "a" + fileCounter++;
340             String fileWithPorts = "GBports" + id;
341             String udkHomePath = getUnrealHome();
342             String binariesPath = udkHomePath + File.separator + "Binaries";
343 
344             // default ucc executable for Windows
345             //udk.exe creates a separate console for output and the output is thus unavaliable to this process, udk.com does not
346             String uccFile = "Win32" + File.separator + "UDK.com"; 
347             String execStr = binariesPath + File.separator + uccFile;
348 
349             // determine OS type, if it isn't win suppose it is Linux and try to run UDK through Wine
350             String postOptions = "";
351             String preOptions = "";
352             if (!isWindows()) {
353                 postOptions = " -nohomedir";
354                 preOptions = binariesPath + File.separator + "Win32" + File.separator + "UDK.com";
355                 execStr = Pogamut.getPlatform().getProperty("WINE", "wine");
356             }
357 
358             String portsSetting = configuration.startOnUnusedPort ? "?PortsLog=" + fileWithPorts + "?bRandomPorts=true" : "";
359 
360             ProcessBuilder procBuilder = new ProcessBuilder(execStr, preOptions, "server",
361                     configuration.mapName
362                     + "?game=" + configuration.gameBotsPack + "." + configuration.gameType
363                     + portsSetting + configuration.options + postOptions);
364             procBuilder.directory(new File(binariesPath));
365 
366             uccProcess = procBuilder.start();
367             ScannerSink scanner = new ScannerSink(uccProcess.getInputStream());
368             scanner.start();
369             new StreamSink(uccProcess.getErrorStream()).start();
370 
371             scanner.serverStartedLatch.await();
372             if (scanner.exception != null) {
373                 // ucc failed to start 
374                 uccProcess.destroy();
375                 throw scanner.exception;
376             }
377 
378             controlPort = scanner.controlPort;
379             gbPort = scanner.botsPort;
380             Thread.sleep(5000);
381         } catch (InterruptedException ex) {
382             cleanupAfterException();
383             throw new UCCStartException("Interrupted.", ex);
384         } catch (IOException ex) {
385             cleanupAfterException();
386             throw new UCCStartException("IO Exception.", ex);
387         }
388     }
389 
390     /**
391      * Process of the
392      * @return
393      */
394     public Process getProcess() {
395         return uccProcess;
396     }
397     /** Was this instance already released? */
398     protected boolean stopped = false;
399 
400     private synchronized void stopNoWaiting() {
401         if (!isWindows()) {
402             //kill process in linux with comand 'killall -9 <process_name>'
403 
404             //System.out.println(plannerRunFile.getName());
405             String[] command1 = new String[2];
406             command1[0] = "killall";
407             command1[1] = "UDK.com";
408 
409             String[] command2 = new String[2];
410             command2[0] = "killall";
411             command2[1] = "UDK.exe";
412 
413             try {
414                 Runtime.getRuntime().exec(command1);
415                 Runtime.getRuntime().exec(command2);
416             } catch (IOException ex) {
417                 uccLog.log(Level.SEVERE, "Could not kill the UDK process: " + ex, ex);
418             }
419         } else {
420             //kill process with wnodes task kill
421             String[] command = new String[4];
422             command[0] = "taskkill";
423             command[1] = "/F";
424             command[2] = "/IM";
425             command[3] = "UDK.*";
426 
427 
428             try {
429                 Runtime.getRuntime().exec(command);
430             } catch (IOException ex) {
431                 uccLog.log(Level.SEVERE, "Could not kill the UDK with taskkill: " + ex, ex);
432             }
433             
434         }
435         
436         
437         
438         if(uccProcess != null){
439         
440 //        try {
441 //            uccProcess.getOutputStream().write((byte)3);
442 //        try {
443 //                        Thread.sleep(1000);
444 //                        // give the process some time to terminate
445 //                } catch (InterruptedException e) {
446 //
447 //                }
448 //            uccProcess.getOutputStream().write((byte)3);
449 //        } catch (IOException ex){}
450             
451             uccProcess.destroy();
452         }
453         
454         
455         uccProcess = null;
456         Runtime.getRuntime().removeShutdownHook(shutDownHook);
457     }
458     
459     
460     /**
461      * Stops the UCC server.
462      */
463     public synchronized void stop() {
464         stopped = true;
465         if (uccProcess != null) {
466         	stopNoWaiting();
467         	try {
468 				Thread.sleep(1000);
469 				// give the process some time to terminate
470 			} catch (InterruptedException e) {
471 
472 			}
473         }
474     }
475 
476     /**
477      * @return Port for GameBots connection.
478      */
479     public int getBotPort() {
480         stopCheck();
481         return gbPort;
482     }
483     
484     /**
485      * @return Port of the Observer of GameBots2004.
486      */
487     public int getObserverPort() {
488     	stopCheck();
489     	return observerPort;
490     }
491 
492     /**
493      * @return Port for control connection.
494      */
495     public int getControlPort() {
496         stopCheck();
497         return controlPort;
498     }
499 
500     protected void stopCheck() {
501         if (stopped) {
502             throw new PogamutException("UCC already stopped.", this);
503         }
504     }
505 
506 	public String getHost() {
507 		return "localhost";
508 	}
509 	
510 	public SocketConnectionAddress getBotAddress() {
511 		return new SocketConnectionAddress(getHost(), getBotPort());
512 	}
513 	
514 	public SocketConnectionAddress getServerAddress() {
515 		return new SocketConnectionAddress(getHost(), getControlPort());
516 	}
517 	
518 	public SocketConnectionAddress getObserverAddress() {
519 		return new SocketConnectionAddress(getHost(), getObserverPort());
520 	}
521 	
522 }