I've started writing a tutorial on creating custom tasks, I thought I'd throw the 
first draft out for comments.  I've attached my draft as well as a copy of the custom 
task that I'm using as the basis for the tutorial.   I'd appreciate any feedback 
people have.  The early parts of the article are mostly complete, but the later parts 
are still in outline form.
using System;
using System.ServiceProcess;
using SourceForge.NAnt;
using SourceForge.NAnt.Tasks;
using SourceForge.NAnt.Attributes;

namespace Galileo.NAnt.Tasks
{
        /// <summary>
        /// Task for controlling Windows Services via the Service Control Manager.  
This task allows you
        /// to start, stop, pause, and continue services.
        /// </summary>
        /// <example>
        /// &lt;scm servicename="World Wide Web Publishing Service" operation="stop" 
/&gt;
        /// &lt;scm servicename="World Wide Web Publishing Service" operation="start" 
timeout="10"/&gt;
        /// </example>
        [TaskName("scm")]
        public class SCMTask : Task
        {
                public SCMTask()
                {
                        _machineName = Environment.MachineName;
                        _timeout = 10; 
                }

                private string _machineName;

#if false
                private string _arguments;
                /// <summary>
                /// Arguments to pass to the Start function.  Arguments should be 
separated by commas.
                /// </summary>
                [TaskAttribute("servicename",Required=false)]
                public string Arguments
                {
                        get { return _arguments; }
                        set { _arguments = value; }
                }
#endif

                private string _serviceName;
                /// <summary>
                /// The short name that identifies the service to the system.
                /// </summary>
                [TaskAttribute("servicename",Required=true)]
                public string ServiceName
                {
                        get { return _serviceName; }
                        set { _serviceName = value; }
                }

                private string _operation;
                /// <summary>
                /// Either start, stop, pause, or continue
                /// </summary>
                [TaskAttribute("operation",Required=true)]
                public string Operation
                {
                        get { return _operation; }
                        set { _operation = value; }
                }

                private int _timeout;
                /// <summary>
                /// Time, in seconds, that this task will wait for the operation to 
complete before 
                /// failing.
                /// </summary>
                [TaskAttribute("timeout",Required=false)]
        [Int32Validator(0,Int32.MaxValue)]
        public int Timeout
                {
                        get { return _timeout; }
                        set { _timeout = value; }
                }

                /// <summary>
                /// Executes the specified operation on the SCM for the specified 
service.
                /// </summary>
                protected override void ExecuteTask() 
                {
                        try
                        {
                                using (ServiceController scm = new 
ServiceController(ServiceName,_machineName))
                                {
                                        ServiceControllerStatus desiredStatus = 
ServiceControllerStatus.Running;
                                        if (Operation.Equals("start"))
                                        {
                                                desiredStatus = 
ServiceControllerStatus.Running;
                                                if (scm.Status != 
ServiceControllerStatus.Running)
                                                {
                                                        scm.Start();
                                                }
                                                else
                                                {
                                                        Log.WriteLine("{0}Service {1} 
is already running", 
                                                                LogPrefix, 
ServiceName);
                                                }
                                        }
                                        else if (Operation.Equals("stop"))
                                        {
                                                desiredStatus = 
ServiceControllerStatus.Stopped;
                                                if (scm.Status == 
ServiceControllerStatus.Stopped)
                                                {
                                                        Log.WriteLine("{0}Service {1} 
is already stopped", 
                                                                LogPrefix, 
ServiceName);
                                                }
                                                else if (scm.CanStop)
                                                {
                                                        scm.Stop();
                                                }
                                                else 
                                                {
                                                        throw new 
BuildException(String.Format("Service {0} cannot be stopped",ServiceName));
                                                }
                                        }
                                        else if (Operation.Equals("pause"))
                                        {
                                                desiredStatus = 
ServiceControllerStatus.Paused;
                                                if (scm.Status == 
ServiceControllerStatus.Paused)
                                                {
                                                        Log.WriteLine("{0}Service {1} 
is already paused", 
                                                                LogPrefix, 
ServiceName);
                                                }
                                                else if (scm.CanPauseAndContinue)
                                                {
                                                        scm.Pause();
                                                }
                                                else 
                                                {
                                                        Log.WriteLine("{0}Service {1} 
cannot perform pause and continue", 
                                                                LogPrefix, 
ServiceName, Operation);
                                                }
                                        }
                                        else if (Operation.Equals("continue"))
                                        {
                                                desiredStatus = 
ServiceControllerStatus.Running;
                                                if (scm.Status != 
ServiceControllerStatus.Paused)
                                                {
                                                        Log.WriteLine("{0}Service {1} 
is not paused", 
                                                                LogPrefix, 
ServiceName);
                                                }
                                                else if (scm.CanPauseAndContinue)
                                                {
                                                        scm.Continue();                
                                 
                                                }
                                                else
                                                {
                                                        Log.WriteLine("{0}Service {1} 
cannot perform pause and continue", 
                                                                LogPrefix, 
ServiceName);
                                                }
                                        }
                                        else
                                        {
                                                throw new 
BuildException(String.Format("Invalid operation {0}",Operation));
                                        }

                                        // this will throw a TimeoutException if the 
timeout expires.
                                        scm.WaitForStatus(desiredStatus,new 
TimeSpan(0,0,0,Timeout));

                                        Log.WriteLine("{0}Successfully completed {1} 
operation on service {2}", 
                                                LogPrefix, Operation, ServiceName);
                                }
                        }
                        catch (Exception e)
                        {
                                Log.WriteLine("{0}Error \"{2}\" performing operation 
{1} on service {3}", 
                                        LogPrefix, Operation,e.Message, ServiceName);
                                throw new 
SourceForge.NAnt.BuildException(String.Format("Unable to execute task {0} on service 
{1}: {2}",Name,ServiceName,e.Message),e);
                        }
                }
        }
}
        Writing a custom NAnt task

        Intro
        Once you've started to work with NAnt a bit, you'll want to do something in 
your build 
        that the NAnt development team hadn't thought of.  There's a number of ways to 
take care 
        of that problem: you can use the <exec> task to spawn another program, you can 
move that 
        function out to a driver file (a batch file or shell script), or you can write 
a new task 
        for NAnt. 

        Before you start on your task, you should first check the NAntContrib project 
to see if there's 
        already a task out there that matches your need.  You might also check on the 
NAnt-users or NAntContrib 
        mailing lists to see if someone else already has a task that matches your need 
- you might save 
        yourself some work.  


        Basic Task Concepts

        Before you start on your task, lets look at some basics of NAnt tasks.  In 
general, a task is a 
        class that extends the SourceForge.NAnt.Task base class.  A task is 
represented in a NAnt script by 
        an XML element, and expose attributes which allow the script's author to 
provide parameters to the task. 
        For example, the <copy> task can take file and tofile attributes:

                <copy file="myfile.txt" tofile="mycopy.txt"/>

        Generally, a task expects its input through its attributes, but that doesn't 
always work well.  If a task 
        can operate on multiple files, it's convenient to allow the task to operate on 
file sets, like this:

                <copy todir="temp">
                        <fileset basedir="bin">
                                <includes name="*.dll"/>
                        </fileset>
                </copy>

        NAnt also has the concept of an "option set", which allows one to pass in 
options to the task as child 
        elements containing name and value attributes:

                <slingshot solution="MySolution.sln" format="nant" 
output="MySolution.build">
                        <parameters>
                                <option name="build.basedir" value="..\bin"/>
                        </parameters> 
                </slingshot>

        Finally, a task can use a property from the NAnt project for input.  Usually, 
properties are passed as 
        attributes to the task or its child elements, but you could also use a well 
known property from the 
        project as a sort of global variable to control a task.

        Tasks produce output in a couple ways.  The most obvious is in the form of a 
file: the <csc>, <vbc>, and 
        <jsc> tasks produce object and executable code from source, the <style> task 
produces a file based on an 
        XSLT transformation, etc.  However, some tasks produce output through 
properties.  This allows for tasks 
        that return information about the environment, most notably, the <sysinfo> and 
<readregistry> tasks.  Tasks 
        can also use an "up to date" concept to control whether they do anything at 
all; for instance, the <copy> 
        task won't copy older files on top of newer files (though the overwrite 
attribute can override this behavior).  


        Writing the task

        Now I'll get into the mechanics of writing a custom task.  I'll be using an 
example task called the "SCM 
        task".  What this does is control a windows service using the Service Control 
Manager.  This task will allow 
        you to stop, start, pause, and continue a Windows Service from NAnt.  

        First of all, when writing your own task, it's a good idea to use the existing 
NAnt tasks for reference, the source code for NAnt is located in the src 
        subdirectory of your nant base directory.  Your task class needs to extend 
SourceForge.NAnt.Task, but there 
        are several base classes for NAnt tasks besides SourceForge.NAnt.Task:

                SourceForge.NAnt.Tasks.ExternalProgramBase - for tasks that execute 
external applications.  
                SourceForge.NAnt.Tasks.CompilerBase - For Tasks that compile code
                SourceForge.NAnt.Tasks.MsftFXSDKExternalProgramBase - similar to 
ExternalProgramBase, but looks
                        in the .NET framework's bin directory for the program, rather 
than relying on the user's path.
                SourceForge.NAnt.Tasks.MsftFXCompilerBase - similar to CompilerBase, 
but looks
                        in the .NET framework's bin directory for the program, rather 
than relying on the user's path.

                SourceForge.NAnt.Tasks.TaskContainer - [need to investigate how this 
works]

        If your task fits the model for one of these types of tasks, you might 
consider using one of these as your 
        base class.  The design of these tasks is pretty different than tasks based on 
the base Task class, so 
        I'll leave that for another article.  [Maybe I should cover a task using 
ExternalProgramBase?]  While I could 
        base this specific example on ExternalProgramBase using the NET START / NET 
STOP commands, the .NET 
        framework provides classes to control services from code, so I'll go that 
route, as it offers better 
        control mechanisms.

        The simple outline of our task looks like this:
        namespace NAntContrib.Tasks {
                [TaskName("scm")]
                public class SCMTask : Task {
                        public SCMTask() {
                                _timeout = 10; 
                        }

                        private string _serviceName;
                        [TaskAttribute("servicename",Required=true)]
                        public string ServiceName {
                                get { return _serviceName; }
                                set { _serviceName = value; }
                        }

                        private string _operation;
                        [TaskAttribute("operation",Required=true)]
                        public string Operation {
                                get { return _operation; }
                                set { _operation = value; }
                        }

                        private int _timeout;
                        [TaskAttribute("timeout",Required=false)]
                        [Int32Validator(0,Int32.MaxValue)]
                        public string Timeout {
                                get { return _timeout; }
                                set { _timeout = value; }
                        }

                        protected override void ExecuteTask() {
                        }

                }
        }

        You'll note a couple of things here.  First of all, note how the 
TaskNameAttribute is applied to the 
        class SCMTask.  This tells NAnt to allow us to call this task in a script 
using the element <scm>.  Second, 
        notice the TaskAttributeAttribute is applied to each property.  This tells 
NAnt what the names of the 
        attributes to the scm task are, whether they're required or not, and what 
property on the SCMTask class 
        they map to.  So when you invoke the task like this:

                <scm servicename="World Wide Web Publishing Service" operation="stop" 
timeout="90"/>

        The Timeout property will be set to 90, the ServiceName property will be 
"World Wide Web Publishing 
        Service", and the Operation property will be set to "stop".  You should be 
able to see that the timeout attribute is optional, but 
        the other attributes must be specified in the build file.  Also, notice how 
the timeout property 
        has the Int32Validator attribute applied.  This tells NAnt to parse this 
attribute as an integer 
        and ensure that its value is >= 0 and <= Int32.MaxValue.  Finally, note the 
ExecuteTask method.  
        This is the method that NAnt will call when it's time to run this task, so the 
meat of your 
        implementation goes here.  Let's continue by writing the method:

                protected override void ExecuteTask() 
                {
                        try
                        {
                                using (ServiceController scm = new 
ServiceController(ServiceName))
                                {
                                        ServiceControllerStatus desiredStatus = 
ServiceControllerStatus.Running;
                                        if (Operation.Equals("start"))
                                        {
                                                scm.Start();
                                        }
                                        else if (Operation.Equals("stop"))
                                        {
                                                desiredStatus = 
ServiceControllerStatus.Stopped;
                                                scm.Stop();
                                        }
                                        else if (Operation.Equals("pause"))
                                        {
                                                desiredStatus = 
ServiceControllerStatus.Paused;
                                                scm.Pause();
                                        }
                                        else if (Operation.Equals("continue"))
                                        {
                                                scm.Continue();                        
                         
                                        }
                                        else
                                        {
                                                throw new 
BuildException(String.Format("Invalid operation {0}",Operation));
                                        }

                                        scm.WaitForStatus(desiredStatus,new 
TimeSpan(0,0,0,Int32.Parse(Timeout)));
                                }
                        }
                        catch (Exception e)
                        {
                                throw new BuildException(String.Format("Unable to 
execute task {0} on service {1}: {2}",Name,ServiceName,e.Message),e);
                        }
                }

This method works by looking at the Operation property and deciding which method on 
the 
ServiceController object to call.  At this point, it also determines what the status 
of the 
ServiceController should be after the call completes.  After calling the right method, 
ExecuteTask calls 
ServiceController.WaitForStatus(), passing the desiredStatus and the timeout 
specified.  This method 
will throw a TimeoutException if the timeout expires before the service reaches the 
desired 
status; this indicates a failure.  At each point where a failure is detected, 
ExecuteTask will 
throw a SourceForge.NAnt.BuildException describing what happened.  BuildExceptions are 
caught by 
NAnt and echoed to the build log, they also cause the build to fail.  

One thing that you should be aware of is that NAnt doesn't expand properties by 
default.  What this 
means is that if in a script, you call this task using a property for, say, the 
timeout attribute,
the Timeout property will be set to ${timeout}, which isn't what you want. [? 
Question: would the 
Int32 validator choke on this?  Need to check. ?]

        <scm servicename="World Wide Web Publishing Service" operation="stop" 
timeout="${timeout}"/>

The     way to fix this is to use Project.ExpandProperties to expand the value of 
timeout, like this:
        Project.ExpandProperties(Timeout)
        
        * Up to date - explain the concept, in this case, it applies when the service 
is already
                in its desiredStatus (i.e. operation==Start, and scm.Status==Running)
        * Attributes 
                * Validators - demonstrate a custom validator?  Maybe change the task 
to use desiredStatus and
                a validator that checks that the value passed parses as a 
ServiceControllerStatus value.  ie, 
                you'd pass desiredStatus="running" and the task would either start or 
continue the service
                depending on the current status.

 * Installing your task
It's pretty easy to install custom tasks.  All you have to do is compile you task into 
an dll assembly 
ending in the name "Tasks" (e.g. MyTasks.dll), and place you assembly in the same 
directory as nant.exe 
or in a subdirectory named tasks.  

 * Unit Tests
        * Thru nant itself      
        * Automate via NUnit and a self test 

Troubleshooting
 * Why won't it load?
        * Make sure the assembly is named correctly (xxxxTasks.dll) and in the right 
directory
        * Make sure you've applied the TaskNameAttribute to the class definition.

 * Debugging in VS.NET

* Logging code
It's helpful when reading build output to see at least what the task did: whether it 
succeeded, or failed, 
or didn't execute because it didn't need to do anything.  You can write to the log 
like this:

        Log.WriteLine("{0}Successfully completed {1} operation on service {2}", 
                LogPrefix, Operation, ServiceName);

The LogPrefix property is defined in the Task base class.  One attribute that every 
task exposes, via the Task base class, is the verbose attribute.  You can turn 
this on for debugging purposes.  Of course, the attribute doesn't do much good unless 
your code takes advantage 
of it, so you should think about adding logging code to dump out extra debug 
information, like this:

        if (this.Verbose)
        {
                Log.WriteLine("{0}Starting task execution", LogPrefix);
        }

Integrating 
        * Give back to the community
                * NAnt - for core tasks 
                * NAntContrib - for platform specific things
        * Code guidelines - see 
http://sourceforge.net/docman/display_doc.php?docid=6080&group_id=31650



Reply via email to