Saturday, June 29, 2013

How to Add Support for a new CI Server to Siren of Shame

Siren of Shame supports eight CI servers today, but adding additional ones is pretty easy.  Read on if you you're interested in adding support for achievements, reputation, or push notifications to mobile devices to your favorite CI server.

Architecture Overview

We use a plugin model to interact with CI server's via the Managed Extensibility Framework (MEF).  The main project (SirenOfShame.csproj) looks in the \plugins directory to find class libraries (.dll's) that export classes (via MEF's ExportAttribute) that inherit from ICiEntryPoint.  Theoretically you could export multiple CiEntryPoints per assembly, but we only do one per C# project to separate concerns.

If you'd like to start with a reference project for either reading through or copying, consider checking out HudsonServices.csproj as it is one of our more mature plugins.


public interface ICiEntryPoint
ConfigureServerBase CreateConfigurationWindow(
        SirenOfShameSettings settings,
CiEntryPointSetting ciEntryPointSetting

    string Name { get; }

    string DisplayName { get; }

    WatcherBase GetWatcher(SirenOfShameSettings settings);



The ICiEntryPoint interface requires a Name and DisplayName.  Name (e.g. "Hudson") is used extensively in a user's persisted settings as an ID.  It is never displayed to a user, and should never be changed.

DisplayName (e.g. "Hudson/Jenkins") is displayed in the dropdown of available CI servers in the Add CI Server dialog and may be changed over time if necessary.

SirenOfShame calls ICiEntryPoint's CreateConfigurationWindow() when a user adds an instance of a new build definition to watch.  It should return a class that inherits from ConfigureServerBase, which in turn inherits from UserControl.  The control will be embedded in a windows forms page when a user selects the associated CI server on the add CI server dialog or goes to edit an existing CI server.  This form should be used to allow a user to connect to a CI server, select the build definitions to watch, and optionally persist a username and password.  For example:

public ConfigureServerBase CreateConfigurationWindow(
SirenOfShameSettings settings,
CiEntryPointSetting ciEntryPointSetting) {

return new ConfigureHudson(settings, this, ciEntryPointSetting);


Finally, GetWatcher() is called when the system starts watching CI servers (e.g. after adding a CI server, or on startup).  Its job is to instantiate and return a class that inherits from WatcherBase whose job is to handle polling for build status changes.  For example:

public WatcherBase GetWatcher(SirenOfShameSettings settings) {

    return new HudsonWatcher(settings, this);



Pretty must the only responsibility of WatcherBase that's important is overriding:

protected override IList<BuildStatus> GetBuildStatus();

This method is called very frequently by the rules engine (RulesEngine.cs) to check for build status changes.  It is is expected to retrieve the build definitions that the user is watching and return a BuildStatus for each one.  It could utilize a separate class, such as one that inherits from ServiceBase, like HudsonService.cs does, but this isn't strictly necessary.

The build engine is run on a background thread, to free up the UI thread, but it does call GetBuildStatus synchronously.  Consequently any long delays inside GetBuildStatus decrease the frequency that users get build statuses.  For example if the user has set the polling interval as 10 seconds and GetBuildStatus takes 10 seconds to run then the user actually gets build statuses every 20 seconds.  For this reason it might be a good idea to request build statuses for each build definition in parallel like HudsonService.cs does:

public IList<HudsonBuildStatus> GetBuildsStatuses(
string rootUrl,
string userName,
string password,
BuildDefinitionSetting[] watchedBuildDefinitions) {

    rootUrl = GetRootUrl(rootUrl);

var parallelResult = from buildDefinitionSetting in watchedBuildDefinitions
select GetBuildStatus(rootUrl, buildDefinitionSetting, userName, password);
return parallelResult.AsParallel().ToList();

At any time GetBuildStatus() can throw ServerUnavailableException to indicate that something is temporarily wrong (e.g. the web connection is down or the server is down for maintenance).  In response the main engine will notify the user and continue polling the server periodically until the situation has resolved itself.  If you choose the overload with an exception, the user will be given the opportunity to click on the error message and send the result back to us for diagnosis.

Next Steps

If you have any questions feel free to e-mail us at support at our domain name.  And once you get it working we hope you'll consider initiating a pull request from our GitHub project.

No comments:

Post a Comment