Index: console/app.config =================================================================== --- console/app.config (revision 3982) +++ console/app.config (working copy) @@ -3,7 +3,8 @@
- +
+ @@ -13,7 +14,11 @@ - + + + + + @@ -48,22 +53,19 @@ - + - - + + - - - + + - - - + + - @@ -80,4 +82,25 @@ + + + + + + + + + + + + + + + + + + + + + Index: core/configuration/ServerConfigurationHandler.cs =================================================================== --- core/configuration/ServerConfigurationHandler.cs (revision 0) +++ core/configuration/ServerConfigurationHandler.cs (revision 0) @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Xml; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.Core.Config +{ + /// + /// Provides additional configuration settings for the server. + /// + /// + /// Currently this only retrieves a list of type names, but it could be extended in future + /// to load additional settings (perhaps in the same way as the custom builders work). + /// + public sealed class ServerConfigurationHandler + : IConfigurationSectionHandler + { + #region Create() + /// + /// Retrieve the list of extensions to load. + /// + /// The parent. + /// The context. + /// The section that is being loaded. + /// An array of strings containing the type names. + public object Create(object parent, object configContext, XmlNode section) + { + List extensions = new List(); + + foreach (XmlNode node in section.SelectNodes("extension")) + { + ExtensionConfiguration config = new ExtensionConfiguration(); + config.Type = node.Attributes["type"].Value; + extensions.Add(config); + } + + return extensions; + } + #endregion + } +} Index: core/core.csproj =================================================================== --- core/core.csproj (revision 3982) +++ core/core.csproj (working copy) @@ -187,6 +187,7 @@ + Code Index: core/CruiseServer.cs =================================================================== --- core/CruiseServer.cs (revision 3982) +++ core/CruiseServer.cs (working copy) @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Threading; using ThoughtWorks.CruiseControl.Core.Config; @@ -14,12 +15,15 @@ private readonly IConfigurationService configurationService; private readonly ICruiseManager manager; private readonly ManualResetEvent monitor = new ManualResetEvent(true); + private readonly List _extensions = new List(); private bool disposed; private IntegrationQueueManager integrationQueueManager; public CruiseServer(IConfigurationService configurationService, - IProjectIntegratorListFactory projectIntegratorListFactory, IProjectSerializer projectSerializer) + IProjectIntegratorListFactory projectIntegratorListFactory, + IProjectSerializer projectSerializer, + List extensionList) { this.configurationService = configurationService; this.configurationService.AddConfigurationUpdateHandler(new ConfigurationUpdateHandler(Restart)); @@ -32,6 +36,12 @@ IConfiguration configuration = configurationService.Load(); // TODO - does this need to go through a factory? GD integrationQueueManager = new IntegrationQueueManager(projectIntegratorListFactory, configuration); + + // Load the extensions + if (extensionList != null) + { + InitialiseExtensions(extensionList); + } } public void Start() @@ -39,8 +49,15 @@ Log.Info("Starting CruiseControl.NET Server"); monitor.Reset(); integrationQueueManager.StartAllProjects(); - } + // Start the extensions + Log.Info("Starting Extensions"); + foreach (ICruiseServerExtension extension in _extensions) + { + extension.Start(); + } + } + /// /// Start integrator for specified project. /// @@ -54,7 +71,14 @@ /// public void Stop() { - Log.Info("Stopping CruiseControl.NET Server"); + // Stop the extensions + Log.Info("Stopping Extensions"); + foreach (ICruiseServerExtension extension in _extensions) + { + extension.Stop(); + } + + Log.Info("Stopping CruiseControl.NET Server"); integrationQueueManager.StopAllProjects(); monitor.Set(); } @@ -72,7 +96,14 @@ /// public void Abort() { - Log.Info("Aborting CruiseControl.NET Server"); + // Abort the extensions + Log.Info("Aborting Extensions"); + foreach (ICruiseServerExtension extension in _extensions) + { + extension.Abort(); + } + + Log.Info("Aborting CruiseControl.NET Server"); integrationQueueManager.Abort(); monitor.Set(); } @@ -93,7 +124,7 @@ /// public void WaitForExit() { - monitor.WaitOne(); + monitor.WaitOne(); } /// @@ -301,9 +332,33 @@ private IProjectIntegrator GetIntegrator(string projectName) { return integrationQueueManager.GetIntegrator(projectName); - } + } - void IDisposable.Dispose() + #region InitialiseExtensions() + /// + /// Initialise all the extensions for the server. + /// + /// The extensions to load. + private void InitialiseExtensions(List extensionList) + { + foreach (ExtensionConfiguration extensionConfig in extensionList) + { + // See if we can find the type + Type extensionType = Type.GetType(extensionConfig.Type); + if (extensionType == null) throw new NullReferenceException("Unable to find extension: " + extensionConfig.Type); + + // Load and initialise the extension + ICruiseServerExtension extension = Activator.CreateInstance(extensionType) as ICruiseServerExtension; + if (extension == null) throw new NullReferenceException("Unable to create an instance of " + extensionType.FullName); + extension.Initialise(this, extensionConfig); + + // Add to the list of extensions + _extensions.Add(extension); + } + } + #endregion + + void IDisposable.Dispose() { lock (this) { Index: core/CruiseServerFactory.cs =================================================================== --- core/CruiseServerFactory.cs (revision 3982) +++ core/CruiseServerFactory.cs (working copy) @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Configuration; using System.IO; using ThoughtWorks.CruiseControl.Core.Config; @@ -21,10 +22,15 @@ private static ICruiseServer CreateLocal(string configFile) { + // Load the extensions configuration + List extensionList = null; + extensionList = ConfigurationManager.GetSection("cruiseServer") as List; + return new CruiseServer( NewConfigurationService(configFile), new ProjectIntegratorListFactory(), - new NetReflectorProjectSerializer()); + new NetReflectorProjectSerializer(), + extensionList); } private static IConfigurationService NewConfigurationService(string configFile) Index: Remote/ExtensionConfiguration.cs =================================================================== --- Remote/ExtensionConfiguration.cs (revision 0) +++ Remote/ExtensionConfiguration.cs (revision 0) @@ -0,0 +1,25 @@ +using System; + +namespace ThoughtWorks.CruiseControl.Remote +{ + /// + /// Defines the configuration for a server extension. + /// + public class ExtensionConfiguration + { + #region Private fields + private string _type; + #endregion + + #region Public properties + /// + /// Gets or sets the type of the component. + /// + public string Type + { + get { return _type; } + set { _type = value; } + } + #endregion + } +} Index: Remote/ICruiseServerExtension.cs =================================================================== --- Remote/ICruiseServerExtension.cs (revision 0) +++ Remote/ICruiseServerExtension.cs (revision 0) @@ -0,0 +1,39 @@ +using System; + +namespace ThoughtWorks.CruiseControl.Remote +{ + /// + /// Provides an extension to ICruiseServer basic functionality. + /// + public interface ICruiseServerExtension + { + #region Initialise() + /// + /// Initialises the extension. + /// + /// The server that this extension is for. + void Initialise(ICruiseServer server, ExtensionConfiguration extensionConfig); + #endregion + + #region Start() + /// + /// Starts the extension. + /// + void Start(); + #endregion + + #region Stop() + /// + /// Stops the extension. + /// + void Stop(); + #endregion + + #region Abort() + /// + /// Terminates the extension immediately. + /// + void Abort(); + #endregion + } +} Index: Remote/Remote.csproj =================================================================== --- Remote/Remote.csproj (revision 3982) +++ Remote/Remote.csproj (working copy) @@ -129,6 +129,7 @@ Code + Code @@ -141,6 +142,7 @@ Code + Code Index: UnitTests/Core/Config/ServerConfigurationHandlerTest.cs =================================================================== --- UnitTests/Core/Config/ServerConfigurationHandlerTest.cs (revision 0) +++ UnitTests/Core/Config/ServerConfigurationHandlerTest.cs (revision 0) @@ -0,0 +1,25 @@ +using NUnit.Framework; +using System.Collections.Generic; +using System.Configuration; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.UnitTests.Core.Config +{ + /// + /// Tests the ServerConfigurationHandler class. + /// + [TestFixture] + public class ServerConfigurationHandlerTest : CustomAssertion + { + #region GetConfig() + [Test] + public void GetConfig() + { + List config = ConfigurationManager.GetSection("cruiseServer") as List; + Assert.IsNotNull(config); + Assert.AreEqual(1, config.Count); + Assert.AreEqual("ThoughtWorks.CruiseControl.UnitTests.Remote.ServerExtensionStub,ThoughtWorks.CruiseControl.UnitTests", config[0].Type); + } + #endregion + } +} Index: UnitTests/Core/CruiseServerTest.cs =================================================================== --- UnitTests/Core/CruiseServerTest.cs (revision 3982) +++ UnitTests/Core/CruiseServerTest.cs (working copy) @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using NMock; using NUnit.Framework; @@ -40,54 +41,60 @@ [SetUp] protected void SetUp() { - projectSerializerMock = new DynamicMock(typeof (IProjectSerializer)); + InitialiseMocks(); - integratorMock1 = new DynamicMock(typeof (IProjectIntegrator)); - integratorMock2 = new DynamicMock(typeof (IProjectIntegrator)); - integratorMock3 = new DynamicMock(typeof (IProjectIntegrator)); - integrator1 = (IProjectIntegrator) integratorMock1.MockInstance; - integrator2 = (IProjectIntegrator) integratorMock2.MockInstance; - integrator3 = (IProjectIntegrator) integratorMock3.MockInstance; - integratorMock1.SetupResult("Name", "Project 1"); - integratorMock2.SetupResult("Name", "Project 2"); - integratorMock3.SetupResult("Name", "Project 3"); + server = new CruiseServer((IConfigurationService) configServiceMock.MockInstance, + (IProjectIntegratorListFactory) projectIntegratorListFactoryMock.MockInstance, + (IProjectSerializer) projectSerializerMock.MockInstance, + null); + } - integrationQueue = null; // We have no way of injecting currently. + private void InitialiseMocks() + { + projectSerializerMock = new DynamicMock(typeof(IProjectSerializer)); - configuration = new Configuration(); - project1 = new Project(); - project1.Name = "Project 1"; - - project2 = new Project(); - project2.Name = "Project 2"; + integratorMock1 = new DynamicMock(typeof(IProjectIntegrator)); + integratorMock2 = new DynamicMock(typeof(IProjectIntegrator)); + integratorMock3 = new DynamicMock(typeof(IProjectIntegrator)); + integrator1 = (IProjectIntegrator)integratorMock1.MockInstance; + integrator2 = (IProjectIntegrator)integratorMock2.MockInstance; + integrator3 = (IProjectIntegrator)integratorMock3.MockInstance; + integratorMock1.SetupResult("Name", "Project 1"); + integratorMock2.SetupResult("Name", "Project 2"); + integratorMock3.SetupResult("Name", "Project 3"); - mockProject = new DynamicMock(typeof(IProject)); - mockProject.ExpectAndReturn("Name", "Project 3"); - mockProject.ExpectAndReturn("QueueName", "Project 3"); - mockProjectInstance = (IProject) mockProject.MockInstance; - mockProject.ExpectAndReturn("Name", "Project 3"); - integratorMock3.ExpectAndReturn("Project", mockProjectInstance); + integrationQueue = null; // We have no way of injecting currently. - configuration.AddProject(project1); - configuration.AddProject(project2); - configuration.AddProject(mockProjectInstance); + configuration = new Configuration(); + project1 = new Project(); + project1.Name = "Project 1"; - integratorList = new ProjectIntegratorList(); - integratorList.Add(integrator1); - integratorList.Add(integrator2); - integratorList.Add(integrator3); + project2 = new Project(); + project2.Name = "Project 2"; - configServiceMock = new DynamicMock(typeof (IConfigurationService)); - configServiceMock.ExpectAndReturn("Load", configuration); + mockProject = new DynamicMock(typeof(IProject)); + mockProject.ExpectAndReturn("Name", "Project 3"); + mockProject.ExpectAndReturn("QueueName", "Project 3"); + mockProjectInstance = (IProject)mockProject.MockInstance; + mockProject.ExpectAndReturn("Name", "Project 3"); + integratorMock3.ExpectAndReturn("Project", mockProjectInstance); - projectIntegratorListFactoryMock = new DynamicMock(typeof (IProjectIntegratorListFactory)); - projectIntegratorListFactoryMock.ExpectAndReturn("CreateProjectIntegrators", integratorList, configuration.Projects, integrationQueue); + configuration.AddProject(project1); + configuration.AddProject(project2); + configuration.AddProject(mockProjectInstance); - server = new CruiseServer((IConfigurationService) configServiceMock.MockInstance, - (IProjectIntegratorListFactory) projectIntegratorListFactoryMock.MockInstance, - (IProjectSerializer) projectSerializerMock.MockInstance); - } + integratorList = new ProjectIntegratorList(); + integratorList.Add(integrator1); + integratorList.Add(integrator2); + integratorList.Add(integrator3); + configServiceMock = new DynamicMock(typeof(IConfigurationService)); + configServiceMock.ExpectAndReturn("Load", configuration); + + projectIntegratorListFactoryMock = new DynamicMock(typeof(IProjectIntegratorListFactory)); + projectIntegratorListFactoryMock.ExpectAndReturn("CreateProjectIntegrators", integratorList, configuration.Projects, integrationQueue); + } + private void VerifyAll() { configServiceMock.Verify(); @@ -314,5 +321,50 @@ integratorMock1.Verify(); integratorMock2.Verify(); } - } + + /// + /// Attempts to start a new cruise server with an extension. + /// + [Test] + public void NewServerWithExtension() + { + // Define the extension + ExtensionConfiguration extensionConfig = new ExtensionConfiguration(); + extensionConfig.Type = "ThoughtWorks.CruiseControl.UnitTests.Remote.ServerExtensionStub,ThoughtWorks.CruiseControl.UnitTests"; + List extensions = new List(); + extensions.Add(extensionConfig); + + // Re-initialise all the mocks so we start with a clean slate + InitialiseMocks(); + + // See if we can start a new server + CruiseServer server = new CruiseServer((IConfigurationService)configServiceMock.MockInstance, + (IProjectIntegratorListFactory)projectIntegratorListFactoryMock.MockInstance, + (IProjectSerializer)projectSerializerMock.MockInstance, + extensions); + } + + /// + /// Attempts to start a new cruise server with an extension. + /// + [Test] + [ExpectedException(typeof(NullReferenceException), "Unable to find extension: ThoughtWorks.CruiseControl.UnitTests.Remote.ServerExtensionStub2,ThoughtWorks.CruiseControl.UnitTests")] + public void NewServerWithInvalidExtension() + { + // Define the extension + ExtensionConfiguration extensionConfig = new ExtensionConfiguration(); + extensionConfig.Type = "ThoughtWorks.CruiseControl.UnitTests.Remote.ServerExtensionStub2,ThoughtWorks.CruiseControl.UnitTests"; + List extensions = new List(); + extensions.Add(extensionConfig); + + // Re-initialise all the mocks so we start with a clean slate + InitialiseMocks(); + + // See if we can start a new server + CruiseServer server = new CruiseServer((IConfigurationService)configServiceMock.MockInstance, + (IProjectIntegratorListFactory)projectIntegratorListFactoryMock.MockInstance, + (IProjectSerializer)projectSerializerMock.MockInstance, + extensions); + } + } } Index: UnitTests/Remote/ServerExtensionStub.cs =================================================================== --- UnitTests/Remote/ServerExtensionStub.cs (revision 0) +++ UnitTests/Remote/ServerExtensionStub.cs (revision 0) @@ -0,0 +1,31 @@ +using System; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.UnitTests.Remote +{ + public class ServerExtensionStub + : ICruiseServerExtension + { + #region ICruiseServerExtension Members + public void Initialise(ICruiseServer server, ExtensionConfiguration extensionConfig) + { + } + + public void Start() + { + } + + public void Stop() + { + } + + public void Abort() + { + } + + public void WaitForExit() + { + } + #endregion + } +} Index: UnitTests/test.config =================================================================== --- UnitTests/test.config (revision 3982) +++ UnitTests/test.config (working copy) @@ -7,9 +7,13 @@
-
-
+
+
+
+ + + Index: UnitTests/UnitTests.csproj =================================================================== --- UnitTests/UnitTests.csproj (revision 3982) +++ UnitTests/UnitTests.csproj (working copy) @@ -336,6 +336,8 @@ Code + + Code Index: WcfExtension/CruiseControlImplementation.cs =================================================================== --- WcfExtension/CruiseControlImplementation.cs (revision 0) +++ WcfExtension/CruiseControlImplementation.cs (revision 0) @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.ServiceModel; +using ThoughtWorks.CruiseControl.Core.Util; +using ThoughtWorks.CruiseControl.Remote; +using ThoughtWorks.CruiseControl.WcfExtension.DataObjects; + +namespace ThoughtWorks.CruiseControl.WcfExtension +{ + /// + /// Implements the CruiseControl contract. + /// + [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)] + public class CruiseControlImplementation + : ICruiseControlContract + { + #region Private fields + private ICruiseServer _cruiseServer; + #endregion + + #region Constructors + /// + /// Initialises a new instance of this implementation. + /// + /// The associated server. + public CruiseControlImplementation(ICruiseServer server) + { + // Validate the input parameters + server.ValidateNotNull("Cannot pass in a null server"); + + // Store the parameters that we need for later + _cruiseServer = server; + } + #endregion + + #region Public methods + #region RetrieveVersion() + /// + /// Retrieves the current server version. + /// + /// The current version of the server. + public string RetrieveVersion() + { + Log.Debug("RetrieveVersion()"); + return _cruiseServer.GetVersion(); + } + #endregion + + #region RetrieveSnapshot() + /// + /// Retrieves a snapshot of the current status of the server. + /// + /// The snapshot of the current status. + public Snapshot RetrieveSnapshot() + { + Log.Debug("RetrieveSnapshot()"); + + // Retrieve the current snapshot and return it in a format that WCF can handle + var oSnapshot = new Snapshot(_cruiseServer.GetCruiseServerSnapshot()); + + // Add some extra details that aren't in the base snapshot + oSnapshot.ServerVersion = RetrieveVersion(); + + // Return the completed snapshot + return oSnapshot; + } + #endregion + + #region RetrieveProjects() + /// + /// Retrieves a list of all the projects configured on the server. + /// + /// The list of projects. + public List RetrieveProjects() + { + Log.Debug("RetrieveProjects()"); + + // Retrieve the current projects + var snapshot = _cruiseServer.GetCruiseServerSnapshot(); + var projectsList = (from project in snapshot.ProjectStatuses + select new Project + { + Name = project.Name + }).ToList(); + + return projectsList; + } + #endregion + + #region ForceBuild() + /// + /// Forces the build of a project. + /// + /// The name of the project to build. + /// + /// TDetail may be typed as: + /// + /// : Thrown when is invalid (null, empty or not in the list of projects). + /// + /// + public void ForceBuild(string projectName) + { + Log.Debug("ForceBuild(" + projectName + ")"); + + // Validate the input data + if (string.IsNullOrEmpty(projectName)) throw new FaultException(new ServerFault("ForceBuild", "Project name cannot be null or empty"), new FaultReason("Invalid project name")); + if (!CheckProjectExists(projectName)) throw new FaultException(new ServerFault("ForceBuild", "Project name does not exist"), new FaultReason("Invalid project name")); + + // Get the current user + string userName = string.Empty; + if (ServiceSecurityContext.Current != null) + { + IIdentity currentUser = ServiceSecurityContext.Current.PrimaryIdentity; + if (currentUser != null) userName = currentUser.Name; + } + + // Force the build + _cruiseServer.ForceBuild(projectName, userName); + } + #endregion + #endregion + + #region Private methods + #region CheckProjectExists() + /// + /// Check that the project name exists on the server. + /// + /// The project name to check for. + /// True if the project name exists, false otherwise. + private bool CheckProjectExists(string projectName) + { + Log.Debug("CheckProjectExists(" + projectName + ")"); + + // Retrieve the list of current projects + ProjectStatus[] currentProjects = _cruiseServer.GetProjectStatus(); + + // Check if the project is in the list + bool projectExists = false; + foreach (var project in currentProjects) + { + if (string.Equals(projectName, project.Name, StringComparison.InvariantCulture)) projectExists = true; + } + + // Return the result of the check + return projectExists; + } + #endregion + #endregion + } +} Index: WcfExtension/DataObjects/Project.cs =================================================================== --- WcfExtension/DataObjects/Project.cs (revision 0) +++ WcfExtension/DataObjects/Project.cs (revision 0) @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.WcfExtension.DataObjects +{ + /// + /// Defines a project. + /// + [DataContract(Namespace = "http://ccnet.thoughtworks.com/1/5/wcf")] + public class Project + { + /// + /// Starts a new blank project. + /// + public Project() { } + + /// + /// Starts a new project from a project status. + /// + /// The source project status. + public Project(ProjectStatus status) + { + Name = status.Name; + Status = status.BuildStatus.ToString(); + LastBuildTime = status.LastBuildDate; + Activity = status.Activity.ToString(); + LastBuildLabel = status.LastBuildLabel; + WebUrl = status.WebURL; + } + + /// + /// The name of the project. + /// + [DataMember] + public string Name { get; set; } + + /// + /// The status of the project. + /// + [DataMember] + public string Status { get; set; } + + /// + /// The current activity of the project. + /// + [DataMember] + public string Activity { get; set; } + + /// + /// The last build label of the project. + /// + [DataMember] + public string LastBuildLabel { get; set; } + + /// + /// The date and time this project was last built. + /// + [DataMember] + public DateTime LastBuildTime { get; set; } + + /// + /// The URL to the dashboard for this project. + /// + [DataMember] + public string WebUrl { get; set; } + } +} Index: WcfExtension/DataObjects/Queue.cs =================================================================== --- WcfExtension/DataObjects/Queue.cs (revision 0) +++ WcfExtension/DataObjects/Queue.cs (revision 0) @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.WcfExtension.DataObjects +{ + /// + /// Defines a queue. + /// + [DataContract(Namespace = "http://ccnet.thoughtworks.com/1/5/wcf")] + public class Queue + { + /// + /// Starts a new blank queue. + /// + public Queue() { } + + /// + /// Starts a new queue from a queue snapshot. + /// + /// The source snapshot. + public Queue(QueueSnapshot snapshot) + { + Name = snapshot.QueueName; + Requests = (from request in snapshot.Requests.OfType() + select new QueueRequest(request)).ToList(); + } + + /// + /// The name of the queue. + /// + [DataMember] + public string Name { get; set; } + + /// + /// The currently queued requests. + /// + [DataMember] + public List Requests { get; set; } + } +} Index: WcfExtension/DataObjects/QueueRequest.cs =================================================================== --- WcfExtension/DataObjects/QueueRequest.cs (revision 0) +++ WcfExtension/DataObjects/QueueRequest.cs (revision 0) @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.WcfExtension.DataObjects +{ + /// + /// Defines a queue. + /// + [DataContract(Namespace = "http://ccnet.thoughtworks.com/1/5/wcf")] + public class QueueRequest + { + /// + /// Starts a new blank queue. + /// + public QueueRequest() { } + + /// + /// Starts a new queue from a queue snapshot. + /// + /// The queued request. + public QueueRequest(QueuedRequestSnapshot request) + { + ProjectName = request.ProjectName; + Activity = request.Activity.ToString(); + } + + /// + /// The name of the queued project. + /// + [DataMember] + public string ProjectName { get; set; } + + /// + /// The current activity of the queued project. + /// + [DataMember] + public string Activity { get; set; } + } +} Index: WcfExtension/DataObjects/Snapshot.cs =================================================================== --- WcfExtension/DataObjects/Snapshot.cs (revision 0) +++ WcfExtension/DataObjects/Snapshot.cs (revision 0) @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.WcfExtension.DataObjects +{ + /// + /// Provides a snapshot of the current server status. + /// + [DataContract(Namespace = "http://ccnet.thoughtworks.com/1/5/wcf")] + public class Snapshot + { + /// + /// Initialise a new blank snapshot. + /// + public Snapshot() { } + + /// + /// Initialise a new snapshot from a cruise server snapshot. + /// + /// The cruise server snapshot. + public Snapshot(CruiseServerSnapshot serverSnapshot) + { + // Copy over the projects + Projects = new List(); + Projects.AddRange(from project in serverSnapshot.ProjectStatuses + select new Project(project)); + + // Copy over the queues + Queues = new List(); + foreach (QueueSnapshot queueSnapshot in serverSnapshot.QueueSetSnapshot.Queues) + { + Queues.Add(new Queue(queueSnapshot)); + } + } + + /// + /// The current list of projects. + /// + [DataMember] + public List Projects { get; set; } + + /// + /// The current list of queues. + /// + [DataMember] + public List Queues { get; set; } + + /// + /// The version of the server. + /// + [DataMember] + public string ServerVersion { get; set; } + } +} Index: WcfExtension/ICruiseControlContract.cs =================================================================== --- WcfExtension/ICruiseControlContract.cs (revision 0) +++ WcfExtension/ICruiseControlContract.cs (revision 0) @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ServiceModel; +using System.ServiceModel.Web; +using ThoughtWorks.CruiseControl.Remote; +using ThoughtWorks.CruiseControl.WcfExtension.DataObjects; + +namespace ThoughtWorks.CruiseControl.WcfExtension +{ + /// + /// The contract of operations that will be provided. + /// + [ServiceContract(Namespace = "http://ccnet.thoughtworks.com/1/5/wcf")] + public interface ICruiseControlContract + { + #region RetrieveVersion() + /// + /// Gets the current version of the server. + /// + /// The current version number of the server. + [OperationContract] + string RetrieveVersion(); + #endregion + + #region RetrieveSnapshot() + /// + /// Retrieves a snapshot of the current status of the server. + /// + /// The snapshot of the server. + [OperationContract] + [WebGet(UriTemplate="snapshot", ResponseFormat=WebMessageFormat.Xml)] + Snapshot RetrieveSnapshot(); + #endregion + + #region RetrieveProjects() + /// + /// Retrieves a list of all the projects configured on the server. + /// + /// The list of projects. + [OperationContract] + [WebGet(UriTemplate = "projects", ResponseFormat = WebMessageFormat.Xml)] + List RetrieveProjects(); + #endregion + + #region ForceBuild() + /// + /// Forces the build of a project. + /// + /// The name of the project to build. + [OperationContract] + [WebInvoke(UriTemplate="projects/{projectName}/build")] + [FaultContract(typeof(ServerFault))] + void ForceBuild(string projectName); + #endregion + } +} Index: WcfExtension/ObjectExtensions.cs =================================================================== --- WcfExtension/ObjectExtensions.cs (revision 0) +++ WcfExtension/ObjectExtensions.cs (revision 0) @@ -0,0 +1,22 @@ +using System; + +namespace ThoughtWorks.CruiseControl.WcfExtension +{ + /// + /// Provides some helper methods for working with object data. + /// + public static class ObjectExtensions + { + #region ValidateNotNull() + /// + /// Validates that the value is not null. + /// + /// The object to validate. + /// The error message to throw if the value is null. + public static void ValidateNotNull(this object value, string errorMessage) + { + if (value == null) throw new NullReferenceException(errorMessage); + } + #endregion + } +} Index: WcfExtension/Properties/AssemblyInfo.cs =================================================================== --- WcfExtension/Properties/AssemblyInfo.cs (revision 0) +++ WcfExtension/Properties/AssemblyInfo.cs (revision 0) @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WcfExtension")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("healthAlliance")] +[assembly: AssemblyProduct("WcfExtension")] +[assembly: AssemblyCopyright("Copyright © healthAlliance 2008")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("03d45135-c2c2-4727-aa8e-1dc56af828b9")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] Index: WcfExtension/ServerFault.cs =================================================================== --- WcfExtension/ServerFault.cs (revision 0) +++ WcfExtension/ServerFault.cs (revision 0) @@ -0,0 +1,39 @@ +using System; +using System.Runtime.Serialization; + +namespace ThoughtWorks.CruiseControl.WcfExtension +{ + /// + /// Defines a standard fault contract that can be passed back to the client saying why an operation failed. + /// + [DataContract(Namespace = "http://ccnet.thoughtworks.com/1/5/wcf")] + public class ServerFault + { + #region Constructors + /// + /// Starts a new fault with an operation and reason. + /// + /// The operation that failed. + /// The reason for the failure. + public ServerFault(string operation, string reason) + { + Operation = operation; + Reason = reason; + } + #endregion + + #region Public properties + /// + /// Gets the operation that failed. + /// + [DataMember] + public string Operation { get; private set; } + + /// + /// Gets the reason the operation failed. + /// + [DataMember] + public string Reason { get; private set; } + #endregion + } +} Index: WcfExtension/WcfExtension.csproj =================================================================== --- WcfExtension/WcfExtension.csproj (revision 0) +++ WcfExtension/WcfExtension.csproj (revision 0) @@ -0,0 +1,88 @@ + + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {EBB38881-14B2-48A6-9DB6-3AFDC21159F6} + Library + Properties + ThoughtWorks.CruiseControl.WcfExtension + WcfExtension + v3.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + bin\Debug\WcfExtension.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + 3.5 + + + 3.0 + + + 3.0 + + + 3.5 + + + 3.5 + + + 3.5 + + + + + + + + + + + + + + + + + + + {F8113DB9-6C47-4FD1-8A01-655FCF151747} + core + + + {E820CF3B-8C5A-4002-BC16-B7818D3D54A8} + Remote + + + + + \ No newline at end of file Index: WcfExtension/WcfServerExtension.cs =================================================================== --- WcfExtension/WcfServerExtension.cs (revision 0) +++ WcfExtension/WcfServerExtension.cs (revision 0) @@ -0,0 +1,118 @@ +using System; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Description; +using System.Runtime.Serialization; +using ThoughtWorks.CruiseControl.Core.Util; +using ThoughtWorks.CruiseControl.Remote; + +namespace ThoughtWorks.CruiseControl.WcfExtension +{ + /// + /// This class extends CruiseServer and provides a WCF interface to it. + /// + public class WcfServerExtension + : ICruiseServerExtension, IDisposable + { + #region Private fields + private ServiceHost _wcfServiceHost; + private ICruiseServer _cruiseServer; + #endregion + + #region Public properties + #region IsRunning + /// + /// Gets whether the service is currently listening for requests. + /// + public bool IsRunning + { + get { return (_wcfServiceHost.State == CommunicationState.Opened); } + } + #endregion + #endregion + + #region Public methods + #region Initialise() + /// + /// Initialises the service host to use. + /// + /// The CruiseServer that is initialising this extension. + /// The configuration for the extension. + public void Initialise(ICruiseServer server, ExtensionConfiguration extensionConfig) + { + // Validate the input parameters + server.ValidateNotNull("Cannot pass in a null server"); + + // Store the parameters that we need for later + _cruiseServer = server; + + // Create a new service host + _wcfServiceHost = new ServiceHost(new CruiseControlImplementation(_cruiseServer)); + } + #endregion + + #region Start() + /// + /// Starts listening for WCF requests. + /// + public void Start() + { + // Start the service host + if ((_wcfServiceHost.State != CommunicationState.Opened) && + (_wcfServiceHost.State != CommunicationState.Opening)) + { + Log.Info("Opening service host"); + _wcfServiceHost.Open(); + Log.Debug("Service host opened"); + } + } + #endregion + + #region Stop() + /// + /// Stops listening for WCF requests. + /// + public void Stop() + { + // Stop the service host without waiting + if ((_wcfServiceHost.State != CommunicationState.Closed) && + (_wcfServiceHost.State != CommunicationState.Closing) && + (_wcfServiceHost.State != CommunicationState.Faulted)) + { + Log.Info("Closing service host"); + _wcfServiceHost.Close(); + Log.Debug("Service host closed"); + } + } + #endregion + + #region Abort() + /// + /// Stops listening for WCF requests. + /// + public void Abort() + { + // Stop the service host and wait for it to close + if ((_wcfServiceHost.State != CommunicationState.Closed) && + (_wcfServiceHost.State != CommunicationState.Closing) && + (_wcfServiceHost.State != CommunicationState.Faulted)) + { + Log.Info("Aborting service host"); + _wcfServiceHost.Abort(); + Log.Debug("Service host aborted"); + } + } + #endregion + + #region Dispose() + /// + /// Make sure everything is closed. + /// + public void Dispose() + { + Abort(); + } + #endregion + #endregion + } +} Index: WcfExtensionUnitTests/CruiseControlImplementationTests.cs =================================================================== --- WcfExtensionUnitTests/CruiseControlImplementationTests.cs (revision 0) +++ WcfExtensionUnitTests/CruiseControlImplementationTests.cs (revision 0) @@ -0,0 +1,90 @@ +using NMock; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.ServiceModel; +using System.Text; +using ThoughtWorks.CruiseControl.Remote; +using ThoughtWorks.CruiseControl.WcfExtension; +using ThoughtWorks.CruiseControl.WcfExtension.DataObjects; + +namespace ThoughtWorks.CruiseControl.WcfExtension.UnitTests +{ + [TestFixture] + public class CruiseControlImplementationTests + { + #region Private fields + private CruiseControlImplementation _implementation; + private DynamicMock _cruiseServerMock; + private ProjectStatus[] _projectsList = new ProjectStatus[]{ + new ProjectStatus("Test Project", IntegrationStatus.Unknown, DateTime.MinValue) + }; + #endregion + + #region Fixture methods + [SetUp] + public void Initialise() + { + _cruiseServerMock = new DynamicMock(typeof(ICruiseServer)); + _implementation = new CruiseControlImplementation((ICruiseServer)_cruiseServerMock.MockInstance); + } + + [TearDown] + public void CleanUp() + { + } + #endregion + + #region Tests + [Test] + [ExpectedException(typeof(NullReferenceException), ExpectedMessage = "Cannot pass in a null server")] + public void Create_WithoutServer() + { + var testValue = new CruiseControlImplementation(null); + } + + [Test] + public void RetrieveVersion() + { + _cruiseServerMock.ExpectAndReturn("GetVersion", "Test Version"); + var result = _implementation.RetrieveVersion(); + _cruiseServerMock.Verify(); + Assert.AreEqual("Test Version", result); + } + + [Test] + public void RetrieveSnapshot() + { + var expected = new CruiseServerSnapshot(); + _cruiseServerMock.ExpectAndReturn("GetCruiseServerSnapshot", expected); + var result = _implementation.RetrieveSnapshot(); + _cruiseServerMock.Verify(); + Assert.IsInstanceOfType(typeof(Snapshot), result); + } + + [Test] + public void ForceBuild_Valid() + { + _cruiseServerMock.ExpectAndReturn("GetProjectStatus", _projectsList); + _cruiseServerMock.Expect("ForceBuild", "Test Project", string.Empty); + _implementation.ForceBuild("Test Project"); + _cruiseServerMock.Verify(); + } + + [Test] + [ExpectedException(typeof(FaultException), ExpectedMessage="Invalid project name")] + public void ForceBuild_NoProjectName() + { + _implementation.ForceBuild(string.Empty); + } + + [Test] + [ExpectedException(typeof(FaultException), ExpectedMessage = "Invalid project name")] + public void ForceBuild_InvalidProjectName() + { + _cruiseServerMock.SetupResult("GetProjectStatus", _projectsList); + } + #endregion + } +} Index: WcfExtensionUnitTests/ObjectExtensionsTests.cs =================================================================== --- WcfExtensionUnitTests/ObjectExtensionsTests.cs (revision 0) +++ WcfExtensionUnitTests/ObjectExtensionsTests.cs (revision 0) @@ -0,0 +1,26 @@ +using System; +using NUnit.Framework; + +namespace ThoughtWorks.CruiseControl.WcfExtension.UnitTests +{ + [TestFixture] + public class ObjectExtensionsTests + { + #region Tests + [Test] + [ExpectedException(typeof(NullReferenceException), ExpectedMessage="Testing")] + public void ValidateNotNull_NullValue() + { + object testValue = null; + testValue.ValidateNotNull("Testing"); + } + + [Test] + public void ValidateNotNull_NonNullValue() + { + object testValue = "Testing"; + testValue.ValidateNotNull("Testing"); + } + #endregion + } +} Index: WcfExtensionUnitTests/Properties/AssemblyInfo.cs =================================================================== --- WcfExtensionUnitTests/Properties/AssemblyInfo.cs (revision 0) +++ WcfExtensionUnitTests/Properties/AssemblyInfo.cs (revision 0) @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WcfExtensionUnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("healthAlliance")] +[assembly: AssemblyProduct("WcfExtensionUnitTests")] +[assembly: AssemblyCopyright("Copyright © healthAlliance 2008")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("35b873cb-a7d8-4c71-b295-a6b36be30547")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] Index: WcfExtensionUnitTests/test.config =================================================================== --- WcfExtensionUnitTests/test.config (revision 0) +++ WcfExtensionUnitTests/test.config (revision 0) @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file Index: WcfExtensionUnitTests/WcfExtensionUnitTests.csproj =================================================================== --- WcfExtensionUnitTests/WcfExtensionUnitTests.csproj (revision 0) +++ WcfExtensionUnitTests/WcfExtensionUnitTests.csproj (revision 0) @@ -0,0 +1,92 @@ + + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {3361E47A-0ABC-491B-A4D4-08FFCCDA5FEB} + Library + Properties + ThoughtWorks.CruiseControl.WcfExtension.UnitTests + WcfExtensionUnitTests + v3.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\lib\nmock.dll + + + False + ..\..\tools\nunit\nunit.framework.dll + + + + 3.5 + + + 3.0 + + + 3.5 + + + 3.5 + + + + + + + + + + + + + {E820CF3B-8C5A-4002-BC16-B7818D3D54A8} + Remote + + + {EBB38881-14B2-48A6-9DB6-3AFDC21159F6} + WcfExtension + + + + + + + + + + + + + + + \ No newline at end of file Index: WcfExtensionUnitTests/WcfServerExtensionTests.cs =================================================================== --- WcfExtensionUnitTests/WcfServerExtensionTests.cs (revision 0) +++ WcfExtensionUnitTests/WcfServerExtensionTests.cs (revision 0) @@ -0,0 +1,68 @@ +using NMock; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ThoughtWorks.CruiseControl.Remote; +using ThoughtWorks.CruiseControl.WcfExtension; + +namespace ThoughtWorks.CruiseControl.WcfExtension.UnitTests +{ + [TestFixture] + public class WcfServerExtensionTests + { + #region Private fields + private WcfServerExtension _extension = new WcfServerExtension(); + private DynamicMock _cruiseServerMock; + #endregion + + #region Fixture methods + [SetUp] + public void Initialise() + { + _cruiseServerMock = new DynamicMock(typeof(ICruiseServer)); + _extension.Initialise((ICruiseServer)_cruiseServerMock.MockInstance, null); + } + + [TearDown] + public void CleanUp() + { + _extension.Dispose(); + } + #endregion + + #region Tests + [Test] + [ExpectedException(typeof(NullReferenceException), ExpectedMessage = "Cannot pass in a null server")] + public void Initialise_WithoutServer() + { + var testValue = new WcfServerExtension(); + testValue.Initialise(null, null); + } + + [Test] + public void Start() + { + _extension.Start(); + Assert.IsTrue(_extension.IsRunning); + } + + [Test] + public void Stop() + { + _extension.Start(); + _extension.Stop(); + Assert.IsFalse(_extension.IsRunning); + } + + [Test] + public void Abort() + { + _extension.Start(); + _extension.Abort(); + Assert.IsFalse(_extension.IsRunning); + } + #endregion + } +}