-
-
Notifications
You must be signed in to change notification settings - Fork 38
Interesting Techniques
Often the values reported by PRTG need to be transformed in some way during or after the deserialization process, such as an empty string being converted into null
or a DateTime/TimeSpan being correctly parsed. As the System.Xml
XmlSerializer
requires that all target properties be public
, this presents a variety of issues, requiring "dummy" properties that accept the raw deserialized output and then are correctly parsed via the getter of the "actual" property. Such "raw" properties make a mess of your object's interface, bloat your intellisense and mess up your PowerShell output.
PrtgAPI works around this by implementing its own custom XmlSerializer
. Unlike the built in XmlSerializer
which generates a dynamic assembly for highly efficient deserialization, PrtgAPI relies upon reflection to iterate over each object property and bind each XML value based the value of each type/property'sXmlElement
, XmlAttribute
and XmlEnum
attributes. This allows PrtgAPI to bind raw values to protected
members that are then parsed by the getters of their public
counterparts. The PrtgAPI XmlSerializer
also has the sense to eliminate a number of common pain points, such as converting empty strings to null.
To improve the performance of deserializing objects, PrtgAPI implements a reflection cache, storing properties, fields and enums of deserialized types. While deserialization performance is not usually noticeable in normal operation of PrtgAPI, this becomes greatly beneficial when executing unit tests, where tests that attempt to deserialize tens of thousands of objects find their performance improved by over 200%.
PrtgClient
exposes two event handlers LogVerbose
and RetryRequest
that expose informational status messages from PrtgAPI's request engine, such as the URL of the request that is being executed as well as that a failed request is being retried. As PowerShell allows for more free-form, batch oriented programming, it is useful to be able to expose this information from secondary output streams such as the verbose and warning streams.
Unfortunately however, due to the nature of PowerShell's execution model, event handlers cannot be simply "wired up" to a single cmdlet and used throughout the life of a pipeline. When a pipeline is being executed, only a single cmdlet may write to an output stream at any given time.
For example, consider the following cmdlet
Get-Sensor | Get-Channel
When this cmdlet executes, the following may occur
-
Get-Sensor
, as the first cmdlet in the chain, subscribes to theRetryRequest
event handler -
Get-Sensor
retrieves an initial 500 sensors from a PRTG Server.Get-Sensor
is currently the top most cmdlet -
Get-Channel
retrieves the channels for each of the first 500 sensors.Get-Channel
is now the top most cmdlet -
Get-Sensor
attempts to retrieve an additional 500 sensors (since it is capable of streaming), however PRTG times out, causing the request engine to invoke itsRetryRequest
event handler -
Get-Sensor
attempts toWriteWarning
theRetryRequest
message - PowerShell throws an exception as
Get-Sensor
is not currently in a position to write to the warning stream
Having each cmdlet subscribe to the RetryRequest
event handler is even worse, as now multiple cmdlets are trying to write to write the same warning at once. We could make each cmdlet "replace" the previous event handler, however this causes a new issue in that in a long chain of cmdlets, cmdlets will be continually stopped and started. We want PrtgAPI to somehow know who is the active cmdlet, so that they will be made responsible for all events triggered by an event handler.
PrtgAPI solves this problem using an EventStack
. When the ProcessRecord
method of a cmdlet is executed, the current cmdlet's event handlers are activated. When ProcessRecord
completes, the event handlers are completely removed. In this way, whoever is currently processing records is the only person who is both capable and allowed to execute an event handler
protected override void ProcessRecord()
{
RegisterEvents();
try
{
ProcessRecordEx();
}
catch
{
UnregisterEvents(false);
throw;
}
finally
{
if (Stopping)
{
UnregisterEvents(false);
}
}
if (IsTopmostRecord())
{
UnregisterEvents(true);
}
}
//Real work is done here.
protected abstract void ProcessRecordEx();
Multiple scenarios exist which can cause a cmdlet to stop running. Even if an exception is thrown from a cmdlet, the cmdlet may be executed again by the previous cmdlet if the ErrorActionPreference
is Continue
. As such, a boolean flag is used to specify whether to pretend to start from scratch as if the cmdlet had never executed in the first place.
By placing this code in a base class, all derived classes can be forced to implement a ProcessRecordEx
method, reducing the likelihood they will accidentally overwrite the ProcessRecord
method, thereby breaking this functionality.
Before and after each test begins, a number of common tasks must be performed. For example, in PowerShell we must load the PrtgAPI and PrtgAPI.Tests.* assemblies into the session. We cannot just do this once in one test file and forget about it, as tests are split across a number of files and could be run one at a time via Test Explorer.
.NET tests perform common initialization/cleanup via AssemblyInitialize
/AssemblyCleanup
/TestInitialize
methods defined in common base classes of all tests.
Common startup/shutdown tasks can be defined in Pester via the BeforeAll
/AfterAll
functions, however PrtgAPI abstracts that a step further by completely impersonating the Describe
function. When tests call the Describe
function they trigger PrtgAPI's Describe
, which in turn triggers the Pester Describe
with our BeforeAll
/AfterAll
blocks pre-populated
. $PSScriptRoot\Common.ps1
function Describe($name, $script) {
Pester\Describe $name {
BeforeAll {
PerformStartupTasks
}
AfterAll {
PerformShutdownTasks
}
& $script
}
}
Different Describe
overrides can be defined in different files, allowing tests to perform cleanup in different ways based on their functionality (such as Get-
only tests not needing to perform cleanup on the integration test server). Methods such as AssemblyInitialize
in our .NET test assembly can be triggered via our common startup functions, allowing existing testing functionality to be reused.
Integration tests can take an extremely long time to complete, can run in any order and can even cross contaminate. By intercepting key test methods and sprinkling tests with basic logging code, detailed state information can be written to a log file (%temp%\PrtgAPI.IntegrationTests.log) which can be tailed and monitored during the execution of tests
24/06/2017 11:46:00 AM [22952:58] C# : Pinging ci-prtg-1
24/06/2017 11:46:00 AM [22952:58] C# : Connecting to local server
24/06/2017 11:46:00 AM [22952:58] C# : Retrieving service details
24/06/2017 11:46:00 AM [22952:58] C# : Backing up PRTG Config
24/06/2017 11:46:01 AM [22952:58] C# : Refreshing CI device
24/06/2017 11:46:01 AM [22952:58] C# : Ready for tests
24/06/2017 11:46:01 AM [22952:58] PS : Running unsafe test 'Acknowledge-Sensor_IT'
24/06/2017 11:46:01 AM [22952:58] PS : Running test 'can acknowledge indefinitely'
24/06/2017 11:46:01 AM [22952:58] PS : Acknowledging sensor indefinitely
24/06/2017 11:46:01 AM [22952:58] PS : Refreshing object and sleeping for 30 seconds
24/06/2017 11:46:31 AM [22952:58] PS : Pausing object for 1 minute and sleeping 5 seconds
24/06/2017 11:46:36 AM [22952:58] PS : Resuming object
24/06/2017 11:46:37 AM [22952:58] PS : Refreshing object and sleeping for 30 seconds
24/06/2017 11:47:07 AM [22952:58] PS !!! : Expected: {Down} But was: {PausedUntil}
24/06/2017 11:47:07 AM [22952:58] PS : Running test 'can acknowledge for duration'
24/06/2017 11:47:07 AM [22952:58] PS : Acknowledging sensor for 1 minute
24/06/2017 11:47:07 AM [22952:58] PS : Sleeping for 60 seconds
24/06/2017 11:48:07 AM [22952:58] PS : Refreshing object and sleeping for 30 seconds
24/06/2017 11:48:37 AM [22952:58] PS : Test completed successfully
24/06/2017 11:48:37 AM [22952:58] PS : Running test 'can acknowledge until'
24/06/2017 11:48:38 AM [22952:58] PS : Acknowledging sensor until 24/06/2017 11:49:38 AM
24/06/2017 11:48:38 AM [22952:58] PS : Sleeping for 60 seconds
24/06/2017 11:49:38 AM [22952:58] PS : Refreshing object and sleeping for 30 seconds
24/06/2017 11:50:08 AM [22952:58] PS : Test completed successfully
24/06/2017 11:50:08 AM [22952:58] PS : Performing cleanup tasks
24/06/2017 11:50:08 AM [22952:58] C# : Cleaning up after tests
24/06/2017 11:50:08 AM [22952:58] C# : Connecting to server
24/06/2017 11:50:08 AM [22952:58] C# : Retrieving service details
24/06/2017 11:50:08 AM [22952:58] C# : Stopping service
24/06/2017 11:50:21 AM [22952:58] C# : Restoring config
24/06/2017 11:50:21 AM [22952:58] C# : Starting service
24/06/2017 11:50:24 AM [22952:58] C# : Finished
24/06/2017 11:50:25 AM [22952:63] PS : PRTG service may still be starting up; pausing for 60 seconds
24/06/2017 11:51:31 AM [22952:63] PS : Running safe test 'Get-NotificationAction_IT'
DateTime, PID, TID, execution environment and exception details are all easily visible. Showing three exclamation marks against rows that contain a failure is probably the greatest feature of the entire project.