Tâches planifiées avec Sharepoint

Stéphane Eyskens

Ce tutoriel a pour but de vous initier au développement de tâches planifiées dans MOSS 2007

- Titre : Tâches planifiées avec Sharepoint

- Auteur : Stéphane Eyskens

- Parution : 20 décembre 2007

- Licence : Copyright ®  Developpez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Les tâches intégrées

- III. Planification

- IV. Construction du projet pas à pas

- IV-A. Corps de notre job - classe Suppression.cs

- IV-B. Enregistrement du job - Fichiers EnregistrementJob.cs et feature.xml

- IV-B-1. Le fichier feature.xml

- IV-B-2. Le code exécuté lors de l'activation/désactivation de la feature

- IV-C. Déploiement du job via le fichier .bat

- V. Et après, il se passe quoi?

- VI. Comment debugger une tâche?

- VII. Comment forcer l'exécution de notre job?

- VIII. Téléchargement du projet

I - Introduction

Les tâches planifiées de Sharepoint permettent au développeur d'exécuter de manière récurrente certaines actions à la manière des tâches planifiées de Windows. Dans les anciennes versions de Sharepoint, il fallait d'ailleurs passer par le gestionnaire des tâches Windows pour effectuer des opérations régulières sur le(s) serveur(s) Sharepoint. Via le mécanisme de tâches intégré, il est désormais plus simple et plus pratique de centraliser toutes ces tâches dans un store spécifique.

Il est indiqué d'utiliser des tâches par exemple lorsque l'on souhaite synchroniser des données issues de systèmes externes tels que l'active directory, une base de données...,lorsque l'on souhaite activer des mécanismes d'archivage etc...

Dans cet exemple, nous allons créer un job qui supprimera automatiquement les éléments d'une liste lorsqu'ils auront été créés depuis plus d'un nombre de minutes défini en paramètre dans un fichier de config.

II - Les tâches intégrées

Dès que vous installez MOSS, pas mal de tâches sont déjà présentes dans le système. Elles sont accessibles via la centrale d'administration, dans l'onglet opération tel qu'illustré par la capture d'écran ci-dessous

Cet écran nous présente les tâches actuellement définies dans le système, l'application concernée par leur exécution et le type de fréquence (journalière, hebdomadaire etc...)

Il va sans dire que la tâche que vous créerez sera également présente dans cette liste.

III - Planification

Il existe plusieurs classes nous permettant de planifier une tâche. Voici le détail de chacune

Nom de la classe

Description

Exemple d'utilisation

Exécution

SPWeeklySchedule

Permet de planifier une tâche hebdomadaire

SPWeeklySchedule JobSchedule = new SPWeeklySchedule();
JobSchedule.BeginDayOfWeek = DayOfWeek.Monday;
JobSchedule.EndDayOfWeek = DayOfWeek.Monday;
JobSchedule.BeginHour = 10;
JobSchedule.BeginMinute = 15;
JobSchedule.EndMinute = 25;
JobSchedule.EndHour = 10;

Tous les lundis à 10h15

SPDailySchedule

Permet de planifier une tâche journalière

SPDailySchedule JobSchedule = new SPDailySchedule(); JobSchedule.BeginHour = 10;
JobSchedule.BeginMinute = 15;
JobSchedule.EndMinute = 25;
JobSchedule.EndHour = 10;

tous les jours à 10h15

SPMonthlySchedule

Permet de planifier une tâche mensuelle

SPMonthlySchedule JobSchedule = new SPMonthlySchedule();
JobSchedule.BeginDay = 1;
JobSchedule.EndDay = 1;
JobSchedule.BeginHour = 10;
JobSchedule.EndHour = 10;
JobSchedule.BeginMinute = 15;
JobSchedule.EndMinute = 25;

Le 1er jour de chaque mois à 10h15

SPYearlySchedule

Permet de planifier une tâche annuelle

SPYearlySchedule JobSchedule = new SPYearlySchedule();
JobSchedule.BeginMonth = 1;
JobSchedule.EndMonth = 1;
JobSchedule.BeginDay = 1;
JobSchedule.EndDay = 1;
JobSchedule.BeginHour = 10;
JobSchedule.EndHour = 10;
JobSchedule.BeginMinute = 15;
JobSchedule.EndMinute = 25;

Tous les ans, le 1er janvier à 10h15

SPHourlySchedule

Permet de planifier une tâche toutes les heures

SPHourlySchedule JobSchedule = new SPHourlySchedule();
JobSchedule.BeginMinute = 1;
JobSchedule.EndMinute = 2;

Toutes les heures à la première minute

SPMinuteSchedule

Permet de planifier une tâche toutes les x minutes

SPMinuteSchedule JobSchedule = new SPMinuteSchedule();
JobSchedule.BeginSecond = 1;
JobSchedule.EndSecond = 59;
JobSchedule.Interval = 2;

Toutes les deux minutes

Note:il est indispensable de définir les propriétés End....(EndMinute, EndHour etc...) car le temps réel auquel une tâche doit démarrer est calculé par Sharepoint en se basant sur ces données. Si vous ne spécifiez rien, votre job ne s'exécutera jamais.

IV - Construction du projet pas à pas

A l'heure actuelle, il n'existe pas de template spécifique pour créer des jobs. On peut utiliser une class library.

- Création d'un projet de type class library

- référencement de Microsoft.Sharepoint.dll qui se trouve dans le répertoire ISAPI de votre installation MOSS

- ajout des directives incluant les espaces de noms Microsoft.Sharepoint,Microsoft.Sharepoint.Administration et Microsoft.Sharepoint.Utilities

- ajout d'un fichier XML que l'on nommera feature.xml qui nous permettra d'ajouter notre job

- ajout d'une classe qui nous permettra d'ajouter notre job lors de l'activation de la feature et de le supprimer lors de la désactivation

- ajout d'un fichier .bat qui s'exécutera lors du post build pour déployer notre job

La structure résultante doit donc ressembler à ceci

IV-A - Corps de notre job - classe Suppression.cs

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;
namespace AutoSuppression
{
public class Suppression : SPJobDefinition
{
//Constructeur vide nécessaire pour la séralisation
public Suppression()
{
}

//Constructeur qui sera exécuté lors de l'enregistrement du job
public Suppression(string JobName, SPWebApplication WebApplication,
string TargetSite,string TargetList, string MaxLifeTime)
: base(JobName, WebApplication, null, SPJobLockType.ContentDatabase)
{
this.Title = JobName;
//Création des propriétés du job pour que l'info
//reçue de feature.xml persiste au sein du job.
this.Properties.Add("TargetSite", TargetSite);
this.Properties.Add("TargetList", TargetList);
this.Properties.Add("MaxLifeTime", MaxLifeTime);

}
//Méthode polymorphe que l'on réécrit pour définir
//le corps d'exécution du job
public override void Execute(Guid targetInstanceId)
{
//Temps vie max
int MaxLifeTime = 0;
//Liste ciblée par la suppression
string TargetList = null;
//Site ciblé par la suppression
string TargetSite = null;
base.Execute(targetInstanceId);
try
{
//Récupération des paramètres définis dans feature.xml
MaxLifeTime = Convert.ToInt16(this.Properties["MaxLifeTime"].ToString());
TargetList = this.Properties["TargetList"].ToString();
TargetSite = this.Properties["TargetSite"].ToString();
//On obtient une référence de l'application web courante
SPWebApplication WebApplication = this.Parent as SPWebApplication;
if (WebApplication != null)
{
//On pointe sur la DB de contenu
SPContentDatabase ContentDb = WebApplication.ContentDatabases[targetInstanceId];
//On crée une référence vers la liste cible
SPList TargetListObject = ContentDb.Sites[TargetSite].RootWeb.Lists[TargetList];
//Date calculée en tenant compte de l'heure qui sera
//utilisée dans la requête CAML
string CalculatedDate = SPUtility.CreateISO8601DateTimeFromSystemDateTime(
DateTime.Now.AddMinutes(-MaxLifeTime)).ToString();

SPQuery Query = new SPQuery();
//Requête
Query.Query = "<Where><Lt><FieldRef Name='Created' /><Value Type='DateTime' IncludeTimeValue='TRUE'>" +
CalculatedDate + "</Value></Lt></Where>";
//On parcourt le résultat et on supprime les items
SPListItemCollection Items = TargetListObject.GetItems(Query);
int ItemCount = Items.Count;
for (int i = 0; i < ItemCount; i++)
{
SPListItem Item = Items[0] as SPListItem;
Item.Delete();
}

}

}
catch (FormatException)
{
throw new ApplicationException("Nombre de minutes incorrect");
}
catch (Exception Ex)
{
throw new ApplicationException("Une erreur s'est produite:" + Ex.Message);
}

}
}
}

Quatre choses importantes sont à retenir dans ce code

- Notre classe doit dériver de SPJobDefinition

- On doit impérativement définir un constructeur vide pour la sérialisation

- On doit ajouter des propriétés à notre job afin de faire persister les paramètres définis dans feature.xml

- On doit réécrire la méthode Execute() pour exécuter le corps de notre job

IV-B - Enregistrement du job - Fichiers EnregistrementJob.cs et feature.xml

Pour enregistrer la tâche, on peut utiliser une feature. Les features sont un moyen traditionnel d'encapsuler les composants. Il y a toutefois une petite nuance pour les tâches. Il faut éviter que n'importe qui puisse activer cette feature car l'enregistrement du job nécessite certains droits sur la db de config. Il faut dès lors s'assurer que ce soit un administrateur qui active la feature.

Pour s'en assurer, on peut déployer la feature en tant que feature "cachée" (hidden) afin qu'elle n'apparaisse pas dans la liste des features lorsque l'on va dans site settings => site features. Ceci force l'activation de la feature via stsadm, donc un accès console sur le serveur donc forcément en théorie un administrateur.

IV-B-1 - Le fichier feature.xml

<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
Id="8A6E09E8-2A7E-4cd4-8BE4-44883B737732"
Title="Auto suppression d'éléments"
Description="Supprime les éléments automatiquement"
Scope="Web"
Hidden="TRUE"
Version="1.0.0.0"
ReceiverAssembly="AutoSuppression, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a2ba62857f5113af"
ReceiverClass="AutoSuppression.EnregistrementJob">
<Properties>
<Property Key="TargetSite" Value="url de votre site"/>
<Property Key="TargetList" Value="le nom de la liste"/>
<!--Nombre de minutes de vie-->
<Property Key="MaxLifeTime" Value="5"/>
</Properties>
</Feature>

Ici, également quatre choses importantes à retenir

- Le scope (web) : ceci signifie que la feature sera activable/désactivable pour un site et non pour une collection

- Le paramètre Hidden (TRUE) : pour forcer l'activation par un administrateur, on cache la feature afin qu'elle n'apparaisse pas dans la liste dans les GUI

- ReceiverAssembly et ReceiverClass : ces paramètres nous permettent de dire que l'on souhaite que la classe EnregistrementJob soit exécutée lors d'actions spécifiques sur la feature

- Les propriétés : ce sont les paramètres que l'on passera à notre job. C'est une manière flexible de travailler.

Note: cela va de soi mais je l'indique quand même, vous devez bien sûr définir vos propres valeurs pour les paramètres TargetSite et TargetList

IV-B-2 - Le code exécuté lors de l'activation/désactivation de la feature

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace AutoSuppression
{
class EnregistrementJob : SPFeatureReceiver
{
//Nom du job
const string JobName = "AutoSuppression";
/// <summary>
/// Méthode appelée lors de l'activation de la feature
/// </summary>
/// <param name="properties"></param>
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWeb TargetWeb = properties.Feature.Parent as SPWeb;
//Suppression du job si il existe déjà
foreach (SPJobDefinition Job in TargetWeb.Site.WebApplication.JobDefinitions)
{
if (Job.Name == JobName)
{
Job.Delete();
break;
}
}
//Instanciation de notre job et passage des paramètres
//définis dans feature.xml
Suppression JobSuppression = new Suppression(JobName, TargetWeb.Site.WebApplication,
properties.Definition.Properties["TargetSite"].Value,
properties.Definition.Properties["TargetList"].Value,
properties.Definition.Properties["MaxLifeTime"].Value);

//Création du schedule, dans notre cas, le job sera exécuté
//toutes les 2 minutes
SPMinuteSchedule JobSchedule = new SPMinuteSchedule();
JobSchedule.BeginSecond = 0;
JobSchedule.EndSecond = 59;
JobSchedule.Interval = 2;
JobSuppression.Schedule = JobSchedule;
JobSuppression.Update();

}
/// <summary>
/// Méthode appelée lors de la désactivation de la feature.
/// </summary>
/// <param name="properties"></param>
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
SPWeb TargetWeb = properties.Feature.Parent as SPWeb;
//Suppression du job
foreach (SPJobDefinition Job in TargetWeb.Site.WebApplication.JobDefinitions)
{
if (Job.Name == JobName)
{
Job.Delete();
break;
}
}

}
/// <summary>
/// Méthode abstraite qu'il faut au minimum déclarer
/// </summary>
/// <param name="properties"></param>
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{

}
/// <summary>
/// Méthode abstraite qu'il faut au minimum déclarer
/// </summary>
/// <param name="properties"></param>
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{

}

}
}

Ici, quatres choses à retenir

- Il faut déclarer toutes les méthodes abstraites

- Il faut implémenter FeatureActivated et FeatureDeactivating afin d'enregistrer/supprimer notre job lors de l'activation/désactivation de la feature

- Il faut créer un job schedule pour déterminer la fréquence d'exécution. Comme spécifié dans la section III ne pas oublier de spécifier les propriétés End...(ici EndSecond)

IV-C - Déploiement du job via le fichier .bat

Vous devez signer la DLL (ceci n'est pas impératif) et avoir paramétré Visual Studio afin qu'il exécute le fichier .bat après un build.

REM ceci déploie la feature physiquement sur le disque
rd /s /q "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\AutoSuppression"
mkdir "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\AutoSuppression"
copy /Y feature.xml  "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\AutoSuppression\"

REM Suppression et Ajout de la DLL dans la GAC
"%programfiles%\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" -uf http://urldevotresite
"%programfiles%\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" -if bin\Debug\AutoSuppression.dll

REM Désinstallation/Désactivation/Installation/Activation de la feature

pushd %programfiles%\common files\microsoft shared\web server extensions\12\bin
stsadm -o deactivatefeature -filename AutoSuppression\feature.xml -url http://urldevotresite
stsadm -o uninstallfeature -filename AutoSuppression\feature.xml
stsadm -o installfeature -filename AutoSuppression\feature.xml -force
stsadm -o activatefeature -filename AutoSuppression\feature.xml -url http://urldevotresite

REM Redémarrage de IIS
iisreset

REM Redémarrage du service Timer (process OWSTimer.exe)
net stop SPTimerV3
net start SPTimerV3

L'objet du fichier bat est de faciliter le déploiement. En paramétrant son exécution automatique via les post-build events de Visual Studio, vous automatisez le déploiement.

V - Et après, il se passe quoi?

Lorsque vous avez suivi les étapes décrites préalablement, vous devriez voir apparaître votre job dans la liste des jobs dans la centrale d'administration

Après une très brève attente, le job s'exécutera et vous pourrez voir son statut.

VI - Comment debugger une tâche?

Habituellement, lorsque l'on debugge un composant personnel dans Sharepoint, on s'attache à l'un des processus w3p.exe en charge d'exécuter le code de ceux-ci. Pour les tâches planifiées, il faut s'attacher au processus OWSTIMER.exe car c'est lui qui est responsable de l'exécution des tâches.

VII - Comment forcer l'exécution de notre job?

Vous l'aurez sans doute constaté, la centrale d'administration nous propose une interface affichant tous les jobs présents et nous donnant la possibilité d'en désactiver mais pas de les exécuter. La commande stsadm -o execadmsvcjobs permet d'exécuter tous les jobs inhérants à l'administration de Sharepoint mais pas des jobs personnels tels que celui présenté dans ce tutoriel.

Le seul moyen que je connaisse actuellement est l'API, on peut démarrer un job comme ceci:

using (SPSite TargetSite = new SPSite("lesite"))
{
foreach (SPJobDefinition Job in TargetSite.WebApplication.JobDefinitions)
{
if (Job.Name == "AutoSuppression")
{
Job.Execute(TargetSite.WebApplication.ContentDatabases[0]);
break;
}
}
}

Si vous disposez du GUID du job, cette méthode est plus efficace

using (SPSite TargetSite = new SPSite("lesite"))
{
try
{
SPJobDefinition Job = TargetSite.WebApplication.JobDefinitions[new Guid("le guid")];
Job.Execute(TargetSite.WebApplication.ContentDatabases[0]);
}
catch(NullReferenceException)
{
throw new ApplicationException("Le job n'existe pas");
}
catch(Exception Ex)
{
throw new ApplicationException("problème lors de la tentative d'exécution:"+Ex.Message);
}
}

Il est en théorie possible de faire la même chose en utilisant le nom du job plutôt que le GUID mais cela ne fonctionne pas...Lorsque l'on travaille avec le nom, on semble forcé de faire une boucle traversant la collection.

VIII - Téléchargement du projet

Vous pouvez télécharger le projet ici

Avant de déployer le projet, veillez à définir vos propres paramètres dans le fichier feature.xml ainsi que dans le fichier Install.bat