SharePoint Automation Gary Lapointe – Founding Partner, Aptillon, Inc.

20Sep/099

Setting Back Connection Host Names for SharePoint 2007 Using STSADM

Not too long ago Microsoft introduced a security fix which addresses a possible attack vector in which malicious software tries to impersonate a local request, thereby bypassing certain constraints.  The problem with this fix is that it introduces some issues for SharePoint servers, effectively resulting in 401.1 Access Denied errors.  Spence Harbar does a great write-up of the fix and the options available to get your SharePoint environment working again so I won't re-hash all that here: http://www.harbar.net/archive/2009/07/02/disableloopbackcheck-amp-sharepoint-what-every-admin-and-developer-should-know.aspx.

As Spence points out, the preferred way to fix this is to add the host names to the BackConnectionHostNames registry key and to not set the DisableLoopbackCheck registry key.  You can of course do this using Group Policy but for those not managing their servers using GPO I decided to implement a custom STSADM command that would make setting the BackConnectionHostNames registry key really simple.  I called this new command, oddly enough, gl-setbackconnectionhostnames.

The command has two ways to run it, you can run it without any parameters which will cause it to update only the server in which the command is executed on, or you can pass in an -updatefarm parameter along with a username and password which will cause it to update every server in the farm.  There's no need to pass in the host header names as the code will dynamically determine them by inspecting each web application and their alternate access mappings (alternate URLs) and perform some logic to determine whether the host header is pointing to a local IP address or to a specific SharePoint server (I do this to exclude Central Admin which is usually accessed using a server name and non-standard port).

I accomplish the farm update by using a custom Timer Job which executes on each server.  Unfortunately the timer service account does not have access to write to the registry (unless you've given it rights, which you shouldn't) so it was necessary to pass in a username and password and then use impersonation to update the registry.  The custom timer job code is shown below, notice that all the core work is being done via the SetBackConnectionHostNames class which is shown below the timer job code:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Runtime.InteropServices;
   4: using System.Security.Principal;
   5: using System.Text;
   6: using Microsoft.SharePoint;
   7: using Microsoft.SharePoint.Administration;
   8:  
   9: namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
  10: {
  11:     public class SetBackConnectionHostNamesTimerJob : SPJobDefinition
  12:     {
  13:         public const int LOGON32_LOGON_INTERACTIVE = 2;
  14:         public const int LOGON32_LOGON_SERVICE = 3;
  15:         public const int LOGON32_PROVIDER_DEFAULT = 0;
  16:  
  17:         [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
  18:         public static extern bool LogonUser(
  19:           String lpszUserName,
  20:           String lpszDomain,
  21:           String lpszPassword,
  22:           int dwLogonType,
  23:           int dwLogonProvider,
  24:           ref IntPtr phToken
  25:         );
  26:  
  27:         [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
  28:         public extern static bool CloseHandle(IntPtr handle);
  29:  
  30:         private const string JOB_NAME = "job-set-back-connection-host-names-";
  31:         private const string KEY_USER = "userName";
  32:         private const string KEY_PWD = "password";
  33:  
  34:         private static readonly string jobId = Guid.NewGuid().ToString();
  35:  
  36:         public SetBackConnectionHostNamesTimerJob() : base() { }
  37:  
  38:         /// <summary>
  39:         /// Initializes a new instance of the <see cref="SetBackConnectionHostNamesTimerJob"/> class.
  40:         /// </summary>
  41:         public SetBackConnectionHostNamesTimerJob(SPService service)
  42:             : base(JOB_NAME + jobId, service, null, SPJobLockType.None)
  43:         {
  44:             Title = "Set BackConnectionHostNames Registry Key";
  45:         }
  46:  
  47:         /// <summary>
  48:         /// Executes the job definition.
  49:         /// </summary>
  50:         /// <param name="targetInstanceId">For target types of <see cref="T:Microsoft.SharePoint.Administration.SPContentDatabase"></see> this is the database ID of the content database being processed by the running job. This value is Guid.Empty for all other target types.</param>
  51:         public override void Execute(Guid targetInstanceId)
  52:         {
  53:             string user = Properties[KEY_USER] as string;
  54:             string password = Properties[KEY_PWD] as string;
  55:  
  56:             if (string.IsNullOrEmpty(user) || password == null)
  57:                 throw new ArgumentNullException("Username and password is required.");
  58:  
  59:             if (user.IndexOf('\\') < 0)
  60:                 throw new ArgumentException("Username must be in the form \"DOMAIN\\USER\"");
  61:  
  62:             IntPtr userHandle = new IntPtr(0);
  63:             WindowsImpersonationContext impersonatedUser = null;
  64:  
  65:             bool returnValue = LogonUser(
  66:               user.Split('\\')[1],
  67:               user.Split('\\')[0],
  68:               password,
  69:               LOGON32_LOGON_INTERACTIVE,
  70:               LOGON32_PROVIDER_DEFAULT,
  71:               ref userHandle
  72:               );
  73:  
  74:             if (!returnValue)
  75:             {
  76:                 throw new Exception("Invalid Username");
  77:             }
  78:             WindowsIdentity newId = new WindowsIdentity(userHandle);
  79:             impersonatedUser = newId.Impersonate();
  80:  
  81:             SetBackConnectionHostNames.SetBackConnectionRegKey(SetBackConnectionHostNames.GetUrls());
  82:  
  83:             impersonatedUser.Undo();
  84:             CloseHandle(userHandle);
  85:         }
  86:  
  87:         /// <summary>
  88:         /// Submits the job.
  89:         /// </summary>
  90:         public void SubmitJob(string user, string password)
  91:         {
  92:             Properties[KEY_USER] = user;
  93:             Properties[KEY_PWD] = password;
  94:             Schedule = new SPOneTimeSchedule(DateTime.Now);
  95:             Update();
  96:         }
  97:     }
  98: }

The following is the code for the SetBackConnectionHostNames class:

   1: using System;
   2: using System.Collections;
   3: using System.Collections.Generic;
   4: using System.Collections.Specialized;
   5: using System.Net;
   6: using System.Text;
   7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   8: using Microsoft.SharePoint;
   9: using Microsoft.SharePoint.Administration;
  10: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  11: using Microsoft.Win32;
  12:  
  13: namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
  14: {
  15:     public class SetBackConnectionHostNames : SPOperation
  16:     {
  17:         /// <summary>
  18:         /// Initializes a new instance of the <see cref="SetBackConnectionHostNames"/> class.
  19:         /// </summary>
  20:         public SetBackConnectionHostNames()
  21:         {
  22:             
  23:             SPParamCollection parameters = new SPParamCollection();
  24:             parameters.Add(new SPParam("updatefarm", "uf"));
  25:             parameters.Add(new SPParam("username", "user", false, null, new SPNonEmptyValidator()));
  26:             parameters.Add(new SPParam("password", "pwd", false, null, new SPNullOrNonEmptyValidator()));
  27:  
  28:             StringBuilder sb = new StringBuilder();
  29:             sb.Append("\r\n\r\nSets the BackConnectionHostNames registry key with the URLs associated with each web application.\r\n\r\nParameters:");
  30:             sb.Append("\r\n\t[-updatefarm (update all servers in the farm)]");
  31:             sb.Append("\r\n\t[-username <DOMAIN\\user (must have rights to update the registry on each server)>]");
  32:             sb.Append("\r\n\t[-password <password>]");
  33:  
  34:             Init(parameters, sb.ToString());
  35:         }
  36:  
  37:         #region ISPStsadmCommand Members
  38:  
  39:         /// <summary>
  40:         /// Gets the help message.
  41:         /// </summary>
  42:         /// <param name="command">The command.</param>
  43:         /// <returns></returns>
  44:         public override string GetHelpMessage(string command)
  45:         {
  46:             return HelpMessage;
  47:         }
  48:  
  49:         /// <summary>
  50:         /// Runs the specified command.
  51:         /// </summary>
  52:         /// <param name="command">The command.</param>
  53:         /// <param name="keyValues">The key values.</param>
  54:         /// <param name="output">The output.</param>
  55:         /// <returns></returns>
  56:         public override int Execute(string command, StringDictionary keyValues, out string output)
  57:         {
  58:             output = string.Empty;
  59:  
  60:             if (!Params["updatefarm"].UserTypedIn)
  61:                 SetBackConnectionRegKey(GetUrls());
  62:             else
  63:             {
  64:                 SPTimerService timerService = SPFarm.Local.TimerService;
  65:                 if (null == timerService)
  66:                 {
  67:                     throw new SPException("The Farms timer service cannot be found.");
  68:                 }
  69:                 SetBackConnectionHostNamesTimerJob job = new SetBackConnectionHostNamesTimerJob(timerService);
  70:  
  71:                 string user = Params["username"].Value;
  72:                 if (user.IndexOf('\\') < 0)
  73:                     user = Environment.UserDomainName + "\\" + user;
  74:                 job.SubmitJob(user, Params["password"].Value + "");
  75:  
  76:                 output += "Timer job successfully created.";
  77:             }
  78:  
  79:             return OUTPUT_SUCCESS;
  80:         }
  81:  
  82:         public override void Validate(StringDictionary keyValues)
  83:         {
  84:             base.Validate(keyValues);
  85:  
  86:             if (Params["updatefarm"].UserTypedIn)
  87:             {
  88:                 if (!Params["username"].UserTypedIn)
  89:                     throw new SPSyntaxException("A valid username with rights to edit the registry is required.");
  90:             }
  91:         }
  92:  
  93:         #endregion
  94:  
  95:         public static List<string> GetUrls()
  96:         {
  97:             List<string> urls = new List<string>();
  98:             foreach (SPService svc in SPFarm.Local.Services)
  99:             {
 100:                 if (!(svc is SPWebService))
 101:                     continue;
 102:  
 103:                 foreach (SPWebApplication webApp in ((SPWebService)svc).WebApplications)
 104:                 {
 105:                     
 106:                     foreach (SPAlternateUrl url in webApp.AlternateUrls)
 107:                     {
 108:                         string host = url.Uri.Host.ToLower();
 109:                         if (!urls.Contains(host) && // Don't add if we already have it
 110:                             !url.Uri.IsLoopback && // Quick check to short circuit the more elaborate checks
 111:                             host != Environment.MachineName.ToLower() && // Quick check to short circuit the more elaborate checks
 112:                             IsLocalIpAddress(host) && // If the host name points locally then we need to add it
 113:                             !IsSharePointServer(host)) // Don't add if it matches an SP server name (handles central admin)
 114:                         {
 115:                             urls.Add(host);
 116:                         }
 117:                     }
 118:                 }
 119:             }
 120:             return urls;
 121:         }
 122:  
 123:         private static bool IsSharePointServer(string host)
 124:         {
 125:             foreach (SPServer server in SPFarm.Local.Servers)
 126:             {
 127:                 if (server.Address.ToLower() == host)
 128:                     return true;
 129:             }
 130:             return false;
 131:         }
 132:  
 133:         private static bool IsLocalIpAddress(string host)
 134:         {
 135:             try
 136:             { 
 137:                 IPAddress[] hostIPs = Dns.GetHostAddresses(host);
 138:                 IPAddress[] localIPs = Dns.GetHostAddresses(Dns.GetHostName());
 139:  
 140:                 // test if any host IP equals to any local IP or to localhost
 141:                 foreach (IPAddress hostIP in hostIPs)
 142:                 {
 143:                     // is localhost
 144:                     if (IPAddress.IsLoopback(hostIP)) return true;
 145:                     // is local address
 146:                     foreach (IPAddress localIP in localIPs)
 147:                     {
 148:                         if (hostIP.Equals(localIP)) return true;
 149:                     }
 150:                 }
 151:             }
 152:             catch { }
 153:             return false;
 154:         }
 155:  
 156:         public static void SetBackConnectionRegKey(List<string> urls)
 157:         {
 158:             const string KEY_NAME = "SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0";
 159:             const string KEY_VAL_NAME = "BackConnectionHostNames";
 160:  
 161:             RegistryKey reg = Registry.LocalMachine.OpenSubKey(KEY_NAME, true);
 162:             if (reg != null)
 163:             {
 164:                 string[] existing = (string[])reg.GetValue(KEY_VAL_NAME);
 165:                 if (existing != null)
 166:                 {
 167:                     foreach (string val in existing)
 168:                     {
 169:                         if (!urls.Contains(val.ToLower()))
 170:                             urls.Add(val.ToLower());
 171:                     }
 172:                 }
 173:                 string[] multiVal = new string[urls.Count];
 174:                 urls.CopyTo(multiVal);
 175:                 
 176:                 reg.SetValue(KEY_VAL_NAME, multiVal, RegistryValueKind.MultiString);
 177:             }
 178:             else
 179:             {
 180:                 throw new SPException("Unable to open registry key.");
 181:             }
 182:         }
 183:  
 184:     }
 185: }

There's two core methods, GetUrls and SetBackConnectionRegKey.  The SetBackConnectionRegKey method started out from a bit of sample code that my friend Ben Robb sent me - there's no much of his original code but it saved me some time in trying to remember how to manipulate the registry using C#.  Essentially all this method does is get the current list of host names, add any missing items to the passed in list, and then reset the list (thus avoiding duplicate entries).  The GetUrls method is the more interesting piece - I'm looping through all the Farm's Web Applications and their corresponding Alternate URLs and then building a list of URLs that meet some basic inclusion criteria:

  • Don't add duplicates - you can get duplicates when both HTTP and HTTPS are used so we make sure that we exclude them
  • Don't add loopback URLs - this shouldn't come up but if the URL is localhost or 127.0.0.1 it will be flagged as a loopback URL so we exclude them
  • Don't add URLs that match the server name - if the host name matches the server name then exclude it (this is essentially just a short circuit for the next check which is a bit more thorough)
  • Exclude host names that map to the local IP address - this is the most crucial bit (the previous steps were just short circuits for this step to avoid the additional querying necessary); I use the System.Net.Dns class's static GetHostAddresses method to check the local addresses against those associated with the provided host name
  • Exclude host names that map to SharePoint servers - this step is necessary to address host names such as those belonging to Central Administration

The help for the command is shown below:

C:\>stsadm -help gl-setbackconnectionhostnames

stsadm -o gl-setbackconnectionhostnames


Sets the BackConnectionHostNames registry key with the URLs associated with each web application.
Parameters:
        [-updatefarm (update all servers in the farm)]
        [-username <DOMAIN\user (must have rights to update the registry on each server)>]
        [-password <password>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setbackconnectionhostnames WSS v3, MOSS 2007 Released: 9/20/2009

Parameter Name Short Form Required Description Example Usage
updatefarm uf No If provided then update the BackConnectionHostNames registry key on all servers in the farm. -updatefarm

-uf
username user Yes if updatefarm is provided The username with sufficient rights to update the registry.  If no domain part is specified then the current users domain is used. -username domain\spadmin

-user spadmin
password pwd No If the users password is blank then this parameter is not required (please change your password if this is the case!); otherwise, this parameter is required if the updatefarm parameter is provided. -password pa$$w0rd

-pwd pa$$w0rd

The following is an example of how to update the BackConnectionHostNames registry key on the current server only:

stsadm -o gl-setbackconnectionhostnames

The following is an example of how to update the BackConnectionHostNames registry key on all servers in the farm:

stsadm -o gl-setbackconnectionhostnames -updatefarm -username domain\spadmin -password pa$$w0rd

6Jul/091

Deploying SharePoint Files Not Handled by the WSP Solution Schema

I was working on a project recently where I had to deploy a settings file to the root of my web applications folder (where the web.config file resides).  If you've ever had to do something like this before then you know that you cannot do this declaratively using the WSP's Solution schema.  The Solution schema is really quite limiting as to where you can actually deploy files - as a result your only option is to create a custom Feature that runs some code when executed (because we certainly don't want to go the xcopy route).

To do this we're going to create a custom Feature which contains all the files that we need to copy and then we'll provision a one-time timer job to copy the file to the target location on each server.

Here's our Feature.xml file:

<?xml version="1.0" encoding="utf-8"?>
<Feature
Id="1960C4A0-7A47-42A8-A382-F7A91214BA39"
Title="Settings Provisioner"
Description="This Feature deploys a settings file to a the web application root."
Version="1.0.0.0"
Scope="WebApplication"
Hidden="false"
ReceiverAssembly="MyCustomFeature, Version=1.0.0.0, Culture=neutral, PublicKeyToken=39b13c54ceef5193"
ReceiverClass="MyCustomFeature.FeatureReceivers.SettingsFeatureReceiver" xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementFile Location="Files\settings.config" />
</ElementManifests>
</Feature>

As you can see we are including a "settings.config" file which is located in a folder called "Files" directly under the Feature folder.  You could easily have any number of files here by simply adding additional ElementFile elements.  Also note that we are linking a feature receiver to the Feature which will execute upon activation and deactivation.

Here's our feature receiver class:

   1: public class SettingsFeatureReceiver : SPFeatureReceiver
   2: {
   3:     /// <summary>
   4:     /// Occurs after a Feature is activated.
   5:     /// </summary>
   6:     /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
   7:     public override void FeatureActivated(SPFeatureReceiverProperties properties)
   8:     {
   9:         SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
  10:         try
  11:         {
  12:             TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
  13:             job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
  14:         }
  15:         catch (Exception ex)
  16:         {
  17:             Logger.WriteException(ex);
  18:         }
  19:  
  20:         
  21:     }
  22:  
  23:     /// <summary>
  24:     /// Occurs when a Feature is deactivated.
  25:     /// </summary>
  26:     /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
  27:     public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  28:     {
  29:         try
  30:         {
  31:             TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
  32:             job.SubmitJob(false, properties.Feature.Definition.RootDirectory);
  33:         }
  34:         catch (Exception ex)
  35:         {
  36:             Logger.WriteException(ex);
  37:         }
  38:     }
  39:  
  40:     public override void FeatureInstalled(SPFeatureReceiverProperties properties)
  41:     {
  42:         /* no op */
  43:     }
  44:     public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
  45:     {
  46:         /* no op */
  47:     }
  48: }

Notice that within the FeatureActivated method I'm getting a reference to the SPWebApplication object and passing that to a CopySettingsJob class which is our timer job that will do all the work.  On the FeatureDeactivating event you can see similar code but I'm passing in false instead of true to the Submit method.  The Boolean value indicates whether we are activating or deactivating our Feature.  I'm also passing in the path to the Feature folder in the 12 hive as that is where our source files are located.

Lets look at the timer job class now:

   1: public class CopySettingsJob : SPJobDefinition
   2: {
   3:     private const string KEY_ACTIVATING = "Activating";
   4:     private const string KEY_FEATUREFOLDER = "FeatureFolder";
   5:     private const string JOB_NAME = "job-settings-copy-";
   6:     private static readonly string jobId = Guid.NewGuid().ToString();
   7:  
   8:     public CopySettingsJob() : base() { }
   9:  
  10:     /// <summary>
  11:     /// Initializes a new instance of the <see cref="CopySettingsJob"/> class.
  12:     /// </summary>
  13:     /// <param name="webApp">The web app.</param>
  14:     public CopySettingsJob(SPWebApplication webApp)
  15:         : base(JOB_NAME + jobId, webApp, null, SPJobLockType.None)
  16:     {
  17:         Title = "Copy Settings Job";
  18:     }
  19:  
  20:     /// <summary>
  21:     /// Executes the job definition.
  22:     /// </summary>
  23:     /// <param name="targetInstanceId">For target types of <see cref="T:Microsoft.SharePoint.Administration.SPContentDatabase"></see> this is the database ID of the content database being processed by the running job. This value is Guid.Empty for all other target types.</param>
  24:     public override void Execute(Guid targetInstanceId)
  25:     {
  26:         Logger.WriteInformation(string.Format("Starting {0} timer job.", Name));
  27:  
  28:         try
  29:         {
  30:             string settingsFilePath = Path.Combine(Properties[KEY_FEATUREFOLDER].ToString(), "Files\\Settings.config");
  31:             string targetPath = Path.Combine(WebApplication.IisSettings[SPUrlZone.Default].Path.ToString(), "Settings.config");
  32:             if ((bool)Properties[KEY_ACTIVATING])
  33:             {
  34:                 Logger.WriteInformation(string.Format("Copying file from \"{0}\" to \"{1}\".", settingsFilePath, targetPath));
  35:                 File.Copy(settingsFilePath, targetPath, true);
  36:             }
  37:             else
  38:             {
  39:                 Logger.WriteInformation(string.Format("Deleting file from \"{0}\"", targetPath));
  40:                 File.Delete(targetPath);
  41:             }
  42:         }
  43:         catch (Exception ex)
  44:         {
  45:             Logger.WriteException(ex);
  46:             return;
  47:         }
  48:         Logger.WriteSuccessAudit(string.Format("Timer job {0} completed successfully", Name));
  49:     }
  50:  
  51:     /// <summary>
  52:     /// Submits the job.
  53:     /// </summary>
  54:     /// <param name="activating">if set to <c>true</c> [activating].</param>
  55:     public void SubmitJob(bool activating, string featureFolder)
  56:     {
  57:         Properties[KEY_ACTIVATING] = activating;
  58:         Properties[KEY_FEATUREFOLDER] = featureFolder;
  59:         Schedule = new SPOneTimeSchedule(DateTime.Now);
  60:         Title += " (" + jobId + ")";
  61:         Update();
  62:     }
  63: }

As you can see the code simply stores the Feature folder as a property and then sets a one-time schedule.  When the code runs it copies the source file to the target.  Because we're using an SPJobLockType value of "None" in the constructor the code will execute on every server (set it to "Job" if you want it to run on just the server in which the Feature was actually activated).

Of course the code above isn't very generic as it hard codes the settings.config file which isn't very reusable but I wanted to keep this sample nice and simple.  A better approach would be to require an either an XML file to be stored in the Feature folder and then read by the timer job or have the SubmitJob method take in parameters that describe what files to move and where to move them.

One key thing to remember is that this code will run once on each server for every web application on which the Feature has been activated.  If you need a Farm scoped Feature because perhaps you are copying the noise words file for instance then you'll want to change the constructor of the timer job to take in an SPService object and change the FeatureActivated method as shown below:

   1: try
   2: {
   3:     string featurePath = properties.Feature.Definition.RootDirectory;
   4:  
   5:     SPTimerService timerService = SPFarm.Local.TimerService;
   6:     if (null == timerService)
   7:     {
   8:         throw new SPException("The Farms timer service cannot be found.");
   9:     }
  10:     TimerJobs.CopySettingsJob job = timerService.JobDefinitions.GetValue<TimerJobs.CopySettingsJob>(TimerJobs.CopySettingsJob.JOB_NAME);
  11:     if (null == job)
  12:     {
  13:         job = new TimerJobs.CopySettingsJob(timerService);
  14:     }
  15:     job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
  16: }
  17: catch (Exception ex)
  18: {
  19:     Logger.WriteException(ex);
  20: }

Hopefully this simple example helps you to solve your file deployment challenges.

25Oct/085

A Better execadmsvcjobs STSADM Command

This is something that's been bugging me for a long time - when you run the out of the box execadmsvcjobs command on a server it only ensures that pending jobs on that one server are executed - when it completes it doesn't mean that jobs on other servers in the farm have completed.  This gets real annoying when you are using a script to deploy solution because end up getting errors about pending timer jobs needing to complete.

I tried a couple of different approaches to address this problem - the first was to use WMI to execute the execadmsvcjobs command remotely on each server.  Problem with this approach is that for some reason the security context kept getting to changed to "NT AUTHORITY\ANONYMOUS LOGON" even though the process showed that it was running as my executing account - never figured out what the heck was going on with that so I decided to try a different approach.  The next thing I tried was to reverse engineer the out of the box command and change it to execute all jobs for each server, not just the local server.  This appeared to work but upon further inspection it became clear that it wasn't working at all - there's definitely something going on that gets whacked out when executing this way - so I was left with trying to find another approach.

What I eventually ended up with was a simple command that leveraged what I had done while trying to recreate the out of the box execadmsvcjobs command but instead of executing the job on each server it simply blocks until the jobs have all completed.  It's not exactly what I wanted but the end result is the same - the command blocks my script until the pending jobs have finished on each server thus allowing my subsequent commands to run without error.  The name of this new command is gl-execadmsvcjobs.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   5: using Microsoft.SharePoint;
   6: using Microsoft.SharePoint.Administration;
   7: using System.Threading;
   8:  
   9: namespace Lapointe.SharePoint.STSADM.Commands.TimerJob
  10: {
  11:     public class ExecAdmSvcJobs : SPOperation
  12:     {
  13:         /// <summary>
  14:         /// Initializes a new instance of the <see cref="ExecAdmSvcJobs"/> class.
  15:         /// </summary>
  16:         public ExecAdmSvcJobs()
  17:         {
  18:             SPParamCollection parameters = new SPParamCollection();
  19:             parameters.Add(new SPParam("local", "l"));
  20:  
  21:             StringBuilder sb = new StringBuilder();
  22:             sb.Append("\r\n\r\nExecutes pending timer jobs on all servers in the farm.\r\n\r\n\r\n\r\nParameters:");
  23:             sb.Append("\r\n\t[-local]");
  24:             Init(parameters, sb.ToString());
  25:         }
  26:  
  27:         /// <summary>
  28:         /// Gets the help message.
  29:         /// </summary>
  30:         /// <param name="command">The command.</param>
  31:         /// <returns></returns>
  32:         public override string GetHelpMessage(string command)
  33:         {
  34:             return HelpMessage;
  35:         }
  36:  
  37:         /// <summary>
  38:         /// Executes the specified command.
  39:         /// </summary>
  40:         /// <param name="command">The command.</param>
  41:         /// <param name="keyValues">The key values.</param>
  42:         /// <param name="output">The output.</param>
  43:         /// <returns></returns>
  44:         public override int Execute(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
  45:         {
  46:             output = string.Empty;
  47:  
  48:             Execute(Params["local"].UserTypedIn);
  49:  
  50:             return OUTPUT_SUCCESS;
  51:         }
  52:  
  53:         /// <summary>
  54:         /// Executes the timer jobs.
  55:         /// </summary>
  56:         /// <param name="local">if set to <c>true</c> [local].</param>
  57:         public static void Execute(bool local)
  58:         {
  59:             Execute(local, false);
  60:         }
  61:  
  62:         /// <summary>
  63:         /// Executes the timer jobs.
  64:         /// </summary>
  65:         /// <param name="local">if set to <c>true</c> [local].</param>
  66:         /// <param name="quiet">if set to <c>true</c> [quiet].</param>
  67:         public static void Execute(bool local, bool quiet)
  68:         {
  69:             // First run the OOTB execadmsvcjobs on the local machine to make sure that any local jobs get executed
  70:             if (!quiet)
  71:                 Console.WriteLine("\r\nExecuting jobs on {0}", SPServer.Local.Name);
  72:  
  73:             Utilities.RunStsAdmOperation("-o execadmsvcjobs", quiet);
  74:             // If local was passed in then we're basically just using the OOTB command - I included this for testing only - it's not
  75:             // really helpful otherwise.
  76:             if (!local)
  77:             {
  78:                 foreach (SPServer server in SPFarm.Local.Servers)
  79:                 {
  80:                     // Only look at servers with a valid role.
  81:                     if (server.Role == SPServerRole.Invalid)
  82:                         continue;
  83:  
  84:                     // Don't need to check locally as we just ran the OOTB command locally so skip the local server.
  85:                     if (server.Id.Equals(SPServer.Local.Id))
  86:                         continue;
  87:  
  88:                     bool stillExecuting;
  89:                     if (!quiet)
  90:                         Console.WriteLine("\r\nChecking jobs on {0}", server.Name);
  91:  
  92:                     do
  93:                     {
  94:                         stillExecuting = CheckApplicableRunningJobs(server, quiet);
  95:  
  96:                         // If jobs are still executing then sleep for 1 second.
  97:                         if (stillExecuting)
  98:                             Thread.Sleep(1000);
  99:                     } while (stillExecuting);
 100:                 }
 101:             }
 102:         }
 103:         /// <summary>
 104:         /// Checks for applicable running jobs.
 105:         /// </summary>
 106:         /// <param name="server">The server.</param>
 107:         /// <param name="quiet">if set to <c>true</c> [quiet].</param>
 108:         /// <returns></returns>
 109:         private static bool CheckApplicableRunningJobs(SPServer server, bool quiet)
 110:         {
 111:             foreach (KeyValuePair<Guid, SPService> current in GetProvisionedServices(server))
 112:             {
 113:                 SPService service = current.Value;
 114:                 SPAdministrationServiceJobDefinitionCollection definitions = new SPAdministrationServiceJobDefinitionCollection(service);
 115:                 if (CheckApplicableRunningJobs(server, definitions, quiet))
 116:                     return true; // We've found running jobs so no point looking any further.
 117:  
 118:                 SPWebService service2 = service as SPWebService;
 119:                 if (service2 != null)
 120:                 {
 121:                     foreach (SPWebApplication webApplication in service2.WebApplications)
 122:                     {
 123:                         definitions = new SPAdministrationServiceJobDefinitionCollection(webApplication);
 124:                         if (CheckApplicableRunningJobs(server, definitions, quiet))
 125:                             return true;
 126:                     }
 127:                 }
 128:             }
 129:             return false;
 130:         }
 131:  
 132:         /// <summary>
 133:         /// Checks for applicable running jobs.
 134:         /// </summary>
 135:         /// <param name="server">The server.</param>
 136:         /// <param name="jds">The job definitions to consider.</param>
 137:         /// <param name="quiet">if set to <c>true</c> [quiet].</param>
 138:         /// <returns></returns>
 139:         private static bool CheckApplicableRunningJobs(SPServer server, SPAdministrationServiceJobDefinitionCollection jds, bool quiet)
 140:         {
 141:             bool stillExecuting = false;
 142:  
 143:             foreach (SPJobDefinition definition in jds)
 144:             {
 145:                 if (string.IsNullOrEmpty(definition.Name))
 146:                     continue;
 147:  
 148:                 bool isApplicable = false;
 149:                 if (!definition.IsDisabled)
 150:                     isApplicable = ((definition.Server == null) || definition.Server.Id.Equals(server.Id));
 151:  
 152:                 if (!isApplicable)
 153:                 {
 154:                     // If it's not applicable then we don't really care if it's running or not.
 155:                     continue;
 156:                 }
 157:                 
 158:                 if (!quiet)
 159:                     Console.Write("Waiting on {0}.\r\n", definition.Name);
 160:  
 161:                 stillExecuting = true;
 162:             }
 163:             return stillExecuting;
 164:         }
 165:  
 166:  
 167:         /// <summary>
 168:         /// Gets the provisioned services.
 169:         /// </summary>
 170:         /// <param name="server">The server.</param>
 171:         /// <returns></returns>
 172:         private static Dictionary<Guid, SPService> GetProvisionedServices(SPServer server)
 173:         {
 174:             Dictionary<Guid, SPService> dictionary = new Dictionary<Guid, SPService>(8);
 175:             foreach (SPServiceInstance serviceInstance in server.ServiceInstances)
 176:             {
 177:                 SPService service = serviceInstance.Service;
 178:                 if (serviceInstance.Status == SPObjectStatus.Online)
 179:                 {
 180:                     if (dictionary.ContainsKey(service.Id))
 181:                         continue;
 182:                     dictionary.Add(service.Id, service);
 183:                 }
 184:             }
 185:             return dictionary;
 186:  
 187:         }
 188:  
 189:         /// <summary>
 190:         /// This class mimics the internal equivalent and is used because the base class is abstract.
 191:         /// </summary>
 192:         internal class SPAdministrationServiceJobDefinitionCollection : SPPersistedChildCollection<SPAdministrationServiceJobDefinition>
 193:         {
 194:             /// <summary>
 195:             /// Initializes a new instance of the <see cref="SPAdministrationServiceJobDefinitionCollection"/> class.
 196:             /// </summary>
 197:             /// <param name="service">The service.</param>
 198:             internal SPAdministrationServiceJobDefinitionCollection(SPService service) : base(service)
 199:             {
 200:             }
 201:  
 202:             /// <summary>
 203:             /// Initializes a new instance of the <see cref="SPAdministrationServiceJobDefinitionCollection"/> class.
 204:             /// </summary>
 205:             /// <param name="webApplication">The web application.</param>
 206:             internal SPAdministrationServiceJobDefinitionCollection(SPWebApplication webApplication) : base(webApplication)
 207:             {
 208:             }
 209:         }
 210:  
 211:     }
 212: }

The help for the command is shown below:

C:\>stsadm -help gl-execadmsvcjobs

stsadm -o gl-execadmsvcjobs

Executes pending timer jobs on all servers in the farm.


Parameters:
        [-local]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-execadmsvcjobs WSS v3, MOSS 2007 Released: 10/25/2008

Parameter Name Short Form Required Description Example Usage
local l No If passed in then do not consider other servers in the farm - this basically just treats the command exactly as the out of the box execadmsvcjobs command (in fact it just calls out to that command). -local

-l

The following is an example of how to make sure that all pending timer jobs have run on all servers in the farm:

stsadm -o gl-execadmsvcjobs

14Aug/081

Setting the Audience Compilation Schedule via STSADM

In an effort to wrap up my audience related STSADM commands I created a command that allows me to set the audience compilation schedule via STSADM.  I had to do some disassembling to figure out how to do this and it turned out that the code was virtually identical to what I had done for the gl-setuserprofileimportschedule command.  So it turned out that I was able to create this command by simply coping the code from my other command and then just tweaking a couple lines to load up different class types.  I named the command gl-setaudiencecompilationschedule.  The downside of this code (and the code it's based off of) is that I had to use reflection to get it done as all the classes are marked internally (no idea why).  If anyone knows of a way to do this without all the reflect I'm all ears.

Here's the code - it's ugly, but it works:

   1: #if MOSS
   2: using System;
   3: using System.Collections.Specialized;
   4: using System.Reflection;
   5: using System.Text;
   6: using System.Threading;
   7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   8: using Microsoft.Office.Server;
   9: using Microsoft.Office.Server.UserProfiles;
  10: using Microsoft.SharePoint;
  11: using Microsoft.SharePoint.Administration;
  12: using Microsoft.SharePoint.StsAdmin;
  13: using PropertyInfo=System.Reflection.PropertyInfo;
  14: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  15:  
  16: namespace Lapointe.SharePoint.STSADM.Commands.TimerJob
  17: {
  18:     public class SetAudienceCompilationSchedule : SPOperation
  19:     {
  20:         private enum OccurrenceType
  21:         {
  22:             daily,
  23:             weekly,
  24:             monthly
  25:         }
  26:  
  27:         /// <summary>
  28:         /// Initializes a new instance of the <see cref="SetAudienceCompilationSchedule"/> class.
  29:         /// </summary>
  30:         public SetAudienceCompilationSchedule()
  31:         {
  32:             SPParamCollection parameters = new SPParamCollection();
  33:             parameters.Add(new SPParam("sspname", "ssp", false, null, new SPNonEmptyValidator(), "Please specify the SSP name."));
  34:             parameters.Add(new SPParam("occurrence", "oc", true, null, new SPRegexValidator("^daily$|^weekly$|^monthly$")));
  35:             parameters.Add(new SPParam("hour", "hour", true, null, new SPIntRangeValidator(0, 23)));
  36:             parameters.Add(new SPParam("day", "day", false, null, new SPIntRangeValidator(1, 31)));
  37:             string regex = "^" + string.Join("$|^", Enum.GetNames(typeof (DayOfWeek))) + "$";
  38:             parameters.Add(new SPParam("dayofweek", "dayofweek", false, null, new SPRegexValidator(regex.ToLowerInvariant() + "|" + regex)));
  39:             parameters.Add(new SPParam("enabled", "enabled", false, "true", new SPTrueFalseValidator()));
  40:             parameters.Add(new SPParam("runjob", "run"));
  41:            
  42:             StringBuilder sb = new StringBuilder();
  43:             sb.Append("\r\n\r\nSets the audience compilation schedule.\r\n\r\nParameters:");
  44:             sb.Append("\r\n\t[-sspname <SSP name>]");
  45:             sb.Append("\r\n\t-occurrence <daily|weekly|monthly>");
  46:             sb.Append("\r\n\t-hour <hour to run (0-23)>");
  47:             sb.Append("\r\n\t[-day <the day to run if monthly is specified>]");
  48:             sb.AppendFormat("\r\n\t[-dayofweek <the day of week to run if weekly is specified ({0})>]", string.Join("|", Enum.GetNames(typeof(DayOfWeek))).ToLowerInvariant());
  49:             sb.Append("\r\n\t[-enabled <true|false> (default is true)]");
  50:             sb.Append("\r\n\t[-runjob]");
  51:             Init(parameters, sb.ToString());
  52:         }
  53:  
  54:         #region ISPStsadmCommand Members
  55:  
  56:         /// <summary>
  57:         /// Gets the help message.
  58:         /// </summary>
  59:         /// <param name="command">The command.</param>
  60:         /// <returns></returns>
  61:         public override string GetHelpMessage(string command)
  62:         {
  63:             return HelpMessage;
  64:         }
  65:  
  66:         /// <summary>
  67:         /// Runs the specified command.
  68:         /// </summary>
  69:         /// <param name="command">The command.</param>
  70:         /// <param name="keyValues">The key values.</param>
  71:         /// <param name="output">The output.</param>
  72:         /// <returns></returns>
  73:         public override int Execute(string command, StringDictionary keyValues, out string output)
  74:         {
  75:             output = string.Empty;
  76:  
  77:             
  78:  
  79:             #region Check Arguments
  80:  
  81:             OccurrenceType occurrence = (OccurrenceType)Enum.Parse(typeof(OccurrenceType), Params["occurrence"].Value, true);
  82:             if (occurrence == OccurrenceType.monthly && !Params["day"].UserTypedIn)
  83:             {
  84:                 output = "Please specify the day to run the import.";
  85:                 output += GetHelpMessage(command);
  86:                 return (int)ErrorCodes.SyntaxError;
  87:             }
  88:             if (occurrence == OccurrenceType.weekly && !Params["dayofweek"].UserTypedIn)
  89:             {
  90:                 output = "Please specify the day of week to run the import.";
  91:                 output += GetHelpMessage(command);
  92:                 return (int)ErrorCodes.SyntaxError;
  93:             }
  94:  
  95:             #endregion
  96:  
  97:             string day = Params["day"].Value;
  98:             string dayofweek = Params["dayofweek"].Value;
  99:             string sspname = Params["sspname"].Value;
 100:             int hour = int.Parse(Params["hour"].Value);
 101:             bool enabled = bool.Parse(Params["enabled"].Value);
 102:             bool runJob = Params["runjob"].UserTypedIn;
 103:             if (!enabled && runJob)
 104:                 throw new SPSyntaxException("The runjob parameter cannot be specified when enabled is set to false.");
 105:  
 106:             ServerContext current;
 107:             if (Params["sspname"].UserTypedIn)
 108:                 current = ServerContext.GetContext(sspname);
 109:             else
 110:                 current = ServerContext.Default;
 111:  
 112:             // What follows is a whole lot of reflection which is required in order to get the SPScheduledJob object.
 113:             // Problem is that the only way to get the correct instance of this object is to use several internal
 114:             // classes, methods, and properties - why on earth these were not made public is absolutely beyond me!
 115:  
 116:             // The bulk of the reflection is recreating the following which was taken from 
 117:             // Microsoft.SharePoint.Portal.UserProfiles.AdminUI.Sched.InitializeComponent().
 118:             // Once we have the job objects we can start setting properties.
 119:             /*
 120:             private void InitializeComponent()
 121:             {
 122:                 ServerContext current = ServerContext.Current;
 123:                 UserProfileApplication userProfileApplication = current.UserProfileApplication;
 124:                 try
 125:                 {
 126:                     using (PortalApplication.BeginSecurityContext())
 127:                     {
 128:                         JobSchedulerSharedApplicationCollection applications = new JobSchedulerSharedApplicationCollection(SPFarm.Local.Services.GetValue<JobSchedulerService>(string.Empty));
 129:                         JobSchedulerSharedApplication sharedApplication = (JobSchedulerSharedApplication) applications[current.SharedResourceProvider];
 130:                         ScheduledJobCollection jobs = new ScheduledJobCollection(sharedApplication);
 131:                         this.AudienceCompileScheduler.Job = jobs[userProfileApplication.AudienceCompilationJobId];
 132:                     }
 133:                 }
 134:                 catch (Exception)
 135:                 {
 136:                     throw;
 137:                 }
 138:             }
 139:             */
 140:  
 141:             // UserProfileApplication userProfileApplication = current.UserProfileApplication;
 142:             object userProfileApplication = Utilities.GetPropertyValue(current, "UserProfileApplication");
 143:  
 144:             // The SSP is locked down so we need to use reflection to get at it.
 145:             object sharedResourceProvider = Utilities.GetSharedResourceProvider(current);
 146:  
 147:             // JobSchedulerService jobSchedulerService = SPFarm.Local.Services.GetValue(typeof(JobSchedulerService));
 148:             Type jobSchedulerServiceType = Type.GetType("Microsoft.Office.Server.Administration.JobSchedulerService, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 149:  
 150:  
 151:             MethodInfo getValue =
 152:                 SPFarm.Local.Services.GetType().GetMethod("GetValue",
 153:                                                           BindingFlags.NonPublic | BindingFlags.Public |
 154:                                                           BindingFlags.Instance | BindingFlags.InvokeMethod, null, new Type[] {typeof(Type), typeof(string)}, null);
 155:  
 156:             object jobSchedulerService = getValue.Invoke(SPFarm.Local.Services,
 157:                                                           new object[]
 158:                                                               {
 159:                                                                   jobSchedulerServiceType, string.Empty
 160:                                                               });
 161:  
 162:  
 163:             // JobSchedulerSharedApplicationCollection application = new JobSchedulerSharedApplicationCollection(jobSchedulerServiceType);
 164:             Type jobSchedulerSharedApplicationCollectionType = Type.GetType("Microsoft.Office.Server.Administration.JobSchedulerSharedApplicationCollection, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 165:  
 166:             ConstructorInfo jobSchedulerSharedApplicationCollectionConstructor =
 167:                 jobSchedulerSharedApplicationCollectionType.GetConstructor(
 168:                     BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
 169:                     null,
 170:                     new Type[] {jobSchedulerService.GetType()}, null);
 171:             object applications = jobSchedulerSharedApplicationCollectionConstructor.Invoke(new object[] { jobSchedulerService });
 172:  
 173:             // JobSchedulerSharedApplication jobSchedulerSharedApplication = applications[sharedResourceProvider];
 174:             PropertyInfo itemProp = applications.GetType().GetProperty("Item",
 175:                                                                      BindingFlags.NonPublic |
 176:                                                                      BindingFlags.Instance |
 177:                                                                      BindingFlags.InvokeMethod |
 178:                                                                      BindingFlags.GetProperty |
 179:                                                                      BindingFlags.Public);
 180:             object jobSchedulerSharedApplication = itemProp.GetValue(applications, new object[] { sharedResourceProvider });
 181:  
 182:  
 183:             //ScheduledJobCollection scheduledJobCollection = new ScheduledJobCollection(sharedApplication);
 184:             Type scheduledJobCollectionType = Type.GetType("Microsoft.Office.Server.Administration.ScheduledJobCollection, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 185:             ConstructorInfo scheduledJobCollectionConstructor =
 186:                 scheduledJobCollectionType.GetConstructor(
 187:                     BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
 188:                     null,
 189:                     new Type[] {jobSchedulerSharedApplication.GetType()}, null);
 190:             object scheduledJobCollection = scheduledJobCollectionConstructor.Invoke(new object[] { jobSchedulerSharedApplication });
 191:  
 192:  
 193:             // userProfileApplication.AudienceCompilationJobId
 194:             Guid audienceCompilationJobId = (Guid)Utilities.GetPropertyValue(userProfileApplication, "AudienceCompilationJobId");
 195:  
 196:  
 197:  
 198:             // ScheduledJob compilationJob = scheduledJobCollection[audienceCompilationJobId];
 199:             itemProp = scheduledJobCollection.GetType().GetProperty("Item",
 200:                                                                     BindingFlags.NonPublic |
 201:                                                                     BindingFlags.Instance |
 202:                                                                     BindingFlags.InvokeMethod |
 203:                                                                     BindingFlags.GetProperty |
 204:                                                                     BindingFlags.Public);
 205:             object compilationJob = itemProp.GetValue(scheduledJobCollection, new object[] { audienceCompilationJobId });
 206:  
 207:  
 208:             PropertyInfo scheduleProp = compilationJob.GetType().GetProperty("Schedule",
 209:                                                                             BindingFlags.FlattenHierarchy |
 210:                                                                             BindingFlags.NonPublic |
 211:                                                                             BindingFlags.Instance |
 212:                                                                             BindingFlags.InvokeMethod |
 213:                                                                             BindingFlags.GetProperty |
 214:                                                                             BindingFlags.Public);
 215:  
 216:             MethodInfo update =
 217:                 compilationJob.GetType().GetMethod("Update",
 218:                                                   BindingFlags.NonPublic | 
 219:                                                   BindingFlags.Public |
 220:                                                   BindingFlags.Instance | 
 221:                                                   BindingFlags.InvokeMethod |
 222:                                                   BindingFlags.FlattenHierarchy, 
 223:                                                   null,
 224:                                                   new Type[] {typeof (bool)}, null);
 225:             
 226:             // Woohoo!!! We are finally at a point where we can actually set the schedule - what a pain the @$$ that was!!!
 227:             SPSchedule schedule;
 228:             
 229:             if (occurrence == OccurrenceType.daily)
 230:             {
 231:                 schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleDaily(hour);
 232:             }
 233:             else if (occurrence == OccurrenceType.weekly)
 234:             {
 235:                 schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleWeekly((DayOfWeek)Enum.Parse(typeof(DayOfWeek), dayofweek, true), hour);
 236:             }
 237:             else if (occurrence == OccurrenceType.monthly)
 238:             {
 239:                 schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleMonthly(int.Parse(day), hour);
 240:             }
 241:             else
 242:                 throw new Exception("Unknown occurance type.");
 243:  
 244:             Type scheduledJobType = Type.GetType("Microsoft.Office.Server.Administration.ScheduledJob, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 245:  
 246:  
 247:             // fullImportJob.Schedule = schedule;
 248:             scheduleProp.SetValue(compilationJob, schedule, null);
 249:  
 250:             // fullImportJob.Enabled = enabled;
 251:             Utilities.SetPropertyValue(compilationJob, scheduledJobType, "Disabled", !enabled);
 252:  
 253:             // fullImportJob.Update(true);
 254:             update.Invoke(compilationJob, new object[] { true });
 255:  
 256:             if (runJob)
 257:             {
 258:                 // fullImportJob.Execute();
 259:                 Utilities.ExecuteMethod(compilationJob, "Execute", new Type[] { }, new object[] { });
 260:             }
 261:  
 262:             if (runJob)
 263:             {
 264:                 // We want to wait until the import is finished before moving on in case we are being run in a batch that requires this to complete before continueing.
 265:                 UserProfileConfigManager manager = new UserProfileConfigManager(current);
 266:                 while (manager.IsImportInProgress())
 267:                     Thread.Sleep(500);
 268:             }
 269:  
 270:  
 271:             return OUTPUT_SUCCESS;
 272:         }
 273:  
 274:         #endregion
 275:  
 276:     }
 277: }
 278: #endif

The help for the command is shown below:

C:\>stsadm -help gl-setaudiencecompilationschedule

stsadm -o gl-setaudiencecompilationschedule


Sets the audience compilation schedule.

Parameters:
        [-sspname <SSP name>]
        -occurrence <daily|weekly|monthly>
        -hour <hour to run (0-23)>
        [-day <the day to run if monthly is specified>]
        [-dayofweek <the day of week to run if weekly is specified (sunday|monday|tuesday|wednesday|thursday|friday|saturday)>]
        [-enabled <true|false> (default is true)]
        [-runjob]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setaudiencecompilationschedule MOSS 2007 Release: 8/14/2008

Parameter Name Short Form Required Description Example Usage
sspname ssp No The name of the SSP that the audiences to compile are associated with.  If omitted the default SSP will be used. -sspname SSP1

-ssp SSP1
occurrence oc Yes Specifies how frequently the compilation should occur.  Valid values are "daily", "weekly", and "monthly". -occurrence daily

-oc monthly
hour   Yes The hour in which to run the compilation.  This should be an integer between 0 and 23 (where 0 is 12:00am and 23 is 11:00pm). -hour 22
day   No, unless occurrence is monthly The day of the month to run the compilation job.  Valid values are between 1 and 31. -day 1
dayofweek   No, unless occurrence is weekly The day of the week to run the compilation job.  Valid values are "sunday", "monday", "tuesday", "wednesday", "thursday", and "saturday". -dayofweek saturday
enabled   No "true" to enable the compilation schedule, "false" to disable it.  If not specified then the compilation schedule will be enabled. -enabled true
runjob run No If specified then the compilation job will be immediately executed after setting the schedule. -runjob

-run

The following is an example of how to set the compilation schedule to run every Satruday at 10:00pm:

stsadm -o gl-setaudiencecompilationschedule -occurrence weekly -hour 22 -dayofweek saturday -enabled true -runjob

18May/086

Profile Import Timer Job

I was recently trying to debug some issues that I was having with the people picker that is shown when creating audiences and I found that I needed a way to manually trigger the distribution list import quickly but I didn't always want to have to wait for the user profile import to finish.  If you run an import using the SSP admin site you might notice that it imports all the user profile information and then it imports the distribution list information but there's no way (at least that I could find) to do just the distribution list.  So I created a new command which I called gl-runprofileimportjob which allows me to run the profile import.

Fortunately it turned out that the code to do this is really simple - you just need an instance of the UserProfileConfigManager and you call either StartImport() or StartDLImport().  To keep the command from exiting before the import is complete I check the IsImportInProgress method and loop until it is set to false:

   1: public class RunProfileImportJob : SPOperation
   2: {
   3:     /// <summary>
   4:     /// Initializes a new instance of the <see cref="RunProfileImportJob"/> class.
   5:     /// </summary>
   6:     public RunProfileImportJob()
   7:     {
   8:         SPParamCollection parameters = new SPParamCollection();
   9:         parameters.Add(new SPParam("sspname", "ssp", false, null, new SPNonEmptyValidator()));
  10:         parameters.Add(new SPParam("distributionlistonly", "dl"));
  11:         parameters.Add(new SPParam("incremental", "inc"));
  12:  
  13:         StringBuilder sb = new StringBuilder();
  14:         sb.Append("\r\n\r\nExecutes a Profile Import.\r\n\r\nParameters:");
  15:         sb.Append("\r\n\t-sspname <SSP Name>");
  16:         sb.Append("\r\n\t[-distributionlistonly]");
  17:         sb.Append("\r\n\t[-incremental]");
  18:  
  19:         Init(parameters, sb.ToString());
  20:     }
  21:  
  22:     /// <summary>
  23:     /// Gets the help message.
  24:     /// </summary>
  25:     /// <param name="command">The command.</param>
  26:     /// <returns></returns>
  27:     public override string GetHelpMessage(string command)
  28:     {
  29:         return HelpMessage;
  30:     }
  31:  
  32:     /// <summary>
  33:     /// Executes the specified command.
  34:     /// </summary>
  35:     /// <param name="command">The command.</param>
  36:     /// <param name="keyValues">The key values.</param>
  37:     /// <param name="output">The output.</param>
  38:     /// <returns></returns>
  39:     public override int Execute(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
  40:     {
  41:         output = string.Empty;
  42:  
  43:         string sspName = Params["sspname"].Value;
  44:         bool dlOnly = Params["distributionlistonly"].UserTypedIn;
  45:         bool incremental = Params["incremental"].UserTypedIn;
  46:  
  47:         UserProfileConfigManager manager = new UserProfileConfigManager(ServerContext.GetContext(sspName));
  48:         if (!manager.IsImportInProgress())
  49:         {
  50:             if (dlOnly)
  51:                 manager.StartDLImport();
  52:             else
  53:                 manager.StartImport(incremental);
  54:  
  55:             Console.Write("Executing import...");
  56:             while (manager.IsImportInProgress())
  57:             {
  58:                 Thread.Sleep(500);
  59:                 Console.Write(".");
  60:             }
  61:             Console.WriteLine();
  62:             Console.WriteLine();
  63:         }
  64:         else
  65:         {
  66:             Console.WriteLine("Import is already running.");
  67:         }
  68:  
  69:         return OUTPUT_SUCCESS;
  70:     }
  71:  
  72: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-runprofileimportjob

stsadm -o gl-runprofileimportjob


Executes a Profile Import.

Parameters:
        -sspname <SSP Name>
        [-distributionlistonly]
        [-incremental]

Here's an example of how to run the command to import just the distribution lists:

stsadm -o gl-runprofileimportjob -sspname SSP1 -distributionlistonly

Note that the "-incremental" flag is only relevant when the "-distributionlistonly" flag is not provided.

7Sep/070

Enumerate Timer Job Definitions

I created this particular command because I needed to get some additional information about service level timer jobs. If you go to Central Admin -> Operations -> Time Job Definitions (or status) you'll see all the timer jobs for the farm. This includes those associated with a particular web application and a service. I already had a command to get all the jobs for a web app (GetJobInfos) however this command didn't give me all the information I was looking for (and I did not author this command so I wanted to keep changes to it minimal though in the end I did end up having to modify it slightly).

So this command, gl-enumtimerjobdefinitions, won't be entirely useful to most people - I'm just outputting more information than the other and the results are in XML rather than flat text (I suppose if someone wanted to consume the XML output then you could do other programmatic things with that).

The code for this is pretty simplistic. I've got two primary methods which take care of adding either a service's jobs or a web app's jobs and then a third which determines which of the other two should be called. There's also a fourth method which just takes a job and produces the XML nodes for that job. The core code is shown below:

   1: /// Adds the service.
   2: /// 
   3: /// The service.
   4: /// The XML doc.
   5: private static void AddService(SPService service, XmlDocument xmlDoc)
   6: {
   7:     SPWebService service2;
   8:  
   9:     foreach (SPJobDefinition job in service.JobDefinitions)
  10:     {
  11:         Helper.GetJobInformation(job, xmlDoc);
  12:     }
  13:     service2 = service as SPWebService;
  14:     if (service2 == null)
  15:     {
  16:         return;
  17:     }
  18:     foreach (SPWebApplication application in service2.WebApplications)
  19:     {
  20:         AddWebApplication(application, xmlDoc);
  21:     }
  22: }
  23:  
  24: /// 
  25: /// Adds the web application.
  26: /// 
  27: /// The webapp.
  28: /// The XML doc.
  29: private static void AddWebApplication(SPWebApplication webapp, XmlDocument xmlDoc)
  30: {
  31:     foreach (SPJobDefinition job in webapp.JobDefinitions)
  32:     {
  33:         Helper.GetJobInformation(job, xmlDoc);
  34:     }
  35: }
  36:  
  37:  
  38: /// 
  39: /// Builds the XML doc.
  40: /// 
  41: /// The mode.
  42: /// The service.
  43: /// The web app.
  44: public static XmlDocument BuildXmlDoc(JobSelectionMode mode, SPService service, SPWebApplication webApp)
  45: {
  46:     XmlDocument xmlDoc = new XmlDocument();
  47:     xmlDoc.AppendChild(xmlDoc.CreateElement("Jobs"));
  48:  
  49:     switch (mode)
  50:     {
  51:         case JobSelectionMode.Farm:
  52:             foreach (SPService temp in SPFarm.Local.Services)
  53:             {
  54:                 AddService(temp, xmlDoc);
  55:             }
  56:             break;
  57:         case JobSelectionMode.Service:
  58:             if (service == null)
  59:             {
  60:                 break;
  61:             }
  62:             AddService(service, xmlDoc);
  63:             break;
  64:         case JobSelectionMode.WebApplication:
  65:             if (webApp != null)
  66:             {
  67:                 AddWebApplication(webApp, xmlDoc);
  68:             }
  69:             break;
  70:     }
  71:     return xmlDoc;
  72: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-enumtimerjobdefinitions

stsadm -o gl-enumtimerjobdefinitions

Displays Information about the all Timer Jobs for the Farm, Service, or Web Application.

Parameters:
        [-scope <Farm (default) | Service | WebApplication>]
        [-serviceid <ID of the service if Service is specified for scope>]
        [-webappurl <URL of the web application if WebApplication is specified for scope>]

Here’s an example of how to return the timer jobs for the entire farm:

stsadm –o gl-enumtimerjobdefinitions

The results of running the above command are rather lengthy so I'll only show what one particular job looks like:

   1: <Jobs>
   2:   <Job Id="5e8eadcc-95f7-4a36-acf5-e30641d15d63" Title="Workflow Failover" Name="job-workflow-failover">
   3:     <Service>
   4:       <Name></Name>
   5:       <Id>d8952551-e4a4-4b0f-b2d9-5a1fce3de025</Id>
   6:       <DisplayName></DisplayName>
   7:       <TypeName>Windows SharePoint Services Web Application</TypeName>
   8:     </Service>
   9:     <WebApplication>
  10:       <Name>Blogs - 80</Name>
  11:       <Id>527ebc95-750b-45e2-995b-001aaaf9636c</Id>
  12:     </WebApplication>
  13:     <DisplayName>job-workflow-failover</DisplayName>
  14:     <IsDisabled>False</IsDisabled>
  15:     <LastRunTime>9/7/2007 4:15:40 PM</LastRunTime>
  16:     <LockType>ContentDatabase</LockType>
  17:     <Retry>False</Retry>
  18:     <Status>Online</Status>
  19:     <TypeName>Microsoft.SharePoint.Administration.SPWorkflowFailOverJobDefinition</TypeName>
  20:     <Version>5548</Version>
  21:     <ScheduleTypeName>Microsoft.SharePoint.SPMinuteSchedule</ScheduleTypeName>
  22:     <NextOccurrenceBasedOnNow>9/7/2007 4:45:42 PM</NextOccurrenceBasedOnNow>
  23:     <Schedule>
  24:       <BeginSecond>0</BeginSecond>
  25:       <EndSecond>59</EndSecond>
  26:       <Interval>15</Interval>
  27:     </Schedule>
  28:   </Job>
  29: </Jobs>

6Sep/070

Site Directory Links Scan

I'm starting to work on getting our Site Directory configured and the first thing I noticed after performing my test upgrade was there were tons of links in the site directory to dead sites. This is because in SPS2003 there was no mechanism (that I'm aware of at least) to clear out dead items in the list. With MOSS there's now a timer job that can be configured to clean up the site directory.

I decided that I wanted to set this up as part of my upgrade script (which of course meant another new command which I called gl-setsitedirectoryscanviewurls). Setting this up can be done pretty easily via the browser by using the central admin tool (Central Administration > Operations > Site Directory Links Scan). I took all of the code from what I disassembled using Reflector (see One of the things I needed my upgrade script to do was to set the master site directory. This can be done easily enough using the central admin tool (Central Administration > Operations > Site Directory Settings).

I took most of my code from what I disassembled using Reflector (Microsoft.SharePoint.Portal.SiteAdmin.LinksCheckerJobSettings). I decided that I've grown tired of trying to work around the fact that Microsoft has not made more methods and constructors public and so I decided to just use reflection to mimic what the LinksCheckerJobSettings class does thus avoiding a lot of headaches and shortening my development time considerably (of course there's always the risk that MS changes these methods but I feel safe with these particular objects). Basically all I'm doing is instantiating a LinksCheckerJob object and setting the appropriate values.

   1: SPTimerService timerService = SPFarm.Local.TimerService;
   2: if (null == timerService)
   3: {
   4:     throw new LinksCheckerException("Links Checker Timer Service Not Found.");
   5: }
   6:  
   7: LinksCheckerJob job = timerService.JobDefinitions.GetValue("SPS-SiteDirectoryLinksChecker");
   8: if (null == job)
   9: {
  10:     // As I've grown tired of trying to get around the limitations of working with only public accessors I've decided to call the 
  11:     // internal accessors so this code now works exactly as the LinksCheckerJobSettings.SaveViewUrls() method which is what is called
  12:     // when you adjust the settings via the browser.
  13:  
  14:     //job = new LinksCheckerJob(timerService);
  15:     ConstructorInfo nonPublicConstructorInfo = typeof(LinksCheckerJob).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, new Type[] { typeof(SPTimerService) }, null);
  16:     job = (LinksCheckerJob)nonPublicConstructorInfo.Invoke(new object[] { timerService });
  17:  
  18:  
  19:     //job.SetDefaults();
  20:     MethodInfo setDefaults =
  21:         typeof(LinksCheckerJob).GetMethod("SetDefaults", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod);
  22:     setDefaults.Invoke(job, new object[] {});
  23: }
  24: job.SiteDirectoryConfig = urls;
  25: job.IsMetaDataSyncEnabled = updateSiteProperties;
  26: job.Update();

The syntax of the command can be seen below.

C:\>stsadm -help gl-setsitedirectoryscanviewurls

stsadm -o gl-setsitedirectoryscanviewurls

Sets the site directory links scan job options.

Parameters:
        -urls <views to scan (separate multiple views with a comma)>
        -updatesiteproperties <true | false>

Here’s an example of how to set the site directory links scan properties:

stsadm –o gl-setsitedirectoryscanviewurls –urls "http://intranet/sitedirectory/siteslist/allitems.aspx" –updatesiteproperties true

9Aug/075

Set User Profile Import Schedule

Update 9/18/2007: I've modified this command so that it no longer manipulates the database directly. The content below has been updated to reflect the changes.

This particular command which I called gl-setuserprofileimportschedule, really drove me nuts. As far as I could find there is no way to set this information using any Microsoft provided public API. If you disassemble the code that is doing this you'll find lots of great classes that allow programmatic manipulation of this as well as other SSP and Profile related configurations - unfortunately those classes are all marked internal so we can't use them easily.

The two main ones are UserProfileApplication and SharedResourcesProvider. Microsoft uses these two classes for most of the more complex configuration settings. Because manipulating the database directly is not supported by Microsoft I chose to rewrite this command from it's original incarnation so that I now utilize the internal classes, methods, and properties that Microsoft is using when setting the schedule via the browser. Keep in mind that this approach is also not supported by Microsoft but it is their recommended approach over manipulating the database and is generally less frowned upon.

The syntax of the command can be seen below (note that if you were using this command prior to 9/18/2007 then the syntax has changed):

C:\>stsadm -help gl-setuserprofileimportschedule

stsadm -o gl-setuserprofileimportschedule


Sets the profile import schedule.

Parameters:
        -sspname <SSP name>
        -type <incremental|full>
        -occurrence <daily|weekly|monthly>
        -hour <hour to run (0-23)>
        [-day <the day to run if monthly is specified>]
        [-dayofweek <the day of week to run if weekly is specified (sunday|monday|tuesday|wednesday|thursday|friday|saturday)>]
        [-enabled <true|false> (default is true)]
        [-runjob]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setuserprofileimportschedule MOSS 2007 Release: 8/9/2007
Updated: 8/14/2008

Parameter Name Short Form Required Description Example Usage
sspname ssp No The name of the SSP that the user profiles are associated with.  If omitted the default SSP will be used. -sspname SSP1

-ssp SSP1
type t Yes The type of schedule to set.  Valid values are "incremental" and "full". -type full

-t full
occurrence oc Yes Specifies how frequently the import should occur.  Valid values are "daily", "weekly", and "monthly". -occurrence daily

-oc monthly
hour   Yes The hour in which to run the import job.  This should be an integer between 0 and 23 (where 0 is 12:00am and 23 is 11:00pm). -hour 22
day   No, unless occurrence is monthly The day of the month to run the import job.  Valid values are between 1 and 31. -day 1
dayofweek   No, unless occurrence is weekly The day of the week to run the import job.  Valid values are "sunday", "monday", "tuesday", "wednesday", "thursday", and "saturday". -dayofweek saturday
enabled   No "true" to enable the import schedule, "false" to disable it.  If not specified then the import schedule will be enabled. -enabled true
runjob run No If specified then the import job will be immediately executed after setting the schedule. -runjob

-run

Here’s an example of how to set the full import schedule to every Saturday at 3:00AM:

stsadm –o gl-setuserprofileimportschedule –sspname SSP1 -type full -occurrence weekly -hour 3 -dayofweek Saturday

Please note that because this command uses internal only classes, methods, and properties directly it could, according to Microsoft, put your environment into an un-supported state (though this is Microsoft's recommended approach over directly manipulating the database). Please make sure you understand what the command is doing and what your support options with Microsoft are.