1 /*
2 * ========================================================================
3 *
4 * Licensed to the Apache Software Foundation (ASF) under one or more
5 * contributor license agreements. See the NOTICE file distributed with
6 * this work for additional information regarding copyright ownership.
7 * The ASF licenses this file to You under the Apache License, Version 2.0
8 * (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 * ========================================================================
20 */
21 package org.apache.cactus.container;
22
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.HttpURLConnection;
26 import java.net.URL;
27
28 import org.apache.cactus.integration.api.exceptions.CactusRuntimeException;
29 import org.codehaus.cargo.util.log.Logger;
30
31 /**
32 * Support class that handles the lifecycle of a container, which basically
33 * consists of startup and shutdown.
34 *
35 * @version $Id: ContainerRunner.java 239130 2005-01-29 15:49:18Z vmassol $
36 */
37 public final class ContainerRunner
38 {
39 // Instance Variables ------------------------------------------------------
40
41 /**
42 * The container to run.
43 */
44 //private org.codehaus.cargo.container.Container container;
45
46 private ContainerWrapper containerWrapper = null;
47
48 /**
49 * The URL that is continuously pinged to check if the container is running.
50 */
51 private URL testURL;
52
53 /**
54 * Timeout in milliseconds after which to give up connecting to the
55 * container.
56 */
57 private long timeout = 180000;
58
59 /**
60 * The time interval in milliseconds to sleep between polling the container.
61 */
62 private long checkInterval = 500;
63
64 /**
65 * The time to sleep after the container has shut down.
66 */
67 private long shutDownWait = 2000;
68
69 /**
70 * Whether the container had already been running before.
71 */
72 private boolean alreadyRunning;
73
74 /**
75 * The server name as returned in the 'Server' header of the server's
76 * HTTP response.
77 */
78 private String serverName;
79
80 /**
81 * The logger to use.
82 */
83 private transient Logger logger;
84
85 // Constructors ------------------------------------------------------------
86
87 /**
88 * Constructor.
89 *
90 * @param theContainerWrapper The container to run
91 */
92 public ContainerRunner(ContainerWrapper theContainerWrapper)
93 {
94 //this.container = theContainer;
95 this.containerWrapper = theContainerWrapper;
96 }
97
98 // Public Methods ----------------------------------------------------------
99
100 /**
101 * Returns the server name as reported in the 'Server' header of HTTP
102 * responses from the server.
103 *
104 * @return The server name
105 */
106 public String getServerName()
107 {
108 return this.serverName;
109 }
110
111 /**
112 * Method called by the task to perform the startup of the container. This
113 * method takes care of starting the container in another thread, and
114 * polling the test URL to check whether startup has completed. As soon as
115 * the URL is available (or the timeout is exceeded), control is returned to
116 * the caller.
117 *
118 * @throws IllegalStateException If the 'url' property is <code>null</code>
119 */
120 public void startUpContainer() throws IllegalStateException
121 {
122 if (this.testURL == null)
123 {
124 throw new IllegalStateException("Property [url] must be set");
125 }
126
127 // Try connecting in case the server is already running. If so, does
128 // nothing
129 this.alreadyRunning = isAvailable(testConnectivity(this.testURL));
130 if (this.alreadyRunning)
131 {
132 // Server is already running. Record this information so that we
133 // don't stop it afterwards.
134 this.logger.debug("Server is already running",
135 getClass().toString());
136 return;
137 }
138
139 // Now start the server in another thread
140 Thread thread = new Thread(new Runnable()
141 {
142 public void run()
143 {
144 containerWrapper.startUp();
145 }
146 });
147 thread.start();
148
149 // Continuously try calling the test URL until it succeeds or
150 // until a timeout is reached (we then throw a build exception).
151 long startTime = System.currentTimeMillis();
152 int responseCode = -1;
153 do
154 {
155 if ((System.currentTimeMillis() - startTime) > this.timeout)
156 {
157 throw new CactusRuntimeException("Failed to start the container after "
158 + "more than [" + this.timeout + "] ms. Trying to connect "
159 + "to the [" + this.testURL + "] test URL yielded a ["
160 + responseCode + "] error code. Please run in debug mode "
161 + "for more details about the error.");
162 }
163 sleep(this.checkInterval);
164 this.logger.debug("Checking if server is up ...",
165 getClass().toString());
166 responseCode = testConnectivity(this.testURL);
167 } while (!isAvailable(responseCode));
168
169 // Wait a few ms more (just to be sure !)
170 sleep(this.containerWrapper.getStartUpWait());
171
172 this.serverName = retrieveServerName(this.testURL);
173 this.logger.info("Server [" + this.serverName + "] started",
174 getClass().toString());
175 }
176
177 /**
178 * Method called by the task to perform the stopping of the container. This
179 * method takes care of stopping the container in another thread, and
180 * polling the test URL to check whether shutdown has completed. As soon as
181 * the URL stops responding, control is returned to the caller.
182 *
183 * @throws IllegalStateException If the 'url' property is <code>null</code>
184 */
185 public void shutDownContainer() throws IllegalStateException
186 {
187 if (this.testURL == null)
188 {
189 throw new IllegalStateException("Property [url] must be set");
190 }
191
192 // Don't shut down a container that has not been started by us
193 if (this.alreadyRunning)
194 {
195 return;
196 }
197
198 if (!isAvailable(testConnectivity(this.testURL)))
199 {
200 this.logger.debug("Server isn't running!", getClass().toString());
201 return;
202 }
203
204 // Call the target that stops the server, in another thread. The called
205 // target must be blocking.
206 Thread thread = new Thread(new Runnable()
207 {
208 public void run()
209 {
210 containerWrapper.shutDown();
211 }
212 });
213 thread.start();
214
215 // Continuously try calling the test URL until it fails
216 do
217 {
218 sleep(this.checkInterval);
219 } while (isAvailable(testConnectivity(this.testURL)));
220
221 // sleep a bit longer to be sure the container has terminated
222 sleep(this.shutDownWait);
223
224 this.logger.debug("Server stopped!", getClass().toString());
225 }
226
227 /**
228 * Sets the time interval to sleep between polling the container.
229 *
230 * The default interval is 500 milliseconds.
231 *
232 * @param theCheckInterval The interval in milliseconds
233 */
234 public void setCheckInterval(long theCheckInterval)
235 {
236 this.checkInterval = theCheckInterval;
237 }
238
239 /**
240 * Sets the log to write to.
241 *
242 * @param theLogger The log to set
243 */
244 public void setLogger(Logger theLogger)
245 {
246 this.logger = theLogger;
247 }
248
249 /**
250 * Sets the time to wait after the container has been shut down.
251 *
252 * The default time is 2 seconds.
253 *
254 * @param theShutDownWait The time to wait in milliseconds
255 */
256 public void setShutDownWait(long theShutDownWait)
257 {
258 this.shutDownWait = theShutDownWait;
259 }
260
261 /**
262 * Sets the timeout after which to stop trying to call the container.
263 *
264 * The default timeout is 3 minutes.
265 *
266 * @param theTimeout The timeout in milliseconds
267 */
268 public void setTimeout(long theTimeout)
269 {
270 this.timeout = theTimeout;
271 }
272
273 /**
274 * Sets the HTTP/HTTPS URL that will be continuously pinged to check if the
275 * container is running.
276 *
277 * @param theTestURL The URL to set
278 */
279 public void setURL(URL theTestURL)
280 {
281 if (!(theTestURL.getProtocol().equalsIgnoreCase("http")
282 || theTestURL.getProtocol().equalsIgnoreCase("https")))
283 {
284 throw new IllegalArgumentException("Not a HTTP or HTTPS URL");
285 }
286 this.testURL = theTestURL;
287 }
288
289 // Private Methods ---------------------------------------------------------
290
291 /**
292 * Tests whether we are able to connect to the HTTP server identified by the
293 * specified URL.
294 *
295 * @param theUrl The URL to check
296 * @return the HTTP response code or -1 if no connection could be
297 * established
298 */
299 public int testConnectivity(URL theUrl)
300 {
301 int code;
302 try
303 {
304 HttpURLConnection connection =
305 (HttpURLConnection) theUrl.openConnection();
306 connection.setRequestProperty("Connection", "close");
307 connection.connect();
308 readFully(connection);
309 connection.disconnect();
310 code = connection.getResponseCode();
311 }
312 catch (IOException e)
313 {
314 this.logger.debug("Failed to connect to [" + theUrl + "]",
315 e.getMessage());
316 code = -1;
317 }
318 return code;
319 }
320
321
322 /**
323 * Tests whether an HTTP return code corresponds to a valid connection
324 * to the test URL or not. Success is 200 up to but excluding 300.
325 *
326 * @param theCode the HTTP response code to verify
327 * @return <code>true</code> if the test URL could be called without error,
328 * <code>false</code> otherwise
329 */
330 private boolean isAvailable(int theCode)
331 {
332 boolean result;
333 if ((theCode != -1) && (theCode < 300))
334 {
335 result = true;
336 }
337 else
338 {
339 result = false;
340 }
341 return result;
342 }
343
344 /**
345 * Retrieves the server name of the container.
346 *
347 * @param theUrl The URL to retrieve
348 * @return The server name, or <code>null</code> if the server name could
349 * not be retrieved
350 */
351 private String retrieveServerName(URL theUrl)
352 {
353 String retVal = null;
354 try
355 {
356 HttpURLConnection connection =
357 (HttpURLConnection) theUrl.openConnection();
358 connection.connect();
359 retVal = connection.getHeaderField("Server");
360 connection.disconnect();
361 }
362 catch (IOException e)
363 {
364 this.logger.debug("Could not get server name from ["
365 + theUrl + "]", e.getMessage());
366 }
367 return retVal;
368 }
369
370 /**
371 * Fully reads the input stream from the passed HTTP URL connection to
372 * prevent (harmless) server-side exception.
373 *
374 * @param theConnection the HTTP URL connection to read from
375 * @exception IOException if an error happens during the read
376 */
377 static void readFully(HttpURLConnection theConnection)
378 throws IOException
379 {
380 // Only read if there is data to read ... The problem is that not
381 // all servers return a content-length header. If there is no header
382 // getContentLength() returns -1. It seems to work and it seems
383 // that all servers that return no content-length header also do
384 // not block on read() operations!
385 if (theConnection.getContentLength() != 0)
386 {
387 byte[] buf = new byte[256];
388 InputStream in = theConnection.getInputStream();
389 while (in.read(buf) != -1)
390 {
391 // Make sure we read all the data in the stream
392 }
393 }
394 }
395
396 /**
397 * Pauses the current thread for the specified amount.
398 *
399 * @param theMs The time to sleep in milliseconds
400 * @throws CactusRuntimeException If the sleeping thread is interrupted
401 */
402 private void sleep(long theMs) throws CactusRuntimeException
403 {
404 try
405 {
406 Thread.sleep(theMs);
407 }
408 catch (InterruptedException e)
409 {
410 throw new CactusRuntimeException("Interruption during sleep", e);
411 }
412 }
413 }