2025-11-28 00:35:46 +09:00

517 lines
21 KiB
C#

// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Storage;
using System.Runtime.InteropServices.ComTypes;
using System.IO;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Threading;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Globalization;
namespace Microsoft.Samples.Fsrm.PowerShellHostClassifier
{
/// <summary>
/// This class provides a way of running a FSRM Classification rule inside powershell
/// Encapsulates creating a powershell pipeline, running it, restarting it on errors
/// </summary>
public class PowerShellRuleHoster: IDisposable
{
#region statics
private static string ScriptFileNameString = "ScriptFileName";
#endregion
// The runspace running powershell inside of, and holds references to global objects such as rule deffinition
// *Note this does not need to be recreated if the pipeline fails
private Runspace m_runSpace;
private Pipeline m_pipeLine;
private string m_ruleName;
// The most recent value emitted from the pipeline
private PSObject m_propertyValue;
// An enumerator that causes the powershell script to hang until it gets input
private BlockablePropertyBagEnumerator m_propertyBagWriter;
// Whether the rule applies to the current property bag
private bool m_ruleNoApply;
// A reference to the event log (log - Application, provider - SRMREPORTS)
private EventLog m_eventLog;
// The wrapped text of the script to run
private string m_scriptText;
// this is used to add the GetStream method to the propertyBag
private System.Reflection.MethodInfo m_getStreamMethodInfo;
// Allows to determine if the pipeline has hanged awaiting input or terminated or emitted data
private WaitHandle[] m_powershellPaused;
// The index in m_powershellPaused representing which handle is being accessed
// Note the enumerator will be the highest index because it is a manual reset event and we only want to know about
// after the pipe line has been consumed
private enum m_waitHandleIndex
{
PipeLine = 0,
Enumerator,
Count
}
/// <summary>
/// Parse strParameters into an easy to consume dictionary
/// Useful for parsing rule or module definition strParameters
/// </summary>
/// <param name="parameters">The strParameters to parse</param>
/// <returns>A dictionary of parameter and parameter values</returns>
public static Dictionary<string, string> ParseParameters(
object[] parameters
)
{
Dictionary<string, string> strParameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// create the dictionary of the strParameters
foreach (string param in parameters)
{
//split param into string before first =
// and string after first =
string[] splitParams = param.Split( "=".ToCharArray(),
2 );
strParameters[splitParams[0]] = splitParams[1];
}
return strParameters;
}
/// <summary>
/// Gets the text of the powershell script
/// </summary>
/// <param name="rule">The rule definition (contains the script to get)</param>
/// <returns>The text of the powershell script</returns>
public static string GetScriptText(
IFsrmClassificationRule rule
)
{
// parse the rule strParameters to determine what type of encoding there is
// otherwise use default encoding to get the text of the file
Dictionary<string, string> ruleParameters = ParseParameters(rule.Parameters);
string scriptFileName;
try
{
scriptFileName = ruleParameters[ScriptFileNameString];
}
catch (KeyNotFoundException e)
{
string message = String.Format(
"PowerShellHostClassifier failed processing parameters for rule {0}. Make sure the rule parameter name is ScriptFileName.",
rule.Name);
EventLog eventLog = new EventLog("Application", ".", "SRM_PS_CLS");
eventLog.WriteEntry(message, EventLogEntryType.Error);
throw new COMException(message, e);
}
// read the text of the file and return it
string fileText;
using (StreamReader fs = new StreamReader(scriptFileName))
{
fileText = fs.ReadToEnd();
}
return fileText;
}
/// <summary>
/// Initialize all values, as well as start the pipeline and await for data
/// </summary>
/// <param name="moduleDefinition">The module definition for the classifier</param>
/// <param name="rule">The rule definition for the rule that this represents</param>
/// <param name="propertyDefinition">The property definition for the property that this rule modifies</param>
public PowerShellRuleHoster(
IFsrmPipelineModuleDefinition moduleDefinition,
IFsrmClassificationRule rule,
IFsrmPropertyDefinition propertyDefinition
)
{
m_propertyBagWriter = new BlockablePropertyBagEnumerator();
m_ruleName = rule.Name;
// create the waitHandles and initialize them
// *note the pipeline waitHandle is created in CreateAndBeginPipeline
m_powershellPaused = new WaitHandle[(int)m_waitHandleIndex.Count];
m_powershellPaused[(int)m_waitHandleIndex.Enumerator] = m_propertyBagWriter.RequestedDataWaitHandle;
// cache the method info for adding the GetStream to PropertyBag
Type extensionClassForPropertyBag = typeof(ExtensionClassForPropertyBag);
m_getStreamMethodInfo = extensionClassForPropertyBag.GetMethod("GetStream");
string fileText = GetScriptText(rule);
// this enables users to create a scriptblock rather then enumerate over the PropertyBagList
m_scriptText = "$PropertyBagList | &{" + fileText + "}\n";
// construct the runspace and set global proxy values for powershell script to use
m_runSpace = RunspaceFactory.CreateRunspace();
m_runSpace.Open();
m_runSpace.SessionStateProxy.SetVariable("ModuleDefinition", moduleDefinition);
m_runSpace.SessionStateProxy.SetVariable("PropertyBagList", m_propertyBagWriter);
m_runSpace.SessionStateProxy.SetVariable("Rule", rule);
m_runSpace.SessionStateProxy.SetVariable("PropertyDefinition", propertyDefinition);
//launch the pipeline creation
CreateAndBeginPipeline();
}
/// <summary>
/// Dispose Method
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose Method
/// </summary>
/// <param name="disposing">If should free managed objects</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (m_eventLog != null)
{
m_eventLog.Dispose();
m_eventLog = null;
}
if (m_propertyBagWriter != null)
{
m_propertyBagWriter.Dispose();
m_propertyBagWriter = null;
}
if (m_pipeLine != null)
{
m_pipeLine.Dispose();
m_pipeLine = null;
}
if (m_runSpace != null)
{
m_runSpace.Close();
m_runSpace = null;
}
}
}
/// <summary>
/// Creates/Recreates the powersehll pipeline and ensures that everything is at a virgin state
/// Starts the powershell pipeline and waits for it to request the first element from the pipeline
/// The pipeline MUST be shutdown before calling this function! the reason for this is we cannot relaibly reset the BlockableEnum otherwise
/// </summary>
public void CreateAndBeginPipeline()
{
ResetRuleResults();
// Create/reset writer into pipeline and the pipeline
m_propertyBagWriter.ResetBlockableEnum();
m_pipeLine = m_runSpace.CreatePipeline();
m_powershellPaused[(int)m_waitHandleIndex.PipeLine] = m_pipeLine.Output.WaitHandle;
// set the text of the pipeline and run it
m_pipeLine.Commands.AddScript(m_scriptText);
m_pipeLine.InvokeAsync();
// wait for the pipeline to fail or request its first element
m_waitHandleIndex lockIndex = 0;
do
{
lockIndex = (m_waitHandleIndex)WaitHandle.WaitAny(m_powershellPaused);
} while (lockIndex != m_waitHandleIndex.Enumerator && m_pipeLine.Output.IsOpen);
// errors are not permitted before processing elements
if (m_pipeLine.Error.Count > 0)
{
Exception error = (Exception)m_pipeLine.Error.Read();
string message = string.Format( CultureInfo.InvariantCulture,
"Powershell Classifier threw errors before processing files in rule [{0}] - details: [{1}]",
m_ruleName,
error.Message );
ThrowNonPropertyBagException(message, error);
}
// if pipeline terminated try and get termination reason
if (m_pipeLine.Output.EndOfPipeline)
{
string message = string.Format( CultureInfo.InvariantCulture,
"Powershell Classifier terminated abruptly before processing files in rule [{0}] - details: [{1}]",
m_ruleName,
m_pipeLine.PipelineStateInfo.Reason.Message );
ThrowNonPropertyBagException(message, m_pipeLine.PipelineStateInfo.Reason);
}
}
/// <summary>
/// Runs one step of the pipeline using propertyBag as the pipeline input
/// Saves last outputed object
/// Saves whether the pipeline returned 0 or 1 object
/// throws exception if pipeline outputed more than 1 object
/// <param name="propertyBag">The property bag to run the powershell script's pipeline on</param>
public void StepPipeline(
IFsrmPropertyBag propertyBag
)
{
ResetRuleResults();
m_waitHandleIndex lockIndex;
bool readAValue = false;
bool tooManyValues = false;
m_ruleNoApply = false;
// insert the property bag into the pipeline after adding the GetStream Method
PSObject psPropertyBag = new PSObject(propertyBag);
psPropertyBag.Methods.Add(new PSCodeMethod("GetStream", m_getStreamMethodInfo));
m_propertyBagWriter.InsertData(psPropertyBag);
// wait for either the pipeline to close or
// for another property to be requested from the enumerator
// If for input a in the script value 1 is emitted, it must be emitted before value b is requested
// Handle multiple values being emitted but fail the current property bag if it happens
do
{
lockIndex = (m_waitHandleIndex)WaitHandle.WaitAny(m_powershellPaused);
//pipeline terminated unexpectedly save message and restart it
if (m_pipeLine.Output.EndOfPipeline)
{
string message;
if (m_pipeLine.PipelineStateInfo.State == PipelineState.Failed)
{
message = string.Format(CultureInfo.InvariantCulture,
"Powershell Classifier terminated abruptly due to failuer while processing file [{0}] in rule [{1}] - failure details: [{2}]",
propertyBag.VolumeName + propertyBag.RelativePath + "\\" + propertyBag.Name,
m_ruleName,
m_pipeLine.PipelineStateInfo.Reason.Message);
}
else
{
message = string.Format(CultureInfo.InvariantCulture,
"Powershell Classifier exited abruptly without failures while processing file [{0}] in rule [{1}].",
propertyBag.VolumeName + propertyBag.RelativePath + "\\" + propertyBag.Name,
m_ruleName);
}
propertyBag.AddMessage(message);
CreateAndBeginPipeline();
throw new COMException( message, m_pipeLine.PipelineStateInfo.Reason );
}
// if we haven't read a value pop one off and save it
if (m_pipeLine.Output.Count >= 1 && !readAValue)
{
readAValue = true;
m_propertyValue = m_pipeLine.Output.Read();
}
// if we have read a value and there are values in the pipeline, then
// set the tooManyValues flag and eat everythign in the pipeline
while (m_pipeLine.Output.Count > 0 && readAValue)
{
// if the m_propertyValue currently points to the first value ouput for the pipeline
// the add a message for it, other wise output messages for duplicate values after popping them off
if (!tooManyValues)
{
string message1 = string.Format( CultureInfo.InvariantCulture,
"Powershell Classifier returned too many values while processing file [{0}] in rule [{1}] - returned object of type [{2}], and value [{3}]",
propertyBag.VolumeName + propertyBag.RelativePath + "\\" + propertyBag.Name,
m_ruleName,
m_propertyValue.BaseObject.GetType().ToString(),
m_propertyValue.BaseObject.ToString() );
propertyBag.AddMessage(message1);
}
// cleanup pipeline
m_propertyValue = m_pipeLine.Output.Read();
// ouput message for current object
string message2 = string.Format( CultureInfo.InvariantCulture,
"Powershell Classifier returned too many values while processing file [{0}] in rule [{1}] - returned object of type [{2}], and value [{3}]",
propertyBag.VolumeName + propertyBag.RelativePath + "\\" + propertyBag.Name,
m_ruleName,
m_propertyValue.BaseObject.GetType().ToString(),
m_propertyValue.BaseObject.ToString() );
propertyBag.AddMessage(message2);
tooManyValues = true;
}
} while (lockIndex != m_waitHandleIndex.Enumerator && m_pipeLine.Output.IsOpen);
// if script didn't output any values for this property bag record it
if (!readAValue)
{
m_ruleNoApply = true;
}
// if output too many values finish failing this property
if (tooManyValues)
{
//already added messages for reason why we are failing
string message3 = string.Format( CultureInfo.InvariantCulture,
"Powershell Classifier returned too many values while processing file [{0}] in rule [{1}]",
propertyBag.VolumeName + propertyBag.RelativePath + "\\" + propertyBag.Name,
m_ruleName );
throw new COMException( message3, HRESULTS.PS_CLS_E_TOO_MANY_VALUES );
}
// if there were errors in the pipeline pop them all off and then fail current property bag
if (m_pipeLine.Error.Count > 0)
{
Exception firstError = null;
while(m_pipeLine.Error.Count > 0)
{
Exception exception = (Exception)m_pipeLine.Error.Read();
if (firstError == null)
{
firstError = exception;
}
// add message to pipeline
propertyBag.AddMessage(exception.Message);
}
throw (Exception)firstError;
}
}
/// <summary>
/// Reset the rule
/// </summary>
private void ResetRuleResults()
{
m_propertyValue = null;
m_ruleNoApply = true;
}
/// <summary>
/// Unload the classifier
/// </summary>
public void UnloadRule()
{
if (m_pipeLine.Output.IsOpen)
{
m_propertyBagWriter.EndInput();
m_pipeLine.Input.Close();
//wait for the script to finish
m_pipeLine.Output.WaitHandle.WaitOne();
if (m_pipeLine.Output.Count > 0)
{
m_eventLog.WriteEntry("Some output available from pipeline after pipeline closed. This is unexpected and may leave some output unprocessed.",
EventLogEntryType.Error);
}
}
else
{
if (m_pipeLine.PipelineStateInfo.State == PipelineState.Failed)
{
ThrowNonPropertyBagException("Pipeline failed and closed unexpectedly.", m_pipeLine.PipelineStateInfo.Reason);
}
else
{
ThrowNonPropertyBagException("Pipeline exited unexpectedly without failures.", m_pipeLine.PipelineStateInfo.Reason);
}
}
m_pipeLine.Dispose();
m_pipeLine = null;
m_runSpace.Close();
m_runSpace = null;
Dispose(true);
}
/// <summary>
/// Throw an error and log it because this classifier CRASHED!
/// </summary>
/// <param name="message">The message to save to the log file and throw</param>
private void ThrowNonPropertyBagException(string message, Exception exception)
{
if (m_eventLog == null)
{
m_eventLog = new EventLog("Application", ".", "SRM_PS_CLS");
}
m_eventLog.WriteEntry( message, EventLogEntryType.Error );
throw new COMException( message, exception );
}
// the property value from powershell
public object PropertyValue
{
get
{
return m_propertyValue.BaseObject;
}
}
// Whether the rule applies to the current property bag (if powershell pipeline emitted any objects or not)
public bool RuleNoApply
{
get
{
return m_ruleNoApply;
}
}
public string RuleName
{
get
{
return m_ruleName;
}
}
}
/// <summary>
/// A class that makes it easy to add methods to the property bag in poweshell
/// </summary>
public static class ExtensionClassForPropertyBag
{
/// <summary>
/// A function which returns a Stream object which encapsulates an IStream interface
/// This makes it much easier to get the file contents in managed code / powershell
/// </summary>
/// <param name="parentObject">This will be the PSObject that encapsulates a IFsrmPropertyBag</param>
/// <returns>A stream which wraps the parentObject's IStream</returns>
public static StreamWrapperForIStream GetStream(PSObject parentObject)
{
IFsrmPropertyBag propertyBag = (IFsrmPropertyBag)parentObject.BaseObject;
IStream istream = (IStream)propertyBag.GetFileStreamInterface(_FsrmFileStreamingMode.FsrmFileStreamingMode_Read, _FsrmFileStreamingInterfaceType.FsrmFileStreamingInterfaceType_IStream );
return new StreamWrapperForIStream(istream);
}
}
}