通过WebService控制CruiseControl进行构建
CruiseControl(CC)作为持续集成的工具,为了方便管理,在其源码中就提供了许多JMX的接口,其中有主程序本身的,有Dashboard的,还有我们今天想要提到的Project的JMX接口,这使得我们可以很方便的通过CC的RMI端口去调用JMX对Project进行构建
这其中涉及到的接口和类主要有以下几个:
ProjectMBean接口,我们今天调用的方法是build()方法
public interface ProjectMBean { /** * Pauses the controlled project. */ public void pause(); /** * Resumes the controlled project. */ public void resume(); /** * Runs a build now */ public void build(); /** * Runs a build now, overriding the target of the used builder * @param target the target to invoke */ public void buildWithTarget(String target); /** * Runs a build now, overriding the target of the used builder * and passing additional properties * @param target the target to invoke * @param addedProperties the additional properties that will be passed to the build */ public void buildWithTarget(String target, Map<String, String> addedProperties); /** * Serialize the project */ public void serialize(); /** * Is the project paused? * * @return Pause state */ public boolean isPaused(); /** * * @return start time of the last build, using the format 'yyyyMMddHHmmss' */ public String getBuildStartTime(); /** * Change the Project label * * @param label a new label; should be valid for the current * LabelIncrementer */ public void setLabel(String label); public String getLabel(); /** * Change the last built date. This can be used to manipulate whether * builds will be initiated. * * @param date date string in the form yyyyMMddHHmmss * @throws CruiseControlException if an invalid date string is given */ public void setLastBuild(String date) throws CruiseControlException; public String getLastBuild(); public boolean isLastBuildSuccessful(); /** * Change the last built date. This can be used to manipulate whether * builds will be initiated. * * @param date date string in the form yyyyMMddHHmmss * @throws CruiseControlException if an invalid date string is given */ public void setLastSuccessfulBuild(String date) throws CruiseControlException; public String getLastSuccessfulBuild(); /** * Change the directory where CruiseControl logs are kept * * @param logdir Relative or absolute path to the log directory */ public void setLogDir(String logdir); public String getLogDir(); /** * @return a list with the names of the available log files */ public List<String> getLogLabels(); /** * @param logLabel a valid build label, must exist in the list returned by {@link #getLogLabels()}. * @param firstLine the starting line in the log for the given build label * @return lines from the given firstLine up to max lines, or an empty array if no more lines exist. */ public String[] getLogLabelLines(String logLabel, int firstLine); /** * Change the project name. May cause problems if configuration file is * not also changed. * @param name the new project name */ public void setProjectName(String name); public String getProjectName(); /** * Change the interval between builds * * @param buildInterval Build interval in milliseconds */ public void setBuildInterval(long buildInterval); public long getBuildInterval(); /** * Gets the human-readable version of the project status. * @return the human-readable version of the project status */ public String getStatus(); /** * @return the commit message includes the commiter and message */ public String[][] commitMessages(); /** * @param firstLine The starting line to skip to. * @return Output from the live output buffer, after line specified (inclusive). */ public String[] getBuildOutput(Integer firstLine); /** * @return A unique (for this VM) identifying string for this logger instance. * Intended to allow reporting apps (eg: Dashboard) to check if * the "live output" log file has been reset and to start asking for output from the first line * of the current output file if the logger has changed. * * Before the first call to retrieveLines(), the client should call getOutputLoggerID(), and hold that ID value. * If a client later calls retrieveLines() with a non-zero 'firstLine' parameter, and receives an empty array * as a result, that client should call getOutputLoggerID() again, and if the ID value differs, start reading * using a zero 'firstLine' parameter. * @see net.sourceforge.cruisecontrol.util.BuildOutputLogger#retrieveLines(int)}. */ public String getOutputLoggerID(); }
以及实现ProjectMBean接口的ProjectController类,build()方法会结束Project的持续构建等待直接开始构建
public class ProjectController extends NotificationBroadcasterSupport implements ProjectControllerMBean, BuildProgressListener, BuildResultListener { public static final String OBJECT_NAME_PREFIX = "CruiseControl Project:name="; private static final Logger LOG = Logger.getLogger(ProjectController.class); private final Project project; private static int sequence = 0; private static final Object SEQUENCE_LOCK = new Object(); public ProjectController(final Project project) { this.project = project; project.addBuildProgressListener(this); project.addBuildResultListener(this); } private int nextSequence() { synchronized (SEQUENCE_LOCK) { return ++sequence; } } public void handleBuildProgress(final BuildProgressEvent event) { log("build progress event: " + event.getState().getDescription()); if (checkSourceProject(event.getProject())) { final Notification notification = new Notification("cruisecontrol.progress.event", this, nextSequence()); notification.setUserData(event.getState().getName()); sendNotification(notification); } } public void handleBuildResult(final BuildResultEvent event) { log("build result event: build " + String.valueOf(event.isBuildSuccessful() ? "successful" : "failed")); if (checkSourceProject(event.getProject())) { final Notification notification = new Notification("cruisecontrol.result.event", this, nextSequence()); notification.setUserData((event.isBuildSuccessful()) ? Boolean.TRUE : Boolean.FALSE); sendNotification(notification); } } private boolean checkSourceProject(final Project sourceProject) { boolean projectsMatch = false; if (project == sourceProject) { projectsMatch = true; } else { if (sourceProject == null) { LOG.warn("source project was null"); } else { LOG.warn("source project " + sourceProject.getName() + " didn't match internal project " + project.getName()); } } return projectsMatch; } public void pause() { log("pausing"); project.setPaused(true); } public void resume() { log("resuming"); project.setPaused(false); } public void build() { log("forcing build"); project.setBuildForced(true); } public void buildWithTarget(final String buildTarget) { log("forcing build with target \"" + buildTarget + "\""); project.forceBuildWithTarget(buildTarget); } public void buildWithTarget(String buildTarget, Map<String, String> addedProperties) { log("forcing build with target \"" + buildTarget + "\" with added Properties"); project.forceBuildWithTarget(buildTarget, addedProperties); } public void serialize() { log("serializing"); project.serializeProject(); } public boolean isPaused() { return project.isPaused(); } public void setLabel(final String label) { log("setting label to [" + label + "]"); project.setLabel(label); } public String getLabel() { return project.getLabel(); } public void setLastBuild(final String date) throws CruiseControlException { log("setting last build to [" + date + "]"); project.setLastBuild(date); } public String getLastBuild() { return project.getLastBuild(); } public boolean isLastBuildSuccessful() { return project.isLastBuildSuccessful(); } public void setLastSuccessfulBuild(final String date) throws CruiseControlException { log("setting last successful build to [" + date + "]"); project.setLastSuccessfulBuild(date); } public String getLastSuccessfulBuild() { return project.getLastSuccessfulBuild(); } public String getBuildStartTime() { final String buildStartTime = project.getBuildStartTime(); return buildStartTime == null ? "" : buildStartTime; } public void setLogDir(final String logdir) { log("setting log dir to [" + logdir + "]"); project.getLog().setDir(logdir); } public String getLogDir() { return project.getLogDir(); } public List<String> getLogLabels() { return project.getLogLabels(); } public String[] getLogLabelLines(final String logLabel, final int firstLine) { return project.getLogLabelLines(logLabel, firstLine); } public void setProjectName(final String name) { log("setting project name to [" + name + "]"); project.setName(name); } public String getProjectName() { return project.getName(); } public void setBuildInterval(long buildInterval) { log("setting build interval to [" + buildInterval + "]"); project.overrideBuildInterval(buildInterval); } public long getBuildInterval() { return project.getBuildInterval(); } public String getStatus() { return project.getStatusWithQueuePosition(); } private void log(final String message) { LOG.info(project.getName() + " Controller: " + message); } public void register(final MBeanServer server) throws JMException { final ObjectName projectName = new ObjectName(OBJECT_NAME_PREFIX + project.getName()); // Need to attempt to unregister the old mbean with the same name since // CruiseControlControllerJMXAdaptor keeps calling every time a change // is made to the config.xml file via JMX. try { server.unregisterMBean(projectName); } catch (InstanceNotFoundException noProblem) { } catch (MBeanRegistrationException noProblem) { } server.registerMBean(this, projectName); } /** * @return All the commit messages associated with the "current" modification set as * string[user name][commit message]. */ public String[][] commitMessages() { final ModificationSet modificationSet = project.getProjectConfig().getModificationSet(); final List<SourceControl> sourceControls = modificationSet.getSourceControls(); final Iterator<SourceControl> iterator = sourceControls.iterator(); final List<Modification> modifications = new ArrayList<Modification>(); while (iterator.hasNext()) { final SourceControl sourcecontrol = iterator.next(); modifications.addAll( sourcecontrol.getModifications(project.getLastBuildDate(), new Date()) ); } final String[][] commitMessages = new String[modifications.size()][]; for (int i = 0; i < modifications.size(); i++) { final Modification modification = modifications.get(i); commitMessages[i] = new String[2]; commitMessages[i][0] = modification.userName; commitMessages[i][1] = modification.comment; } return commitMessages; } /** * Output from the live output buffer, after line specified (inclusive). * @see net.sourceforge.cruisecontrol.util.BuildOutputLogger */ public String[] getBuildOutput(final Integer firstLine) { return BuildOutputLoggerManager.INSTANCE.lookup(getProjectName()).retrieveLines(firstLine); } /** * @return A unique (for this VM) identifying string for this logger instance. * This is intended to allow reporting apps (eg: Dashboard) to check if * the "live output" log file has been reset and to start asking for output from the first line * of the current output file if the logger has changed. * * Before the first call to retrieveLines(), the client should call getOutputLoggerID(), and hold that ID value. * If a client later calls retrieveLines() with a non-zero 'firstLine' parameter, and receives an empty array * as a result, that client should call getOutputLoggerID() again, and if the ID value differs, start reading * using a zero 'firstLine' parameter. * @see net.sourceforge.cruisecontrol.util.BuildOutputLogger#retrieveLines(int)}. */ public String getOutputLoggerID() { return BuildOutputLoggerManager.INSTANCE.lookup(getProjectName()).getID(); } }
我们可以注意到在ProjectController注册到代理时,使用的ObjectName是CruiseControl Project:name=ProjectName
public static final String OBJECT_NAME_PREFIX = "CruiseControl Project:name="; public void register(final MBeanServer server) throws JMException { final ObjectName projectName = new ObjectName(OBJECT_NAME_PREFIX + project.getName()); // Need to attempt to unregister the old mbean with the same name since // CruiseControlControllerJMXAdaptor keeps calling every time a change // is made to the config.xml file via JMX. try { server.unregisterMBean(projectName); } catch (InstanceNotFoundException noProblem) { } catch (MBeanRegistrationException noProblem) { } server.registerMBean(this, projectName); }
让我们来尝试通过RMI调用CC的JMX接口构建示例Project:connectfour,新建一个Test类
environment中存放的是RMI服务端的访问地址、RMI的JNDI工厂类,由于CC运行在本地因此为localhost,RMI端口则是CC配置中默认的1099
public class Test { public static void main(String[] args) { try { JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://localhost:1099/jndi/jrmp"); Map<String, String> environment = new HashMap<String, String>(); environment.put("java.naming.provider.url", "rmi://localhost:1099"); environment.put("java.naming.factory.initial", "com.sun.jndi.rmi.registry.RegistryContextFactory"); JMXConnector jmxc = JMXConnectorFactory.connect(url, environment); MBeanServerConnection mbsc = jmxc.getMBeanServerConnection(); ObjectName mbeanName = new ObjectName("CruiseControl Project:name=connectfour"); mbsc.invoke(mbeanName, "build", null, null); } catch (Exception e) { // TODO Auto-generated catch block System.out.println(e.getMessage()); } } }
先把CC启动起来,确认Project:connectfour的存在,然后运行Test类中的main函数,可以在CC的命令行窗口看到connectfour开始进行构建
到这里我们已经成功的通过RMI调用CC的JMX接口进行Project的构建了,但是如果我们希望非Java语言的系统也能进行这样的调用的话,就得请出WebService了
首先新建一个CruiseControlAdmin类,实现构建Project的方法BuildProject()
public class CruiseControlAdmin { public void BuildProject(String projectName) { try { JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://localhost:1099/jndi/jrmp"); Map<String, String> environment = new HashMap<String, String>(); environment.put("java.naming.provider.url", "rmi://localhost:1099"); environment.put("java.naming.factory.initial", "com.sun.jndi.rmi.registry.RegistryContextFactory"); JMXConnector jmxc = JMXConnectorFactory.connect(url, environment); MBeanServerConnection mbsc = jmxc.getMBeanServerConnection(); ObjectName mbeanName = new ObjectName("CruiseControl Project:name=" + projectName); mbsc.invoke(mbeanName, "build", null, null); } catch (Exception e) { // TODO Auto-generated catch block System.out.println(e.getMessage()); } } }
然后为了能够被识别为WebService,我们需要services.xml
<service name="CruiseControlAdminService"> <description>CruiseControlAdminService</description> <messageReceivers> <messageReceiver mep="http://www.w3.org/ns/wsdl/in-only" class="org.apache.axis2.rpc.receivers.RPCInOnlyMessageReceiver" /> <messageReceiver mep="http://www.w3.org/ns/wsdl/in-out" class="org.apache.axis2.rpc.receivers.RPCMessageReceiver"/> </messageReceivers> <parameter name="ServiceClass" locked="false">com.reno.cruisecontrol.adminservice.main.CruiseControlAdmin</parameter> </service>
将编译出的CruiseControlAdmin.class和services.xml打成一个aar包,比如CruiseControlAdmin.aar
将这个aar包放在如Apache Axis2的WEB-INF\services下,或者直接通过Apache Axis2的admin管理页面upload这个aar包,这样就算部署WebService成功了
最后就是验证一下WebService是否可用了,写文章的时间不早了,就不再进行验证了
到这里,有了WebService的帮助,我们就可以在多平台多语言下控制CruiseControl进行Project的构建了