Yesterday I had written a blog post on Sitecore PowerShell Extensions History engine? As it was pointed out in the blog post and by Mike Reynolds & Kamruz Jaman (who refuse to be my friends). We have to improve the code by adding a pipeline. It bothered me enough all night, that I had to get this out of the way. One its a bit difficult as we are dealing with Windows PowerShell at its code. So post script execution, I decided to add a new pipeline which gets executed along with the commands and the errors reported.

Anyone can tap into this pipeline and add additional processors. Again, I hope this gets integrated into SPE some how, or Adam/Mikey will make this better.

Here goes. First lets setup to have a brand new pipeline but defining the PipelineArgs, here is the sample code:

using Sitecore.Pipelines; using System; using System.Collections.Generic; using System.Management.Automation; using System.Management.Automation.Runspaces; namespace SPE.Logging.Pipelines { public class SPECommandProcessedArgs:PipelineArgs { public CommandCollection Commands { get; set; } public IEnumerable LastErrors { get; set; } public string UserName { get; set; } public DateTime Date { get; set; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using Sitecore . Pipelines ; using System ; using System . Collections . Generic ; using System . Management . Automation ; using System . Management . Automation . Runspaces ; namespace SPE . Logging . Pipelines { public class SPECommandProcessedArgs : PipelineArgs { public CommandCollection Commands { get ; set ; } public IEnumerable LastErrors { get ; set ; } public string UserName { get ; set ; } public DateTime Date { get ; set ; } } }

Once that is done, lets define the interface the pipeline processor will be based on, here is the sample code:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SPE.Logging.Pipelines { interface ISPECommandProcessor { void Process(SPECommandProcessedArgs args); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using System ; using System . Collections . Generic ; using System . Linq ; using System . Text ; using System . Threading . Tasks ; namespace SPE . Logging . Pipelines { interface ISPECommandProcessor { void Process ( SPECommandProcessedArgs args ) ; } }

Next, lets define the processor it self. This processor’s job is to log the commands into the MongoDb connection we made using the previous blog post. Here we are passing in the MongoDB connection string by config.

Here is the sample code:

using SPE.Logging.Log; using System.Configuration; namespace SPE.Logging.Pipelines { public class SPECommandProcessor:ISPECommandProcessor { public string MongoConnectionString { get; set; } public void Process(SPECommandProcessedArgs args) { CommandHistoryRepository commandHistoryRepository = new CommandHistoryRepository(ConfigurationManager.ConnectionStrings[MongoConnectionString].ConnectionString); foreach (var cmd in args.Commands) { var logCommand = new SPECommand { UserName = args.UserName, Command = cmd.CommandText, Date = args.Date }; commandHistoryRepository.Set(logCommand); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using SPE . Logging . Log ; using System . Configuration ; namespace SPE . Logging . Pipelines { public class SPECommandProcessor : ISPECommandProcessor { public string MongoConnectionString { get ; set ; } public void Process ( SPECommandProcessedArgs args ) { CommandHistoryRepository commandHistoryRepository = new CommandHistoryRepository ( ConfigurationManager . ConnectionStrings [ MongoConnectionString ] . ConnectionString ) ; foreach ( var cmd in args . Commands ) { var logCommand = new SPECommand { UserName = args . UserName , Command = cmd . CommandText , Date = args . Date } ; commandHistoryRepository . Set ( logCommand ) ; } } } }

Finally in the Cognifide.PowerShell.Core.Host.ScriptSession class, we will tap directly into the ExecuteCommand method. We are going to add some code right after the command is executed around line 740. We are going to load the Args with the commands and the errors if we see the HadErrors is set true. One thing to notice is, that depending on the command, we might see more than 1 in the commands collection. Here is the sample code:

LastErrors?.Clear(); // execute the commands in the pipeline now var execResults = powerShell.Invoke(); if (powerShell.HadErrors) { LastErrors = powerShell.Streams.Error.ToList(); } if (execResults != null && execResults.Any()) { foreach (var record in execResults.Where(r => r != null).Select(p => p.BaseObject).OfType()) { LogUtils.Debug(record + record.InvocationInfo.PositionMessage, this); } } // START execute Sitecore pipeline speCommandProcessed SPECommandProcessedArgs pipelineArgs = new SPECommandProcessedArgs(); pipelineArgs.Commands = powerShell.Commands.Commands; pipelineArgs.LastErrors = LastErrors; pipelineArgs.Date = DateTime.Now; pipelineArgs.UserName = Sitecore.Context.GetUserName(); CorePipeline.Run("speCommandProcessed", pipelineArgs); //END 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 LastErrors ? . Clear ( ) ; / / execute the commands in the pipeline now var execResults = powerShell . Invoke ( ) ; if ( powerShell . HadErrors ) { LastErrors = powerShell . Streams . Error . ToList ( ) ; } if ( execResults != null & amp ; & amp ; execResults . Any ( ) ) { foreach ( var record in execResults . Where ( r =& gt ; r != null ) . Select ( p =& gt ; p . BaseObject ) . OfType ( ) ) { LogUtils . Debug ( record + record . InvocationInfo . PositionMessage , this ) ; } } / / START execute Sitecore pipeline speCommandProcessed SPECommandProcessedArgs pipelineArgs = new SPECommandProcessedArgs ( ) ; pipelineArgs . Commands = powerShell . Commands . Commands ; pipelineArgs . LastErrors = LastErrors ; pipelineArgs . Date = DateTime . Now ; pipelineArgs . UserName = Sitecore . Context . GetUserName ( ) ; CorePipeline . Run ( "speCommandProcessed" , pipelineArgs ) ; / / END

Once that is all set, we are ready to create the new config to load the pipeline and the processor. Here is the sample config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <speCommandProcessed> <processor type="SPE.Logging.Pipelines.SPECommandProcessor, SPE.Logging"> <MongoConnectionString>specommandhistory</MongoConnectionString> </processor> </speCommandProcessed> </pipelines> </sitecore> </configuration> 1 2 3 4 5 6 7 8 9 10 11 12 13 <configuration xmlns : patch = "http://www.sitecore.net/xmlconfig/" > <sitecore> <pipelines> <speCommandProcessed> <processor type = "SPE.Logging.Pipelines.SPECommandProcessor, SPE.Logging" > <MongoConnectionString> specommandhistory </MongoConnectionString> </processor> </speCommandProcessed> </pipelines> </sitecore> </configuration>

Rebuild and run a few commands as two different users. As you can see from the screenshot, it records them in Mongo.

If you have any questions or concerns, please get in touch with me. (@akshaysura13 on twitter or on Slack).