001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: ********************************************************************************/package net.sourceforge.cruisecontrol.listeners;
037:
038: import java.io.File;
039: import java.io.IOException;
040: import java.io.Serializable;
041: import java.util.ArrayList;
042: import java.util.Iterator;
043: import java.util.List;
044: import java.util.Properties;
045:
046: import org.apache.log4j.Logger;
047:
048: import net.sourceforge.cruisecontrol.CruiseControlException;
049: import net.sourceforge.cruisecontrol.Listener;
050: import net.sourceforge.cruisecontrol.ProjectEvent;
051: import net.sourceforge.cruisecontrol.ProjectState;
052: import net.sourceforge.cruisecontrol.sourcecontrols.CMSynergy;
053: import net.sourceforge.cruisecontrol.util.ManagedCommandline;
054: import net.sourceforge.cruisecontrol.util.Util;
055: import net.sourceforge.cruisecontrol.util.ValidationHelper;
056:
057: /**
058: * Monitors a set of one or more CM Synergy sessions, launching new sessions as needed. The session information is
059: * persisted and made available to other CM Synergy plugins through the session file - a simple properties file which
060: * maps a session name to a CM Synergy session ID.
061: *
062: * @author <a href="mailto:rjmpsmith@gmail.com">Robert J. Smith </a>
063: */
064: public class CMSynergySessionMonitor implements Listener {
065:
066: private static final long serialVersionUID = -9139730492970870720L;
067:
068: private static final Logger LOG = Logger
069: .getLogger(CMSynergySessionMonitor.class);
070:
071: private File sessionFile;
072: private String ccmExe = CMSynergy.CCM_EXE;
073: private ArrayList sessions = new ArrayList();
074:
075: /**
076: * Sets the name of the CM Synergy executable to use when issuing commands.
077: *
078: * @param ccmExe
079: * the name of the CM Synergy executable
080: */
081: public void setCcmExe(String ccmExe) {
082: this .ccmExe = ccmExe;
083: }
084:
085: /**
086: * Sets the file which contains the mapping between CM Synergy session names and IDs. This file should be in the
087: * standard properties file format. Each line should map one name to a CM Synergy session ID (as returned by the
088: * "ccm status" command).
089: * <p>
090: * example: <br>
091: * <br>
092: * session1=localhost:65024:192.168.1.17
093: *
094: * @param sessionFile
095: * The session file
096: */
097: public void setSessionFile(String sessionFile) {
098: this .sessionFile = new File(sessionFile);
099: }
100:
101: /**
102: * Creates a new <code>CMSynergySession</code> object and adds it to our list of monitored sessions.
103: *
104: * @return The newly created <code>CMSynergySession</code> object.
105: */
106: public CMSynergySession createSession() {
107: CMSynergySession session = new CMSynergySession();
108: sessions.add(session);
109: return session;
110: }
111:
112: /**
113: * A simple representation of a CM Synergy commandline session
114: *
115: * @author <a href="mailto:rjmpsmith@hotmail.com">Robert J. Smith</a>
116: */
117: public class CMSynergySession implements Serializable {
118:
119: private static final long serialVersionUID = -4131028240579472518L;
120:
121: private String name;
122: private String db;
123: private String role;
124: private String user;
125: private String password;
126: private String host;
127:
128: /**
129: * Gets the given name of the session.
130: *
131: * @return The name.
132: */
133: public String getName() {
134: return name;
135: }
136:
137: /**
138: * Sets the name of the session as it will be referenced in the Cruise Control config file
139: *
140: * @param name
141: * The session's given name.
142: */
143: public void setName(String name) {
144: this .name = name;
145: }
146:
147: /**
148: * Gets the password used to start the session.
149: *
150: * @return The password.
151: */
152: public String getPassword() {
153: return password;
154: }
155:
156: /**
157: * Sets the password which will be used to start the session.
158: *
159: * @param password
160: * The password.
161: */
162: public void setPassword(String password) {
163: this .password = password;
164: }
165:
166: /**
167: * Gets the CM Synergy role under which the session was started.
168: *
169: * @return The role.
170: */
171: public String getRole() {
172: return role;
173: }
174:
175: /**
176: * Sets the CM Synergy role under which the session will be started.
177: *
178: * @param role
179: * The role.
180: */
181: public void setRole(String role) {
182: this .role = role;
183: }
184:
185: /**
186: * Gets the user ID under which the session was started.
187: *
188: * @return The user ID.
189: */
190: public String getUser() {
191: return user;
192: }
193:
194: /**
195: * Sets the user ID under which the session will be started.
196: *
197: * @param user
198: * The user ID.
199: */
200: public void setUser(String user) {
201: this .user = user;
202: }
203:
204: /**
205: * Gets the CM Synergy database with which the session is associated
206: *
207: * @return The database.
208: */
209: public String getDatabase() {
210: return db;
211: }
212:
213: /**
214: * Sets the CM Synergy database with which the session will be associated
215: *
216: * @param db
217: * The database.
218: */
219: public void setDatabase(String db) {
220: this .db = db;
221: }
222:
223: /**
224: * Gets the host upon which the session is running.
225: *
226: * @return The host.
227: */
228: public String getHost() {
229: return host;
230: }
231:
232: /**
233: * Sets the host upon which the session will run.
234: *
235: * @param host
236: * The host.
237: */
238: public void setHost(String host) {
239: this .host = host;
240: }
241:
242: /**
243: * Sets the attribute (properties) file from which the session information will be loaded.
244: *
245: * @param attributeFile
246: * The file from which to read our session attributes.
247: */
248: public void setAttributeFile(String attributeFile) {
249: try {
250: Properties properties = Util
251: .loadPropertiesFromFile(new File(attributeFile));
252: db = properties.getProperty("database");
253: role = properties.getProperty("role");
254: user = properties.getProperty("user");
255: password = properties.getProperty("password");
256: host = properties.getProperty("host");
257: } catch (Exception e) {
258: LOG.error(
259: "Could not load CM Synergy session properties from file \""
260: + attributeFile + "\".", e);
261: }
262: }
263:
264: /**
265: * Validates the fields of this object.
266: *
267: * @throws CruiseControlException
268: */
269: public void validate() throws CruiseControlException {
270: ValidationHelper.assertIsSet(name, "name",
271: "the <session> child element");
272: ValidationHelper.assertIsSet(db, "db",
273: "the <session> child element");
274: ValidationHelper.assertIsSet(role, "role",
275: "the <session> child element");
276: ValidationHelper.assertIsSet(user, "user",
277: "the <session> child element");
278: ValidationHelper.assertIsSet(password, "password",
279: "the <session> child element");
280: }
281: }
282:
283: /**
284: * Checks the given session file. If it is does not exist, it is created. This method is synchronized to prevent
285: * multiple threads from attempting to create the same file.
286: *
287: * @param sessionFile
288: * The session file to check
289: * @throws CruiseControlException
290: */
291: private static synchronized void checkSessionFile(File sessionFile)
292: throws CruiseControlException {
293: // Create the session file if it does not already exist
294: if (!sessionFile.exists()) {
295: try {
296: if (sessionFile.createNewFile()) {
297: LOG.info("Created CM Synergy session file at "
298: + sessionFile.getAbsolutePath());
299: }
300: } catch (IOException e) {
301: throw new CruiseControlException(
302: "Could not create CM Synergy session file at "
303: + sessionFile.getAbsolutePath(), e);
304: }
305: }
306:
307: // Make certain that it's writable
308: if (!sessionFile.canWrite()) {
309: throw new CruiseControlException("Session file \""
310: + sessionFile.getAbsolutePath()
311: + "\" does not exist, or is not writable.");
312: }
313: }
314:
315: /**
316: * Checks that all named sessions given to the listener are still running (and accessible). If they are not, new
317: * sessions are started as needed. This method is synchronized to prevent multiple threads from each starting their
318: * own CM Synergy sessions.
319: *
320: * @param ccmExe
321: * The CM Synergy command line executable
322: * @param sessionFile
323: * The CM Synergy session map file
324: * @param sessions
325: * A list of monitored CM Synergy sessions
326: * @throws CruiseControlException
327: */
328: private static synchronized void checkSessions(String ccmExe,
329: File sessionFile, List sessions)
330: throws CruiseControlException {
331: LOG.debug("Using persisted data from "
332: + sessionFile.getAbsolutePath());
333:
334: // Load the persisted session information from file
335: Properties sessionMap;
336: try {
337: sessionMap = Util.loadPropertiesFromFile(sessionFile);
338: } catch (IOException e) {
339: throw new CruiseControlException(e);
340: }
341:
342: // Get a list of currently running CM Synergy sessions
343: ManagedCommandline cmd = new ManagedCommandline(ccmExe);
344: cmd.createArgument("status");
345: String availableSessions;
346: try {
347: cmd.execute();
348: cmd.assertExitCode(0);
349: availableSessions = cmd.getStdoutAsString();
350: } catch (Exception e) {
351: LOG
352: .warn(
353: "CM Synergy failed to provide a list of valid sessions.",
354: e);
355: availableSessions = "";
356: }
357:
358: // Check each monitored session in turn
359: for (Iterator it = sessions.iterator(); it.hasNext();) {
360: CMSynergySession session = (CMSynergySession) it.next();
361: String name = session.getName();
362: String id = sessionMap.getProperty(name);
363: LOG.info("Checking " + name + ".");
364: if (id == null || availableSessions.indexOf(id) < 0) {
365: // Start a new session and record the ID in the map
366: String newID = startSession(ccmExe, session);
367: if (newID != null) {
368: LOG.info("Started CM Synergy session \"" + newID
369: + "\".");
370: sessionMap.setProperty(name, newID);
371: }
372: } else {
373: LOG.info("Using existing session \"" + id + "\".");
374: }
375: }
376:
377: // Update the persisted session information
378: try {
379: Util.storePropertiesToFile(sessionMap,
380: "CM Synergy session map", sessionFile);
381: } catch (IOException e) {
382: throw new CruiseControlException(e);
383: }
384: }
385:
386: /**
387: * Launches a new CM Synergy command line session
388: *
389: * @param session
390: * The session information
391: */
392: private static String startSession(String ccmExe,
393: CMSynergySession session) {
394:
395: LOG.info("Starting a new CM Synergy session for \""
396: + session.getName() + "\".");
397:
398: // Create CM Synergy startup command
399: ManagedCommandline cmd = new ManagedCommandline(ccmExe);
400: cmd.createArgument("start");
401: cmd.createArgument("-q");
402: cmd.createArgument("-nogui");
403: cmd.createArgument("-m");
404: cmd.createArguments("-d", session.getDatabase());
405: cmd.createArguments("-r", session.getRole());
406: cmd.createArguments("-n", session.getUser());
407: cmd.createArguments("-pw", session.getPassword());
408: if (session.getHost() != null) {
409: cmd.createArguments("-h", session.getHost());
410: }
411:
412: try {
413: cmd.execute();
414: cmd.assertExitCode(0);
415: } catch (Exception e) {
416: LOG.error("Could not start a CM Synergy session for "
417: + session.getName(), e);
418: return null;
419: }
420:
421: return cmd.getStdoutAsString().trim();
422: }
423:
424: /*
425: * (non-Javadoc)
426: *
427: * @see net.sourceforge.cruisecontrol.Listener#handleEvent(net.sourceforge.cruisecontrol.ProjectEvent)
428: */
429: public void handleEvent(ProjectEvent event)
430: throws CruiseControlException {
431: if (event instanceof ProjectStateChangedEvent) {
432: final ProjectStateChangedEvent stateChanged = (ProjectStateChangedEvent) event;
433: // Check sessions before the bootstrappers run
434: if (stateChanged.getNewState().getCode() == ProjectState.BOOTSTRAPPING
435: .getCode()) {
436: checkSessionFile(sessionFile);
437: checkSessions(ccmExe, sessionFile, sessions);
438: }
439: }
440: }
441:
442: /*
443: * (non-Javadoc)
444: *
445: * @see net.sourceforge.cruisecontrol.Listener#validate()
446: */
447: public void validate() throws CruiseControlException {
448: // We must have at least one session to monitor
449: ValidationHelper
450: .assertTrue(sessions.size() > 0,
451: "You must provide at least one nested <session> element.");
452:
453: // Validate the details of each provided session
454: for (Iterator it = sessions.iterator(); it.hasNext();) {
455: CMSynergySession session = (CMSynergySession) it.next();
456: session.validate();
457: }
458:
459: // If no session file was provided, use the default
460: if (sessionFile == null) {
461: sessionFile = new File(CMSynergy.CCM_SESSION_FILE);
462: }
463: }
464:
465: }
|