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>
/// <scm servicename="World Wide Web Publishing Service" operation="stop"
/>
/// <scm servicename="World Wide Web Publishing Service" operation="start"
timeout="10"/>
/// </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