Update SharePoint Timer Job’s progress bar – the easy way

Every time I develop a timer job, I always add a progress bar. Well, if the job runs long enough that is. It adds that bit more refinement to the whole project and it’s pleasant to see your job crunching through the numbers.

For this example I’ve set up a standard timer job project:

timer job project structure

  • A Web Application scoped feature which installs the timer job
  • A static class ProgressUpdater which will handle the timer job’s progress updating
  • A class service which will do something
  • A class TimerJob which is our core job definition

I’ve chosen to add a class service to demonstrate the update process from outside the JobDefinition scope.

For the timer job I have the following code:

using Microsoft.SharePoint.Administration;
using System;

namespace SharePointTimerJob
{
    public class TimerJob : SPJobDefinition
    {
        private const string JOBNAME = "Custom Timer Job";

        public TimerJob()
            : base()
        {
        }

        public TimerJob(SPWebApplication webApplication)

            : base(JOBNAME, webApplication, null, SPJobLockType.Job)
        {
            this.Title = JOBNAME;
        }

        public TimerJob(string jobName, SPService service, SPServer server, SPJobLockType targetType)

            : base(jobName, service, server, targetType)
        {
        }

        public static string JobName
        {
            get { return JOBNAME; }
        }

        public override void Execute(Guid contentDbId)
        {
            ProgressUpdater.Job = this;

            var service = new Service();
            service.Process();
        }
    }
}

As you can see it’s pretty straight forward. One note here is the ProgressUpdater.Job = this; line. This sets the Job property of the ProgressUpdater class to the executing instance.

The next piece of code is the ProgressUpdater:

namespace SharePointTimerJob
{
    public static class ProgressUpdater
    {
        public static TimerJob Job
        {
            get;
            set;
        }

        public static void Update(int percentage)
        {
            if (Job == null)
            {
                // throw or return
                return;
            }

            if (percentage >= 0 && percentage <= 100)
                Job.UpdateProgress(percentage);
        }
    }
}

And the code of the service to trigger an Update call:

using System.Threading;

namespace SharePointTimerJob
{
    public class Service
    {
        public void Process()
        {
            int[] collection = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            int count = 0;
            foreach (var item in collection)
            {
                Thread.Sleep(5000);
                count++;
                ProgressUpdater.Update((count * 100) / collection.Length);
            }
        }
    }
}

All this does is sleep the thread and update afterwards, but you get the idea.

When you run the timer job you’ll see that the progress updates nicely in ticks of 10% because of the collection, which is 10 items:

progress 10

progress 40

Deep dive into SharePoint Timer Jobs and Health Rules

In a previous post, I raised an issue regarding a SQL stored procedure not being created upon the creation of the ASP.NET Session State Service Application. This stored procedure takes care of the deletion of the expired sessions. When we check our timer jobs, we also see we have a definition which is called “State Service Delete Expired Sessions”. This obviously is connected to the regular State Service, but let’s take a closer look on how this timer job works.

Before we go deeper into the fundamentals, you’ll need a tool, which is free, called ILSpy. It’s a .NET decompiler, so if you already have one, you’re good to go. When you’ve downloaded ILSpy, extract it somewhere and you can fire it up. The main screen looks like this:

ilspy main window

Select any open libraries and remove them. This gives us a clean start:

ilspy remove standard libs

Next, we’re gonna look at the timer job itself. We need to know in which library it’s located so we can decompile it with ILSpy. Leave ILSpy open and fire up the SharePoint 2010 Management Shell. Type in the following command:

Get-SPTimerJob | ?{$_.Title -eq "State Service Delete Expired Sessions"} | Select TypeName

This will give you the following output:

Microsoft.Office.Server.Administration.StateServiceExpiredSessionJobDefinition

Great! Let’s go back to ILSpy and open the Microsoft.Office.Server library from the GAC:

ilspy open from gac

Next select the Microsoft.Office.Server library and click “Open”:

ilspy open microsoft office server from gac

Expand the library to view all the namespaces. Expand the Administration namespace and scroll down until you find the StateServiceExpiredSessionJobDefinition class. When you decompile this class, you can expand any of the methods and properties. We’re interested in the Execute method, cause that’s what fires when the timer job is executed:

ilspy decompile class

So all it does is call the static DeleteExpiredSessions(current) method on the StateSqlSession class. When we go deeper and click on the method, we can see what happens behind the scene:

ilspy click on method

Seems that all that happens is call a SQL Stored Procedure “proc_DeleteExpiredItems” from this Timer Job:

ilspy deep into method

Deep diving into Timer Jobs can give you a lot of insight into the engine of SharePoint. The following PowerShell command lists all the Titles and TypeNames from the registered Timer Job Definitions so you can go even deeper:

Get-SPTimerJob | Select Title, TypeName | fl

You’ll notice that health rule jobs are registered differently. Getting their TypeName is also a bit different. You have to use the local SPHealthRulesList:

[Microsoft.SharePoint.Administration.Health.SPHealthRulesList]::Local.Items | Select Title, @{ Label="HealthRuleType"; Expression= { $_["HealthRuleType"] } } | fl

Deploying Custom Document Type Icons to your Farm

Deploying custom icons, like the PDF one, can be an annoying task if your farm has more than 2 servers. Even when you deploy a whole new farm, or add a new server, you have to dive into your hive folder again, edit your DOCICON.XML file and copy/paste that icon into your images folder.

When thinking about it, this could be easily automated.

The requirements are quite simple:

  1. Store the icon somewhere centrally
  2. Keep track of which extension it belongs to
  3. Periodically check for new servers to deploy the custom icons to

A basic fill in:

  1. A custom list residing on the Central Administration
  2. A custom column with a text field for instance where the extension can be filled in
  3. A timer job which will check for changes and apply if needed

In a quick scheme it looks like the following when you deploy the solution:

icon deployment

The code should execute on every server in the farm. Why not on just one server? Because we need to edit a file in the hive on every specific server. The least privilege security model does not allow direct access to other servers’ 14 hive. Not only should it execute just once, it should check everyday if the entry is there, and if not, because it’s a new server, add the correct line in DOCICON.XML. This is where a custom timer job comes into the scene. We want the timer job to execute on every server, not just webfrontends, so the timer job will inherit SPServiceJobDefinition. An example timer job class looks like this:

[Guid("some ugly guid goes here xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx")]
class InstallIcon : SPServiceJobDefinition
{
    public InstallIcon()
        : base()
    { }

    public InstallIcon(string name, SPService service)
        : base(name, service)
    { }

    public override string DisplayName
    {
        get
        {
            return "Configure extra document icons";
        }
    }

    public override string Description
    {
        get
        {
            return "Configures extra icons to be displayed for non standard files.";
        }
    }

    public override void Execute(SPJobState jobState)
    {
        //implement code here
    }
}

In order to register the timer job on every server, we’ll add our custom job to the timer service; Hence the SPServiceJobDefinition inheritance. Create a new farm feature and add a feature receiver. Add the following FeatureActivated block:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    // make this a global const
    string timerJobName = "install-icon-job";
    bool exists = false;
    foreach (SPJobDefinition job in SPFarm.Local.TimerService.JobDefinitions)
    {
        if (job.Name == timerJobName)
        {
            exists = true;
            break;
        }
    }
    if (!exists)
    {
        InstallIcon timerJob = new InstallIcon(timerJobName, SPFarm.Local.TimerService);
        SPDailySchedule sched = new SPDailySchedule();
        sched.BeginHour = 0;
        sched.EndHour = 1;
        timerJob.Schedule = sched;
        timerJob.Update();
        timerJob.RunNow();
    }
}

Add the following FeatureDeactivating block:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    // make this a global const
    string timerJobName = "install-icon-job";
    foreach (SPJobDefinition job in SPFarm.Local.TimerService.JobDefinitions)
    {
        if (job.Name == timerJobName)
        {
            job.Delete();
            break;
        }
    }
}

Now the timer job gets registered and unregistered with the timer service based on the feature’s state. In order to place the icon correctly in your hive you need to implement the following logic:

  1. Get the path of the DOCICON.XML
  2. Edit the DOCICON.XML with the correct key/value pair
  3. Save
  4. Copy the image file to the correct directory

But before we do that, we’ll create the picture library on the central administration site collection. I’m doing this outside of the solution, but you can always implement it using code or XML so you have everything in your package.

picture library custom column AssociatedExtension

It’s a standard picture library with the name “FileIcons”, and I’ve added an extra column called “AssociatedExtension” which I use later on to associate the picture with a certain extension.

Now it’s time to implement the logic into our timer job’s execute method. Warning: the following codeblock is a wall of text. In a production environment, it’s best to split this up into different classes / methods. But for this post I decided to put it all into one code block so you can easily see the steps that get processed.

// Get the DOCICON.XML path
string docIconPath = SPUtility.GetGenericSetupPath(@"TEMPLATE\XML\DOCICON.XML");
XmlDocument docIconXML = new XmlDocument();
// Read it into an editable XML object
using (XmlReader reader = XmlReader.Create(docIconPath))
    docIconXML.Load(reader);

XmlNode byExtension = docIconXML.SelectSingleNode("/DocIcons/ByExtension");
XmlNodeList mappingList = docIconXML.GetElementsByTagName("Mapping");

// Get the picture library on the central administration
SPAdministrationWebApplication caWebApp = SPAdministrationWebApplication.Local;
SPSite oSite = caWebApp.Sites[0];
SPWeb oWeb = oSite.OpenWeb();
SPPictureLibrary oPictureLibrary = (SPPictureLibrary)oWeb.Lists["FileIcons"];

// For every picture in the library, write the image to IMAGES
foreach (SPListItem item in oPictureLibrary.Items)
{
    Byte[] fileContentsArray = null;
    MemoryStream imageStream = null;
    SPFile file = item.File;
    string fileName = file.Name;
    using (Stream fileContents = file.OpenBinaryStream())
    {
        long length = fileContents.Length;
        fileContentsArray = new Byte[length];
        fileContents.Read(fileContentsArray, 0, Convert.ToInt32(length));
    }
    imageStream = new MemoryStream(fileContentsArray);
    FileStream fStream = File.OpenWrite(SPUtility.GetGenericSetupPath(@"TEMPLATE\IMAGES\" + fileName));
    imageStream.WriteTo(fStream);
    fStream.Flush();
    fStream.Close();
    imageStream.Flush();
    imageStream.Close();

    // Replace existing entries in the DOCICON.XML
    bool exists = false;
    foreach (XmlNode mapping in mappingList)
    {
        if (mapping.Attributes["Key"] != null && mapping.Attributes["Key"].Value == item["AssociatedExtension"].ToString())
        {
            exists = true;
        }
    }
    if (!exists)
    {
        XmlElement pdfEntry = docIconXML.CreateElement("Mapping");

        byExtension.AppendChild(pdfEntry);

        XmlAttribute key = docIconXML.CreateAttribute("Key");
        XmlAttribute value = docIconXML.CreateAttribute("Value");

        key.Value = item["AssociatedExtension"].ToString();
        value.Value = fileName;

        pdfEntry.SetAttributeNode(key);
        pdfEntry.SetAttributeNode(value);
        docIconXML.Save(docIconPath);
    }
}
oWeb.Dispose();
oSite.Dispose();

When you deploy your solution to your farm, add some pictures to your picture library and run the timer job, you’ll see that the changes get applied to your local 14 hive!

For more refinement, you may implement deletion events and tweak some settings with application pages.