Creating the timer job

Sometimes you need to deploy settings to every web front end, or have a piece of code execute on every web front end.  Your choices in SharePoint are to either have an administrator run a script on each front end, or use a timer job.  This blog post describes how to use a timer job to accomplish this. 

When you log to the event log using the native event log APIs or using the new SharePoint 2010 logging capabilities the event source used for the event logged must already exist, or the code must be running in a context that has privileges to create the event source, which means permissions to write to the registry.  If the code doesn't have those permissions, then a security exception will be thrown.  This condition may also show up as an security exception for attempting to access logs to which you don't have access.  This may occur as the Event Log searches for the event source registration in different event logs, in particular the security log, to which your code may also not have access to.  In any case, if you are attempting to log an event using the SharePoint 2010 APIs and are receiving a security exception, there is a good chance that the event source being used hasn't been created on the machine.  Since event sources require registration in the local machine registry, the registration process must take place on each physical machine (i.e., code must run in a high privileged account on each WFE to register the event sources).

 

I'll state the obvious first.  The timer job must be running under an account that can write to the registry.  The timer job runs under the managed account called "farm account", and its pretty common to make this a relatively high permission account.  By default it is running as NETWORK SERVICE, and NETWORK SERVICE will not have permission to write the settings to the registry, and therefore the process described in this blog will not work with default settings.  This obvious one got me when I first developed this solution.  You can added managed accounts in central admin->security->configure managed accounts, and you can set the farm account in central admin->security->configure service accounts.

There are two components that need to be developed; the timer job, and a feature to activate the timer job.  We'll start with the timer job.  A SharePoint timer job runs under a SharePoint managed process for timer execution (OWSTimer.exe, SharePoint 2010 Timer service).  Timers run as one might expect periodically.  The timer jobs that are scheduled, and the intervals at which they run are described here: http://technet.microsoft.com/en-us/library/cc678870.aspx.  The type of timer job we'll create for this particular task will run only once, but will run on every web front end.

This example implementation uses the patterns and practices SharePoint Guidance Library that can be downloaded from http://www.microsoft.com/spg

The following listing shows the full source code for the timer job:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using Microsoft.SharePoint.Administration;
   6: using Microsoft.Practices.SharePoint.Common.Logging;
   7: using Microsoft.SharePoint;
   8: using Microsoft.Practices.SharePoint.Common.ServiceLocation;
   9:  
  10: namespace CreateEventSourcesJob
  11: {
  12:     /// <summary>
  13:     /// This class defines a timer job that will run on each WFE once.  The timer job will create the event 
  14:     /// sources for the logger if the event sources do not already exist.  Note that the account the timer 
  15:     /// job runs under must have sufficient permissions to write to the registry.  The default account 
  16:     /// is NetworkService, which does not have permission to write to the directory.  This account is 
  17:     /// changed through central admin.  You will first need to create a managed account 
  18:     /// (CentralAdmin->Security->Configure Managed Accounts) with an account that has sufficent permission.
  19:     /// You then assign the account (CentralAdmin->Security->Configure Service Accounts) to the Farm Account, 
  20:     /// which is used by the timer.  At this point this task will work.
  21:     /// </summary>
  22:     public class CreateLoggingEventSourceJob: SPJobDefinition
  23:     {
  24:         public static string JobName { get { return "CreateLoggingEventSources"; } }
  25:         public static string JobTitle { get { return "Create Logging Event Sources"; } }
  26:  
  27:         /// <summary>
  28:         /// Creates the job instance
  29:         /// </summary>
  30:         public CreateLoggingEventSourceJob()
  31:             : base(JobName, SPFarm.Local.TimerService, null, SPJobLockType.None)
  32:         {
  33:         }
  34:  
  35:         /// <summary>
  36:         /// A description of the job being ran
  37:         /// </summary>
  38:         public override string Description
  39:         {
  40:             get
  41:             {
  42:                 return "Registers the event sources on WFE's for patterns and practices Logger";
  43:             }
  44:         }
  45:  
  46:         /// <summary>
  47:         /// The name to display for the job.
  48:         /// </summary>
  49:         public override string DisplayName
  50:         {
  51:             get
  52:             {
  53:                 return "patterns and practices Event Source Registration job";
  54:             }
  55:         }
  56:  
  57:         /// <summary>
  58:         /// Called by SharePoint when this job is ran.  This is where the work is done, whihc is simply
  59:         /// calling EnsureConfiguredAreasRegistered.  This method reads the areas from configuration, and
  60:         /// creates an event source for each if it doesn't exist.  It will also create the event source
  61:         /// for the default category if it does not exist.
  62:         /// </summary>
  63:         /// <param name="targetInstance">The target instance IDfor the job</param>
  64:         public override void Execute(Guid targetInstance)
  65:         {
  66:             try
  67:             {
  68:                 DiagnosticsAreaEventSource.EnsureConfiguredAreasRegistered();
  69:             }
  70:             catch (Exception e)
  71:             {
  72:                 var logger = SharePointServiceLocator.GetCurrent().GetInstance<ILogger>();
  73:                 logger.TraceToDeveloper(e);
  74:                 logger.LogToOperations(e.ToString() + " INNER EXCEPTION: e.InnerException.ToString()", 
  75:                     EventSeverity.ErrorCritical);
  76:                 throw;
  77:             }
  78:         }
  79:  
  80:         /// <summary>
  81:         /// This method sets up the timer to fire.  This is the method after the logging categories are 
  82:         /// setup that an application can call to register the event sources.
  83:         /// </summary>
  84:         public static void ScheduleJob()
  85:         {
  86:             CleanUpJobs(SPFarm.Local.TimerService.JobDefinitions);
  87:             var job = new CreateLoggingEventSourceJob();
  88:             job.Schedule = new SPOneTimeSchedule(DateTime.Now);
  89:              job.Update();
  90:         }
  91:  
  92:         /// <summary>
  93:         /// Cleans up and old versions found of the job.
  94:         /// </summary>
  95:         /// <param name="jobs">The job list.</param>
  96:         private static void CleanUpJobs(SPJobDefinitionCollection jobs)
  97:         {
  98:             foreach (SPJobDefinition job in jobs)
  99:             {
 100:                 if (job.Name.Equals(CreateLoggingEventSourceJob.JobName,
 101:                                         StringComparison.OrdinalIgnoreCase))
 102:                 {
 103:                     job.Delete();
 104:                 }
 105:             }
 106:         }
 107:     }
 108: }

We’ll take a look now at specific portions of the timer job.  First every timer job will derive from SPJobDefinition.  When the timer job is constructed, the base class is initialized with a few parameters:

  • JobName – the name of our job.
  • SPFarm.Logal.TimerService – We need to associate either a service or a web application with the timer job we want to run.  In this case we specify the the timer service as the associated service.
  • null – this specifies the SharePoint server to run the job on.  By specifying null it will be ran on all servers.
  • SPJobLockType.None – this isn’t obvious, but by defining a lock job type of none, we are telling SharePoint to run this on every machine.  For more information, see http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.administration.spjoblocktype.aspx.

There are two other interesting pieces to the timer job.  First we need to schedule the job to run.  The class provides a convenience function to schedule this timer job for code that has registered a new logging category for the logger to use.  The convenience function is a static function that registers an instance of the timer to execute.  The timer is a single shot timer that will only run once.  It is scheduled to run immediately with the timer service.

   1: /// <summary>
   2: /// This method sets up the timer to fire.  This is the method after the logging categories are 
   3: /// setup that an application can call to register the event sources.
   4: /// </summary>
   5: public static void ScheduleJob()
   6: {
   7:     CleanUpJobs(SPFarm.Local.TimerService.JobDefinitions);
   8:     var job = new CreateLoggingEventSourceJob();
   9:     job.Schedule = new SPOneTimeSchedule(DateTime.Now);
  10:      job.Update();
  11: }

When the job is registered we need to clean up any previous jobs otherwise the job will fail due to a duplicate definition. CleanUpJobs will clean out any previous instances for us before the current job is scheduled.  CleanupJobs will construct an instance of the job, so you can’t do this logic easily in the constructor, rather it’s done before the timer is rescheduled as shown above.

   1: /// <summary>
   2: /// Cleans up and old versions found of the job.
   3: /// </summary>
   4: /// <param name="jobs">The job list.</param>
   5: private void CleanUpJobs(SPJobDefinitionCollection jobs)
   6: {
   7:     foreach (SPJobDefinition job in jobs)
   8:     {
   9:         if (job.Name.Equals(CreateLoggingEventSourceJob.JobName,
  10:                                 StringComparison.OrdinalIgnoreCase))
  11:         {
  12:             job.Delete();
  13:         }
  14:     }
  15: }

The final piece is where the work is actually done.  The timer will construct this class to run the job, then call the Execute method.  The work is actually done in the Execute method.

   1: /// <summary>
   2: /// Called by SharePoint when this job is ran.  This is where the work is done, whihc is simply
   3: /// calling EnsureConfiguredAreasRegistered.  This method reads the areas from configuration, and
   4: /// creates an event source for each if it doesn't exist.  It will also create the event source
   5: /// for the default category if it does not exist.
   6: /// </summary>
   7: /// <param name="targetInstance">The target instance IDfor the job</param>
   8: public override void Execute(Guid targetInstance)
   9: {
  10:     try
  11:     {
  12:         DiagnosticsAreaEventSource.EnsureConfiguredAreasRegistered();
  13:     }
  14:     catch (Exception e)
  15:     {
  16:         var logger = SharePointServiceLocator.GetCurrent().GetInstance<ILogger>();
  17:         logger.TraceToDeveloper(e);
  18:         logger.LogToOperations(e.ToString() + " INNER EXCEPTION: e.InnerException.ToString()", 
  19:             EventSeverity.ErrorCritical);
  20:         throw;
  21:     }
  22: }

In this method, a convenience method is called in the SharePoint Guidance Library that will read all of the diagnostic areas, and register an event source for each (SharePoint logging has diagnostic areas, and each diagnostic area has one or more diagnostic category.  An example of a diagnostic area would be search or taxonomy).  The following code is provided by the guidance library – if you had your own implementation based upon the SPDiagnosticsServiceBase class, you would need to implement similar logic:

   1: /// <summary>
   2:  /// Ensures that all configured DiagnosticAreas are registered as event sources.
   3:  /// </summary>
   4:  [SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
   5:  [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
   6:  public static void EnsureConfiguredAreasRegistered()
   7:  {
   8:      var mgr = SharePointServiceLocator.GetCurrent().GetInstance<IConfigManager>();
   9:      var areas = new DiagnosticsAreaCollection(mgr);
  10:      RegisterAreas(areas);
  11:  }
  12:  
  13:  /// <summary>
  14:  /// Takes all the configured areas and set them as event sources.
  15:  /// </summary>
  17:  public static void RegisterAreas(DiagnosticsAreaCollection areas)
  18:  {
  19:      Validation.ArgumentNotNull(areas, "areas");
  20:  
  21:      foreach (DiagnosticsArea area in areas)
  22:      {
  23:          if (!EventLog.SourceExists(area.Name))
  24:          {
  25:              EventLog.CreateEventSource(area.Name, Constants.EventLogName);
  26:          }
  27:      }
  28:  
  29:      if (!EventLog.SourceExists(DiagnosticsArea.DefaultSPDiagnosticsArea.Name))
  30:      {
  31:          EventLog.CreateEventSource(DiagnosticsArea.DefaultSPDiagnosticsArea.Name, Constants.EventLogName);
  32:      }
  33:  }

At this point all of the logic has been demonstrated for registering the events. 

Registering Diagnostic Areas and Categories with SharePoint Logger

The SharePointLogger supports adding new diagnostic areas and categories through configuration rather than implementation.  Typically this will be done through a feature receiver.  These configuration values are stored using the ConfigurationManager in farm level configuration.  SharePoint prevents writing to Farm Level configuration from a content web.  FeatureActivated for site and web scoped features will run in the context of the content web (unless activated from powershell/stsadm).  Therefore you should not register this information in the FeatureActivated event of a web or site scoped feature.  There are two approaches that can be taken. 

  • The first, and recommended approach is to scope the feature as Farm.  Since the configuration settings apply across the farm, its reasonable to use a farm scoped feature to set the values.  A farm scoped feature will run at a sufficiently high permission level, outside of the content web, to write to farm level configuration.  In the case of a farm scoped feature, the logic may either be in the FeatureActivated or FeatureInstalled event, and by default SharePoint will activate a farm scoped feature when it is installed. 
  • The second choice is to put the logic in the FeatureInstalled event of a Site or Web scoped feature.  This method is less preferred, but still reasonable since the feature must be deactivated on all webs or sites in the farm before it can be removed. 

The following example shows how to register the events from a farm scoped event receiver:

   1: public override void FeatureActivated(SPFeatureReceiverProperties properties)
   2: {
   3:     var mgr = SharePointServiceLocator.GetCurrent().GetInstance<IConfigManager>();
   4:     DiagnosticsAreaCollection areas = new DiagnosticsAreaCollection(mgr);
   5:  
   6:  
   7:     if (areas["loonytunes"] == null)
   8:     {
   9:         var area = new DiagnosticsArea("loonytunes");
  10:         area.DiagnosticsCategories.Add(new DiagnosticsCategory("bugs bunny"));
  11:         areas.Add(area);
  12:         areas.SaveConfiguration();
  13:     }
  14:     CreateLoggingEventSourceJob.ScheduleJob();
  15: }

In this example the areas are added for the logger, and then the job is scheduled to create the event sources.

One more caution.  The timer will tend to lock the assembly.  You should recycle the timer service whenever you are rebuilding to ensure the assembly isn’t locked and therefore not updated in the GAC.

I have attached the full source as well.  Hope this is helpful.