TFS Power Shell Monitor

TeamFoundationServer2015.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="TeamFoundationServer2015.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 Microsoft.TeamFoundation.Scom.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 = "14.0.24807.0"
[string] $Script:tfsVersion = "14.0"
[int] $Script:mpVersionEnd = 0
[string] $Script:baseRegesityKey = "Software\Microsoft\TeamFoundationServer\14.0"
# PRODUCT CONSTANTS - END

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

[int] $Script:EventLogId = 9000 + $Script:mpVersionEnd
[string] $Script:namespaceName = "Microsoft.TeamFoundation.Scom.ManagementPack"


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

#
# List of Included Assemblies
$Script:InstallFiles = @(
# FILE RESOURCES - START
"$FileResource[Name='Res.Microsoft.TeamFoundation.Scom.ManagementPack.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.DistributedTask.Agent.Common.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.DistributedTask.Agent.Interfaces.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.Build.Client.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.Build.Common.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.Client.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.VersionControl.Client.dll']/Path$"
"$FileResource[Name='Res.Microsoft.VisualStudio.Services.Common.dll']/Path$"
"$FileResource[Name='Res.Microsoft.VisualStudio.Services.WebApi.dll']/Path$"
"$FileResource[Name='Res.Microsoft.TeamFoundation.Common.dll']/Path$"
"$FileResource[Name='Res.Microsoft.VisualStudio.Services.Client.dll']/Path$"
"$FileResource[Name='Res.Newtonsoft.Json.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
}
else
{
if (-not($baseInstallPath.EndsWith("\")))
{
$baseInstallPath += "\"
}
}

[string] $result = $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
$message += [Environment]::NewLine + "Loaded Assemblies:"
[System.AppDomain]::CurrentDomain.GetAssemblies() | %{ $message += ([Environment]::NewLine + $_.Location) }

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

#
# Loads an Assembly, and creates a instance of an object from that assembly
# All the TFS Scom MP Objects are Disposable. Failing to create a valid object will get cleaned up here, but if an object is returned it needs to be disposed.
function Get-ObjectFromAssembly
{
param ([string] $fullPath, [string] $fullClassName)

$message = "Loading {0} from {1}" -f $fullClassName, $fullPath
Log-Message -message $message -severity $Script:severityInfo

Add-Type -path $fullPath

$result = $null

try
{
$result = New-Object $fullClassName

$location = $result.GetType().Assembly.Location
$message = "Loaded from {0}" -f $location
Log-Message -message $message -severity $Script:severityInfo

if ($location.ToLower() -ne $fullPath.ToLower())
{
# We set your Discoveries as Isolation="OwnProcess" however that does not effect Monitors, their PowerShell Instance is shared.
throw [Exception] ("Failed to load {0} from the correct location, got {1} instead. Please stop and restart the Health Service" -f $fullPath, $location)
}
}
catch
{
if ($result -ne $null)
{
try
{
$result.Dispose()
# Leaving this as info, the original exception will get logged
Log-Message -message ("Exception was thrown while creating {0} from {1}. Disposed before re-throwing error" -f $fullClassName, $fullPath) -severity $Script:severityInfo
}
catch
{
# This can happen if they have an old copy of the assembly loaded which does not have a Dispose Method
# Marking this as an error, something is going wrong here.
Log-Message -message ("Exception was thrown while creating {0} from {1}, Second Exception was throw during dispose" -f $fullClassName, $fullPath) -severity $Script:severityError
}
$result = $null
}
throw
}

return $result
}

#
# 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-MomSciptApi

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 SCOM Script API
function Get-MomSciptApi
{
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# Class
function Get-Status
{
param([string] $baseInstallPath, [string]$className, [string]$objectPath)

[string] $path = Get-InstallPath -baseInstallPath $baseInstallPath

$dll = $Script:namespaceName + ".dll"
$fullClassName = $Script:namespaceName + "." + $className

[string] $fullPath = $path + $dll

$result = "Error"
$monitor = $null
try
{
$monitor = Get-ObjectFromAssembly -fullPath $fullPath -fullClassName $fullClassName

$result = $monitor.Status($objectPath)
$message = ("Result: {0} {1}" -f $result, $monitor.GetAndDumpLog())
}
finally
{
if ($monitor -ne $null)
{
$monitor.Dispose()
Log-Message -message ("Disposed instance of {0} from {1}" -f $fullClassName, $fullPath) -severity $Script:severityInfo
$monitor = $null
}
}

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

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-MomSciptApi
$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>