Execute Sitecore Powershell from Sitecron Schedule Job

Yes you heard it right.  I was planning on writing a couple of sample jobs to do indexing etc but then I realized why not make it a lot more powerful.  So I decided to add support to run Powershell scripts.

As I mentioned earlier you can implement the IJob interface and implement what ever you choose to.  I am including a default Powershell execute job which can take in the parameters you would like to pass to your script and specify a link to the Powershell Script item.

This gives you a lot of power in terms of the kind of functionality you can build. Better yet, you can tweak this functionality and re-run it without recompiling the code.

First I would like to go over the fields and what you can do with them:

powershell_sitecron

  1. Type: class namespace, assembly (e.g. Sitecron.Powershell.ExecuteScript, Sitecron)
  2. Cron Expression: 0 15 10 * * ? (e.g. Fire at 10:15am every day)
  3. Parameters: test=1&p=first (e.g. you can pass in anything like a guid/some value or values similar to the Sitecore parameters so that you can get a NameValueCollection. It all depends on your need.)
  4. Items: Select one or more items you need to do the job. For instance if you need specific items to do import/rearrange items/rename etc.

I tried to make this generic so that based on your needs you can implement the IJob Quartz.NET interface and achieve the functionality you need.

From my testing the scheduled tasks always executed on time every time.

The other thing to note is that as part of the output, you can output the execution time and next scheduled execution time.

Log.Info(string.Format("Sitecron: Powershell.ExecuteScript Instance {0} of ExecuteScript Job - {5}Parameters: {1} ScriptIDs: {2} {5}Fired at: {3} 
{5}Next Scheduled For:{4}", context.JobDetail.Key, rawParameters, scriptIDs, context.FireTimeUtc.Value.ToString("r"), context.NextFireTimeUtc.Value.ToString("r"), Environment.NewLine), this);

Now for the Powershell script. Here is the code I implemented so that its part of Sitecron and you can utilize it or implement your own. Needless to say that in order for this Powershell job to run you would need to install Sitecore PowerShell module 3.1+

Adam Najmanowicz was nice enough to take time to suggest ways to implement it.

using Cognifide.PowerShell.Core.Host;
using Cognifide.PowerShell.Core.Settings;
using Quartz;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecron.SitecronSettings;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;

namespace Sitecron.Powershell
{
    public class ExecuteScript : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            JobDataMap dataMap = context.JobDetail.JobDataMap;

            string scriptIDs = dataMap.GetString(FieldNames.Items);
            string rawParameters = dataMap.GetString(FieldNames.Parameters);

            Log.Info(string.Format("Sitecron: Powershell.ExecuteScript Instance {0} of ExecuteScript Job - {5}Parameters: {1} ScriptIDs: {2} {5}Fired at: {3} {5}Next Scheduled For:{4}", context.JobDetail.Key, rawParameters, scriptIDs, context.FireTimeUtc.Value.ToString("r"), context.NextFireTimeUtc.Value.ToString("r"), Environment.NewLine), this);

            if (!string.IsNullOrEmpty(scriptIDs))
            {
                Database masterDb = Factory.GetDatabase("master");

                List scriptItems = new List();
                string id = scriptIDs.Split('|').FirstOrDefault(); //only doing the first script item, running multiple scripts can cause issues especially if they call other scripts etc.

                Item scriptItem = masterDb.GetItem(new ID(id));

                Log.Info(string.Format("Sitecron: Powershell.ExecuteScript: Adding Script: {0} {1}", scriptItem.Name, id), this);

                NameValueCollection parameters = Sitecore.Web.WebUtil.ParseUrlParameters(rawParameters);

                Run(scriptItem, parameters);
            }
            else
                Log.Info("Sitecron: Powershell.ExecuteScript: No scripts found to execute!", this);
        }


        private void Run(Item speScript, NameValueCollection parameters)
        {
            var script = speScript[ScriptItemFieldNames.Script];
            if (!string.IsNullOrEmpty(script))
            {
                Log.Info(string.Format("Sitecron: Powershell.ExecuteScript: Running Script: {0} {1}", speScript.Name, speScript.ID.ToString()), this);

                //reset session for each script otherwise the position of the items and env vars set by the previous script will be inherited by the subsequent scripts
                using (var session = ScriptSessionManager.NewSession(ApplicationNames.Default, true))
                {
                    //here we are passing the param collection to the script
                    var paramItems = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { Key = k, Value = v });
                    foreach (var p in paramItems)
                    {
                        if (String.IsNullOrEmpty(p.Key)) continue;
                        if (String.IsNullOrEmpty(p.Value)) continue;

                        if (session.GetVariable(p.Key) == null)
                        {
                            session.SetVariable(p.Key, p.Value);
                        }
                    }

                    session.SetExecutedScript(speScript);
                    session.SetItemLocationContext(speScript);
                    session.ExecuteScriptPart(script);
                }
            }

        }


        #region Multiple Scripts Scenario
        ////public void Execute(IJobExecutionContext context)
        ////{
        ////    JobDataMap dataMap = context.JobDetail.JobDataMap;

        ////    string scriptIDs = dataMap.GetString(FieldNames.Items);
        ////    string rawParameters = dataMap.GetString(FieldNames.Parameters);

        ////    Log.Info(string.Format("Sitecron: Powershell.ExecuteScript Instance {0} of ExecuteScript Job - {4}Parameters: {1} {4}Fired at: {2} {4}Next Scheduled For:{3}", context.JobDetail.Key, scriptParams, context.FireTimeUtc.Value.ToString("r"), context.NextFireTimeUtc.Value.ToString("r"), Environment.NewLine), this);

        ////    if (!string.IsNullOrEmpty(scriptIDs))
        ////    {
        ////        Database masterDb = Factory.GetDatabase("master");

        ////        List scriptItems = new List();
        ////        string[] ids = scriptIDs.Split('|');
        ////        foreach (string id in ids)
        ////        {
        ////            scriptItems.Add(masterDb.GetItem(new ID(id)));
        ////            Log.Info(string.Format("Sitecron: Powershell.ExecuteScript: Adding Script: {0}", id), this);
        ////        }

        ////        NameValueCollection parameters = Sitecore.Web.WebUtil.ParseUrlParameters(rawParameters);

        ////        Run(scriptItems.ToArray(), parameters);
        ////    }
        ////    else
        ////        Log.Info("Sitecron: Powershell.ExecuteScript: No scripts found to execute!", this);
        ////}


        //////general bad practive here to allow multiple powershell scripts as there might be dependency
        //////of one script on another etc, should be one script per scheduled task
        //////this is shown as an example
        ////private void Run(Item[] speScripts, NameValueCollection parameters)
        ////{
        ////    foreach (var item in speScripts)
        ////    {
        ////        var script = item[ScriptItemFieldNames.Script];
        ////        if (!String.IsNullOrEmpty(script))
        ////        {
        ////            Log.Info(string.Format("Sitecron: Powershell.ExecuteScript: Running Script: {0} {1}", item.Name, item.ID.ToString()), this);

        ////            //reset session for each script otherwise the position of the items and env vars set by the previous script will be inherited by the subsequent scripts
        ////            using (var session = ScriptSessionManager.NewSession(ApplicationNames.Default, true))
        ////            {
        ////                //here we are passing the same param collection to all the scripts
        ////                var paramItems = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { Key = k, Value = v });
        ////                foreach (var p in paramItems)
        ////                { 
        ////                    if (String.IsNullOrEmpty(p.Key)) continue;
        ////                    if (String.IsNullOrEmpty(p.Value)) continue;

        ////                    if (session.GetVariable(p.Key) == null)
        ////                    {
        ////                        session.SetVariable(p.Key, p.Value);
        ////                    }
        ////                }

        ////                session.SetExecutedScript(item);
        ////                session.SetItemLocationContext(item);
        ////                session.ExecuteScriptPart(script);
        ////            }
        ////        }
        ////    }
        ////}
        #endregion
    }
}

One thing to note here is that although we have the ability to select multiple items, in this case multiple Powershell Scripts, it is not a good practice to execute multiple scripts via code consecutively.  There could be inter-dependencies, one of those scripts could be calling another script(s) etc. creating issues where there shouldn’t be one.

The intent is to select one item of type “PowerShell Script”.  If multiple are selected this specific implementation will only pick the first one and ignore the others.  I have included commented code which is capable of executing multiple script items but use it at your own risk.

I am posting the update to the module to the Sitecore Marketplace shortly. I updated GitHub.

GitHub: https://github.com/akshaysura/Sitecron
Market Place: https://marketplace.sitecore.net/Modules/S/Sitecron
For help with Cron Expressions you can use http://www.cronmaker.com/

If you have any questions or comments, please get in touch with me.

 

Leave a Reply

Your email address will not be published. Required fields are marked *