Posted on:
Categories:
Description:

As I mentioned in my previous post Executing PowerShell in a SharePoint 2013 Timer Job - Part 1 there were issues upgrading some SharePoint 2010 Timer Jobs to SharePoint 2013. These were due to legacy CAS (Code Access Security) support being enabled for the SharePoint Timer Service. This post will describe a workaround that I created so the SharePoint 2013 Timer Jobs will be able to execute PowerShell as well leave the legacy CAS support enabled to ensure that SharePoint 2010 mode sites and solutions will still continue to work on the farm.

To do this I decided to break my timer job solution into 3 components:

  • SP2013TimerJobWorkaround.SP: A SharePoint solution for deploying the timer jobs
  • SP2013TimerJobWorkaround.Console: A console application that executes the business logic for the timer job
  • SP2013TimerJobWorkaround.Common: A class library to contain the majority of the code used so both the console and SharePoint projects can share code

Project Structure 

To demonstrate the issues with the legacy CAS support I have added two timer jobs to the solution “SP2013TimerJobWorkaround.SP Failing Timer Job” and “SP2013TimerJobWorkaround.SP Working Timer Job”.

Now for some quick notes on the solution’s configuration:

  • The SP2013TimerJobWorkaround.Common project:
    • Added references to both Microsoft.SharePoint (GAC) and System.Management.Automation (\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll)
  • The SP2013TimerJobWorkaround.Console project:
    • Added references to both Microsoft.SharePoint (GAC) and SP2013TimerJobWorkaround.Common
    • Added a post-build event (more on this in a bit)
    • Unchecked ‘Prefer 32-bit’ for both debug and release configurations (build tab of the project properties)
  • The SP2013TimerJobWorkaround.SP project:
    • Added a reference to SP2013TimerJobWorkaround.Common
    • Created a SharePoint mapped folder at ISAPI\SP2013TimerJobWorkaround.SP

 Now to get my console application packaged and deployed with my SharePoint solution I did the following:

  • Configured the project build order to ensure the console application builds before the SharePoint solution:
    • SP2013TimerJobWorkaround.Common
    • SP2013TimerJobWorkaround.Console
    • SP2013TimerJobWorkaround.SP
  • Built the solution one time so I can link the files properly
  • Added the console application to the SP2013TimerJobWorkaround.SP project:
    • Right click the SP2013TimerJobWorkaround.SP subfolder under the mapped ISAPI folder
    • Add existing items
    • Browsed to ‘\SP2013TimerJobWorkaround.Console\bin\Debug’ and added both SP2013TimerJobWorkaround.Console.exe and SP2013TimerJobWorkaround.Console.exe.config files to the project
  • Created a post-build event in the SP2013TimerJobWorkaround.Console project to copy the latest build of the console application over
    • Right click the SP2013TimerJobWorkaround.Console project and select properties
    • Selected the build events tab and added the following:
      xcopy "$(TargetDir)*.*" "$(SolutionDir)SP2013TimerJobWorkaround.SP\ISAPI\SP2013TimerJobWorkaround.SP\*.*" /Y
      Note: This will copy all the files from the bin\debug or bin\release folder into the SP2013TimerJobWorkaround.SP\ISAPI\SP2013TimerJobWorkaround.SP folder of the SharePoint project, but on the exe and the config file will be packaged into the WSP.

So now that the project is all set up we can look at some of the code.

The PowerShell commands that I’m executing are located in the SP2013TimerJobWorkaround.Common class library in the PowerShellHelper class:

public static void ExecPowerShellCmdlets()
{
 try
 {
  //define some PowerShell to run (this currently writes out all the site collection sizes)
  const string command =
   @"foreach($site in Get-SPSite -Limit All) { Echo ""Site: $($site.Url) - Storage: $($site.Usage.Storage) bytes"" }";

  var runspace = RunspaceFactory.CreateRunspace(InitialSessionState.CreateDefault());
  runspace.Open();

  using (var powerShellCommand = PowerShell.Create())
  {
   powerShellCommand.Runspace = runspace;
   //Add the SharePoint SnapIn
   powerShellCommand.AddScript("Add-PsSnapin Microsoft.SharePoint.PowerShell");
   //add the defined PowerShell
   powerShellCommand.AddScript(command);

   //Write out the results to the ULS logs (if anything comes back)
   foreach (var result in powerShellCommand.Invoke())
   {
    ErrorLogging.WriteUlsEntry(result, TraceSeverity.High, EventSeverity.Information,
     Definitions.UlsCategory);
   }
  }
  runspace.Close();
 }
 catch (Exception ex)
 {
  ErrorLogging.WriteUlsEntry(ex, TraceSeverity.Unexpected, EventSeverity.ErrorCritical,
   Definitions.UlsCategory);
 }
}

This method will run the PowerShell commands defined and then write the output to the ULS logs for us.

If we look now at the console application all it does is call this method (with a few extra log entries to trace the execution better):

static void Main(string[] args)
{
 try
 {
  ErrorLogging.WriteUlsEntry("------ Start Console App ------", TraceSeverity.High, EventSeverity.Information, Definitions.UlsCategory);

  //Run the predefined PowerShell commands
  PowerShellHelper.ExecPowerShellCmdlets();
  
  ErrorLogging.WriteUlsEntry("------ End Console App ------", TraceSeverity.High, EventSeverity.Information, Definitions.UlsCategory);
 }
 catch (Exception ex)
 {
  ErrorLogging.WriteUlsEntry(ex, TraceSeverity.Unexpected, EventSeverity.ErrorCritical, Definitions.UlsCategory);
 }
}

Now for the SharePoint Timer Jobs.  In the SP2013TimerJobWorkaround.SP project there are 2 timer job classes FailingTimerJob and WorkingTimerJob.

The FailingTimerJob runs the following code:

public override void Execute(Guid contentDbId)
{
 ErrorLogging.WriteUlsEntry("------ Start Failing Timer Job ------", TraceSeverity.High, EventSeverity.Information, Definitions.UlsCategory);
 
 try
 {
  //Run the PowerShell cmd
  PowerShellHelper.ExecPowerShellCmdlets();
 }
 catch (Exception ex)
 {
  ErrorLogging.WriteUlsEntry(ex, TraceSeverity.Unexpected, EventSeverity.ErrorCritical,
   Definitions.UlsCategory);
 }
 ErrorLogging.WriteUlsEntry("------ End Failing Timer Job ------", TraceSeverity.High, EventSeverity.Information, Definitions.UlsCategory);
}

It's pretty straight forward but as you can see it doesn't run due to the legacy CAS support being enabled for the SharePoint Timer Service.

 

Now for the WorkingTimerJob, it runs this code:

public override void Execute(Guid contentDbId)
{
 ErrorLogging.WriteUlsEntry("------ Start Working Timer Job ------", TraceSeverity.High, EventSeverity.Information, Definitions.UlsCategory);

 try
 {
  //Run the console application
  var consoleApp =
   SPUtility.GetVersionedGenericSetupPath(Definitions.ConsoleApplicationPath,
    SPUtility.CompatibilityLevel15);
  var process = new Process
  {
   StartInfo = new ProcessStartInfo {FileName = consoleApp}
  };
  process.Start();
  process.WaitForExit();
 }
 catch (Exception ex)
 {
  ErrorLogging.WriteUlsEntry(ex, TraceSeverity.Unexpected, EventSeverity.ErrorCritical,
   Definitions.UlsCategory);
 }
 ErrorLogging.WriteUlsEntry("------ End Working Timer Job ------", TraceSeverity.High, EventSeverity.Information, Definitions.UlsCategory);
}

The difference here is that the working timer job is calling the console application which runs the PowerShell for it, instead of directly trying to call the PowerShell commands.

As you can see it executes as expected and writes out all the site collection sizes to the ULS logs

 

I've included my sample project here so you can look at the source code in a bit more detail.  Hope this helps you out and provides you with another option when running into issues with SharePoint 2013 and legacy CAS support.