Skip to content

Commit

Permalink
-Implemented -Hashtable parameter on New-SensorFactoryDefinition
Browse files Browse the repository at this point in the history
-Implemented type conversion on values specified to cmdlet parameters invoked by PSCmdletInvoker
  • Loading branch information
lordmilko committed Sep 19, 2020
1 parent 349e043 commit a191fe9
Show file tree
Hide file tree
Showing 16 changed files with 734 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Management.Automation;
using PrtgAPI.PowerShell.Base;
using System.Management.Automation.Language;
using System.Reflection;
using System.Text;
using PrtgAPI.Linq;
using PrtgAPI.Reflection;
using PrtgAPI.Reflection.Cache;
using PrtgAPI.Utilities;

namespace PrtgAPI.PowerShell.Cmdlets
{
Expand All @@ -18,8 +25,8 @@ namespace PrtgAPI.PowerShell.Cmdlets
///
/// <para type="description">When specifying an Expression, the default expression and current sensor can be accessed via the $expr and $_ automatic variables.
/// Unless you wish to modify the sensor ID or channel ID to be used for a specific sensor, it is generally recommended to avoid recalculating
/// the base channel definition and use the automatic $expr variable, which is defined as "channel(sensorId, channelID)" where sensorID is the ID of the current
/// sensor, and channelID is the value passed to -<see cref="ChannelId"/>. If a -<see cref="ChannelId"/> is not specified, channel ID 0 will automatically be used.</para>
/// the base channel definition and use the automatic $expr variable, which is defined as "channel(sensorId, channelId)" where sensorId is the ID of the current
/// sensor, and channelId is the value passed to -<see cref="ChannelId"/>. If a -<see cref="ChannelId"/> is not specified, channel ID 0 will automatically be used.</para>
///
/// <para type="description">Aggregation channels, representing values derived from multiple sensor channels, can be created using the -<see cref="Aggregator"/> and -<see cref="SummaryExpression"/> parameters.
/// When -<see cref="Aggregator"/> is specified, New-SensorFactoryDefinition creates a single channel based on all of the channels piped in to the -<see cref="Sensor"/> parameter. By contrast,
Expand All @@ -46,9 +53,15 @@ namespace PrtgAPI.PowerShell.Cmdlets
///
/// <para type="description">Horizontal lines can be generated by specifying the position the line should appear at to the -<see cref="Value"/> parameter. When
/// specifying a horizontal line the channel unit the line should apply to should be specified in square brackets at the end of the channel name. It
/// does not matter what the ID of any horizontal lines are, as long as they do not conflict with any other channel definitions.</para>
/// does not matter what the ID of any horizontal lines are, as long as they do not conflict with any other channel definitions.</para>
///
/// <para type="description">To automatically copy the output of New-SensorFactoryDefinition to the clipboard, you can pipe the cmdlet to clip.exe.</para>
/// <para type="description">When creating a complex sensor factory definition that may require multiple invocations of New-SensorFactoryDefinition to achieve,
/// you can alternatively specify a collection of <see cref="Hashtable"/> values to the -<see cref="HashTable"/> parameter, with each table containing the cmdlet parameters you would specify
/// if you were to invoke New-SensorFactoryDefinition normally. This allows you to fully describe the nature of your entire factory definition in one go, without having to worry
/// about offsetting the -<see cref="StartId"/> for each successive invocation of New-SensorFactoryDefinition. If a -<see cref="StartId"/> is specified
/// in a <see cref="Hashtable"/>, the running channel record ID counter will effectively be overridden, with all successive channel definitions continuing from this newly specified ID.</para>
///
/// <para type="description">To automatically copy the output of New-SensorFactoryDefinition to the clipboard on Windows, you can pipe the cmdlet to clip.exe.</para>
///
/// <example>
/// <code>C:\> Get-Sensor -Tags wmicpuloadsensor | fdef { $_.Device }</code>
Expand Down Expand Up @@ -76,7 +89,7 @@ namespace PrtgAPI.PowerShell.Cmdlets
///
/// C:\> $sensors | fdef { $_.Device } -sn "Average CPU Load" -se { "$acc + $expr" } -sf { "$acc / $($sensors.Count)" }
/// </code>
/// <para>Create a channel definition showing the average CPU Load of all devices as well as channel definitions for each individual device using a summary expression and finalizer formula</para>
/// <para>Create a channel definition showing the average CPU Load of all devices as well as channel definitions for each individual device using a custom summary expression and finalizer formula</para>
/// <para/>
/// </example>
/// <example>
Expand All @@ -96,7 +109,15 @@ namespace PrtgAPI.PowerShell.Cmdlets
/// </example>
/// <example>
/// <code>C:\> fdef "Line at 40.2 [msec]" -Value 40.2 -StartId 3</code>
/// <para>Create a channel definition for a horizontal line against channels that use the "msec" unit using a channel ID of 3.</para>
/// <para>Create a channel definition for a horizontal line against channels that use the "msec" unit using a starting channel record ID of 3.</para>
/// <para/>
/// </example>
/// <example>
/// <code>
/// C:\> $sensors | Get-Sensor -Tags wmiterm*
///
/// C:\> $sensors | fdef @{name={$_.Device + " Total Sessions"}; sn="Total Sessions"; se="Sum"},@{name={$_.Device + " Active Sessions"}; sn="Active Sessions"; se="Sum"; channelId=1}</code>
/// <para>Create a channel definition for WMI Terminal Services sensors, showing the total number of sessions overall/on each server and the active number of sessions overall/on each server.</para>
/// </example>
///
/// <para type="link" uri="https://github.com/lordmilko/PrtgAPI/wiki/Sensor-Factories#create-sensor-factory-definitions">Online version:</para>
Expand All @@ -113,6 +134,7 @@ public class NewSensorFactoryDefinition : PSCmdlet
[Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = ParameterSet.Default)]
[Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = ParameterSet.Aggregate)]
[Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = ParameterSet.Summary)]
[Parameter(Mandatory = false, ValueFromPipeline = true, ParameterSetName = ParameterSet.HashTable)]
public Sensor Sensor { get; set; }

/// <summary>
Expand Down Expand Up @@ -153,6 +175,7 @@ public class NewSensorFactoryDefinition : PSCmdlet
[Parameter(Mandatory = false, ParameterSetName = ParameterSet.Aggregate)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet.Summary)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet.Manual)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet.HashTable)]
public int StartId { get; set; } = 1;

/// <summary>
Expand Down Expand Up @@ -205,6 +228,13 @@ public class NewSensorFactoryDefinition : PSCmdlet
[Parameter(Mandatory = false, ParameterSetName = ParameterSet.Summary)]
public ScriptBlock SummaryFinalizer { get; set; }

/// <summary>
/// <para type="description">A collection of one or more hashtable objects specifying the parameters to use for performing one or more invocations
/// of New-SensorFactoryDefinition, while retaining the latest channel ID across each invocation.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, ParameterSetName = ParameterSet.HashTable)]
public Hashtable[] HashTable { get; set; }

private int id;
private readonly PSVariable accumulation = new PSVariable("acc");
private List<Sensor> summarySensors = new List<Sensor>();
Expand Down Expand Up @@ -242,6 +272,10 @@ protected override void ProcessRecord()
case ParameterSet.Summary:
summarySensors.Add(Sensor);
break;
case ParameterSet.HashTable:
if (Sensor != null)
summarySensors.Add(Sensor);
break;
case ParameterSet.Default:
case ParameterSet.Manual:
ProcessNormal(Sensor);
Expand Down Expand Up @@ -290,6 +324,9 @@ protected override void EndProcessing()
case ParameterSet.Summary:
ProcessSummary();
break;
case ParameterSet.HashTable:
ProcessHashTable();
break;
default:
throw new UnknownParameterSetException(ParameterSetName);
}
Expand Down Expand Up @@ -346,6 +383,180 @@ private void ProcessSummary()
ProcessNormal(sensor);
}

private void ProcessHashTable()
{
id = StartId;

foreach(var table in HashTable)
{
var dict = table.ToDictionary();

var bindingResult = GetStaticBindingResult(dict);

var boundParameters = GetInvokerBoundParameters(bindingResult);
var parameterSetName = GetInvokerParameterSetName(bindingResult);

if (!boundParameters.ContainsKey(nameof(StartId)))
boundParameters[nameof(StartId)] = id;

ValidateMandatoryParameters(boundParameters, parameterSetName, dict);

var instance = new NewSensorFactoryDefinition();

var invoker = new PSCmdletInvoker(this, instance, parameterSetName, (c, b) =>
{
object s;

//Not all parameter sets (e.g. Manual) require a sensor!
if (b.TryGetValue(nameof(Sensor), out s))
c.Sensor = (Sensor)s;
});

invoker.BindParameters(boundParameters);
invoker.BeginProcessing(boundParameters);

if (boundParameters.ContainsKey(nameof(Sensor)))
{
foreach (var sensor in summarySensors)
{
boundParameters[nameof(Sensor)] = sensor;
invoker.ProcessRecord(boundParameters);
}
}
else
{
invoker.ProcessRecord(boundParameters);
}

invoker.EndProcessing();

id = instance.id;

foreach(var item in invoker.Output)
WriteObject(item);
}
}

private void ValidateMandatoryParameters(Dictionary<string, object> boundParameters, string parameterSetName, Dictionary<object, object> dict)
{
var properties = ReflectionCacheManager.Get(GetType()).Properties.Where(p => p.GetAttribute<ParameterAttribute>() != null).ToList();

var required = properties.Select(
p => Tuple.Create(
p.Property.Name,
p.GetAttributes<ParameterAttribute>().Where(a => a.ParameterSetName == parameterSetName && a.Mandatory).FirstOrDefault()
)
).Where(t => t.Item2 != null).Select(t => t.Item1).ToArray();

var missing = required.Except(boundParameters.Keys).ToArray();

if (missing.Length > 0)
throw new ParameterBindingException($"Cannot process hashtable '{PrettyPrintHashtable(dict)}' on parameter set '{parameterSetName}': parameter '{missing[0]}' is mandatory.");
}

private string PrettyPrintHashtable(Dictionary<object, object> dict)
{
var builder = new StringBuilder();

builder.Append("@{");

builder.Append(string.Join("; ", dict.Select(e => $"{e.Key}={FormatValue(e.Value)}")));

builder.Append("}");

return builder.ToString();
}

private string FormatValue(object v)
{
if (v is ScriptBlock)
return $"{{{v}}}";

if (v is string)
return $"\"{v}\"";

return v?.ToString();
}

private Dictionary<string, object> GetInvokerBoundParameters(StaticBindingResult bindingResult)
{
var dict = bindingResult.BoundParameters.Select(p => Tuple.Create(p.Key, p.Value.ConstantValue)).ToDictionary(i => i.Item1, i => i.Item2);

var bindingInfo = GetBindingInfo(bindingResult);

var boundArguments = (IDictionary) bindingInfo.GetInternalProperty("BoundArguments");

if (summarySensors.Count > 0 && boundArguments.Contains(nameof(Sensor)))
dict[nameof(Sensor)] = null;

return dict;
}

private string GetInvokerParameterSetName(StaticBindingResult bindingResult)
{
var bindingInfo = GetBindingInfo(bindingResult);

var boundParameters = bindingInfo.GetInternalProperty("BoundParameters").ToIEnumerable().ToArray();
var parameterSetFlag = (uint)bindingInfo.GetInternalProperty("ValidParameterSetsFlags");

var parameterSetData = boundParameters[0]
.GetInternalField("value")
.GetInternalProperty("Parameter")
.GetInternalProperty("ParameterSetData")
.ToIEnumerable()
.ToArray();

var parameterSetFlags = parameterSetData
.Select(p => Tuple.Create(
(string)p.GetPublicProperty("Key"),
(uint)p.GetPublicProperty("Value").GetInternalProperty("ParameterSetFlag")
)).ToArray();

var selectedSet = parameterSetFlags.First(f => f.Item2 == parameterSetFlag).Item1;

return selectedSet;
}

private object GetBindingInfo(StaticBindingResult bindingResult)
{
return bindingResult.PSGetInternalField("_bindingInfo", "bindingInfo", this);
}

private StaticBindingResult GetStaticBindingResult(Dictionary<object, object> dict)
{
var attrib = typeof(NewSensorFactoryDefinition).GetCustomAttribute<CmdletAttribute>();
var cmdletName = $"{attrib.VerbName}-{attrib.NounName}";

var commandElements = new List<CommandElementAst>
{
AstFactory.StringConstantExpression(cmdletName, StringConstantType.BareWord)
};

foreach (var item in dict)
{
commandElements.Add(AstFactory.CommandParameter(item.Key.ToString(), null, new EmptyScriptExtent(true)));
commandElements.Add(AstFactory.ConstantExpression(item.Value));
}

var commandPipeline = AstFactory.Pipeline(
AstFactory.CommandExpression(
AstFactory.VariableExpression("sensors")
),
AstFactory.Command(commandElements)
);

var bindingResult = StaticParameterBinder.BindCommand((CommandAst) commandPipeline.PipelineElements[1]);

if (bindingResult.BindingExceptions.Count > 0)
{
var exception = bindingResult.BindingExceptions.First().Value.BindingException;

throw new ParameterBindingException($"Failed to parse hashtable '{PrettyPrintHashtable(dict)}': {exception.Message.EnsurePeriod()}", exception);
}

return bindingResult;
}

private Tuple<ScriptBlock, ScriptBlock> GetScriptBlockFromSummaryMode(EnumOrScriptBlock<FactorySummaryMode> summary, ScriptBlock finalizer)
{
ScriptBlock agg = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ internal void Connect(PSCmdlet cmdlet)
}
}

private PSEdition GetPSEdition(PSCmdlet cmdlet)
internal static PSEdition GetPSEdition(PSCmdlet cmdlet)
{
var variable = cmdlet.GetVariableValue("global:PSEdition")?.ToString();

Expand Down
6 changes: 3 additions & 3 deletions src/PrtgAPI.PowerShell/PowerShell/DummyRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace PrtgAPI.PowerShell
[ExcludeFromCodeCoverage]
class DummyRuntime : ICommandRuntime
{
internal static long _lastUsedSourceId => (long) typeof(PSCmdlet).Assembly.GetType("System.Management.Automation.MshCommandRuntime").PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId");
internal static long _lastUsedSourceId => (long) typeof(PSCmdlet).Assembly.GetType("System.Management.Automation.MshCommandRuntime").PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId", null);

public List<object> Output { get; } = new List<object>();

Expand All @@ -21,9 +21,9 @@ class DummyRuntime : ICommandRuntime

internal object OutputPipe => Owner.CommandRuntime.GetInternalProperty("OutputPipe");

internal PrtgCmdlet Owner { get; }
internal PSCmdlet Owner { get; }

public DummyRuntime(PrtgCmdlet Owner)
public DummyRuntime(PSCmdlet Owner)
{
this.Owner = Owner;
}
Expand Down
2 changes: 2 additions & 0 deletions src/PrtgAPI.PowerShell/PowerShell/ParameterSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,7 @@ static class ParameterSet
internal const string ManualWithManual = "ManualWithManualSet";

internal const string Object = "ObjectSet";

internal const string HashTable = "HashTableSet";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ private PrtgCmdlet GetPreviousPrtgCmdletInternal()
}

if (cmdlet.CommandRuntime is DummyRuntime)
return ((DummyRuntime) cmdlet.CommandRuntime).Owner;
return (PrtgCmdlet) ((DummyRuntime) cmdlet.CommandRuntime).Owner;

return null;
}
Expand Down Expand Up @@ -414,7 +414,7 @@ public Pipeline GetSelectPipelineOutput()
{
var command = (PSCmdlet)GetUpstreamCmdletNotOfType<WhereObjectCommand>();

var queue = (Queue<PSObject>) command.PSGetInternalField("_selectObjectQueue", "selectObjectQueue");
var queue = (Queue<PSObject>) command.PSGetInternalField("_selectObjectQueue", "selectObjectQueue", command);

var cmdletPipeline = GetCmdletPipelineInput();

Expand Down Expand Up @@ -465,7 +465,7 @@ private static Pipeline GetCmdletPipelineInput(ICommandRuntime commandRuntime, I
if (declaringType == typeof(Array) || enumeratorType.Name == "SZArrayEnumerator") //It's a SZArrayEnumerator (piping straight from a variable). In .NET Core 3.1 SZArrayEnumerator is no longer nested
list = ((object[]) enumerator.GetInternalField("_array"));
else if (declaringType == typeof(List<>)) //It's a List<T>.Enumerator (piping from $groups[0].Group)
list = enumerator.PSGetInternalField("_list", "list").ToIEnumerable();
list = enumerator.PSGetInternalField("_list", "list", null).ToIEnumerable();
else if (enumeratorType.IsGenericType && enumeratorType.GetGenericTypeDefinition() == typeof(ReadOnlyListEnumerator<>))
list = enumerator.GetInternalField("list").ToIEnumerable();
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ internal long GetLastSourceId()
if (cmdlet.CommandRuntime is DummyRuntime)
val = DummyRuntime._lastUsedSourceId;
else
val = Convert.ToInt64(cmdlet.CommandRuntime.PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId"));
val = Convert.ToInt64(cmdlet.CommandRuntime.PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId", cmdlet));

if (PipeFromVariableWithProgress && FirstInChain)
{
Expand Down
2 changes: 2 additions & 0 deletions src/PrtgAPI.PowerShell/PrtgAPI.PowerShell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@
<Compile Include="PowerShell\Tree\PowerShellProgressDepthManager.cs" />
<Compile Include="PowerShell\Tree\PowerShellTreeProgressCallback.cs" />
<Compile Include="PowerShell\TriggerParameterParser.cs" />
<Compile Include="Utilities\AstFactory.cs" />
<Compile Include="Utilities\EmptyScriptExtent.cs" />
<Compile Include="Utilities\PSCmdletInvoker.cs" />
<Compile Include="Utilities\PSObjectUtilities.cs" />
<Compile Include="PowerShell\PSRawSensorParameters.cs" />
Expand Down
Loading

0 comments on commit a191fe9

Please sign in to comment.