The other day I received an intersting comment on my previous blog enty. The comment was asking how to send the details of a build error via email to a recipient(s). When I was starting with MSBuild I was also wondering how to do this, so I would like to address this topic now. This is a very useful scenario to implement if you builds are automated. If you are lucky enough to be using Team Foundation Server then you job is a lot easier because this is taken care of for you. But if you are like most .NET developers odd are that you aren't. So we need a good way to do this.
There is an SmtpTask that you can use to send an email through an MSBuild task. At first thought you might think that this will work, but you'll be saddended to find out that it doesn't. This is because the only way to trigger this task to be executed is from the project file. The only way to achieve that is by using the MSBuild OnError element to state what to do in the case of an error occuring during the course of a target executing. When you build a VS project you are most likely to execute the Build (or Rebuild which calls Build) target. This target actually perfroms no work itself, it is delegated to other targets. So having and OnError element in that target is of no use. You'd have to place one in each of the other targets that get executed from its dependencies. This means sticking that element in a lot of different places, which is no fun. So there is no way to say "If an error occurs at any time during my build execute this target". But if we change that phrase to "If an error occurs anywhere during my build then do ...", we can achive that. We do that by writing and using a custom logger!
Writing a custom logger may sound to be daunting at first, but rest assured it is not! Once you write one logger you can write many, and I'll show you how to write your first one right here! Let's cover some basics first. MSBuild ships with 2 loggers, the ConsoleLogger and the FileLogger. These classes are a part of the Microsoft.Build.BuildEngine namespace, and you can extend them. So this means that we can use the already written functionality and extend it to fit our needs. This is exactly what we are going to do in this example. We will extend the FileLogger to email the log file if an error has occured. Let's get started on this.
First create a new C# (or other managed lang of you choice) Class Library project. I named this project EmailLogger. You'll first want to add references to the following libraries.
- Microsoft.Build.BuildEngine
- Microsoft.Build.Utilities
- Microsoft.Build.Framework
At this point we are ready to start coding. We want to make the EmailLogger class a sub-class of the FileLogger to get its behavior. Here are some logger basics now. The logging mechanism in MSBuild is an event based system. Events include such things as ProjectStarted, BuildFinished, ErrorRaised and others. We're obviously very interested in the last one. The method
public void Initialize(Microsoft.Build.Framework.IEventSource eventSource)
will be called before MSBuild starts to build your project. In this method is where you tell MSBuild what events you are interested in handling. In our case we will be handling only two events. Those events are; ProjectStarted and ErrorRaised. We handle the ProjectStarted because we need to grab all of the properties defined in the project file and put them somewhere. And we handle ErrorRaised to make a flag that an error has occured. Along with Initilize() we are promised that
public override void Shutdown()
will be called after the build has finished and its time for the logger to say goodnight. This is where we will actually send the email. Let's have a look at the Initalize method shown below.
///
/// This is guarenteed to be called by MSBuild before any build steps occur
///
///
public override void Initialize(Microsoft.Build.Framework.IEventSource eventSource)
{
//This call will set the correct values from the parameters
base.Initialize(eventSource);
this.SetParameters(base.Parameters);
//add the logger delegates here
eventSource.ErrorRaised += new BuildErrorEventHandler(this.BuildError);
eventSource.ProjectStarted += new ProjectStartedEventHandler(this.ProjectStarted);
}
In this method we aren't doing anything fancy, we call SetParameters because we need to get the file location where the log is going to be saved to. When you are writing loggers remember it is upto you to parse and validate any parameters that are passed to it. If you are using the Logger class as your base class then the Verbosity is handled for you. You should chose this over implementing the ILogger interface directly. Anywayz back to the topic at hand, besides that we are registering our delegates to the appropriate events. After this the build starts, our ProjectStarted delegate gets invoked. In this method we are storing all the properties defined in the build file to a dictionary so we know how to send the email.
This logger works like this, it will gather the settings for sending the email from the project's properties. These can either be defined in the project file itself, or passed in as command line arguments to msbuild.exe using the /p switch. The properties that it will look for are named:
EmailLogger_SmtpHost
EmailLogger_From
EmailLogger_To
EmalLogger_CC
EmailLogger_SmtpPort
EmailLogger_TimeOut
EmailLogger_SendOnSuccess
The alternative to this approach which would actually be more correct is to send all of these in a parameters to the logger. But that would be really annoying to have to type all of these values in at the command line each time you want to perform a build. Even putting these into an msbuild.rsp file is not as convienent. So I figured to put them into the project is not such a bad idea.
The ProjectStarted handler is shown below.
///
/// The event that is raised when the project is started.
/// This will examine all of the properties defined in the project file
/// and store them in the properties dictionary
///
///
///
void ProjectStarted(object sender, ProjectStartedEventArgs e)
{
if (e.Properties != null)
{
foreach (DictionaryEntry entry in e.Properties)
{
this.properties.Add(entry.Key.ToString(), entry.Value.ToString());
}
}
}
The BuildError method is very simple, it is shown below.
///
/// This is called in the event of an error.
/// In this example we just mark that there was an error and the email is sent
/// at the end of the build in the Shutdown method.
///
///
///
void BuildError(object sender, BuildErrorEventArgs e)
{
this.errorOccurred = true;
}
Besides this we are pretty much just left with the Shutdown() method. It is shown below.
///
/// This is called before msbuild exits.
/// This is the method that will actually send the email out if there was an error
/// or if send on success was specified as a property to 'true'
///
public override void Shutdown()
{
//this should close down the log file
base.Shutdown();
//get the values for the email from the properties
if (this.properties == null || this.properties.Count <= 0)
{
return;
}
Emailer emailer = new Emailer();
emailer.SmtpHost = this.GetProperty(EmailLoggerStrings.SmtpHostString);
emailer.From = this.GetProperty(EmailLoggerStrings.FromString);
emailer.To = this.GetProperty(EmailLoggerStrings.ToStr);
emailer.Cc = this.GetProperty(EmailLoggerStrings.CcString);
emailer.SetSmtpPort(this.GetProperty(EmailLoggerStrings.SmtpPortString));
emailer.SetTimeout(this.GetProperty(EmailLoggerStrings.TimeoutString));
bool emailOnSuccess = false;
string emailOnSuccessStr = this.properties[EmailLoggerStrings.EmailOnSuccessString];
if (!string.IsNullOrEmpty(emailOnSuccessStr))
{
emailOnSuccess = bool.Parse(emailOnSuccessStr);
}
if ( errorOccurred || emailOnSuccess )
{
//we need to send to email the log file to recipient here
emailer.Subject = "Build log " + DateTime.Now.ToShortDateString();
emailer.Body = "Log file attached";
emailer.Attachment = this.logFile;
emailer.SendEmail();
}
}
In this method we let the base (FileLogger) shutdown first. This will cause the logfile to be written out to disk, then we create the email and send if if there was an error, or if it was stated to send even on success. This is pretty much it from an MSBuild perspective. We have the Emailer class, but there is no MSBuild stuff there, just stuff to send the email.
Now we need to know how to use this logger. You are going to want to place the dll, in my case it's named EmailLogger.dll in a location that is accessible by other projects during the build process. In my case I have a SharedLoggers directory that contains these guys. So I copied it there. Then at the command line when we perform a build we need to tell MSBuild to use this logger. Do that by:
>msbuild.exe
PROJECT_FILE_NAME /l:EmailLogger,(PATH_TO_LOGGER)\EmailLogger.dll;logfile=test.log;verbosity=detailed
And you'll have to specify those above properties in your project file that you are building like so:
<PropertyGroup>
<EmailLogger_SmtpHost>fill this inEmailLogger_SmtpHost>
<EmailLogger_From>sayed.hashimi@gmail.comEmailLogger_From>
<EmailLogger_To> sayed.hashimi@gmail.com EmailLogger_To>
<EmalLogger_CC>EmalLogger_CC>
<EmailLogger_SendOnSuccess>trueEmailLogger_SendOnSuccess>
PropertyGroup>
Then start your build. If you are building a solution, then these properties have to be defined in a project file that is being built, or at the command line. If you have multiple definitions this could get confusing from this implementation. Maybe more on that at a later date, too late now. That should be about all you need to know. The files are made available here for your convienence. Oh yeah, this hasn't been throughly tested, I just put this together so use it at your own risk. If you have modifications I'd appreciate you letting me know so I can fix my version as well.
EmailLoggerProjectFiles.zip (23.26 KB) You may have to right-click Save as to get this file.
EmailLogger.dll (20 KB) You may have to right-click Save as to get this file
Sayed Ibrahim Hashimi
Sorry, I'm still a beginner at this, but I was wondering where the dll file is located that I am to move. I couldn't find it in the .zip attachment at all.
It wasn't posted actually, I just posted the entire solution, you could build the dll from that. But I have posted the dll now as well. You can get to it at: http://www.sedodream.com/content/binary/EmailLogger.dll
Let me know if this logger fits your needs.
Sayed Ibrahim Hashimi
Here is my command line arguments:
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild "C:\Builds\2\Quote\UAT\BuildType\TFSBuild.proj" /m:1 "@C:\Builds\2\Quote\UAT\BuildType\TfsBuild.rsp" /nologo /logger:EmailLogger,C:\BuildTools\Loggers\MSBuildLoggers\EmailLogger\bin\Debug\EmailLogger.dll
The application builds fine, but it looks like my custom logger is doing nothing! I have tried all sorts of things from Console.WriteLines or even trying to write to a file using StreamWriter, I've even tried to throw an exception inside this logger but nothing happens. The project file just builds. I've tried this in differant methods in the logger from the constructer to Initialize to Shutdown but it just seems like it's completely ignoring my logger. In the command line I tried compiling it changing little things like EmailLogger2.dll and it immediately crashes out saying it can't find the logger so I know it is at least successfully finding the dll when I type it correctly.
What have I missed, I am running out of ideas and time!
Comments are closed.