This post continues a thread about how to support multiple build configurations from CruiseControl.NET and NAnt.
My goal was to expose some method through the web dashboard by which the user could select a build configuration and kick off a build. To accomplish that goal, some modification or extension of the dashboard is required. I decided against modifying the actual ccnet source, and instead chose to develop a dashboard plugin.
I started with the official ccnet docs regarding the subject, available here. I suggest readers acquaint themselves with the material.
Plugins Won’t Load
Before I get to the meat of the post, I must warn you that as it stands *today*, custom dashboard plugins will not load. The documentation states that plugin assemblies should be named “ccnet.*.plugin.dll”, and be placed in the dashboard’s bin folder. Presently, the code for loading plugins for the dashboard looks for assemblies via the path returned from Assembly.GetExecutingAssembly().Location(the Assembly.GetExecutingAssembly() call refers to the actual dashboard assembly). This line of code is logical, but it returns the path of the assembly as it is hosted by asp.net, which is some deep-dark directory buried within Windows because of asp.net’s shadow copy.
The dashboard’s source code must be changed. The Assembly.CodeBase property should be used instead, which returns a string uri to the assembly. This uri then needs to be parsed to obtain the actual physical location of the assembly so that satellite assembly lookup can work. I started a thread in the ccnet-devl groups. A ticket was opened, and as far as I know, the issue has been corrected. I suspect the correct code will be present for version 1.4, whenever that is released. Please note, however, that that is my assumption only, and has not been promised in any way by the ccnet team. Details on the defect, and how to fix it, are available here. Also please note that until the correction for the defect is available, you must pull down a copy of the source code, make the fix yourself, and deploy the updated dashboard binary.
Goal
I’ve already established my goal. Now let me describe how I will achieve that goal. Building upon my previous post about using NAnt properties to build debug or release configurations via the csc NAnt task, I have decided that I would like to allow the user to modify the initial values of NAnt properties for each configured ccnet project. The modified values for these properties must ultimately be recognized by ccnet so that these values can be passed to the root NAnt build script for the project when it is launched. For these reasons I have named my dashboard plugin “PropertiesPlugin”.
Source Code & Debugging
It’s possible to develop a dashboard plugin without benefit of the ccnet source code, but I strongly encourage you to download the source as it will make things much easier. For my development, I used version 1.2.1 of the source.
To debug your plugin, you will need to make sure you have the ccnet server running(either as a service or from the console), and you will need to attach to the aspnet_wp.exe process. Make sure that the build output of the project is the dashboard’s bin directory, otherwise the debugger won’t hit your breakpoints because the assembly will be out of date. Another issue that I sometimes face is that objects provided to your plugin from the dashboard will not evaluate and cannot be browsed. In such cases, I use the IIS control panel applet to stop/start the ccnet www app, which usually corrects the problem.
Starting the plugin, interfaces, and NetReflector
To start the plugin, create a public class that implements the ThoughtWorks.CruiseControl.WebDashboard.MVC.Cruise.ICruiseAction interface, and the ThoughtWorks.CruiseControl.WebDashboard.Dashboard.IPlugin interface. Additionally, the class must be decorated with the ReflectoryType attribute(exposed via the Exortech.NetReflector interface).
At a minimum, your plugin assembly will require references to NetReflector.dll, ThoughtWorks.CruiseControl.Core.dll, ThoughtWorks.CruiseControl.Remote.dll, and ThoughtWorks.CruiseControl.WebDashboard.dll (each is present in the dashboard’s bin folder).
Example:
[ReflectorType( "propertiesPlugin" )]
public class PropertiesPlugin : ICruiseAction , IPlugin
{
private const string PLUGIN_DESCRIPTION = "NAnt Project Properties";
private const string ACTION_NAME = "ViewProjectProperties";
#region IPlugin Members
public IResponse Execute( ICruiseRequest cruiseRequest )
{
return null;
} //END Execute method
#endregion
#region IPlugin Members
/// <summary>
/// Returns the description for the link
/// </summary>
public string LinkDescription
{
get
{
return PLUGIN_DESCRIPTION;
}
} //END LinkDescription property
public INamedAction[] NamedActions
{
get
{
return new INamedAction[] { new ImmutableNamedAction( ACTION_NAME , this ) };
}
} //END NamedActions property
}
The above code is the bare minimum for creating a plugin. As it stands, it will not execute correctly, because the ICruiseAction.Execute implementation returns null. Ultimately we will need to return a valid IResponse by using the Velocity view generator, but I’ll touch on that subject later. Regardless, at this point the plugin should compile, and you should be able to debug it.
ReflectorType Attribute
As noted earlier, the class is decorated with the ReflectorType attribute. This attribute is significant. When the dashboard loads the configuration(dashboard.config), it will attempt to load named plugins from all assemblies(internal and external) via NetReflector. This plugin is named “propertiesPlugin”, and NetReflector will look for any public types decorated with this value.
ICruiseAction interface
This class implements the ICruiseAction interface. This interface is used to perform any number of actions from the dashboard user interface. Almost every link you click within the dashboard is represented by an accompanying action implementation.
This interface defines but a single method: IResponse Execute( ICruiseRequest req ). The basic idea is that the action accepts the current request and generates a response. It’s pretty easy really. The request identifies all kinds of things, including the current ccnet server and project. The response is expected to be consumable html that will ultimately be sent to the browser. More on that later.
IPlugin interface
The IPlugin interface is what allows the dashboard to expose your plugin via its user interface. The IPlugin interface is rather simple:
interface IPlugin
{
string LinkDescription
{
get;
}
INamedAction[] NamedActions
{
get;
}
}
The LinkDescription property is the text for the link as it will be displayed to the user. The INamedAction[] property returns an array of named actions that will be executed when the link is clicked by the user.
I have not yet experimented with what happens when more than one named action is returned in the array, but I assume the actions are executed in succession. I do wonder however what happens to the IResponse from each action’s Execute method. Are they just appended to the stream? Unknown.
In the previous example, the plugin’s LinkDescription implementation simply returns a constant value. However, examine the NamedActions implementation:
public INamedAction[] NamedActions
{
get
{
return new INamedAction[] { new ImmutableNamedAction( ACTION_NAME , this ) };
}
}
This code defines an INamedAction array inline, with only a single element. That element is of type ImmutableNamedAction, with an action name and a reference to the actual action implementation(this – the plugin).
At this point I would like to explain that I didn’t inherently know to use the ImmutableNamedAction class. In fact, I didn’t know how to implement this property. To figure that part out, I consulted the dashboard source code, and found the ProjectReportProjectPlugin class. I saw how it implemented this property, and borrowed its behavior.
Regardless, it is important to note the two entities being used by the dashboard here, and the interaction of these entities.
The dashboard will load the configuration, see that a plugin is defined, and load the plugin. This plugin must implement the IPlugin interface. This interface allows the dashboard to expose the plugin to the user interface. It also instructs the dashboard as to what actions are available from the plugin.
The actions for the plugin are altogether different from the plugin. An action is what happens when the link for the plugin is clicked. Notice the distinction?
In this plugin, the action and the plugin are the same object, only because this plugin is very simple.
Plugin Configuration
The documentation states that dashboard plugins can take one of four forms:
More information about what type of plugin does what is available in the documentation. For my purposes, I chose to implement a project plugin because I wanted to be able to modify the build properties for each project independently.
After you have implemented the basic plugin shell and deployed the assembly, you must configure the dashboard to recognize the plugin. This topic is also covered in the documentation. But for the purposes of this plugin, the configuration was very simple. I simply added a node named “propertiesPlugin” as a child element to the “projectPlugins” node(in the file dashboard.config). Remember, this node name – projectPlugins – must match the value of the ReflectorType attribute.
An example dashboard config follows(boilerplate stuff exempted):
<dashboard>
<plugins>
<farmPlugins>
...
</farmPlugins>
<serverPlugins>
...
</serverPlugins>
<projectPlugins>
<!-- this declaration instructs the dashboard to include the project plugin -->
<propertiesPlugin />
</projectPlugins>
</plugins>
</dashboard>
That’s it for now. The above code(along with the dashboard source code fix) should get any one started with a very basic project plugin. I will detail the Velocity stuff and ccnet’s use of constructor dependency injection in the next post.