TFS Power Shell Monitor

TeamFoundationServer2017.TfsPowerShellMonitor (UnitMonitorType)

Monitor that uses PowerShell script to determine the availability/health of an item

Element properties:

RunAsDefault
AccessibilityPublic
Support Monitor RecalculateFalse

Member Modules:

ID Module Type TypeId RunAs 
DataSource DataSource System.SimpleScheduler Default
Script ProbeAction Microsoft.Windows.PowerShellPropertyBagProbe Default
ErrorFilter ConditionDetection System.ExpressionFilter Default
SuccessFilter ConditionDetection System.ExpressionFilter Default

Overrideable Parameters:

IDParameterTypeSelectorDisplay NameDescription
BaseInstallPathstring$Config/BaseInstallPath$Base Install PathBy default the assemblies necessary to complete the discovery and monitoring are installed under the \%ProgramData\% directory. You can set this to any valid path to install them in a different location.
IntervalSecondsint$Config/IntervalSeconds$Interval SecondsThe interval that this Monitor is run
ScriptTraceEnabledint$Config/ScriptDebugEnabled$Script Trace EnabledTo enable detailed logging of this Monitor set this to 1, setting it to 0 will disable logging. The logs are sent to the Windows Event Log and can be found with the other Operations Manager logs.

Source Code:

<UnitMonitorType ID="TeamFoundationServer2017.TfsPowerShellMonitor" Accessibility="Public">
<MonitorTypeStates>
<MonitorTypeState ID="Error" NoDetection="false"/>
<MonitorTypeState ID="Success" NoDetection="false"/>
</MonitorTypeStates>
<Configuration>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" minOccurs="1" name="ClassName" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" minOccurs="1" name="ObjectPath" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" minOccurs="1" name="BaseInstallPath" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" minOccurs="1" name="IntervalSeconds" type="xsd:integer"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" minOccurs="1" name="ScriptDebugEnabled" type="xsd:integer"/>
</Configuration>
<OverrideableParameters>
<OverrideableParameter ID="BaseInstallPath" Selector="$Config/BaseInstallPath$" ParameterType="string"/>
<OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int"/>
<OverrideableParameter ID="ScriptTraceEnabled" Selector="$Config/ScriptDebugEnabled$" ParameterType="int"/>
</OverrideableParameters>
<MonitorImplementation>
<MemberModules>
<DataSource ID="DataSource" TypeID="System!System.SimpleScheduler">
<IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds>
<SyncTime/>
</DataSource>
<ProbeAction ID="Script" TypeID="Windows!Microsoft.Windows.PowerShellPropertyBagProbe">
<ScriptName>TfsMonitor.ps1</ScriptName>
<ScriptBody><Script>param($baseInstallPath, $className, $objectPath, $scriptDebugEnabled)
###
# This monitor will check the health of a specific Tfs component via a C# Class
# To insure errors don't go on noticed $ErrorActionPreference will be set to Stop.
###
# $baseInstallPath - An override location to install the binary - if not provided a default will be used, see Compute-InstallPath
# $className - The name of the class (from with in ManagementPack namespace/assembly) that should be used for this discovery
# $scriptDebugEnabled - A flag to control if debug information should be logged
# Errors and Warnings are always logged

#
# Constants
[string] $Script:MessagePreFix = "TfsMonitor.ps1"

#
#
# COMMON HEADER - START

# Sets $Script:mpVersion, $Script:tfsVersion, $Script:mpVersionEnd and $Script:baseRegesityKey
# PRODUCT CONSTANTS - START
[string] $Script:mpVersion = "15.113.26130.0"
[string] $Script:tfsVersion = "15.0"
[int] $Script:mpVersionEnd = 0
[string] $Script:baseRegesityKey = "Software\Microsoft\TeamFoundationServer\15.0"
# PRODUCT CONSTANTS - END

[string] $Script:defaultBaseInstallPath = "$env:ProgramData\Microsoft Team Foundation Server $Script:tfsVersion Management Pack\"

[int] $Script:EventLogId = 9000 + $Script:mpVersionEnd

[int] $Script:severityInfo = 0
[int] $Script:severityError = 1
[int] $Script:severityWarn = 2

#
# List of Included Assemblies
$Script:InstallFiles = @(
# FILE RESOURCES - START
"$FileResource[Name='Res.ManagementPack.exe']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.Client.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.Common.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.Core.WebApi.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.VersionControl.Client.dll']/Path$"
"$FileResource[Name='Res.Microsoft.VisualStudio.Services.Client.Interactive.dll']/Path$"
"$FileResource[Name='Res.Microsoft.VisualStudio.Services.Common.dll']/Path$"
"$FileResource[Name='Res.Microsoft.VisualStudio.Services.WebApi.dll']/Path$"
# FILE RESOURCES - END
)

#
#cached values
$Script:MomSciptApi = $null

#
# Script Parameters
[bool] $Script:detailedLogEnabled = $false

#
# Allow cleaning up after ourselves
$originalErrorActionPreference = $ErrorActionPreference

#
# Sets $Script:detailedLogEnabled
function SetDetailedLogEnabled
{
param($detailedLogEnabled)

[int] $val = 0
if ([int]::TryParse($detailedLogEnabled, [ref] $val))
{
$Script:detailedLogEnabled = ($val -ne 0)
}
}

#
# Determines the install path
function Get-InstallPath
{
param ([string] $baseInstallPath)

[string] $installPath = Compute-InstallPath -baseInstallPath $baseInstallPath
Validate-Install -installPath $installPath

return $installPath
}

#
# Computes the Install Path for the MP binary files
function Compute-InstallPath
{
param ([string] $baseInstallPath)

if ([String]::IsNullOrEmpty($baseInstallPath))
{
$baseInstallPath = $Script:defaultBaseInstallPath
}

[string] $result = [System.IO.Path]::Combine($baseInstallPath, $Script:mpVersion) + "\"

Log-Message -message ("Computed Install Path: {0}" -f $result) -severity $Script:severityInfo

return $result
}

#
# Verifies that the binary files has been placed in the provided install directory
# It can fail if we can't a lock using the mutex for the install path
function Validate-Install
{
param ([string] $installPath)

Log-Message -message ("Entered InstallIfNeeded: {0}" -f $installPath) -severity $Script:severityInfo

## Need void here to prevent powershell from adding a space to the caller outout
[void] [System.Threading.Mutex]$mutex
[bool] $gotMutex = $false
try
{
[string] $mutextName = ("Global\{0}" -f $installPath.Replace("\",";"))
Log-Message -message ("Creating Mutex: {0}" -f $mutextName) -severity $Script:severityInfo
[bool]$wasCreated = $false
$mutex = New-Object System.Threading.Mutex($true, $mutextName, [ref] $wasCreated)
if ($wasCreated)
{
Log-Message -message "Created Mutex" -severity $Script:severityInfo
$gotMutex = $true
}
else
{
Log-Message -message "Waiting for up to 3 secs for Mutext" -severity $Script:severityInfo
try
{
$gotMutex = $mutex.WaitOne(3000)
}
catch [System.Threading.AbandonedMutexException]
{
## We have no problem just taking over an Abandoned Mutex
Log-Message -message "Mutex was Previously Abandoned" -severity $Script:severityInfo

$gotMutex = $true
}
Log-Message -message ("gotMutex: {0}" -f $gotMutex) -severity $Script:severityInfo
}

if ($gotMutex)
{
Validate-Install-UnderMutex -installPath $installPath
}
else
{
throw [Exception] ("Failed get Mutext to update {0}" -f $installPath)
}

}
finally
{
if ($gotMutex)
{
$mutex.ReleaseMutex()
}
if ($mutex -ne $null)
{
$mutex.Dispose()
}
}

Log-Message -message "Exiting InstallIfNeeded" -severity $Script:severityInfo
}

#
# Does the work of Verifying that the binaries files have been placed.
# Assumes the caller already obtained the needed Mutext to prevent raise conditions
function Validate-Install-UnderMutex
{
param ([string] $installPath)

Log-Message -message ("Entered Validate-Install-UnderMutex: {0}" -f $installPath) -severity $Script:severityInfo

#
# Make sure the directory is there.
[void] (New-Item -Force -ItemType directory -Path $installPath)

foreach ($file in $Script:InstallFiles)
{
$fileName = [IO.Path]::GetFileName($file)
if (-not(Test-Path -PathType Leaf -Path ($installPath + $fileName)))
{
[string] $tempName = $fileName + '.temp'
if (Test-Path -PathType Leaf -Path ($installPath + $tempName))
{
Log-Message -message ("Removing old temp file {0}{1}" -f $installPath, $tempName) -severity $Script:severityInfo
[void] (Remove-Item -Path ($installPath + $tempName) -Force)
}

Log-Message -message ("Copying to temp location {0} -&gt; {1}{2}" -f $file, $installPath, $tempName) -severity $Script:severityInfo
[void] (Copy-Item -Path $file -Destination ($installPath + $tempName))

Log-Message -message ("Rename {0}{1} -&gt; {2}" -f $installPath, $tempName, $fileName) -severity $Script:severityInfo
[void] (Rename-Item -Path ($installPath + $tempName) -NewName $fileName)
}
}

Log-Message -message ("Exiting Validate-Install-UnderMutex: {0}" -f $installPath) -severity $Script:severityInfo
}

#
# Adds information about the current user to the log
function Log-User
{
$owner = (Get-WmiObject win32_process | Where-Object {$_.ProcessId -eq $pid }).getowner()

$message = "User Info"
$message += [Environment]::NewLine + "Win32_ComputerSystem -&gt; user name: " + (Get-WmiObject Win32_ComputerSystem).username
$message += [Environment]::NewLine + '$env:username:' + $env:username
$message += [Environment]::NewLine + "Owner: " + $owner.Domain + "\" + $owner.User

Log-Message -message $message -severity $Script:severityInfo
}

#
# Calls ManagementPack.exe and logs the response. Additionally parse out the log and returns the pay load
# $forDiscovery should be $true when running a Discovery and $false for monitors
function Run-ManagementPack
{
param ([string] $baseInstallPath, [bool] $forDiscovery, [string] $className, [string] $params)

Log-Message -message "Entered Run-ManagementPack: $baseInstallPath $forDiscovery $className $params" -severity $Script:severityInfo

[string] $installPath = Get-InstallPath -baseInstallPath $baseInstallPath
[string] $fullPath = [System.IO.Path]::Combine($installPath, "ManagementPack.exe")

[string] $action = $null
if ($forDiscovery)
{
$action = "Discover"
}
else
{
$action = "Monitor"
}

[string] $command = (InQuotes -str $fullPath) + " /Action:$action /Class:$className $params"
Log-Message -message "Calling $command" -severity $Script:severityInfo
[array] $output = Invoke-Expression "&amp; $command"
[int] $exitCode = $LASTEXITCODE

[string] $response = [string]::Join([System.Environment]::NewLine, $output)

[int] $severity = $Script:severityInfo
[string] $message = "Exit Code $exitCode from Calling $command $([System.Environment]::NewLine)$response"
# $exitCode = 0 - No errors, log severity info
# $exitCode = 1 - At least one error and we were able get some data, log severity Error
# $exitCode = 2 - Failed to get data, throw
# $exitCode = Anything Else - Unexpected error, throw
if (($exitCode -eq 0) -or ($exitCode -eq 1))
{
if ($status -gt 0)
{
$severity = $Script:severityError
}
Log-Message -message $message -severity $severity
}
else
{
throw [Exception] $message
}

# At this point response should hold a log and a payload, return the payload
[string] $logBarrior = "**[[&lt;&lt;END OF LOG&gt;&gt;]]**"
[int] $index = $response.IndexOf($logBarrior)

if ($index -lt 0)
{
throw [Exception] "Failed to find $logBarrior in response: $response"
}

[string] $result = $response.SubString($index + $logBarrior.Length)

if ($result.StartsWith([System.Environment]::NewLine))
{
$result = $result.Substring([System.Environment]::NewLine.Length)
}

Log-Message -message "Exiting Run-ManagementPack: $baseInstallPath $forDiscovery $className $params" -severity $Script:severityInfo

return $result
}

function InQuotes
{
param ([string] $str)
return '"' + $str + '"'
}

#
# Pulls a specific values from the windows registry
function Get-RegistryValue
{
param ([string] $path, [string] $name)

$message = "Reading Registry " + $path + "[" + $name + "]="
$result = $null
try
{
$result = (Get-ItemProperty -Path $path -Name $name -ErrorAction Stop).$name
$message += $result
}
catch
{
$message += "{" + $_ +"}"
}

Log-Message -message $message -severity $Script:severityInfo
return $result
}

#
# Logs messages to the windows event log
function Log-Message
{
param ([string] $message, [int] $severity)

switch ($severity)
{
$Script:severityInfo {Write-Host ($message) } # Info
$Script:severityWarn {Write-Host ($message) -ForegroundColor Yellow } # Warning
default # Error
{
$severity = $Script:severityError
Write-Host ($message) -ForegroundColor Red
}
}

if (($severity -eq $Script:severityInfo) -and (-not $Script:detailedLogEnabled))
{
# Don't record info to the event log
return
}

$header = $Script:mpVersion + "|" + $Script:MessagePreFix

$script = $MyInvocation.MyCommand.Name

$chunks = Get-Chunks -message $message

$api = Get-MomScriptApi

if ($chunks.Count -eq 1)
{
$api.LogScriptEvent($script, $Script:EventLogId, $severity, $header + $message)
}
else
{
$api.LogScriptEvent($script, $Script:EventLogId, $severity, ("{0} Broken into {1} Chunks" -f $header, $chunks.Count))
for ($i = 0; $i -lt $chunks.Count; $i++)
{
$api.LogScriptEvent($script, $Script:EventLogId, $severity, $header + $chunks[$i])
}
}
}

#
# Takes a string that could be very long and breaks it into smaller chunks
function Get-Chunks
{
param ([string] $message)
$chunkSize = 20480 #20K
$result = @{}

for ($i = 0; $i -lt $message.Length; $i += $chunkSize)
{
$length = $i + $chunkSize
if ($length -gt $message.Length)
{
$length = $message.Length - $i
}
else
{
$length = $chunkSize
}

$result[$result.Count] = $message.SubString($i, $length)
}

return $result
}

#
# Wrapper to obtain and cache the value to MOM COM Script API
function Get-MomScriptApi
{
if ($Script:MomSciptApi -eq $null)
{
$Script:MomSciptApi = new-object -comObject 'MOM.ScriptAPI'
}
return $Script:MomSciptApi
}

# COMMON HEADER - END
#
#

[int] $Script:EventLogId = 19000 + $Script:mpVersionEnd

#
# Start of Specific Functions
########################################################################################

#
# Checks the Status specific component via a C# exe
function Get-Status
{
param([string] $baseInstallPath, [string]$className, [string]$objectPath)

[string] $params = InQuotes -str "/ObjectPath:$objectPath"
[string] $result = Run-ManagementPack -baseInstallPath $baseInstallPath -forDiscovery $false -className $className -params $params

return $result
}

########################################################################################
# End of Functions
#

#
# our return value
$bag = $null

try
{
#
# Setup

$ErrorActionPreference = "Stop"

SetDetailedLogEnabled -detailedLogEnabled $scriptDebugEnabled

$Script:MessagePreFix = ("{0}|{1}|{2}|{3}|{4}" -f $Script:MessagePreFix, $baseInstallPath, $className, $objectPath, [Environment]::NewLine)

#
# Log that we are here and validate our input
Log-Message -message ("First Param baseInstallPath:{0}" -f $baseInstallPath) -severity $Script:severityInfo

Log-Message -message ("Second Param className:{0}" -f $className) -severity $Script:severityInfo
If ([String]::IsNullOrEmpty($className))
{
throw [Exception] "className missing!"
}

Log-Message -message ("Third Param scriptDebugEnabled:{0}" -f $scriptDebugEnabled) -severity $Script:severityInfo

Log-User

#
# Do the work

$api = Get-MomScriptApi
$bag = $api.CreateTypedPropertyBag(3) #3-State Data
$result = Get-Status -baseInstallPath $baseInstallPath -className $className -objectPath $objectPath
$bag.AddValue("Status", $result)
}
catch
{
Log-Message -message ("{0}{1}{2}" -f $_.InvocationInfo.PositionMessage, [Environment]::NewLine, $_.Exception) -severity $Script:severityError
if ($bag -ne $null)
{
$bag.AddValue("Status", "Error See log")
}
}
finally
{
#
# Clean up after our self
$ErrorActionPreference = $originalErrorActionPreference
}
return $bag
</Script></ScriptBody>
<Parameters>
<Parameter>
<Name>BaseInstallPath</Name>
<Value>$Config/BaseInstallPath$</Value>
</Parameter>
<Parameter>
<Name>className</Name>
<Value>$Config/ClassName$</Value>
</Parameter>
<Parameter>
<Name>objectPath</Name>
<Value>$Config/ObjectPath$</Value>
</Parameter>
<Parameter>
<Name>scriptDebugEnabled</Name>
<Value>$Config/ScriptDebugEnabled$</Value>
</Parameter>
</Parameters>
<TimeoutSeconds>300</TimeoutSeconds>
</ProbeAction>
<ConditionDetection ID="ErrorFilter" TypeID="System!System.ExpressionFilter">
<Expression>
<SimpleExpression>
<ValueExpression>
<XPathQuery Type="String">Property[@Name='Status']</XPathQuery>
</ValueExpression>
<Operator>NotEqual</Operator>
<ValueExpression>
<Value Type="String">OK</Value>
</ValueExpression>
</SimpleExpression>
</Expression>
</ConditionDetection>
<ConditionDetection ID="SuccessFilter" TypeID="System!System.ExpressionFilter">
<Expression>
<SimpleExpression>
<ValueExpression>
<XPathQuery Type="String">Property[@Name='Status']</XPathQuery>
</ValueExpression>
<Operator>Equal</Operator>
<ValueExpression>
<Value Type="String">OK</Value>
</ValueExpression>
</SimpleExpression>
</Expression>
</ConditionDetection>
</MemberModules>
<RegularDetections>
<RegularDetection MonitorTypeStateID="Error">
<Node ID="ErrorFilter">
<Node ID="Script">
<Node ID="DataSource"/>
</Node>
</Node>
</RegularDetection>
<RegularDetection MonitorTypeStateID="Success">
<Node ID="SuccessFilter">
<Node ID="Script">
<Node ID="DataSource"/>
</Node>
</Node>
</RegularDetection>
</RegularDetections>
</MonitorImplementation>
</UnitMonitorType>