January 2010 - Posts
This is a continuation from
Deployment of Theme and Resource files
Resource File Deployment (Resx)
The second item was deploying Resource files to the App_GlobalResource directory present as a subdirectory under each IIS site for each SharePoint Web Application zone. Remember that you can have multiple IIS Sites for each “Logical” SharePoint Web Application.
The other requirement we had was that each Web Application needed to have its own set of resource files and were to be deployed and scoped as a Web Application feature.
The way the Timer job for resource was written was it would take a list of full file paths along with the target Web Application. So, it’s the responsibility of the Feature code when it’s calling the CreateJob method to get that list of files, populate the string[] of files, and also obtain the target Web Application instance on the call. All of these things are done in the feature activation code – which, I’ve left out of this sample just for brevity.
The CreateJob method as mentioned is below:
/// <summary>
/// Submit a App_GlobalResources copy job providing a list of files with full path
/// </summary>
/// <param name="webApp"></param>
/// <param name="files"></param>
public static void CreateJob(SPWebApplication webApp, string[] files)
{
try
{
// Create new job
GlobalResourceDeployJob grdj = new GlobalResourceDeployJob(webApp, files);
grdj.SubmitJobNow(JOBNAME);
}
catch (Exception ex)
{
LogHelper.Log("Failed to create job {0} - {1}", JOBNAME, ex.Message);
throw;
}
}
Pretty straight forward, and the constructor is as follows:
/// <summary>
/// Here as it's required by the platform
/// </summary>
public GlobalResourceDeployJob() : base() { }
/// <summary>
/// Primary constructor used
/// </summary>
/// <param name="webApp"></param>
/// <param name="files"></param>
public GlobalResourceDeployJob(SPWebApplication webApp, string[] files)
: base(JOBNAME, webApp, null, SPJobLockType.None)
{
_files = files;
}
Again, no big deal..
The Execute override does most of the work as follows:
/// <summary>
/// Main method to actualy do the copy of the files
/// </summary>
/// <param name="targetInstanceId"></param>
public override void Execute(Guid targetInstanceId)
{
try
{
this.Log("starting job {0}", JOBNAME);
SPWebApplication webApp = this.Parent as SPWebApplication;
if (webApp == null)
{
this.Log("job {0} - invalide WebApp found (isNull)", JOBNAME);
}
foreach (SPUrlZone zone in webApp.IisSettings.Keys)
{
// The settings of the IIS application to update
SPIisSettings oSettings = webApp.IisSettings[zone];
// Determine the destination path
string destPath = Path.Combine(oSettings.Path.ToString(), "App_GlobalResources");
foreach (var filePath in _files)
{
string fileName = Path.GetFileName(filePath);
if (File.Exists(filePath))
this.Log("File {0} exists - will be overwriting", fileName);
if ( !File.Exists(filePath ))
{
this.Log("File {0} to copy doesn't exist at the source - skipping", filePath);
continue;
}
string destination = Path.Combine(destPath, fileName);
this.Log("Copying filePath {0} to {1}", filePath, destination);
try
{
File.Copy(filePath, destination, true);
}
catch (Exception ex)
{
this.Log("Err - couldn't copy file {0} with exception {1}", filePath, ex.Message);
}
}
this.Log("job {0} - For WebApp {1}: zone path: {2}", JOBNAME, webApp.Name, oSettings.Path);
}
this.Log("ending job {0}", JOBNAME);
}
catch (Exception ex)
{
this.Log("Failed Job {0} : {1}", JOBNAME, ex.Message);
throw;
}
}
An finally, to make life a little easier, A couple of extension methods that i use:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint;
namespace SharePointJobs
{
public static class TimerJobExtensions
{
public static void SubmitJobNow(this SPJobDefinition jobDef, string jobName)
{
if (jobDef.Farm.TimerService.Instances.Count < 1)
{
jobDef.Log("Could not find TimerService on this box");
throw new SPException("Could not run job. Timer service not found.");
}
foreach (SPJobDefinition job in jobDef.Service.JobDefinitions)
if (job.Name == jobName)
{
jobDef.Log("Found existing job {0}- deleteing", jobName);
job.Delete();
jobDef.Log("Done deleting existing job {0}", jobName);
}
jobDef.Log("Scheduling job {0}", jobName);
jobDef.Schedule = new SPOneTimeSchedule(DateTime.Now);
jobDef.Update();
}
public static void Log(this SPJobDefinition jobDef, string message)
{
LogHelper.Log(jobDef.Name + ":" + message);
}
public static void Log(this SPJobDefinition jobDef, string message, params object[] args)
{
LogHelper.Log(jobDef.Name + ":" + message, args);
}
}
}
Again, the source for the sample is below, it just contains the Timer Jobs, no feature code (it’s simple to wire it up) and a WinForm app for submitting the jobs.
SharePointTimerJobSlnV3.zip
Enjoy…
Again, thanks to the following:
http://solutionizing.net/2008/07/09/updating-spthemesxml-through-a-feature/
http://www.devexpertise.com/2009/02/11/installing-a-theme-as-a-sharepoint-feature/
http://alonsorobles.com/2008/06/26/packaging-branding-and-themes-for-deployment-in-sharepoint-environments/
Thanks to Joe McPeak, Ketan Shah for feedback, discussions…
Recently, we had a deployment scenario where we needed to deploy a custom theme and some resource files (resx) to the Farm and the Web Application zones respectively.
Theme Deployment via Feature / Timer Job
For the first feature, deployment of a theme, we initially went down the path assuming that we could scope the feature at Farm, and SharePoint would call the FeatureInstalled method in our Feature Receiver. Unfortunately, this all worked in development on our single machine environments. When it came time to deploy to a multi-server SharePoint environment we found that the FeatureInstalled event NEVER fired on all machines; however, the FeatureDeactivating and FeatureUninstalling fired every time when we de-activated/retracted the solution respectively. Even the FeatureActivated didn’t fire on all machines.
While I’ve been told and seen many instances on blogs and internal messages that FeatureInstalled should be firing on all the machines, I just never saw it happen (I had added lots of logging and saw nothing in the ULS logs) – this was tried on a couple of Farms.
So, in comes the timer job. The more I picked apart how jobs are created in the SharePoint timer jobs, the more it was evident that was the only solution that would be reliable and this issue could be put to bed.
So, the feature definition is scoped to Farm, and pointing to a simple Feature Receiver that on Activation:
<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
Id="604C0C13-D59B-4cd4-BCD2-A90F8AF2266C"
SolutionId="db6b8735-a4a0-47dc-a012-64e85373f150"
Title="Theme to deploy"
Description="Provides deployment of Theme to the Farm"
Hidden="FALSE"
ActivateOnDefault="FALSE"
Scope="Farm"
ReceiverAssembly="..."
ReceiverClass ="...."
Version="1.0.0.0">
<Properties>
<Property Key="TemplateId" Value="THEMESAMPLE" />
<Property Key="DisplayName" Value="Theme Sample"/>
<Property Key="Description" Value="Theme Sample"/>
<Property Key="Thumbnail" Value="images/thwheat.gif"/>
<Property Key="Preview" Value="images/thwheat.gif"/>
</Properties>
</Feature>
The FeatureActivated method just creates a custom type that contains the properties, then makes a call into the custom timer job’s static method to CreateJob, which is as follow:
/// <summary>
/// Call this to create the job - set the properties as required
/// If you want to delete (remove) the theme setting from the SPTHEMES.xml file pass in
/// delete == true and the TemplateID
/// </summary>
/// <param name="jobProperties"></param>
public static void CreateJob(ThemeDeploymentJobProperties jobProperties)
{
LogHelper.Log("creating job {0}", JOBNAME);
SPWebService wsvc = SPFarm.Local.Services.GetValue<SPWebService>();
foreach (SPServer server in SPFarm.Local.Servers)
{
switch (server.Role)
{
case SPServerRole.Application:
case SPServerRole.SingleServer:
case SPServerRole.WebFrontEnd:
LogHelper.Log("submitting job {0} for server {1}", JOBNAME, server.Name);
ThemeDeploymentJob job = new ThemeDeploymentJob(wsvc, jobProperties);
job.SubmitJobNow(JOBNAME);
break;
default:
break;
}
}
LogHelper.Log("done creating job {0}", JOBNAME);
}
The key to the above is getting the Web Service (which is present on any machine that has the Web Front End role) and use that in the base type for SPJobDefinition’s constructor, as shown below. Also, submitting the job for each server – this actually assumes that the service is present on all machines in the farm, and generally that’s what / how I configure a Farm and recommend that approach.
public ThemeDeploymentJob() : base() { }
public ThemeDeploymentJob(SPWebService webService, ThemeDeploymentJobProperties properties)
: base(JOBNAME, webService, null, SPJobLockType.None)
{
SetProperties(properties);
}
The rest of the code, and and much inspiration comes from the following samples, etc..
http://solutionizing.net/2008/07/09/updating-spthemesxml-through-a-feature/
http://www.devexpertise.com/2009/02/11/installing-a-theme-as-a-sharepoint-feature/
http://alonsorobles.com/2008/06/26/packaging-branding-and-themes-for-deployment-in-sharepoint-environments/
Next post – Resource File Deployment (Resx) to the App_GlobalResources path…\
Thanks to Joe McPeak, Ketan Shah for feedback, discussions…
Full source to Timer Jobs and WinForm application for job submission here:
SharePointTimerJobSlnV3.zip
We’ve been bombarded by Rosetta Stone commercials, magazine adds, and airport kiosks.
I can’t say I picked up much from High School Spanish – in fact, mostly picked it up pickin peaches on a farm.
Here’s a place you can get your feet wet for free…
Livemocha: Language Learning with Livemocha | Learn a Language Online - Free!
There have been many times that I’ve been handed a SQL instance that doesn’t allow me to do much with it even though the IT Admin for the machine have indicated they’ve done the install and your AD account is part of the Local Admin group.
By default, in SQL 2008 local admin’s are not part of the sysadmin role – unless you’re running in single user mode.
So, this article has a quick HOWTO on getting access back so you can go on your merry sysadmin way…
http://technet.microsoft.com/en-us/library/dd207004.aspx
The trick is, restart with the “-m;” parameter, make the change, the change it back (don’t forget that part). Also, make sure all the other services aren’t running when you need to make this change, otherwise, they connect as THE single user.
Here I am just doing some banking and low and behold the bank I work with seems to be rolling out some “Pilot” stuff.
From the error below, I quickly determine that they’re way overdue for moving to a later version of the Framework (I mean 1.1 come on!!!).
The second thing is, why the heck aren’t you trapping these errors instead of giving me the “debug” mode view that only a developer should see!! And before you write your web.config, at least make sure it’s XML compliant..
How do you get the amateurs out of this business…

Definitely a surprise to hear that NBC would even consider all the late night shuffle after all the crap he’s had to wait for. In support of Conan O’Brien…
http://www.facebook.com/imwithcoco
This has bitten me a couple of times.
This error surfaced yesterday when running a scripted install with psconfig.exe – the error that appears in the log is:
LookupAccountName failed to get the SID for account <domain>\LONG AC NAME > 20 chars
When setting up SharePoint, we usually have a bunch of service accounts that generally are setup by different teams that manage the Active Directory accounts – well, that’s how it should work, but that’s another story.
Many times organizations will have naming standards. These naming standards, well, that’s up the the organization and frankly I don’t always agree with them given the ability to have containers in LDAP/AD, but that’s a long discussion.
Sometimes (actually many times) these standards generate long account names. Can be 20+ characters or more. The thing is, when you setup an account in AD, it will truncate the “samAccountName” attribute for the user object based upon the Distinguished Name of the account. So, it usually chops off the end of the string. I’m not quite sure what it does if if finds a collision with an existing samAccountName and if it has some algorithm to generate a unique on the fly version that doesn’t collide – not my problem…
So, what you just need to do is get the real samAccountName from AD and rerun your script, command, etc. with the corrected <domain>\samAccountName parameter and all is good!!