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
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.
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
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
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>
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