It looks like you need a farm function that installs a service that does this job. Here's how I did it (using code written by a colleague, to be honest, but it's not in SO).
Create a feature.xml file with a function event receiver.
<Feature Id="..." Title="..." Description="..." Scope="Farm" Version="1.0.0.0" Hidden="FALSE" ReceiverAssembly="XXX.FarmService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxx" ReceiverClass="XXX.FarmService.XXXFarmFeatureEventReceiver" xmlns="http://schemas.microsoft.com/sharepoint/"> </Feature>
Inside the event receiver:
public override void OnFeatureActivated(SPFeatureReceiverProperties properties) { try { SPFarm farm = SPFarm.Local; // Get Service, if it already exists JobService myService = null; // JobService is a subclass of SPService foreach (SPService service in farm.Services) { if (String.Compare(service.Name, JobService.XXXServiceName, true) == 0) { myService = (service as JobService); break; } } if (cegService == null) { // Create service myService = new JobService(farm); myService.Update(); // Create service instances JobServiceInstance myServiceInstance; // JobServiceInstance is a subclas of SPServiceInstance foreach (SPServer server in farm.Servers) { myServiceInstance = new JobServiceInstance(server, myService); myServiceInstance.Update(); } } // Dayly schedule SPDailySchedule schedule = new SPDailySchedule(); schedule.BeginHour = 1; schedule.EndHour = 1; schedule.BeginMinute = 0; schedule.EndMinute = 59; // Our own job; JobCheckDocDates is a subclass of SPJobDefinition JobCheckDocDates newJob = new JobCheckDocDates(cegService, null, SPJobLockType.Job); newJob.Schedule = schedule; newJob.Update(); myService.JobDefinitions.Add(newJob); myService.Update(); } catch (Exception e) { Logger.Error("[" + properties.Feature.Definition.DisplayName + "] Error during feature activation", e); } }
This creates a service that is available on each server in the farm.
Additional information on subclasses:
public class JobCheckDocDates: Common.BaseJob { /// <summary> /// The job name /// </summary> public static string JobName = "XXX job"; /// <summary> /// Constructor /// </summary> public JobCheckDocDates() : base() { } /// <summary> /// Constructor /// </summary> /// <param name="service"></param> /// <param name="server"></param> /// <param name="lockType"></param> public JobCheckDocDates(SPService service, SPServer server, SPJobLockType lockType) : base(JobName, service, server, lockType) { }
...
and, of course, the Execute method.
public class JobService : SPService { public static string XXXServiceName = "XXX Service"; public override string DisplayName { get { return XXXServiceName; } } public override string TypeName { get { return "XXX Service Type"; } } public JobService() { } public JobService(SPFarm farm) : base(XXXServiceName, farm) { } } public class JobServiceInstance : SPServiceInstance {
Now, in Central Administration, go to Operations / Services on the server. Select the desired server and start the service.
To answer your list of questions: 1. Deploy the solution only once, regardless of WFE. 2. Since the function has a farm scope, it must be activated in the central administrator. 3. SPJobLockType is SPJobLockType.Job
This is not exactly what you imagine, but it has the advantage of allowing you to easily choose where the task is performed, even after installing this function (for example, if the server is overloaded with other materials).
The OnFeatureActivated method can be smarter and check each server, if the service exists, and add it if necessary. But itβs easier to remove this service from each service in OnFeatureDeactivated. So, if you add new servers, deactivate and then reactivate the function.