[Xymon] [External] Powershell client maintainer

Stef Coene stef.coene at docum.org
Fri Dec 15 13:31:34 CET 2023


Hi,

I cleaned up the code, see the attached patch.

Some bugs:
Remove `r from message
  -> This corrupts the procs check

Use [text.encoding]::ascii.getbytes to encode the data stream
  -> On some server sometimes the original code gives 0 bytes. I never 
found out in the orignal datastream what was the reason. The option 
xymonlogarchive was used to keep the logfiles and the collected data but 
we never found a difference.

Allow to download a file when serverUrl is used via the bb: syntax


Other changes:
Option ping to test connection to the xymon server
  - Test the new version with the 'ping' option to make sure it works

Environment variable YMONCLIENTCFG can be used to point to an 
alternatieve xymonclient_config.xml configuration file
  -> We use this in combination with the 'ping' option to test a new XML 
configuration file with an external script

Download configuration files from xymon server to etc directory
  - Add config option to the clientconfigfile to download configuration 
files to the etc directory
  - Add function XymonManageConfigs to download configuration files to 
the etc directory
  -> We use this to distribute configuration file for external scripts 
to the servers. One of the scripts is used to generate a new xml 
configuration file.

Allow to send something via the 'usermsg' channel
  -> We use this to send inventory data collected by an external script 
to the xymon server. Of course, you need a scripts on the xymon server 
to process this data.

Allow multiple serverUrl that will receiving the same data, separated 
with space
  - Same serverHttpUsername/serverHttpPassword !
  -> We have used this to migrate to a new xymon server so both receive 
all data.

Disable server certification validation when sending data to a https server
  -> This was needed for a Xymon server with https with self signed 
certificates. Maybe do this via an option?

Add xymonlogarchive to the clientconfigfile to copy the logfiles and 
send data to an alternative directory
    - Usefull for debugging
    - Also some changes in XymonLogSend

Add slowscanrate option to the clientconfigfile to overrule the default 
slowscanrate setting of 72

Duplicate bb to xymon in the clientconfigfile

Add scan|<number> to the clientconfigfile so you can run an external 
script every <number> run
  - Also some changes in XymonExecuteExternals

Make slowscanrate a random number during startup


Stef
-------------- next part --------------
1c1
< # ###################################################################################
---
> # ###################################################################################
10a11
> # Copyright (c) 2023 Stef Coene
30a32,74
> # Changelog Stef Coene:
> # Remove `r from message
> #  -> This corrupts the procs check
> # 
> # Use [text.encoding]::ascii.getbytes to encode the data stream
> #  -> On some server sometimes the original code gives 0 bytes. I never found out in the orignal datastream what was the reason. The option xymonlogarchive was used to keep the logfiles and the collected data but we never found a difference.
> # 
> # Allow to download a file when serverUrl is used via the bb: syntax
> # 
> # Option ping to test connection to the xymon server
> #  - Test the new version with the 'ping' option to make sure it works
> # 
> # Environment variable YMONCLIENTCFG can be used to point to an alternatieve xymonclient_config.xml configuration file
> #  -> We use this in combination with the 'ping' option to test a new XML configuration file with an external script
> # 
> # Download configuration files from xymon server to etc directory
> #  - Add config option to the clientconfigfile to download configuration files to the etc directory
> #  - Add function XymonManageConfigs to download configuration files to the etc directory
> #  -> We use this to distribute configuration file for external scripts to the servers. One of the scripts is used to generate a new xml configuration file.
> # 
> # Allow to send something via the 'usermsg' channel
> #  -> We use this to send inventory data collected by an external script to the xymon server. Of course, you need a scripts on the xymon server to process this data.
> # 
> # Allow multiple serverUrl that will receiving the same data, separated with space
> #  - Same serverHttpUsername/serverHttpPassword !
> #  -> We have used this to migrate to a new xymon server so both receive all data.
> # 
> # Disable server certification validation when sending data to a https server
> #  -> This was needed for a Xymon server with https with self signed certificates. Maybe do this via an option?
> # 
> # Add xymonlogarchive to the clientconfigfile to copy the logfiles and send data to an alternative directory
> #    - Usefull for debugging
> #    - Also some changes in XymonLogSend
> # 
> # Add slowscanrate option to the clientconfigfile to overrule the default slowscanrate setting of 72
> # 
> # Duplicate bb to xymon in the clientconfigfile
> # 
> # Add scan|<number> to the clientconfigfile so you can run an external script every <number> run
> #  - Also some changes in XymonExecuteExternals
> # 
> # Make slowscanrate a random number during startup
> 
43c87
< $Version = '2.42'
---
> $Version = '2.436'
47c91,97
< $XymonClientCfg = join-path $xymondir 'xymonclient_config.xml'
---
> 
> if ( -not $env:XYMONCLIENTCFG ) {
>    $XymonClientCfg = join-path $xymondir 'xymonclient_config.xml'
> } else {
>    $XymonClientCfg = join-path $xymondir $env:XYMONCLIENTCFG
> }
> 
1002d1051
<     SetIfNot $script:XymonSettings slowscanrate 72 # repeats of main loop before collecting slowly changing information again
1020a1070
>     $configdir = Join-Path $xymondir 'etc'
1023a1074
>     SetIfNot $script:XymonSettings configlocation $configdir
2854c2905,2911
<                         WriteLog "Sending Xymon message for file $($f.Name) - test $($testName), host $($hostName): $msg"
---
>                         WriteLog "Sending Xymon message for file $($f.Name) - test $($testName), host $($hostName)"
>                         XymonSend $msg $script:XymonSettings.serversList
>                     }
>                     elseif ($statusFileContent -match '^usermsg ')
>                     {
>                         $msg = $statusFileContent
>                         WriteLog "Sending Xymon usermsg"
2991c3048
< function XymonSendViaHttp($msg)
---
> function XymonSendViaHttp($msg, $filePath)
2995,3000c3052,3058
<     $url = $script:XymonSettings.serverUrl
<     if ($url -notmatch '^https?://')
<     {
<         WriteLog "  ERROR: invalid server Url, check config: $url"
<         return ''
<     }
---
>     $script:XymonSettings.serverUrl.Split(" ") | ForEach {
>         $url = $_
>         if ($url -notmatch '^https?://')
>         {
>             WriteLog "  ERROR: invalid server Url, check config: $url"
>             return ''
>         }
3002,3008c3060,3066
<     WriteLog "  Using url $url"
<     $encodedAuth = ''
<     if ($script:XymonSettings.serverHttpUsername -ne '')
<     {
<         $serverHttpPassword = DecryptHttpServerPassword
<         $authString = ('{0}:{1}' -f $script:XymonSettings.serverHttpUsername, `
<             $serverHttpPassword)
---
>         WriteLog "  Using url $url"
>         $encodedAuth = ''
>         if ($script:XymonSettings.serverHttpUsername -ne '')
>         {
>             $serverHttpPassword = DecryptHttpServerPassword
>             $authString = ('{0}:{1}' -f $script:XymonSettings.serverHttpUsername, `
>                 $serverHttpPassword)
3010,3011c3068,3069
<         $encodedAuth = [System.Convert]::ToBase64String(`
<             [System.Text.Encoding]::GetEncoding('ISO-8859-1').GetBytes($authString))
---
>             $encodedAuth = [System.Convert]::ToBase64String(`
>                 [System.Text.Encoding]::GetEncoding('ISO-8859-1').GetBytes($authString))
3014,3015c3072,3073
<         WriteLog "  Using username $($script:XymonSettings.serverHttpUsername)"
<     }
---
>             WriteLog "  Using username $($script:XymonSettings.serverHttpUsername)"
>         }
3017,3019c3075
<     if ($url -match '^https://')
<     {
<         try
---
>         if ($url -match '^https://')
3021c3077,3086
<             [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
---
>             [Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
>             try
>             {
>                 [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
>             }
>             catch
>             {
>                 WriteLog "Error setting TLS options (old version of .NET?): $_"
>                 return $false
>             }
3023c3088,3096
<         catch
---
> 
>         # AXI: verwijderen van ^M, dit stuurt de procs check volledig in de war
>         $msg = $msg.Replace("`r","")
> 
>         # no Invoke-RestMethod before Powershell 3.0
>         $request = [System.Net.HttpWebRequest]::Create($url)
>         $request.Method = 'POST'
>         $request.Timeout = $script:XymonSettings.serverHttpTimeoutMs
>         if ($encodedAuth -ne '')
3025,3026c3098
<             WriteLog "Error setting TLS options (old version of .NET?): $_"
<             return $false
---
>             $request.Headers.Add('Authorization', "Basic $encodedAuth")
3028,3037d3099
<     }
< 
<     # no Invoke-RestMethod before Powershell 3.0
<     $request = [System.Net.HttpWebRequest]::Create($url)
<     $request.Method = 'POST'
<     $request.Timeout = $script:XymonSettings.serverHttpTimeoutMs
<     if ($encodedAuth -ne '')
<     {
<         $request.Headers.Add('Authorization', "Basic $encodedAuth")
<     }
3039,3041c3101,3104
<     $body = [byte[]][char[]]$msg
<     $bodyStream = $request.GetRequestStream()
<     $bodyStream.Write($body, 0, $body.Length)
---
>         # $body = [byte[]][char[]]$msg
>         $body = [text.encoding]::ascii.getbytes($msg)
>         $bodyStream = $request.GetRequestStream()
>         $bodyStream.Write($body, 0, $body.Length)
3043,3052c3106,3115
<     WriteLog "  Connecting to $($url), body length $($body.Length), timeout $($script:XymonSettings.serverHttpTimeoutMs)ms"
<     try
<     {
<         $response = $request.GetResponse()
<     }
<     catch
<     {
<         WriteLog "  Exception connecting to $($url):`n$($_)"
<         return ''
<     }
---
>         WriteLog "  Connecting to $($url), body length $($body.Length), timeout $($script:XymonSettings.serverHttpTimeoutMs)ms"
>         try
>         {
>             $response = $request.GetResponse()
>         }
>         catch
>         {
>             WriteLog "  Exception connecting to $($url):`n$($_)"
>             return ''
>         }
3054,3059c3117,3122
<     $statusCode = [int]($response.StatusCode)
<     if ($response.StatusCode -ne [System.Net.HttpStatusCode]::OK)
<     {
<         WriteLog "  FAILED, HTTP response code: $($response.StatusCode) ($statusCode)"
<         return ''
<     }
---
>         $statusCode = [int]($response.StatusCode)
>         if ($response.StatusCode -ne [System.Net.HttpStatusCode]::OK)
>         {
>             WriteLog "  FAILED, HTTP response code: $($response.StatusCode) ($statusCode)"
>             return ''
>         }
3061,3065c3124,3128
<     $responseStream = $response.GetResponseStream()
<     $readStream = New-Object System.IO.StreamReader $responseStream
<     $output = $readStream.ReadToEnd()
<     WriteLog "  Received $($output.Length) bytes from server"
<     $script:LastTransmissionMethod = 'HTTP'
---
>         $responseStream = $response.GetResponseStream()
>         $readStream = New-Object System.IO.StreamReader $responseStream
>         $output = $readStream.ReadToEnd()
>         WriteLog "  Received $($output.Length) bytes from server"
>         $script:LastTransmissionMethod = 'HTTP'
3066a3130
>     }
3078c3142,3156
<         $outputBuffer = XymonSendViaHttp $msg
---
>         $outputBuffer = XymonSendViaHttp $msg $filePath
> 
>         $line = ($msg -split [environment]::newline)[0]
>         $line = $line -replace '[\t|\s]+', ' '
>         if  ($line -match '(download) (.*$)' ) 
>         {
>             if ($filePath -eq $null -or $filePath -eq "") 
>             {
>                 # save it locally with the same name
>                 $filePath = split-path -leaf $matches[2]
>             }
> 
>             # Save in unix format so the hash is the same as on the (Linux) xymon server
>             Set-Content $filePath ([byte[]][char[]] "$outputBuffer") -Encoding Byte -NoNewLine
>         }
3264a3343
>                 -or $l -match '^xymonlogarchive' `
3268a3348,3349
>                 -or $l -match '^slowscanrate' `
>                 -or $l -match '^config' `
3317a3399,3413
>         # parse slowscanrate if it's there (add if not)
>         $slowscanrate = @($script:clientlocalcfg_entries.keys | `
>             where { $_ -match '^slowscanrate:([0-9]+)$' })
>         if ($slowscanrate.length -gt 1)
>         {
>             WriteLog 'ERROR: more than one slowscanrate directive in config!'
>         }
>         elseif ($slowscanrate.Length -eq 1)
>         {
>             $script:slowscanrate = [int]$matches[1]
>         }
>         else
>         {
>             $script:slowscanrate = 72
>         }
3357a3454
>     XymonManageConfigs
3360c3457
<     XymonExecuteExternals $isSlowScan
---
>     XymonExecuteExternals $isSlowScan $loopcount
3449a3547,3548
>     # test newversion
>     # copy oldversion as backup
3452,3453c3551,3556
<     # re-start service - by exiting, NSSM will notice the process has ended and will
<     # automatically restart it
---
>     # re-start service - by exiting, NSSM will notice the process has ended and will automatically restart it
> 
>     $Process = powershell.exe -File $newversion ping | Out-String
> 
>     if ( $Process -like "*xymond *" ) {
>         WriteLog "New version is working"
3455,3456c3558,3559
<     copy-item "$newversion" "$oldversion" -force
<     remove-item "$newversion"
---
>         # Make backup of old script
>         copy-item "$oldversion" "$oldversion$version" -force
3458,3460c3561,3571
<     WriteLog "Sending final log and restarting service..."
<     XymonLogSend
<     exit
---
>         copy-item "$newversion" "$oldversion" -force
>         remove-item "$newversion"
> 
>         WriteLog "Sending final log and restarting service..."
>         XymonLogSend
>         exit
> 
>     } else {
>         WriteLog "ERROR! New version is not working"
>         WriteLog $Process
>     }
3585c3696
<             elseif ($updatePath -match '^bb')
---
>             elseif ($updatePath -match '^bb' -or $updatePath -match '^xymon')
3607c3718
<                 if ($hashAlgorithm -ne $null)
---
>                 if ($hashAlgorithm -ne $null -and $hashAlgorithm -ne "")
3668c3779
<     elseif ($URI -match '^bb')
---
>     elseif ($URI -match '^bb' -or $URI -match '^xymon')
3679c3790
<     if ($result -and $hashAlgorithm -ne $null)
---
>     if ($result -and $hashAlgorithm -and $hashAlgorithm -ne $null)
3724a3836,3924
> function XymonManageConfigs
> {
>     WriteLog "Executing XymonManageConfigs"
>     $Configs = @($script:clientlocalcfg_entries.keys | `
>         where { $_ -match '^config:' })
> 
>     foreach ($config in $Configs)
>     {
> 
>         if ($config -match '^config:(.+?)(?:\|(MD5|SHA1|SHA256)\|([0-9a-f]+))?$')
>         {
>             # $matches[1] = URL location
>             # $matches[2] = optional hash type
>             # $matches[3] = optional hash value
> 
>             ($ConfigURI, $ConfighashAlgorithm, $ConfighashRequired) = $matches[1..3]
> 
>             $ConfigName = $ConfigURI.SubString($ConfigURI.LastIndexOf('/') + 1)
> 
>             if ( $ConfigName -eq '$ClientName.ini' ) {
>                $ConfigName = $script:clientname + ".ini"
>                $ConfigBaseURI = $ConfigURI.SubString(0,$ConfigURI.LastIndexOf('/') + 1)
>                $ConfigURI = $ConfigBaseURI + $ConfigName
>                WriteLog "Changing config file name to $ConfigName"
>             }
> 
>             $FullName = Join-Path $script:XymonSettings.configlocation $ConfigName
> 
>             $downloadFlag = $false
> 
>             WriteLog "Checking $FullName"
> 
>             # check to see if we have the matching version
>             if (Test-Path $FullName)
>             {
>                 if ($ConfighashAlgorithm -ne $null -and $ConfighashRequired -ne $null)
>                 {
>                     WriteLog "Config file found, $ConfigName - testing against hash"
>                     try
>                     {
>                         $fileHash = GetHashValueForFile -filename $FullName -hashAlgorithm $ConfighashAlgorithm
>                     }
>                     catch
>                     {
>                         WriteLog "Error calculating hash for file: $_"
>                     }
>                     if ($fileHash -ne $ConfighashRequired)
>                     {
>                         WriteLog "Existing script hash mismatch (calculated $fileHash should be $ConfighashRequired)"
>                         # hash mismatch, need to update via download 
>                         $downloadFlag = $true
>                     }
>                 } else {
>                     WriteLog "Configuration file $ConfigName found, but no hash to check against so downloading again"
>                     $downloadFlag = $true
>                 }
>             }
>             else
>             {
>                 WriteLog "Configuration file $FullName not found"
>                 $downloadFlag = $true
>             }
> 
>             if ($downloadFlag)
>             {
>                 WriteLog "Configuration file script $ConfigName not found or requires update, downloading"
>  
>                 try
>                 {
>                     $result = DownloadAndVerify -URI $ConfigURI -name $ConfigName `
>                         -path $script:XymonSettings.configlocation `
>                         -hashAlgorithm $ConfighashAlgorithm -hashRequired $ConfighashRequired
>                     
>                 }
>                 catch
>                 {
>                     WriteLog "Error downloading $ConfigName, ignoring"
>                     WriteLog "Error was: $_"
>                 }
>             }
>         }
>         else
>         {
>             WriteLog "Configuration directive does not match expected format: $config"
>         }
>     } # foreach ... configs
>     WriteLog 'XymonManageConfigs finished'
> }
> 
3734c3934
<         if ($external -match '^external:(?:(\d+):)?(slowscan|everyscan):(sync|async):(.+?)(?:\|(MD5|SHA1|SHA256)\|([0-9a-f]+))?(?:\|(.+)\|(.+))?$')
---
>         if ($external -match '^external:(?:(\d+):)?(slowscan|everyscan|scan\|\d+):(sync|async):(.+?)(?:\|(MD5|SHA1|SHA256)\|([0-9a-f]+))?(?:\|(.+)\|(.+))?$')
3748c3948
<             if ($externalURI -match '^(http|bb)')
---
>             if ($externalURI -match '^(http|bb|xymon)')
3818c4018
<                 WriteLog "External script $externalScriptName not found or requires update, downloading"
---
>                 WriteLog "External script $externalScriptName not found or requires update, downloading from $externalURI"
3846c4046
< function XymonExecuteExternals([boolean] $isSlowscan)
---
> function XymonExecuteExternals ([boolean] $isSlowscan, [int] $loopcount)
3848a4049,4050
>     $env:clientname = $script:clientname
> 
3852a4055
> 
3854a4058,4060
> 
>         [bool] $execute = $true
> 
3857a4064
>             $execute = $false
3859,3860c4066,4078
<         else
<         {
---
> 
>         if ($_.ExecutionFrequency -match '^scan\|(\d+)' ) {
>             $rest = $loopcount % $Matches[1]
>             if ( $loopcount % $Matches[1] -eq 0 )
>             {
>                WriteLog "Execution custom scan: $loopcount % $($Matches[1]) = $rest"
>             } else {
>                WriteLog "Skipping execution custom scan: $loopcount % $($Matches[1]) = $rest"
>                $execute = $false
>             }
>         }
> 
>         if ( $execute -eq $true) {
3882c4100
<             
---
> 
3899a4118
> 
4048a4268,4329
>     if (@($script:clientlocalcfg_entries.Keys -like 'xymonlogarchive*').Length -gt 1)
>     {
>         WriteLog "XymonLogArchive: disabling, more than one xymonlogarchive directive in config"
>     }
>     elseif (@($script:clientlocalcfg_entries.Keys -like 'xymonlogarchive*').Length -eq 0)
>     {
>         WriteLog 'XymonLogArchive: disabling, no entry found in config file'
>     }
>     else
>     {
>         # Keeping older logs in directory $OldSubDirectory for $RententionInDays days
>         # Default values:
>    
>         $script:clientlocalcfg_entries.Keys | where { $_ -match '^xymonlogarchive:(.*):(.*)$' } | foreach {
>             $OldSubDirectory  = $Matches[1]
>             $RententionInDays = $Matches[2]
>         }
> 
>         if ( $OldSubDirectory -ne $null -and $RententionInDays -ne $null ) {
>             WriteLog "XymonLogArchive: rotate logs: $RententionInDays days @ directory $OldSubDirectory"
> 
>             # Format of the old logfile
>             $DateTimeFormat   = "yyyy-MM-dd_HHmmss"
> 
>             $S = Get-Item -LiteralPath $script:XymonSettings.clientlogfile
> 
>             # Make sure the directory for the old log files exists
>             $DestinationPath = Join-Path -Path $S.DirectoryName -ChildPath $OldSubDirectory
>             If (! (Test-Path -LiteralPath $DestinationPath) ) {
>                $Null = New-Item -Path $DestinationPath -Type Directory -Force
>             }
> 
>             # Copy logfile
>             $Destination = Join-Path -Path $DestinationPath -ChildPath ('{0}_{1}{2}' -F $S.BaseName, ((Get-Date).ToString($DateTimeFormat)), $S.Extension)
>             Copy-Item -Path $script:XymonSettings.clientlogfile -Destination $Destination -Force
> 
>             # Cleanup old files
>             Get-ChildItem -LiteralPath $DestinationPath -File -Filter ($Format -F $S.BaseName, '*',$S.Extension) | ? LastWriteTime -le ((Get-Date).AddDays(-$RententionInDays)) | Remove-Item -ErrorAction SilentlyContinue
> 
> 
> 
>             $S = Get-Item -LiteralPath $script:lastcollectfile
> 
>             # Make sure the directory for the old log files exists
>             $DestinationPath = Join-Path -Path $S.DirectoryName -ChildPath $OldSubDirectory
>             If (! (Test-Path -LiteralPath $DestinationPath) ) {
>                $Null = New-Item -Path $DestinationPath -Type Directory -Force
>             }
> 
>             # Copy logfile
>             $Destination = Join-Path -Path $DestinationPath -ChildPath ('{0}_{1}{2}' -F $S.BaseName, ((Get-Date).ToString($DateTimeFormat)), $S.Extension)
>             Copy-Item -Path $script:lastcollectfile -Destination $Destination -Force
> 
>             # Cleanup old files
>             Get-ChildItem -LiteralPath $DestinationPath -File -Filter ($Format -F $S.BaseName, '*',$S.Extension) | ? LastWriteTime -le ((Get-Date).AddDays(-$RententionInDays)) | Remove-Item -ErrorAction SilentlyContinue
> 
>         } else {
>             WriteLog "XymonLogArchive: rotate logs: error in format of setting!"
>         }
>     }
> 
> 
4145a4427,4431
> if($args -eq "ping") {
>     $output = XymonSend "ping" $script:XymonSettings.serversList
>     $output
>     return
> }
4164c4450
< $lastcollectfile = join-path $script:XymonSettings.clientlogpath 'xymon-lastcollect.txt'
---
> $script:lastcollectfile = join-path $script:XymonSettings.clientlogpath 'xymon-lastcollect.txt'
4167c4453
< $loopcount = ($script:XymonSettings.slowscanrate - 1)
---
> $loopcount = Get-Random -Maximum ($script:slowscanrate - 1)
4173c4459
<     RotateLog $lastcollectfile
---
>     RotateLog $script:lastcollectfile
4182,4183c4468,4469
<     WriteLog "This is collection number $($script:collectionnumber), loop count $loopcount"
<     WriteLog "Next 'slow scan' is when loopcount reaches $($script:XymonSettings.slowscanrate)"
---
>     WriteLog "This is collection number $($script:collectionnumber), loopcount $loopcount"
>     WriteLog "Next 'slow scan' is when loopcount reaches $($script:slowscanrate)"
4196c4482
<     if ($loopcount -eq $script:XymonSettings.slowscanrate) { 
---
>     if ($loopcount -eq $script:slowscanrate) { 
4200c4486
<         WriteLog "Doing slow scan tasks"
---
>         WriteLog "Doing slow scan tasks: $loopcount -eq $($script:slowscanrate)"
4236c4522
<     Set-Content -path $lastcollectfile -value $clout
---
>     Set-Content -path $script:lastcollectfile -value $clout
-------------- next part --------------
# ###################################################################################
# 
# Xymon client for Windows
#
# This is a client implementation for Windows systems that support the
# Powershell scripting language.
#
# Copyright (C) 2010 Henrik Storner <henrik at hswn.dk>
# Copyright (C) 2010 David Baldwin
# Copyright (c) 2014-2019 Accenture (zak.beck at accenture.com)
# Copyright (c) 2023 Stef Coene
#
#   Contributions to this project were made by Accenture starting from June 2014.
#   For a list of modifications, please see the SVN change log.
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# ###################################################################################

# Changelog Stef Coene:
# Remove `r from message
#  -> This corrupts the procs check
# 
# Use [text.encoding]::ascii.getbytes to encode the data stream
#  -> On some server sometimes the original code gives 0 bytes. I never found out in the orignal datastream what was the reason. The option xymonlogarchive was used to keep the logfiles and the collected data but we never found a difference.
# 
# Allow to download a file when serverUrl is used via the bb: syntax
# 
# Option ping to test connection to the xymon server
#  - Test the new version with the 'ping' option to make sure it works
# 
# Environment variable YMONCLIENTCFG can be used to point to an alternatieve xymonclient_config.xml configuration file
#  -> We use this in combination with the 'ping' option to test a new XML configuration file with an external script
# 
# Download configuration files from xymon server to etc directory
#  - Add config option to the clientconfigfile to download configuration files to the etc directory
#  - Add function XymonManageConfigs to download configuration files to the etc directory
#  -> We use this to distribute configuration file for external scripts to the servers. One of the scripts is used to generate a new xml configuration file.
# 
# Allow to send something via the 'usermsg' channel
#  -> We use this to send inventory data collected by an external script to the xymon server. Of course, you need a scripts on the xymon server to process this data.
# 
# Allow multiple serverUrl that will receiving the same data, separated with space
#  - Same serverHttpUsername/serverHttpPassword !
#  -> We have used this to migrate to a new xymon server so both receive all data.
# 
# Disable server certification validation when sending data to a https server
#  -> This was needed for a Xymon server with https with self signed certificates. Maybe do this via an option?
# 
# Add xymonlogarchive to the clientconfigfile to copy the logfiles and send data to an alternative directory
#    - Usefull for debugging
#    - Also some changes in XymonLogSend
# 
# Add slowscanrate option to the clientconfigfile to overrule the default slowscanrate setting of 72
# 
# Duplicate bb to xymon in the clientconfigfile
# 
# Add scan|<number> to the clientconfigfile so you can run an external script every <number> run
#  - Also some changes in XymonExecuteExternals
# 
# Make slowscanrate a random number during startup

# -----------------------------------------------------------------------------------
# User configurable settings
# -----------------------------------------------------------------------------------

$xymonservers = @( "xymonhost" )    # List your Xymon servers here
# $clientname  = "winxptest"    # Define this to override the default client hostname

$xymonsvcname = "XymonPSClient"
$xymondir = split-path -parent $MyInvocation.MyCommand.Definition

# -----------------------------------------------------------------------------------

$Version = '2.436'
$XymonClientVersion = "${Id}: xymonclient.ps1  $Version 2019-03-11 zak.beck at accenture.com"
# detect if we're running as 64 or 32 bit
$XymonRegKey = $(if([System.IntPtr]::Size -eq 8) { "HKLM:\SOFTWARE\Wow6432Node\XymonPSClient" } else { "HKLM:\SOFTWARE\XymonPSClient" })

if ( -not $env:XYMONCLIENTCFG ) {
   $XymonClientCfg = join-path $xymondir 'xymonclient_config.xml'
} else {
   $XymonClientCfg = join-path $xymondir $env:XYMONCLIENTCFG
}

$ServiceChecks = @{}
$MaintChecks = @{}

$UnixEpochOriginUTC = New-Object DateTime 1970,1,1,0,0,0,([DateTimeKind]::Utc)

Add-Type -AssemblyName System.Web

#region dotNETHelperTypes
function AddHelperTypes
{
$getprocessowner = @'
// see: http://www.codeproject.com/Articles/14828/How-To-Get-Process-Owner-ID-and-Current-User-SID
// adapted slightly and bugs fixed
using System;
using System.Runtime.InteropServices;
using System.Diagnostics;

public class GetProcessOwner
{

    public const int TOKEN_QUERY = 0X00000008;

    const int ERROR_NO_MORE_ITEMS = 259;

    enum TOKEN_INFORMATION_CLASS                           
    {
        TokenUser = 1,
        TokenGroups,
        TokenPrivileges,
        TokenOwner,
        TokenPrimaryGroup,
        TokenDefaultDacl,
        TokenSource,
        TokenType,
        TokenImpersonationLevel,
        TokenStatistics,
        TokenRestrictedSids,
        TokenSessionId
    }

    [StructLayout(LayoutKind.Sequential)]
    struct TOKEN_USER
    {
        public _SID_AND_ATTRIBUTES User;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct _SID_AND_ATTRIBUTES
    {
        public IntPtr Sid;
        public int Attributes;
    }

    [DllImport("advapi32")]
    static extern bool OpenProcessToken(
        IntPtr ProcessHandle, // handle to process
        int DesiredAccess, // desired access to process
        ref IntPtr TokenHandle // handle to open access token
    );

    [DllImport("kernel32")]
    static extern IntPtr GetCurrentProcess();

    [DllImport("advapi32", CharSet = CharSet.Auto)]
    static extern bool GetTokenInformation(
        IntPtr hToken,
        TOKEN_INFORMATION_CLASS tokenInfoClass,
        IntPtr TokenInformation,
        int tokeInfoLength,
        ref int reqLength
    );

    [DllImport("kernel32")]
    static extern bool CloseHandle(IntPtr handle);

    [DllImport("advapi32", CharSet = CharSet.Auto)]
    static extern bool ConvertSidToStringSid(
        IntPtr pSID,
        [In, Out, MarshalAs(UnmanagedType.LPTStr)] ref string pStringSid
    );

    [DllImport("advapi32", CharSet = CharSet.Auto)]
    static extern bool ConvertStringSidToSid(
        [In, MarshalAs(UnmanagedType.LPTStr)] string pStringSid,
        ref IntPtr pSID
    );

    /// <span class="code-SummaryComment"><summary></span>
    /// Collect User Info
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="pToken">Process Handle</param></span>
    public static bool DumpUserInfo(IntPtr pToken, out IntPtr SID)
    {
        int Access = TOKEN_QUERY;
        IntPtr procToken = IntPtr.Zero;
        bool ret = false;
        SID = IntPtr.Zero;
        try
        {
            if (OpenProcessToken(pToken, Access, ref procToken))
            {
                ret = ProcessTokenToSid(procToken, out SID);
                CloseHandle(procToken);
            }
            return ret;
        }
        catch //(Exception err)
        {
            return false;
        }
    }

    private static bool ProcessTokenToSid(IntPtr token, out IntPtr SID)
    {
        TOKEN_USER tokUser;
        const int bufLength = 256;            
        IntPtr tu = Marshal.AllocHGlobal(bufLength);
        bool ret = false;
        SID = IntPtr.Zero;
        try
        {
            int cb = bufLength;
            ret = GetTokenInformation(token, 
                    TOKEN_INFORMATION_CLASS.TokenUser, tu, cb, ref cb);
            if (ret)
            {
                tokUser = (TOKEN_USER)Marshal.PtrToStructure(tu, typeof(TOKEN_USER));
                SID = tokUser.User.Sid;
            }
            return ret;
        }
        catch //(Exception err)
        {
            return false;
        }
        finally
        {
            Marshal.FreeHGlobal(tu);
        }
    }

    public static string GetProcessOwnerByPId(int PID)
    {                                                                  
        IntPtr _SID = IntPtr.Zero;                                       
        string SID = String.Empty;                                             
        try                                                             
        {                                                                
            Process process = Process.GetProcessById(PID);
            if (DumpUserInfo(process.Handle, out _SID))
            {                                                                    
                ConvertSidToStringSid(_SID, ref SID);
            }

            // convert SID to username
            string account = new System.Security.Principal.SecurityIdentifier(SID).Translate(typeof(System.Security.Principal.NTAccount)).ToString();

            return account;                                          
        }                                                                           
        catch
        {                                                                           
            return "Unknown";
        }
    }
}
'@

$type = Add-Type $getprocessowner

$getprocesscmdline = @'
    // ZB adapted from ProcessHacker (http://processhacker.sf.net)
    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;

    public class ProcessInformation
    {
        [DllImport("ntdll.dll")]
        internal static extern int NtQueryInformationProcess(
            [In] IntPtr ProcessHandle,
            [In] int ProcessInformationClass,
            [Out] out ProcessBasicInformation ProcessInformation,
            [In] int ProcessInformationLength,
            [Out] [Optional] out int ReturnLength
            );

        [DllImport("ntdll.dll")]
        public static extern int NtReadVirtualMemory(
            [In] IntPtr processHandle,
            [In] [Optional] IntPtr baseAddress,
            [In] IntPtr buffer,
            [In] IntPtr bufferSize,
            [Out] [Optional] out IntPtr returnLength
            );

        private const int FLS_MAXIMUM_AVAILABLE = 128;
        
        //Win32
        //private const int GDI_HANDLE_BUFFER_SIZE = 34;
        //Win64
        private const int GDI_HANDLE_BUFFER_SIZE = 60;

        private enum PebOffset
        {
            CommandLine,
            CurrentDirectoryPath,
            DesktopName,
            DllPath,
            ImagePathName,
            RuntimeData,
            ShellInfo,
            WindowTitle
        }

        [Flags]
        public enum RtlUserProcessFlags : uint
        {
            ParamsNormalized = 0x00000001,
            ProfileUser = 0x00000002,
            ProfileKernel = 0x00000004,
            ProfileServer = 0x00000008,
            Reserve1Mb = 0x00000020,
            Reserve16Mb = 0x00000040,
            CaseSensitive = 0x00000080,
            DisableHeapDecommit = 0x00000100,
            DllRedirectionLocal = 0x00001000,
            AppManifestPresent = 0x00002000,
            ImageKeyMissing = 0x00004000,
            OptInProcess = 0x00020000
        }

        [Flags]
        public enum StartupFlags : uint
        {
            UseShowWindow = 0x1,
            UseSize = 0x2,
            UsePosition = 0x4,
            UseCountChars = 0x8,
            UseFillAttribute = 0x10,
            RunFullScreen = 0x20,
            ForceOnFeedback = 0x40,
            ForceOffFeedback = 0x80,
            UseStdHandles = 0x100,
            UseHotkey = 0x200
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct UnicodeString
        {
            public ushort Length;
            public ushort MaximumLength;
            public IntPtr Buffer;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct ListEntry
        {
            public IntPtr Flink;
            public IntPtr Blink;
        }

        [StructLayout(LayoutKind.Sequential)]
        public unsafe struct Peb
        {
            public static readonly int ImageSubsystemOffset =
                Marshal.OffsetOf(typeof(Peb), "ImageSubsystem").ToInt32();
            public static readonly int LdrOffset =
                Marshal.OffsetOf(typeof(Peb), "Ldr").ToInt32();
            public static readonly int ProcessHeapOffset =
                Marshal.OffsetOf(typeof(Peb), "ProcessHeap").ToInt32();
            public static readonly int ProcessParametersOffset =
                Marshal.OffsetOf(typeof(Peb), "ProcessParameters").ToInt32();

            [MarshalAs(UnmanagedType.I1)]
            public bool InheritedAddressSpace;
            [MarshalAs(UnmanagedType.I1)]
            public bool ReadImageFileExecOptions;
            [MarshalAs(UnmanagedType.I1)]
            public bool BeingDebugged;
            [MarshalAs(UnmanagedType.I1)]
            public bool BitField;
            public IntPtr Mutant;

            public IntPtr ImageBaseAddress;
            public IntPtr Ldr; // PebLdrData*
            public IntPtr ProcessParameters; // RtlUserProcessParameters*
            public IntPtr SubSystemData;
            public IntPtr ProcessHeap;
            public IntPtr FastPebLock;
            public IntPtr AtlThunkSListPtr;
            public IntPtr SparePrt2;
            public int EnvironmentUpdateCount;
            public IntPtr KernelCallbackTable;
            public int SystemReserved;
            public int SpareUlong;
            public IntPtr FreeList;
            public int TlsExpansionCounter;
            public IntPtr TlsBitmap;
            public unsafe fixed int TlsBitmapBits[2];
            public IntPtr ReadOnlySharedMemoryBase;
            public IntPtr ReadOnlySharedMemoryHeap;
            public IntPtr ReadOnlyStaticServerData;
            public IntPtr AnsiCodePageData;
            public IntPtr OemCodePageData;
            public IntPtr UnicodeCaseTableData;

            public int NumberOfProcessors;
            public int NtGlobalFlag;

            public long CriticalSectionTimeout;
            public IntPtr HeapSegmentReserve;
            public IntPtr HeapSegmentCommit;
            public IntPtr HeapDeCommitTotalFreeThreshold;
            public IntPtr HeapDeCommitFreeBlockThreshold;

            public int NumberOfHeaps;
            public int MaximumNumberOfHeaps;
            public IntPtr ProcessHeaps;

            public IntPtr GdiSharedHandleTable;
            public IntPtr ProcessStarterHelper;
            public int GdiDCAttributeList;
            public IntPtr LoaderLock;

            public int OSMajorVersion;
            public int OSMinorVersion;
            public short OSBuildNumber;
            public short OSCSDVersion;
            public int OSPlatformId;
            public int ImageSubsystem;
            public int ImageSubsystemMajorVersion;
            public int ImageSubsystemMinorVersion;
            public IntPtr ImageProcessAffinityMask;
            public unsafe fixed byte GdiHandleBuffer[GDI_HANDLE_BUFFER_SIZE];
            public IntPtr PostProcessInitRoutine;

            public IntPtr TlsExpansionBitmap;
            public unsafe fixed int TlsExpansionBitmapBits[32];

            public int SessionId;

            public long AppCompatFlags;
            public long AppCompatFlagsUser;
            public IntPtr pShimData;
            public IntPtr AppCompatInfo;

            public UnicodeString CSDVersion;

            public IntPtr ActivationContextData;
            public IntPtr ProcessAssemblyStorageMap;
            public IntPtr SystemDefaultActivationContextData;
            public IntPtr SystemAssemblyStorageMap;

            public IntPtr MinimumStackCommit;

            public IntPtr FlsCallback;
            public ListEntry FlsListHead;
            public IntPtr FlsBitmap;
            public unsafe fixed int FlsBitmapBits[FLS_MAXIMUM_AVAILABLE / (sizeof(int) * 8)];
            public int FlsHighIndex;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct RtlUserProcessParameters
        {
            public static readonly int CurrentDirectoryOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "CurrentDirectory").ToInt32();
            public static readonly int DllPathOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "DllPath").ToInt32();
            public static readonly int ImagePathNameOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "ImagePathName").ToInt32();
            public static readonly int CommandLineOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "CommandLine").ToInt32();
            public static readonly int EnvironmentOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "Environment").ToInt32();
            public static readonly int WindowTitleOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "WindowTitle").ToInt32();
            public static readonly int DesktopInfoOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "DesktopInfo").ToInt32();
            public static readonly int ShellInfoOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "ShellInfo").ToInt32();
            public static readonly int RuntimeDataOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "RuntimeData").ToInt32();
            public static readonly int CurrentDirectoriesOffset =
                Marshal.OffsetOf(typeof(RtlUserProcessParameters), "CurrentDirectories").ToInt32();

            public struct CurDir
            {
                public UnicodeString DosPath;
                public IntPtr Handle;
            }

            public struct RtlDriveLetterCurDir
            {
                public ushort Flags;
                public ushort Length;
                public uint TimeStamp;
                public IntPtr DosPath;
            }

            public int MaximumLength;
            public int Length;

            public RtlUserProcessFlags Flags;
            public int DebugFlags;

            public IntPtr ConsoleHandle;
            public int ConsoleFlags;
            public IntPtr StandardInput;
            public IntPtr StandardOutput;
            public IntPtr StandardError;

            public CurDir CurrentDirectory;
            public UnicodeString DllPath;
            public UnicodeString ImagePathName;
            public UnicodeString CommandLine;
            public IntPtr Environment;

            public int StartingX;
            public int StartingY;
            public int CountX;
            public int CountY;
            public int CountCharsX;
            public int CountCharsY;
            public int FillAttribute;

            public StartupFlags WindowFlags;
            public int ShowWindowFlags;
            public UnicodeString WindowTitle;
            public UnicodeString DesktopInfo;
            public UnicodeString ShellInfo;
            public UnicodeString RuntimeData;

            public RtlDriveLetterCurDir CurrentDirectories;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct ProcessBasicInformation
        {
            public int ExitStatus;
            public IntPtr PebBaseAddress;
            public IntPtr AffinityMask;
            public int BasePriority;
            public IntPtr UniqueProcessId;
            public IntPtr InheritedFromUniqueProcessId;
        }

        private static string GetProcessCommandLine(IntPtr handle)
        {
            ProcessBasicInformation pbi;

            int returnLength;
            int status = NtQueryInformationProcess(handle, 0, out pbi, Marshal.SizeOf(typeof(ProcessBasicInformation)), out returnLength);

            if (status != 0) throw new InvalidOperationException(string.Format("Exception: status = {0}, expecting 0", status));

            string result = GetPebString(PebOffset.CommandLine, pbi.PebBaseAddress, handle);

            return result;
        }

        private static string GetProcessImagePath(IntPtr handle)
        {
            ProcessBasicInformation pbi;

            int returnLength;
            int status = NtQueryInformationProcess(handle, 0, out pbi, Marshal.SizeOf(typeof(ProcessBasicInformation)), out returnLength);

            if (status != 0) throw new InvalidOperationException(string.Format("Exception: status = {0}, expecting 0", status));

            string result = GetPebString(PebOffset.ImagePathName, pbi.PebBaseAddress, handle);

            return result;
        }

        private static IntPtr IncrementPtr(IntPtr ptr, int value)
        {
            return IntPtr.Size == sizeof(Int32) ? new IntPtr(ptr.ToInt32() + value) : new IntPtr(ptr.ToInt64() + value);
        }

        private static unsafe string GetPebString(PebOffset offset, IntPtr pebBaseAddress, IntPtr handle)
        {
            byte* buffer = stackalloc byte[IntPtr.Size];

            ReadMemory(IncrementPtr(pebBaseAddress, Peb.ProcessParametersOffset), buffer, IntPtr.Size, handle);

            IntPtr processParameters = *(IntPtr*)buffer;
            int realOffset = GetPebOffset(offset);

            UnicodeString pebStr;
            ReadMemory(IncrementPtr(processParameters, realOffset), &pebStr, Marshal.SizeOf(typeof(UnicodeString)), handle);

            string str = System.Text.Encoding.Unicode.GetString(ReadMemory(pebStr.Buffer, pebStr.Length, handle), 0, pebStr.Length);

            return str;
        }

        private static int GetPebOffset(PebOffset offset)
        {
            switch (offset)
            {
                case PebOffset.CommandLine:
                    return RtlUserProcessParameters.CommandLineOffset;
                case PebOffset.CurrentDirectoryPath:
                    return RtlUserProcessParameters.CurrentDirectoryOffset;
                case PebOffset.DesktopName:
                    return RtlUserProcessParameters.DesktopInfoOffset;
                case PebOffset.DllPath:
                    return RtlUserProcessParameters.DllPathOffset;
                case PebOffset.ImagePathName:
                    return RtlUserProcessParameters.ImagePathNameOffset;
                case PebOffset.RuntimeData:
                    return RtlUserProcessParameters.RuntimeDataOffset;
                case PebOffset.ShellInfo:
                    return RtlUserProcessParameters.ShellInfoOffset;
                case PebOffset.WindowTitle:
                    return RtlUserProcessParameters.WindowTitleOffset;
                default:
                    throw new ArgumentException("offset");
            }
        }

        private static byte[] ReadMemory(IntPtr baseAddress, int length, IntPtr handle)
        {
            byte[] buffer = new byte[length];

            ReadMemory(baseAddress, buffer, length, handle);

            return buffer;
        }

        private static unsafe int ReadMemory(IntPtr baseAddress, byte[] buffer, int length, IntPtr handle)
        {
            fixed (byte* bufferPtr = buffer) return ReadMemory(baseAddress, bufferPtr, length, handle);
        }

        private static unsafe int ReadMemory(IntPtr baseAddress, void* buffer, int length, IntPtr handle)
        {
            return ReadMemory(baseAddress, new IntPtr(buffer), length, handle);
        }

        private static int ReadMemory(IntPtr baseAddress, IntPtr buffer, int length, IntPtr handle)
        {
            int status;
            IntPtr retLengthIntPtr;

            if ((status = NtReadVirtualMemory(handle, baseAddress, buffer, new IntPtr(length), out retLengthIntPtr)) > 0)
            {
                throw new InvalidOperationException(string.Format("Exception: status = {0}, expecting 0", status));
            }
            return retLengthIntPtr.ToInt32();
        }

        public static string GetCommandLineByProcessId(int PID)
        {
            string commandLine = "";
            try
            {
                Process process = Process.GetProcessById(PID);
                commandLine = GetProcessCommandLine(process.Handle);
                commandLine = commandLine.Replace((char)0, ' ');
            }
            catch
            {
            }
            return commandLine;
        }
    }
'@

$cp = new-object System.CodeDom.Compiler.CompilerParameters
$cp.CompilerOptions = "/unsafe"
$dummy = $cp.ReferencedAssemblies.Add('System.dll')

$type = Add-Type -TypeDefinition $getprocesscmdline -CompilerParameters $cp

$volumeinfo = @'
    using System;
    using System.Collections;
    using System.Runtime.InteropServices;
    using System.Text;
    using Microsoft.Win32.SafeHandles;

    public class VolumeInfo
    {
        [DllImport("kernel32.dll")]
        public static extern DriveType GetDriveType([MarshalAs(UnmanagedType.LPStr)] string lpRootPathName);

        [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetDiskFreeSpaceEx(string lpDirectoryName,
            out ulong lpFreeBytesAvailable,
            out ulong lpTotalNumberOfBytes,
            out ulong lpTotalNumberOfFreeBytes);

        [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private extern static bool GetVolumeInformation(
            string RootPathName,
            StringBuilder VolumeNameBuffer,
            int VolumeNameSize,
            out uint VolumeSerialNumber,
            out uint MaximumComponentLength,
            out uint FileSystemFlags, // FileSystemFeature
            StringBuilder FileSystemNameBuffer,
            int nFileSystemNameSize);

        [DllImport("kernel32.dll", SetLastError=true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetVolumePathNamesForVolumeNameW(
            [MarshalAs(UnmanagedType.LPWStr)]
            string lpszVolumeName,
            [MarshalAs(UnmanagedType.LPWStr)]
            string lpszVolumePathNames,
            uint cchBuferLength,
            ref UInt32 lpcchReturnLength);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern FindVolumeSafeHandle FindFirstVolume([Out] StringBuilder lpszVolumeName, uint cchBufferLength);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool FindNextVolume(FindVolumeSafeHandle hFindVolume, [Out] StringBuilder lpszVolumeName, uint cchBufferLength);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool FindVolumeClose(IntPtr hFindVolume);

        private static readonly ulong KB = 1024;

        public enum DriveType : uint
        {
            Unknown = 0,    //DRIVE_UNKNOWN
            Error = 1,        //DRIVE_NO_ROOT_DIR
            Removable = 2,    //DRIVE_REMOVABLE
            Fixed = 3,        //DRIVE_FIXED
            Remote = 4,        //DRIVE_REMOTE
            CDROM = 5,        //DRIVE_CDROM
            RAMDisk = 6        //DRIVE_RAMDISK
        }

        private class FindVolumeSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            private FindVolumeSafeHandle()
            : base(true)
            {
            }

            public FindVolumeSafeHandle(IntPtr preexistingHandle, bool ownsHandle)
            : base(ownsHandle)
            {
                SetHandle(preexistingHandle);
            }

            protected override bool ReleaseHandle()
            {
                return FindVolumeClose(handle);
            }
        }

        public class Volume
        {            
            public string VolumeGUID;            
            public string FileSys;
            public DriveType DriveType;
            public uint DriveTypeId;

            public string MountPoint;
            public string FileSystemName;
            public string VolumeName;
            
            public ulong TotalBytes;
            public ulong FreeBytes;
            public ulong UsedBytes;
            public int UsedPercent;

            public ulong TotalBytesKB;
            public ulong FreeBytesKB;
            public ulong UsedBytesKB;

            public uint SerialNumber;
        }

        private static void GetVolumeDetails(string drive, Volume v)
        {
            ulong FreeBytesToCallerDummy;
            if (GetDiskFreeSpaceEx(drive, out FreeBytesToCallerDummy, out v.TotalBytes, out v.FreeBytes))
            {
                StringBuilder volname = new StringBuilder(261);
                StringBuilder fsname = new StringBuilder(261);
                uint flagsDummy, maxlenDummy;
                GetVolumeInformation(drive, volname, volname.Capacity, 
                    out v.SerialNumber, out maxlenDummy, out flagsDummy, fsname, fsname.Capacity);
                v.FileSystemName = fsname.ToString();
                v.VolumeName = volname.ToString();

                if (v.TotalBytes > 0)
                {
                    double used = ((double)(v.TotalBytes - v.FreeBytes) / (double)v.TotalBytes);
                    v.UsedPercent = (int)Math.Round(used * 100.0);
                }

                v.UsedBytes = v.TotalBytes - v.FreeBytes;
                v.TotalBytesKB = v.TotalBytes / KB;
                v.FreeBytesKB = v.FreeBytes / KB;
                v.UsedBytesKB = v.UsedBytes / KB;
            }
        }

        private static void GetVolumeMountPoints(string volumeDeviceName, ArrayList volumes)
        {
            string buffer = "";
            uint lpcchReturnLength = 0;
            GetVolumePathNamesForVolumeNameW(volumeDeviceName, buffer, (uint)buffer.Length, ref lpcchReturnLength);
            if (lpcchReturnLength == 0)
            {
                return;
            }

            buffer = new string(new char[lpcchReturnLength]);

            if (!GetVolumePathNamesForVolumeNameW(volumeDeviceName, buffer, lpcchReturnLength, ref lpcchReturnLength))
            {
                throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());                
            }

            string[] mounts = buffer.Split('\0');
            if (buffer.Length > 1)
            {
                foreach (string mount in mounts)
                {
                    if (mount.Length > 0)
                    {
                        Volume v = new Volume();
                        v.VolumeGUID = volumeDeviceName;
                        v.MountPoint = mount;
                        v.DriveType = GetDriveType(mount);                        
                        v.DriveTypeId = (uint)v.DriveType;
                        if (mount[0] >= 'A' && mount[0] <= 'Z')
                        {
                            v.FileSys = mount[0].ToString();
                        }
                        if (mount.Length > 3)
                        {
                            // per BBWin, replace spaces with underscore in mountpoint name
                            v.FileSys = mount.Substring(3, mount.LastIndexOf('\\') - 3).Replace(' ', '_');                            
                        }
                        GetVolumeDetails(mount, v);
                        volumes.Add(v);
                    }
                }
            }
            else
            {
                // unmounted volume - only add details once
                Volume v = new Volume();
                v.VolumeGUID = volumeDeviceName;
                v.MountPoint = "";
                v.DriveType = GetDriveType(volumeDeviceName);                
                v.DriveTypeId = 99; // special value for unmounted
                v.FileSys = "unmounted";

                GetVolumeDetails(volumeDeviceName, v);
                volumes.Add(v);
            }
        }

        public static Volume[] GetVolumes()
        {
            const uint bufferLength = 1024;
            StringBuilder volume = new StringBuilder((int)bufferLength, (int)bufferLength);
            ArrayList ret = new ArrayList();

            using (FindVolumeSafeHandle volumeHandle = FindFirstVolume(volume, bufferLength))
            {
                if (volumeHandle.IsInvalid)
                {
                    throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
                }

                do
                {
                    GetVolumeMountPoints(volume.ToString(), ret);
                } while (FindNextVolume(volumeHandle, volume, bufferLength));

                return (Volume[])ret.ToArray(typeof(Volume));
            }
        }
    }
'@
$type = Add-Type $volumeinfo

$getsysteminfoType = @'
    using System;
    using System.Runtime.InteropServices;

    public class ProcessorInformation
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct SystemInfo
        {
            public ushort ProcessorArchitecture; // WORD
            public uint PageSize; // DWORD
            public IntPtr MinimumApplicationAddress; // (long)void*
            public IntPtr MaximumApplicationAddress; // (long)void*
            public IntPtr ActiveProcessorMask; // DWORD*
            public uint NumberOfProcessors; // DWORD 
            public uint ProcessorType; // DWORD
            public uint AllocationGranularity; // DWORD
            public ushort ProcessorLevel; // WORD
            public ushort ProcessorRevision; // WORD
        }

        [DllImport("kernel32.dll", SetLastError = false)]
        private static extern void GetNativeSystemInfo(out SystemInfo Info);

        public static SystemInfo GetSystemInfo()
        {
            SystemInfo info;
            GetNativeSystemInfo(out info);

            return info;
        }
    }
'@
$type = Add-Type $getsysteminfoType

}
#endregion 

function SetIfNot($obj,$key,$value)
{
    if($obj.$key -eq $null) { $obj | Add-Member -MemberType noteproperty -Name $key -Value $value }
}

function XymonConfig($startedWithArgs)
{
    if (Test-Path $XymonClientCfg)
    {
        XymonInitXML $startedWithArgs
        $script:XymonCfgLocation = "XML: $XymonClientCfg"
    }
    else
    {
        XymonInitRegistry
        $script:XymonCfgLocation = "Registry"
    }
    XymonInit
}
#'
function XymonInitXML($startedWithArgs)
{
    $xmlconfig = [xml](Get-Content $XymonClientCfg)
    $script:XymonSettings = $xmlconfig.XymonSettings

    # if serverhttppassword is populated and not encrypted, encrypt it
    # only if we were started without arguments - so don't do it for
    # service installation mode
    if ($startedWithArgs -eq $false -and
        $xmlconfig.XymonSettings.serverHttpPassword -ne $null -and
        $xmlconfig.XymonSettings.serverHttpPassword -ne '' -and
        $xmlconfig.XymonSettings.serverHttpPassword -notlike '{SecureString}*')
    {
        WriteLog 'Attempting to encrypt password in config file'
        try
        {
            $securePass = ConvertTo-SecureString -AsPlainText -Force $xmlconfig.XymonSettings.serverHttpPassword
            $encryptedPass = ConvertFrom-SecureString -SecureString $securePass
            $xmlSecPass = "{SecureString}$($encryptedPass)"
            $xmlconfig.XymonSettings.serverHttpPassword = $xmlSecPass
            $xmlconfig.Save($XymonClientCfg)
        }
        catch
        {
            WriteLog "Exception encrypting config file password: $_"
        }
    }
}

function XymonInitRegistry
{
    $script:XymonSettings = Get-ItemProperty -ErrorAction:SilentlyContinue $XymonRegKey
}

function XymonInit
{
    if($script:XymonSettings -eq $null) {
        $script:XymonSettings = New-Object Object
    } 

    $servers = $script:XymonSettings.servers
    SetIfNot $script:XymonSettings serversList $servers
    if ($script:XymonSettings.servers -match " ") 
    {
        $script:XymonSettings.serversList = $script:XymonSettings.servers.Split(" ")
    }
    if ($script:XymonSettings.serversList -eq $null)
    {
        $script:XymonSettings.serversList = $xymonservers
    }

    SetIfNot $script:XymonSettings serverUrl ''
    SetIfNot $script:XymonSettings serverHttpUsername ''
    SetIfNot $script:XymonSettings serverHttpPassword ''
    SetIfNot $script:XymonSettings serverHttpTimeoutMs 100000

    $wanteddisks = $script:XymonSettings.wanteddisks
    SetIfNot $script:XymonSettings wanteddisksList $wanteddisks
    if ($script:XymonSettings.wanteddisks -match " ") 
    {
        $script:XymonSettings.wanteddisksList = $script:XymonSettings.wanteddisks.Split(" ")
    }
    if ($script:XymonSettings.wanteddisksList -eq $null)
    {
        $script:XymonSettings.wanteddisksList = @( 3 ) # 3=Local disks, 4=Network shares, 2=USB, 5=CD
    }

    # Params for default clientname
    SetIfNot $script:XymonSettings clientfqdn 1 # 0 = unqualified, 1 = fully-qualified
    SetIfNot $script:XymonSettings clientlower 1 # 0 = unqualified, 1 = fully-qualified
    
    if ($script:XymonSettings.clientname -eq $null -or $script:XymonSettings.clientname -eq "") 
    { 
        # set name based on rules; first try IP properties
        $ipProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
        $clname  = $ipProperties.HostName
        if ($clname -ne '' -and $script:XymonSettings.clientfqdn -eq 1 -and ($ipProperties.DomainName -ne $null)) 
        { 
            $clname += "." + $ipProperties.DomainName
        }
        if ($clname -eq '')
        {
            # try environment
            $clname = $Env:COMPUTERNAME
            if ($clname -ne '' -and $script:XymonSettings.clientfqdn -eq 1 -and ($Env:USERDNSDOMAIN -ne $null)) 
            {
                $clname += '.' + $Env:USERDNSDOMAIN
            }
        }
        if ($script:XymonSettings.clientlower -eq 1) { $clname = $clname.ToLower() }
        SetIfNot $script:XymonSettings clientname $clname
        $script:clientname = $clname
    }
    else
    {
        $script:clientname = $script:XymonSettings.clientname
    }

    # Params for various client options
    SetIfNot $script:XymonSettings clientbbwinmembug 1 # 0 = report correctly, 1 = page and virtual switched
    SetIfNot $script:XymonSettings clientremotecfgexec 0 # 0 = don't run remote config, 1 = run remote config
    SetIfNot $script:XymonSettings clientconfigfile "$env:TEMP\xymonconfig.cfg" # path for saved client-local.cfg section from server
    SetIfNot $script:XymonSettings clientlogfile "$env:TEMP\xymonclient.log" # path for logfile
    SetIfNot $script:XymonSettings clientsoftware "powershell" # powershell / bbwin
    SetIfNot $script:XymonSettings clientclass "powershell" # 'class' value (default powershell)
    SetIfNot $script:XymonSettings loopinterval 300 # seconds to repeat client reporting loop
    SetIfNot $script:XymonSettings maxlogage 60 # minutes age for event log reporting
    SetIfNot $script:XymonSettings MaxEvents 5000 # maximum number of events per event log
    SetIfNot $script:XymonSettings reportevt 1 # scan eventlog and report (can be very slow)
    SetIfNot $script:XymonSettings EnableWin32_Product 0 # 0 = do not use Win32_product, 1 = do
                        # see http://support.microsoft.com/kb/974524 for reasons why Win32_Product is not recommended!
    SetIfNot $script:XymonSettings EnableWin32_QuickFixEngineering 0 # 0 = do not use Win32_QuickFixEngineering, 1 = do
    SetIfNot $script:XymonSettings EnableWMISections 0 # 0 = do not produce [WMI: sections (OS, BIOS, Processor, Memory, Disk), 1 = do
    SetIfNot $script:XymonSettings EnableIISSection 1 # 0 = do not produce iis_sites section, 1 = do
    SetIfNot $script:XymonSettings EnableDiskPart 0 # 0 = do not collect diskpart data, 1 = do
    SetIfNot $script:XymonSettings ClientProcessPriority 'Normal' # possible values Normal, Idle, High, RealTime, BelowNormal, AboveNormal

    $clientlogpath = Split-Path -Parent $script:XymonSettings.clientlogfile
    SetIfNot $script:XymonSettings clientlogpath $clientlogpath

    SetIfNot $script:XymonSettings clientlogretain 0

    SetIfNot $script:XymonSettings XymonAcceptUTF8 0 # messages sent to Xymon 0 = use "original" ASCII, 1 = convert to UTF8, 2 = use "pure" ASCII
    SetIfNot $script:XymonSettings GetProcessInfoCommandLine 1 # get process command line 1 = yes, 0 = no
    SetIfNot $script:XymonSettings GetProcessInfoOwner 1 # get process owner 1 = yes, 0 = no

    $configdir = Join-Path $xymondir 'etc'
    $extscript = Join-Path $xymondir 'ext'
    $extdata = Join-Path $xymondir 'tmp'
    $localdata = Join-Path $xymondir 'local'
    SetIfNot $script:XymonSettings configlocation $configdir
    SetIfNot $script:XymonSettings externalscriptlocation $extscript
    SetIfNot $script:XymonSettings externaldatalocation $extdata
    SetIfNot $script:XymonSettings localdatalocation $localdata
    SetIfNot $script:XymonSettings servergiflocation '/xymon/gifs/'
    $script:clientlocalcfg = ""
    $script:logfilepos = @{}
    $script:externals = @{}
    $script:diskpartData = ''
    $script:LastTransmissionMethod = 'Unknown'

    $script:HaveCmd = @{}
    foreach($cmd in "query","qwinsta") {
        $script:HaveCmd.$cmd = (get-command -ErrorAction:SilentlyContinue $cmd) -ne $null
    }

    @("cpuinfo","totalload","numcpus","numcores","numvcpus","osinfo","svcs","procs","disks",`
    "netifs","svcprocs","localdatetime","uptime","usercount",`
    "XymonProcsCpu","XymonProcsCpuTStart","XymonProcsCpuElapsed") `
    | %{ if (get-variable -erroraction SilentlyContinue $_) { Remove-Variable $_ }}
    
}

function XymonProcsCPUUtilisation
{
    # XymonProcsCpu is a table with 6 elements:
    #   0 = process object
    #   1 = last tick value
    #   2 = ticks used since last poll
    #   3 = activeflag
    #   4 = command line
    #   5 = owner

    # ZB - got a feeling XymonProcsCpuElapsed should be multiplied by number of cores
    if ((get-variable -erroraction SilentlyContinue "XymonProcsCpu") -eq $null) {
        $script:XymonProcsCpu = @{ 0 = ( $null, 0, 0, $false) }
        $script:XymonProcsCpuTStart = (Get-Date).ticks
        $script:XymonProcsCpuElapsed = 0
    }
    else {
        $script:XymonProcsCpuElapsed = (Get-Date).ticks - $script:XymonProcsCpuTStart
        $script:XymonProcsCpuTStart = (Get-Date).Ticks
    }
    $script:XymonProcsCpuElapsed *= $script:numcores
    
    foreach ($p in $script:procs) {
        # store the process name in XymonProcsCpu
        # and if $p.name differs but id matches, need to pick up new command line etc and zero counters
        # - this covers the case where a process id is reused
        $thisp = $script:XymonProcsCpu[$p.Id]
        if ($p.Id -ne 0 -and ($thisp -eq $null -or $thisp[0].Name -ne $p.Name))
        {
            # either we have not seen this process before ($thisp -eq $null)
            # OR
            # the name of the process for ID x does not equal the cached process name
            if ($thisp -eq $null)
            {
                WriteLog "New process $($p.Id) detected: $($p.Name)"
            }
            else
            {
                WriteLog "Process $($p.Id) appears to have changed from $($thisp[0].Name) to $($p.Name)"
            }

            $cmdline = ''
            $owner = ''
            if ($script:XymonSettings.GetProcessInfoCommandLine -eq 1)
            {
                $cmdline = [ProcessInformation]::GetCommandLineByProcessId($p.Id)
            }
            if ($script:XymonSettings.GetProcessInfoOwner -eq 1)
            {
                $owner = [GetProcessOwner]::GetProcessOwnerByPId($p.Id)
            }
            if ($owner.length -gt 32) { $owner = $owner.substring(0, 32) }

            # New process - create an entry in the curprocs table
            # We use static values here, because some get-process entries have null values
            # for the tick-count (The "SYSTEM" and "Idle" processes).
            $script:XymonProcsCpu[$p.Id] = @($null, 0, 0, $false, $cmdline, $owner)
            $thisp = $script:XymonProcsCpu[$p.Id]
        }

        $thisp[3] = $true
        $thisp[2] = $p.TotalProcessorTime.Ticks - $thisp[1]
        $thisp[1] = $p.TotalProcessorTime.Ticks
        $thisp[0] = $p
    }
}

function UserSessionCount
{
    if ($HaveCmd.qwinsta)
    {
        $script:usersessions = qwinsta /counter
        ($script:usersessions -match ' Active ').Length
    }
    else
    {
        $q = get-wmiobject win32_logonsession | %{ $_.logonid}
        $service = Get-WmiObject -ComputerName $server -Class Win32_Service -Filter "Name='$xymonsvc'"
        $s = 0
        get-wmiobject win32_session | ?{ 2,10 -eq $_.LogonType} | ?{$q -eq $_.logonid} | %{
            $z = $_.logonid
            get-wmiobject win32_sessionprocess | ?{ $_.Antecedent -like "*LogonId=`"$z`"*" } | %{
                if($_.Dependent -match "Handle=`"(\d+)`"") {
                    get-wmiobject win32_process -filter "processid='$($matches[1])'" }
            } | select -first 1 | %{ $s++ }
        }
        $s
    }
}

function XymonCollectInfo([boolean] $isSlowScan)
{
    WriteLog "Executing XymonCollectInfo"

    CleanXymonProcsCpu
    WriteLog "XymonCollectInfo: Process info"
    $script:procs = Get-Process | Sort-Object -Property Id

    WriteLog "XymonCollectInfo: CPU info"
    $script:cpuinfo = [ProcessorInformation]::GetSystemInfo()
    $script:numcores  = $cpuinfo.NumberOfProcessors
    WriteLog "Found $($script:numcores) cores"

    WriteLog "XymonCollectInfo: calling XymonProcsCPUUtilisation"
    XymonProcsCPUUtilisation

    WriteLog "XymonCollectInfo: OS info (including memory) (WMI)"
    $script:osinfo = Get-WmiObject -Class Win32_OperatingSystem
    WriteLog "XymonCollectInfo: Service info (WMI)"
    $script:svcs = Get-WmiObject -Class Win32_Service | Sort-Object -Property Name
    WriteLog "XymonCollectInfo: Disk info"
    $mydisks = @()
    try
    {
        $volumes = [VolumeInfo]::GetVolumes()
        foreach ($disktype in $script:XymonSettings.wanteddisksList) { 
            $mydisks += @( ($volumes | where { $_.DriveTypeId -eq $disktype } ))
        }
    }
    catch
    {
        $volumes = @()
        WriteLog "Error getting volume information: $_"
    }
    $script:disks = $mydisks | Sort-Object FileSys

    WriteLog "XymonCollectInfo: Building table of service processes (uses WMI data)"
    $script:svcprocs = @{([int]-1) = ""}
    foreach ($s in $svcs) {
        if ($s.State -eq "Running") {
            if ($svcprocs[([int]$s.ProcessId)] -eq $null) {
                $script:svcprocs += @{ ([int]$s.ProcessId) = $s.Name }
            }
            else {
                $script:svcprocs[([int]$s.ProcessId)] += ("/" + $s.Name)
            }
        }
    }

    WriteLog "XymonCollectInfo: Date processing (uses WMI data)"
    $script:localdatetime = $osinfo.ConvertToDateTime($osinfo.LocalDateTime)
    $script:uptime = $localdatetime - $osinfo.ConvertToDateTime($osinfo.LastBootUpTime)
    
    WriteLog "XymonCollectInfo: Adding CPU usage etc to main process data"
    XymonProcesses

    WriteLog "XymonCollectInfo: calling UserSessionCount"
    $script:usercount = UserSessionCount

    WriteLog "XymonCollectInfo finished"
}

function WMIProp($class)
{
    $wmidata = Get-WmiObject -Class $class
    $props = ($wmidata | Get-Member -MemberType Property | Sort-Object -Property Name | where { $_.Name -notlike "__*" })
    foreach ($p in $props) {
        $p.Name + " : " + $wmidata.($p.Name)
    }
}

function UnixDate([System.DateTime] $t)
{
    $t.ToString("ddd dd MMM HH:mm:ss yyyy")
}

function epochTimeUtc([System.DateTime] $t)
{
    [int64]($t.ToUniversalTime() - $UnixEpochOriginUTC).TotalSeconds
}

function filesize($file,$clsize=4KB)
{
    return [math]::floor((($_.Length -1)/$clsize + 1) * $clsize/1KB)
}

function du([string]$dir,[int]$clsize=0)
{
    if($clsize -eq 0) {
        $drive = "{0}:" -f [string](get-item $dir | %{ $_.psdrive })
        $clsize = [int](Get-WmiObject win32_Volume | ? { $_.DriveLetter -eq $drive }).BlockSize
        if($clsize -eq 0 -or $clsize -eq $null) { $clsize = 4096 } # default in case not found
    }
    $sum = 0
    $dulist = ""
    get-childitem $dir -Force | % {
        if( $_.Attributes -like "*Directory*" ) {
           $dulist += du ("{0}\{1}" -f [string]$dir,$_.Name) $clsize | out-string
           $sum += $dulist.Split("`n")[-2].Split("`t")[0] # get size for subdir
        } else { 
           $sum += filesize $_ $clsize
        }
    }
    "$dulist$sum`t$dir"
}

function XymonPrintProcess($pobj, $name, $pct)
{
    $pcpu = (("{0:F1}" -f $pct) + "`%").PadRight(8)
    $ppid = ([string]($pobj.Id)).PadRight(9)
    
    if ($name.length -gt 30) { $name = $name.substring(0, 30) }
    $pname = $name.PadRight(32)

    $pprio = ([string]$pobj.BasePriority).PadRight(5)
    $ptime = (([string]($pobj.TotalProcessorTime)).Split(".")[0]).PadRight(9)
    $pmem = ([string]($pobj.WorkingSet64 / 1KB)) + "k"

    $pcpu + $ppid + $pname + $pprio + $ptime + $pmem
}

function XymonDate
{
    "[date]"
    UnixDate $localdatetime
}

function XymonClock
{
    $epoch = epochTimeUtc $localdatetime

    "[clock]"
    "epoch: " + $epoch
    "local: " + (UnixDate $localdatetime)
    "UTC: " + (UnixDate $localdatetime.ToUniversalTime())
    $timesource = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters').Type
    "Time Synchronisation type: " + $timesource
    if ($timesource -eq "NTP") {
        "NTP server: " + (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters').NtpServer
    }
    $w32qs = w32tm /query /status  # will not run on 2003, XP or earlier
    if($?) { $w32qs }
}

function XymonUptime
{
    "[uptime]"
    "sec: " + [string] ([int]($uptime.Ticks / 10000000))
    ([string]$uptime.Days) + " days " + ([string]$uptime.Hours) + " hours " + ([string]$uptime.Minutes) + " minutes " + ([string]$uptime.Seconds) + " seconds"
    "Bootup: " + $osinfo.LastBootUpTime
}

function XymonUname
{
    "[uname]"
    $osinfo.Caption + " " + $osinfo.CSDVersion + " (build " + $osinfo.BuildNumber + ")"
}

function XymonClientVersion
{
    "[clientversion]"
    $Version
}

function XymonProcesses
{
    # gather process and timing information and add this to $script:procs
    # variable
    # XymonCpu and XymonProcs use this information to output
 
    WriteLog "XymonProcesses start"

    foreach ($p in $script:procs)
    {
        if ($svcprocs[($p.Id)] -ne $null) {
            $procname = "SVC:" + $svcprocs[($p.Id)]
        }
        else {
            $procname = $p.Name
        }
           
        Add-Member -MemberType NoteProperty `
            -Name XymonProcessName -Value $procname `
            -InputObject $p

        $thisp = $script:XymonProcsCpu[$p.Id]
        if ($thisp -ne $null -and $thisp[3] -eq $true) 
        {
            if ($script:XymonProcsCpuElapsed -gt 0)
            {
                $usedpct = ([int](10000*($thisp[2] / $script:XymonProcsCpuElapsed))) / 100
            }
            else
            {
                $usedpct = 0
            }
            Add-Member -MemberType NoteProperty `
                -Name CommandLine -Value $thisp[4] `
                -InputObject $p
            Add-Member -MemberType NoteProperty `
                -Name Owner -Value $thisp[5] `
                -InputObject $p
        }
        else 
        {
            $usedpct = 0
        }

        Add-Member -MemberType NoteProperty `
            -Name CPUPercent -Value $usedpct `
            -InputObject $p

        $elapsedRuntime = 0
        if ($p.StartTime -ne $null)
        {
            $elapsedRuntime = ($script:localdatetime - $p.StartTime).TotalMinutes 
        }
        Add-Member -MemberType NoteProperty `
            -Name ElapsedSinceStart -Value $elapsedRuntime `
            -InputObject $p

        $pws     = "{0,8:F0}/{1,-8:F0}" -f ($p.WorkingSet64 / 1KB), ($p.PeakWorkingSet64 / 1KB)
        $pvmem   = "{0,8:F0}/{1,-8:F0}" -f ($p.VirtualMemorySize64 / 1KB), ($p.PeakVirtualMemorySize64 / 1KB)
        $ppgmem  = "{0,8:F0}/{1,-8:F0}" -f ($p.PagedMemorySize64 / 1KB), ($p.PeakPagedMemorySize64 / 1KB)
        $pnpgmem = "{0,8:F0}" -f ($p.NonPagedSystemMemorySize64 / 1KB)

        Add-Member -MemberType NoteProperty `
            -Name XymonPeakWorkingSet -Value $pws `
            -InputObject $p
        Add-Member -MemberType NoteProperty `
            -Name XymonPeakVirtualMem -Value $pvmem `
            -InputObject $p
        Add-Member -MemberType NoteProperty `
            -Name XymonPeakPagedMem -Value $ppgmem `
            -InputObject $p
        Add-Member -MemberType NoteProperty `
            -Name XymonNonPagedSystemMem -Value $pnpgmem `
            -InputObject $p
    }

    WriteLog "XymonProcesses finished."
}


function XymonCpu
{
    WriteLog "XymonCpu start"

    $totalcpu = ($script:procs | Measure-Object -Sum -Property CPUPercent | Select -ExpandProperty Sum)
    $totalcpu = [Math]::Round($totalcpu, 2)

    "[cpu]"
    "up: {0} days, {1} users, {2} procs, load={3}%" -f [string]$uptime.Days, $usercount, $procs.count, [string]$totalcpu
    ""
    "CPU states:"
    "`ttotal`t{0}`%" -f [string]$totalcpu
    "`tcores: {0}" -f [string]$script:numcores

    if ($script:XymonProcsCpuElapsed -gt 0) {
        ""
        "CPU".PadRight(9) + "PID".PadRight(8) + "Image Name".PadRight(32) + "Pri".PadRight(5) + "Time".PadRight(9) + "MemUsage"

        $script:procs | Sort-Object -Descending { $_.CPUPercent } `
            | foreach `
            { 
                $skipFlag = $false
                if ($script:clientlocalcfg_entries.ContainsKey('slimmode'))
                {
                    if ($script:clientlocalcfg_entries.slimmode.ContainsKey('processes'))
                    {
                        # skip this process if we are in slimmode and this process is not one of the 
                        # requested processes
                        if ($script:clientlocalcfg_entries.slimmode.processes -notcontains $_.XymonProcessName)
                        {
                            $skipFlag = $true
                        }
                    }
                }
                
                if (!$skipFlag)
                {
                    XymonPrintProcess $_ $_.XymonProcessName $_.CPUPercent 
                }
            }
    }
    WriteLog "XymonCpu finished."
}

function XymonDisk
{
    $MountpointWidth = 10
    $LabelWidth = 10
    $FilesysWidth = 10

    # work out column widths
    foreach ($d in $script:disks)
    {
        $mplength = "/FIXED/$($d.MountPoint)".Length
        if ($mplength -gt $MountpointWidth)
        {
            $MountpointWidth = $mplength
        }
        if ($d.FileSys.Length -gt $FilesysWidth)
        {
            $FilesysWidth = $d.FileSys.Length
        }
        if ($d.VolumeName.Length -gt $LabelWidth)
        {
            $LabelWidth = $d.VolumeName.Length
        }
    }

    WriteLog "XymonDisk start"
    "[disk]"
    "{0,-$FilesysWidth} {1,12} {2,12} {3,12} {4,9}  {5,-$MountpointWidth} {6,-$LabelWidth} {7}" -f `
        "Filesystem", `
        "1K-blocks", `
        "Used", `
        "Avail", `
        "Capacity", `
        "Mounted", `
        "Label", `
        "Summary(Total\Avail GB)"
    foreach ($d in $script:disks) {
        $diskusedKB = $d.UsedBytesKB
        $disksizeKB = $d.TotalBytesKB

        $dsKB = "{0:F0}" -f ($d.TotalBytes / 1KB); $dsGB = "{0:F2}" -f ($d.TotalBytes / 1GB)
        $duKB = "{0:F0}" -f ($diskusedKB); $duGB = "{0:F2}" -f ($diskusedKB / 1KB);
        $dfKB = "{0:F0}" -f ($d.FreeBytes / 1KB); $dfGB = "{0:F2}" -f ($d.FreeBytes / 1GB)

        $mountpoint = "/FIXED/$($d.MountPoint)"
       
        "{0,-$FilesysWidth} {1,12} {2,12} {3,12} {4,9:0}% {5,-$MountpointWidth} {6,-$LabelWidth} {7}" -f `
            $d.FileSys, `
            $dsKB, `
            $duKB, `
            $dfKB, `
            $d.UsedPercent, `
            $mountpoint, `
            $d.VolumeName, `
            $dsGB + "\" + $dfGB
    }

    $script:diskpartData

    WriteLog "XymonDisk finished."
}

function XymonMemory
{
    WriteLog "XymonMemory start"
    $physused  = [int](($osinfo.TotalVisibleMemorySize - $osinfo.FreePhysicalMemory)/1KB)
    $phystotal = [int]($osinfo.TotalVisibleMemorySize / 1KB)
    $pageused  = [int](($osinfo.SizeStoredInPagingFiles - $osinfo.FreeSpaceInPagingFiles) / 1KB)
    $pagetotal = [int]($osinfo.SizeStoredInPagingFiles / 1KB)
    $virtused  = [int](($osinfo.TotalVirtualMemorySize - $osinfo.FreeVirtualMemory) / 1KB)
    $virttotal = [int]($osinfo.TotalVirtualMemorySize / 1KB)

    "[memory]"
    "memory    Total    Used"
    "physical: $phystotal $physused"
    if($script:XymonSettings.clientbbwinmembug -eq 0) {     # 0 = report correctly, 1 = page and virtual switched
        "virtual: $virttotal $virtused"
        "page: $pagetotal $pageused"
    } else {
        "virtual: $pagetotal $pageused"
        "page: $virttotal $virtused"
    }
    WriteLog "XymonMemory finished."
}

# ContainsLike - whether or not $compare matches
# one of the entries in $arrayOfLikes using the -like operator
# returns $null (no match) or the matching entry from $arrayOfLikes
function ContainsLike([string[]] $ArrayOfLikes, [string] $Compare)
{
    foreach ($l in $ArrayOfLikes)
    {
        if ($Compare -like $l)
        {
            return $l
        }
    }
    return $null
}

function XymonMsgs
{
    if ($script:XymonSettings.reportevt -eq 0) {return}

    $sinceMs = (New-Timespan -Minutes $script:XymonSettings.maxlogage).TotalMilliseconds

    # xml template
    #   {0} = log name e.g. Application
    #   {1} = milliseconds - how far back in time to go
    $filterXMLTemplate = `
@' 
    <QueryList>
      <Query Id="0" Path="{0}">
        <Select Path="{0}">*[System[TimeCreated[timediff(@SystemTime) <= {1}] and ({2})]]</Select>
      </Query>
    </QueryList>
'@

    $eventLevels = @{ 
        '0' = 'Information';
        '1' = 'Critical';
        '2' = 'Error';
        '3' = 'Warning';
        '4' = 'Information';
        '5' = 'Verbose';
    }

    # default logs - may be overridden by config
    $wantedlogs = "Application", "System", "Security"
    $wantedLevels = @('Critical', 'Warning', 'Error', 'Information', 'Verbose')
    $maxpayloadlength = 1024
    $payload = ''

    # $wantedEventLogs
    # each key is an event log name
    # each value is an array of wanted levels
    # defaults set below
    # can be overridden by eventlogswanted config 
    $wantedEventLogs = `
        @{ `
            'Application' = @('Critical', 'Warning', 'Error', 'Information', 'Verbose'); `
            'System' = @('Critical', 'Warning', 'Error', 'Information', 'Verbose'); `
            'Security' = @('Critical', 'Warning', 'Error', 'Information', 'Verbose'); `
        }
    # any config from server should override this default config
    $wantedEventLogsPriority = -1

    # this function no longer uses $script:XymonSettings.wantedlogs
    # - it now uses eventlogswanted from the remote config
    # eventlogswanted:[optional priority]:<logs/levels>:max payload:[optional default levels]
    $script:clientlocalcfg_entries.keys | where { $_ -match '^eventlogswanted:(?:(\d+):)?(.+):(\d+):?(.+)?$' } | foreach `
    {
        $thisSectionPriority = 0
        WriteLog "Processing eventlogswanted config: $($matches[0])"
        # config priority (if present)
        # we only want the configuration with the highest priority
        if ($matches[1] -ne $null)
        {
            $thisSectionPriority = [int]($matches[1])
        }
        if ($wantedEventLogsPriority -gt $thisSectionPriority)
        {
            WriteLog "Previous priority $wantedEventLogsPriority greater than this config ($($thisSectionPriority)), skipping"
            $skip = $true
        }
        else
        {
            WriteLog "This config priority $($thisSectionPriority) greater than/equal to previous config ($($wantedEventLogsPriority)), processing"
            $wantedEventLogsPriority = $thisSectionPriority
            $skip = $false
        }

        # $wantedlogs
        # might be a list of logs - e.g. application,system
        # or a list of logs and levels - e.g. application|information&critical,system|critical&error
        if (-not ($skip))
        {
            $wantedEventLogs = @{}
            $wantedlogs = $matches[2] -split ','
            $maxpayloadlength = $matches[3]
            if ($matches[4] -ne $null)
            {
                $wantedLevels = $matches[4] -split ','
            }

            foreach ($log in $wantedlogs)
            {
                if ($log -like '*|*')
                {
                    $logParams = @($log -split '\|')
                    if ($logParams.Length -eq 2)
                    {
                        $levelParams = $logParams[1] -replace '&', ','
                        $wantedEventLogs[$logParams[0]] = ($levelParams -split ',')
                    }
                    elseif ($logParams.Length -eq 1)
                    {
                        $wantedEventLogs[$logParams[0]] = $wantedLevels
                    }
                    else
                    {
                        WriteLog "Bad configuration item in eventlogswanted: $log"
                    }
                }
                else
                {
                    # if no individual levels specified, then use the defaults - 
                    # either specified in match 3 or script default
                    $wantedEventLogs[$log] = $wantedLevels
                }
            }
        }
    }

    $script:EventLogs = Get-WinEvent -ListLog @($wantedEventLogs.Keys)
    "[msgs:EventlogSummary]"
    $script:EventLogs | Format-Table -AutoSize

    WriteLog "Event Log processing - max payload: $maxpayloadlength"

    foreach ($l in ($script:EventLogs | select -ExpandProperty LogName))
    {
        $wantedEventLogEntry = ContainsLike -ArrayofLikes $wantedEventLogs.Keys -Compare $l
        if ($wantedEventLogEntry -ne $null)
        {
            WriteLog "Event log $l adding to payload"
            $payload += [environment]::newline + "[msgs:eventlog_$l]" + [environment]::newline

            # only scan the current log if there is space in the payload
            if ($payload.Length -lt $maxpayloadlength)
            {
                WriteLog "Processing event log $l"

                $levelcriteria = @()
                $wantedLevels = $wantedEventLogs[$wantedEventLogEntry]
                foreach ($level in $wantedLevels)
                {
                    switch ($level)
                    {
                        'critical' { $levelcriteria += 'Level=1'; break }
                        'warning' { $levelcriteria += 'Level=3'; break }
                        'verbose' { $levelcriteria += 'Level=5'; break }
                        'error' { $levelcriteria += 'Level=2'; break }
                        'information' { $levelcriteria += 'Level=4 or Level=0'; break }
                    }
                }

                $logFilterXML = $filterXMLTemplate -f $l, $sinceMs, ($levelcriteria -join ' or ')
                WriteLog "Log filter $logFilterXML"
                
                try
                {
                    WriteLog 'Setting thread/UI culture to en-US'
                    $currentCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
                    $currentUICulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture
                    [System.Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'
                    [System.Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US'

                    # todo - make this max events number configurable
                    $logentries = @(Get-WinEvent -ErrorAction:SilentlyContinue -FilterXML $logFilterXML `
                        -MaxEvents $script:XymonSettings.MaxEvents)
                }
                catch
                {
                    WriteLog "Error setting culture and getting event log entries: $_"
                }
                finally
                {
                    WriteLog "Resetting thread/UI culture to previous: $currentCulture / $currentUICulture"
                    [System.Threading.Thread]::CurrentThread.CurrentCulture = $currentCulture
                    [System.Threading.Thread]::CurrentThread.CurrentUICulture = $currentUICulture
                }

                $totalEntries = $logentries.Length
                WriteLog "Event log $l entries since last scan: $($logentries.Length)"
                
                # filter based on clientlocal.cfg / clientconfig.cfg
                if ($script:clientlocalcfg_entries -ne $null)
                {
                    $filterkey = $script:clientlocalcfg_entries.keys | where { $_ -match "^eventlog\:$l" }
                    if ($filterkey -ne $null -and $script:clientlocalcfg_entries.ContainsKey($filterkey))
                    {
                        WriteLog "Found a configured filter for log $l"

                        # ignore / include - include has priority over ignore
                        # so if there are any include filters, they get priority and ignores are disregarded
                        $filters = @( $script:clientlocalcfg_entries[$filterkey] | where { $_ -match '^include ' } )
                        $filterMode = 'include'
                        if ($filters -eq $null -or $filters.Length -eq 0)
                        {
                            $filters = @( $script:clientlocalcfg_entries[$filterkey] | where { $_ -match '^ignore ' } )
                            $filterMode = 'exclude'
                        }
                        WriteLog "Filter mode: $filterMode Filter entries: $($filters.Length)"

                        # process filters if we have one or the other
                        $filterCount = 0
                        $output = @()
                        foreach ($entry in $logentries)
                        {
                            if ($filterMode -eq 'exclude')
                            {
                                $excludeItem = $false
                                foreach ($filter in $filters)
                                {
                                    $filter = $filter -replace '^ignore ', ''
                                    if ($entry.ProviderName -match $filter -or $entry.Message -match $filter)
                                    {
                                        ++$filterCount
                                        $excludeItem = $true
                                        break
                                    }
                                }
                                if (-not $excludeItem)
                                {
                                    $output += $entry
                                }
                            }
                            elseif ($filterMode -eq 'include')
                            {
                                $includeItem = $false
                                foreach ($filter in $filters)
                                {
                                    $filter = $filter -replace '^include ', ''
                                    if ($entry.ProviderName -match $filter -or $entry.Message -match $filter)
                                    {
                                        ++$filterCount
                                        $includeItem = $true
                                        break
                                    }
                                }
                                if ($includeItem)
                                {
                                    $output += $entry
                                }
                            }
                        }
                        $logentries = $output
                        WriteLog "Starting entries: $($totalEntries)  Entries filtered: $($filterCount)  Remaining entries: $($logentries.Count)"
                    }
                }

                if ($logentries -ne $null) 
                {
                    WriteLog "Entries to add to payload: $($logentries.Count) "
                    foreach ($entry in $logentries) 
                    {
                        $level = 'Unknown'
                        if ($eventLevels.ContainsKey($entry.Level.ToString()))
                        {
                            $level = $eventLevels[$entry.Level.ToString()]
                        }
                        $payload += [string]$level + " - " +`
                            [string]$entry.TimeCreated + " - " + `
                            "[$($entry.Id)] - " + `
                            [string]$entry.ProviderName + " - " + `
                            [string]$entry.Message + [environment]::newline
                        
                        if ($payload.Length -gt $maxpayloadlength)
                        {
                            WriteLog "Payload length reached $($payload.Length), greater than $($maxpayloadlength)"
                            break;
                        }
                    }
                }
                else
                {
                    WriteLog "No entries to add to payload"
                }
            }
        }
    }
    WriteLog "Event log processing finished"
    $payload
}

function ResolveEnvPath($envpath)
{
    $s = $envpath
    while($s -match '%([\w_]+)%') {
        if(! (test-path "env:$($matches[1])")) { return $envpath }
        $s = $s.Replace($matches[0],$(Invoke-Expression "`$env:$($matches[1])"))
    }
    if(! (test-path $s)) { return $envpath }
    resolve-path $s | Select -ExpandProperty ProviderPath
}

function XymonDir
{
    #$script:clientlocalcfg | ? { $_ -match "^dir:(.*)" } | % {
    $script:clientlocalcfg_entries.keys | where { $_ -match "^dir:(.*)" } |`
        foreach {
        resolveEnvPath $matches[1] | foreach {
            "[dir:$($_)]"
            if(test-path $_ -PathType Container) { du $_ }
            elseif(test-path $_) {"ERROR: The path specified is not a directory." }
            else { "ERROR: The system cannot find the path specified." }
        }
    }
}

function XymonFileStat($file,$hash="")
{
    # don't implement hashing yet - don't even check for it...
    if(test-path $_) {
        $fh = get-item $_
        if(test-path $_ -PathType Leaf) {
            "type:100000 (file)"
        } else {
            "type:40000 (directory)"
        }
        "mode:{0} (not implemented)" -f $(if($fh.IsReadOnly) {555} else {777})
        "linkcount:1"
        "owner:0 ({0})" -f $fh.GetAccessControl().Owner
        "group:0 ({0})" -f $fh.GetAccessControl().Group
        if(test-path $_ -PathType Leaf) { "size:{0}" -f $fh.length }
        "atime:{0} ({1})" -f (epochTimeUtc $fh.LastAccessTimeUtc),$fh.LastAccessTime.ToString("yyyy/MM/dd-HH:mm:ss")
        "ctime:{0} ({1})" -f (epochTimeUtc $fh.CreationTimeUtc),$fh.CreationTime.ToString("yyyy/MM/dd-HH:mm:ss")
        "mtime:{0} ({1})" -f (epochTimeUtc $fh.LastWriteTimeUtc),$fh.LastWriteTime.ToString("yyyy/MM/dd-HH:mm:ss")
        if(test-path $_ -PathType Leaf) {
            "FileVersion:{0}" -f $fh.VersionInfo.FileVersion
            "FileDescription:{0}" -f $fh.VersionInfo.FileDescription
        }
    } else {
        "ERROR: The system cannot find the path specified."
    }
}

function XymonFileCheck
{
    # don't implement hashing yet - don't even check for it...
    #$script:clientlocalcfg | ? { $_ -match "^file:(.*)$" } | % {
    $script:clientlocalcfg_entries.keys | where { $_ -match "^file:(.*)$" } |`
        foreach {
        resolveEnvPath $matches[1] | foreach {
            "[file:$_]"
            XymonFileStat $_
        }
    }
}

function XymonLogCheck
{
    #$script:clientlocalcfg | ? { $_ -match "^log:(.*):(\d+)$" } | % {
    $script:clientlocalcfg_entries.keys | where { $_ -match "^log:([a-z%][a-z:][^:]+):(\d+):?(\d+)?$" } |`
        foreach {
        $positions = 6
        if ($matches[3] -ne $null)
        {
            $positions = $matches[3]
        }
        $sizemax = $matches[2]
        resolveEnvPath $matches[1] | foreach {
            "[logfile:$_]"
            XymonFileStat $_
            "[msgs:$_]"
            XymonLogCheckFile $_ $sizemax $positions
        }
    }
}

function XymonLogCheckFile([string]$file,$sizemax=0, $positions=6)
{
    WriteLog "Executing XymonLogCheckFile"
    WriteLog "File: $file"
    if (Test-Path $file)
    {
        $f = [system.io.file]::Open($file,"Open","Read","ReadWrite")
        $s = get-item $file
        $nowpos = $s.length
        $savepos = 0
        if($script:logfilepos.$($file) -ne $null) { $savepos = $script:logfilepos.$($file)[0] }
        if($nowpos -lt $savepos) {$savepos = 0} # log file rolled over??
        #"Save: {0}  Len: {1} Diff: {2} Max: {3} Write: {4}" -f $savepos,$nowpos, ($nowpos-$savepos),$sizemax,$s.LastWriteTime
        if($nowpos -gt $savepos) { # must be some more content to check
            $s = new-object system.io.StreamReader($f,$true)
            $dummy = $s.readline()
            $enc = $s.currentEncoding
            $charsize = 1
            if($enc.EncodingName -eq "Unicode") { $charsize = 2 }
            if($nowpos-$savepos -gt $charsize*$sizemax) {$savepos = $nowpos-$charsize*$sizemax}
            $seek = $f.Seek($savepos,0)
            $t = new-object system.io.StreamReader($f,$enc)
            $buf = $t.readtoend()
            if($buf -ne $null) { $buf }
            #"Save2: {0}  Pos: {1} Blen: {2} Len: {3} Enc($charsize): {4}" -f $savepos,$f.Position,$buf.length,$nowpos,$enc.EncodingName
        }
        if($script:logfilepos.$($file) -ne $null) {
            $script:logfilepos.$($file) = $script:logfilepos.$($file)[1..$positions]
        } else {
            $script:logfilepos.$($file) = @(0) * $positions
        }
        $script:logfilepos.$($file) += $nowpos # save for next loop
        WriteLog ("File saved positions: " + ($script:logfilepos.$($file) -join ','))
    }
    else
    {
        WriteLog "Cannot open / resolve $file"
        "ERROR: Cannot open / resolve $file" 
    }
    WriteLog "XymonLogCheckFile finished"
}

function XymonDirSize
{
    # dirsize:<path>:<gt/lt/eq>:<size bytes>:<fail colour>
    # match number:
    #        :  1   :   2      :     3      :     4
    # <path> may be a simple path (c:\temp) or contain an environment variable, or a filename
    # e.g. %USERPROFILE%\temp
    WriteLog "Executing XymonDirSize"
    $outputtext = ''
    $groupcolour = 'green'
    $script:clientlocalcfg_entries.keys | where { $_ -match '^dirsize:([a-z%][a-z:][^:]+):([gl]t|eq):(\d+):(.+)$' } |`
        foreach {
            resolveEnvPath $matches[1] | foreach {

                WriteLog "DirSize: $_"
                $objFSO = new-object -com Scripting.FileSystemObject

                if (test-path $_ -PathType Container)
                {
                    # could use "get-childitem ... -recurse | measure ..." here 
                    # but that does not work well when there are many files/subfolders
                    $size = $objFSO.GetFolder($_).Size
                }
                elseif (test-path $_)
                {
                    $size = (Get-Item $_).Length
                }
                else
                {
                    # file / directory does not exist
                    WriteLog "File $_ not found, setting size = -1"
                    $size = -1
                }

                $criteriasize = ($matches[3] -as [long])
                $conditionmet = $false
                if ($matches[2] -eq 'gt')
                {
                    $conditionmet = $size -gt $criteriasize
                    $conditiontype = '>'
                }
                elseif ($matches[2] -eq 'lt')
                {
                    $conditionmet = $size -lt $criteriasize
                    $conditiontype = '<'
                }
                else
                {
                    # eq
                    $conditionmet = $size -eq $criteriasize
                    $conditiontype = '='
                }
                if ($conditionmet)
                {
                    $alertcolour = $matches[4]
                }
                else
                {
                    $alertcolour = 'green'
                }

                # report out - 
                #  {0} = colour (matches[4])
                #  {1} = folder name
                #  {2} = folder size
                #  {3} = condition symbol (<,>,=)
                #  {4} = alert size
                $outputtext += (('<img src="{5}{0}.gif" alt="{0}" ' +`
                    'height="16" width="16" border="0">' +`
                    '{1} size is {2} bytes. Alert if {3} {4} bytes.<br>') `
                    -f $alertcolour, $_, $size, $conditiontype, $matches[3], $script:XymonSettings.servergiflocation)
                # set group colour to colour if it is not already set to a 
                # higher alert state colour
                if ($groupcolour -eq 'green' -and $alertcolour -eq 'yellow')
                {
                    $groupcolour = 'yellow'
                }
                elseif ($alertcolour -eq 'red')
                {
                    $groupcolour = 'red'
                }
            }
        }

    if ($outputtext -ne '')
    {
        $outputtext = (get-date -format G) + '<br><h2>Directory Size</h2>' + $outputtext
        $output = ('status {0}.dirsize {1} {2}' -f $script:clientname, $groupcolour, $outputtext)
        WriteLog "dirsize: Sending $output"
        XymonSend $output $script:XymonSettings.serversList
    }
}

function XymonDirTime
{
    # dirtime:<path>:<unused>:<gt/lt/eq>:<alerttime>:<colour>
    # match number:
    #        :  1   :    2   :     3    :     4     :   5
    # <path> may be a simple path (c:\temp) or contain an environment variable
    # e.g. %USERPROFILE%\temp
    # <alerttime> = number of minutes to alert after
    # e.g. if a directory should be modified every 10 minutes
    # alert for gt 10
    WriteLog "Executing XymonDirTime"
    $outputtext = ''
    $groupcolour = 'green'
    $script:clientlocalcfg_entries.keys | where { $_ -match '^dirtime:([a-z%][a-z:][^:]+):([^:]*):([gl]t|eq):(\d+):(.+)$' } |`
        foreach {
            resolveEnvPath $matches[1] | foreach {

                $skip = $false
                WriteLog "DirTime: $_"
                try
                {
                    $minutesdiff = ((get-date) - (Get-Item $_ -ErrorAction Stop).LastWriteTime).TotalMinutes
                }
                catch 
                {
                    $outputtext += (('<img src="{2}{0}.gif" alt="{0}"' +`
                        'height="16" width="16" border="0">' +`
                        '{1}') `
                        -f 'red', $_, $script:XymonSettings.servergiflocation)
                    $groupcolour = 'red'
                    $skip = $true
                }
                if (-not $skip)
                {
                    $criteriaminutes = ($matches[4] -as [int])
                    $conditionmet = $false
                    if ($matches[3] -eq 'gt')
                    {
                        $conditionmet = $minutesdiff -gt $criteriaminutes
                        $conditiontype = '>'
                    }
                    elseif ($matches[3] -eq 'lt')
                    {
                        $conditionmet = $minutesdiff -lt $criteriaminutes
                        $conditiontype = '<'
                    }
                    else
                    {
                        $conditionmet = $minutesdiff -eq $criteriaminutes
                        $conditiontype = '='
                    }
                    if ($conditionmet)
                    {
                        $alertcolour = $matches[5]
                    }
                    else
                    {
                        $alertcolour = 'green'
                    }
                    # report out - 
                    #  {0} = colour (matches[5])
                    #  {1} = folder name
                    #  {2} = folder modified x minutes ago
                    #  {3} = condition symbol (<,>,=)
                    #  {4} = alert criteria minutes
                    $outputtext += (('<img src="{5}{0}.gif" alt="{0}"' +`
                        'height="16" width="16" border="0">' +`
                        '{1} updated {2:F1} minutes ago. Alert if {3} {4} minutes ago.<br>') `
                        -f $alertcolour, $_, $minutesdiff, $conditiontype, $criteriaminutes, $script:XymonSettings.servergiflocation)
                    # set group colour to colour if it is not already set to a 
                    # higher alert state colour
                    if ($groupcolour -eq 'green' -and $alertcolour -eq 'yellow')
                    {
                        $groupcolour = 'yellow'
                    }
                    elseif ($alertcolour -eq 'red')
                    {
                        $groupcolour = 'red'
                    }
                }
            }
        }

    if ($outputtext -ne '')
    {
        $outputtext = (get-date -format G) + '<br><h2>Last Modified Time In Minutes</h2>' + $outputtext
        $output = ('status {0}.dirtime {1} {2}' -f $script:clientname, $groupcolour, $outputtext)
        WriteLog "dirtime: Sending $output"
        XymonSend $output $script:XymonSettings.serversList
    }
}

function XymonPorts
{
    WriteLog "XymonPorts start"
    $filter = ''
    if ($script:clientlocalcfg_entries.ContainsKey('ports:listenonly'))
    {
        $filter = 'LISTENING'
    }

    "[ports]"
    netstat -an | where { $_ -like "*$($filter)*" }
    WriteLog "XymonPorts finished."
}

function XymonIpconfig
{
    WriteLog "XymonIpconfig start"
    "[ipconfig]"
    ipconfig /all | %{ $_.split("`n") } | ?{ $_ -match "\S" }  # for some reason adds blankline between each line
    WriteLog "XymonIpconfig finished."
}

function XymonRoute
{
    WriteLog "XymonRoute start"
    "[route]"
    netstat -rn
    WriteLog "XymonRoute finished."
}

function XymonNetstat
{
    WriteLog "XymonNetstat start"
    "[netstat]"
    $pref=""
    netstat -s | ?{$_ -match "=|(\w+) Statistics for"} | %{ if($_.contains("=")) {("$pref$_").REPLACE(" ","")}else{$pref=$matches[1].ToLower();""}}
    WriteLog "XymonNetstat finished."
}

function XymonIfstat
{
    WriteLog "XymonIfstat start"
    $families = @{ 'IPv4' = [System.Net.Sockets.AddressFamily]::InterNetwork; 
        'IPv6' = [System.Net.Sockets.AddressFamily]::InterNetworkV6;
    }

    $wantedFamilies = @()
    $script:clientlocalcfg_entries.keys | where { $_ -match '^ifstat:((ipv[46],?)+)$' } |
        foreach {
            foreach ($wanted in ($matches[1] -split ','))
            {
                if ($families.ContainsKey($wanted))
                {
                    $wantedFamilies += $families[$wanted]
                }
            }
            $wantedFamilies = ($wantedFamilies | Sort-Object -Unique)
        }
    if (@($wantedFamilies).Length -eq 0)
    {
        $wantedFamilies += $families['IPv4']
    }
    WriteLog "wanted address families: $wantedFamilies"

    "[ifstat]"
    [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | 
        where { $_.OperationalStatus -eq "Up" -and $_.NetworkInterfaceType -ne 'loopback' } |
        foreach {
        $ad = $_.GetIPv4Statistics() | select BytesSent, BytesReceived
        $ip = $_.GetIPProperties().UnicastAddresses | select Address
        # some interfaces have multiple IPs, so iterate over them reporting same stats
        # also replace statement removes zone information (adaptor) from IPv6 addresses
        $ip | where { $wantedFamilies -contains $_.Address.AddressFamily } |
            foreach { "{0} {1} {2}" -f ($_.Address.IPAddressToString -replace '%\d+$'),$ad.BytesReceived,$ad.BytesSent }
    }
    WriteLog "XymonIfstat finished."
}

function XymonSvcs
{
    WriteLog "XymonSvcs start"
    "[svcs]"
    "Name".PadRight(39) + " " + "StartupType".PadRight(12) + " " + "Status".PadRight(14) + " " + "DisplayName"
    foreach ($s in $svcs) 
    {
        if ($script:clientlocalcfg_entries.ContainsKey('slimmode'))
        {
            if ($script:clientlocalcfg_entries.slimmode.ContainsKey('services'))
            {
                # skip this service if we are in slimmode and this service is not one of the 
                # requested services
                if ($script:clientlocalcfg_entries.slimmode.services -notcontains $s.Name)
                {
                    continue
                }
            }
        }
        if ($s.StartMode -eq "Auto") { $stm = "automatic" } else { $stm = $s.StartMode.ToLower() }
        if ($s.State -eq "Running")  { $state = "started" } else { $state = $s.State.ToLower() }
        $s.Name.Replace(" ","_").PadRight(39) + " " + $stm.PadRight(12) + " " + $state.PadRight(14) + " " + $s.DisplayName
    }
    WriteLog "XymonSvcs finished."
}

function XymonProcs
{
    WriteLog "XymonProcs start"
    "[procs]"
    "{0,8} {1,-35} {2,-17} {3,-17} {4,-17} {5,8} {6,-7} {7,5} {8,-19} {9,7} {10} {11}" -f `
        "PID", "User", "WorkingSet/Peak", "VirtualMem/Peak", "PagedMem/Peak", "NPS", `
        "Handles", "%CPU", 'Start Time', 'Elapsed', "Name", "Command"
    
    # output sorted process table
    $script:procs | Sort-Object -Descending { $_.CPUPercent } `
        | foreach {
            $startTime = ''
            if ($_.StartTime -ne $null)
            {
                $startTime = Get-Date -Date $_.StartTime -uformat '%Y-%m-%d %H:%M:%S'
            }

            $skipFlag = $false
            if ($script:clientlocalcfg_entries.ContainsKey('slimmode'))
            {
                if ($script:clientlocalcfg_entries.slimmode.ContainsKey('processes'))
                {
                    # skip this process if we are in slimmode and this process is not one of the 
                    # requested processes
                    if ($script:clientlocalcfg_entries.slimmode.processes -notcontains $_.XymonProcessName)
                    {
                        $skipFlag = $true
                    }
                }
            }
            
            if (!$skipFlag)
            {
                "{0,8} {1,-35} {2} {3} {4} {5} {6,7:F0} {7,5:F1} {8,19} {9,7:F0} {10} {11}" -f $_.Id, $_.Owner, `
                    $_.XymonPeakWorkingSet, $_.XymonPeakVirtualMem,`
                     $_.XymonPeakPagedMem, $_.XymonNonPagedSystemMem, `
                     $_.Handles, $_.CPUPercent, `
                     $startTime, $_.ElapsedSinceStart, $_.XymonProcessName, $_.CommandLine
            }
    }
    WriteLog "XymonProcs finished."
}

function CleanXymonProcsCpu
{
    # reset cache flags and clear terminated processes from the cache
    WriteLog "CleanXymonProcsCpu start"
    if (Test-Path variable:script:XymonProcsCpu)
    {
        if ($script:XymonProcsCpu.Count -gt 0)
        {
            foreach ($p in @($script:XymonProcsCpu.Keys)) {
                $thisp = $script:XymonProcsCpu[$p]
                if ($thisp[3] -eq $true) {
                    # reset flag to catch a dead process on the next run
                    # this flag will be updated back to $true by XymonProcsCPUUtilisation
                    # if the process still exists
                    $thisp[3] = $false  
                }
                else {
                    # flag was set to $false previously = process has been terminated
                    WriteLog "Process id $p has disappeared, removing from cache"
                    $script:XymonProcsCpu.Remove($p)
                }
            }
        }
        WriteLog ("DEBUG: cached process ids: " + (($script:XymonProcsCpu.Keys | sort-object) -join ', '))
    }
    WriteLog "CleanXymonProcsCpu finished."
}

function XymonWho
{
    WriteLog "XymonWho start"
    if( $HaveCmd.qwinsta) 
    {
        "[who]"
        if ($script:usersessions -eq $null)
        {
            qwinsta.exe /counter
        }
        else
        {
            $script:usersessions
        }
    }
    WriteLog "XymonWho finished."
}

function XymonUsers
{
    WriteLog "XymonUsers start"
    if( $HaveCmd.query) {
        "[users]"
        query user
    }
    WriteLog "XymonUsers finished."
}

function XymonIISSites
{
    WriteLog "XymonIISSites start"
    if ($script:XymonSettings.EnableIISSection -eq 1)
    {
        $objSites = [adsi]("IIS://localhost/W3SVC")
        if($objSites.path -ne $null) {
            "[iis_sites]"
            foreach ($objChild in $objSites.Psbase.children | where {$_.KeyType -eq "IIsWebServer"} ) {
                ""
                $objChild.servercomment
                $objChild.path
                if($objChild.path -match "\/W3SVC\/(\d+)") { "SiteID: "+$matches[1] }
                foreach ($prop in @("LogFileDirectory","LogFileLocaltimeRollover","LogFileTruncateSize","ServerAutoStart","ServerBindings","ServerState","SecureBindings" )) {
                    if( $($objChild | gm -Name $prop ) -ne $null) {
                        "{0} {1}" -f $prop,$objChild.$prop.ToString()
                    }
                }
            }
            clear-variable objChild
        }
        clear-variable objSites
    }
    else
    {
        WriteLog 'Skipping XymonIISSites, EnableIISSection = 0 in config'
    }
    WriteLog "XymonIISSites finished."
}

function XymonWMIOperatingSystem
{
    "[WMI:Win32_OperatingSystem]"
    WMIProp Win32_OperatingSystem
}

function XymonWMIQuickFixEngineering
{
    if ($script:XymonSettings.EnableWin32_QuickFixEngineering -eq 1)
    {
        "[WMI:Win32_QuickFixEngineering]"
        Get-WmiObject -Class Win32_QuickFixEngineering | where { $_.Description -ne "" } | Sort-Object HotFixID | Format-Wide -Property HotFixID -AutoSize
    }
    else
    {
        WriteLog "Skipping XymonWMIQuickFixEngineering, EnableWin32_QuickFixEngineering = 0 in config"
    }
}

function XymonWMIProduct
{
    if ($script:XymonSettings.EnableWin32_Product -eq 1)
    {
        # run as job, since Win32_Product WMI dies on some systems (e.g. XP)
        $job = Get-WmiObject -Class Win32_Product -AsJob | wait-job
        if($job.State -eq "Completed") {
            "[WMI:Win32_Product]"
            $fmt = "{0,-70} {1,-15} {2}"
            $fmt -f "Name", "Version", "Vendor"
            $fmt -f "----", "-------", "------"
            receive-job $job | Sort-Object Name | 
            foreach {
                $fmt -f $_.Name, $_.Version, $_.Vendor
            }
        }
        remove-job $job
    }
    else
    {
        WriteLog "Skipping XymonWMIProduct, EnableWin32_Product = 0 in config"
    }
}

function XymonWMIComputerSystem
{
    "[WMI:Win32_ComputerSystem]"
    WMIProp Win32_ComputerSystem
}

function XymonWMIBIOS
{
    "[WMI:Win32_BIOS]"
    WMIProp Win32_BIOS
}

function XymonWMIProcessor
{
    "[WMI:Win32_Processor]"
    $cpuinfo | Format-List DeviceId,Name,CurrentClockSpeed,NumberOfCores,NumberOfLogicalProcessors,CpuStatus,Status,LoadPercentage
}

function XymonWMIMemory
{
    "[WMI:Win32_PhysicalMemory]"
    Get-WmiObject -Class Win32_PhysicalMemory | Format-Table -AutoSize BankLabel,Capacity,DataWidth,DeviceLocator
}

function XymonWMILogicalDisk
{
    "[WMI:Win32_LogicalDisk]"
    Get-WmiObject -Class Win32_LogicalDisk | Format-Table -AutoSize
}

function XymonDiskPart
{
    WriteLog 'XymonDiskPart start'

    try
    {
        $diskpart = 'list disk' | diskpart
        $dpOutput = $diskpart | where { $_ -match '^  Disk \d+' }
        $dpOutput = $dpOutput -replace '^\s+', ''
        $dpOutput = $dpOutput -replace '\s+$', ''
        "[diskpart]"
        
        $diskDetailCmd = "select disk {0}`r`ndetail disk"
        $noVolumeRX = '^There are no volumes.'

        $dpOutput | foreach {
            $dpColumns = $_ -split '\s{2,}'
            $diskNum = $dpColumns[0] -replace 'Disk ', ''
            $cmd = $diskDetailCmd -f $diskNum
            $detailOutput = $cmd | diskpart
            $detailDisk = $detailOutput | where { $_ -match '^Clustered' -or $_ -match $noVolumeRX }
        
            if ($detailDisk -match '^Clustered Disk  : No')
            {
                $clusterOutput = 'Not Clustered'
            }
            else
            {
                if (-not ($detailDisk -match '^Clustered'))
                {
                    $clusterOutput = 'Clustered Unknown'
                }
                else
                {
                    $clusterOutput = 'Clustered Active'
                    if ($detailDisk -match $noVolumeRX)
                    {
                        $clusterOutput = 'Clustered Inactive'
                    }
                }
            }

            "diskpart:{0}:{1}:{2}" -f $dpColumns[0], $dpColumns[2], $clusterOutput
        }
    }
    catch
    {
        WriteLog "Xymondisk diskpart - error $_"
    }

    WriteLog 'XymonDiskPart finished'
}

function XymonServiceCheck
{
    WriteLog "Executing XymonServiceCheck"
    if ($script:clientlocalcfg_entries -ne $null)
    {
        $servicecfgs = @($script:clientlocalcfg_entries.keys | where { $_ -match '^servicecheck' })
        foreach ($service in $servicecfgs)
        {
            # parameter should be 'servicecheck:<servicename>:<duration>'
            $checkparams = $service -split ':'
            # validation
            if ($checkparams.length -ne 3)
            {
                WriteLog "ERROR: not enough parameters (should be servicecheck:<servicename>:<duration>) - $checkparams[1]"
                continue
            }
            else
            {
                $duration = $checkparams[2] -as [int]
                if ($checkparams[1] -eq '' -or $duration -eq $null)
                {
                    WriteLog "ERROR: config error (should be servicecheck:<servicename>:<duration>) - $checkparams[1]"
                    continue
                }
            }
            # check for maintenance window
            $days = ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday')
            $serviceexclds = @($script:clientlocalcfg_entries.keys | where { $_ -match '^noservicecheck' })
            
            if ($serviceexclds -ne '')
            {
                foreach ($maintservice in $serviceexclds)
                {
                    # parameter should be 'noservicecheck:<servicename>:<numeric day of week Sun=0>:<military start hour>:<duration in Hours>'
                    $checkMparams = $maintservice -split ':'
                    if ($checkparams[1] -eq $checkMparams[1])
                    {
                        # validation of number of parameters
                        if ($checkMparams.length -ne 5)
                        {
                            WriteLog ("ERROR: not enough parameters (noservicecheck:<servicename>:<numeric day of week Sun=0>:<start hour (24h)>:<duration Hrs> {0}" -f $checkMparams[1])
                            continue
                        }
                        else
                        {
                            # get values
                            $MaintDay = $checkMparams[2] -as [int]
                            $MaintStartHour = $checkMparams[3] -as [int]
                            $MaintDuration = $checkMparams[4] -as [int]
                            # validation of basic values
                            if ($checkMparams[1] -eq '' -or $MaintDuration -eq $null -or (0..6 -notcontains $MaintDay) -or (0..23 -notcontains $MaintStartHour))
                            {
                                WriteLog ("ERROR: config error (noservicecheck:<servicename>:<numeric day of week Sun=0>:<start hour (24h)>:<duration Hrs>) {0}" -f $checkMparams[1])
                                continue
                            }
                            $MaintWeekDay = $days[$MaintDay]
                        }
                        
                        if (((get-date).DayofWeek -eq $MaintWeekDay) -and ((get-date).Hour -eq $MaintStartHour) ) 
                        { 
                            if ($script:MaintChecks.ContainsKey($checkMparams[1])) 
                            {
                                $MaintWindowEnd = $script:MaintChecks[$checkMparams[1]].AddHours($MaintDuration)
                                if ((get-date) -lt $MaintWindowEnd)
                                {
                                    WriteLog (" Maintenance: Skipping Service Check until after $($MaintWindowEnd) for {0}" -f $checkMparams[1])
                                    continue
                                }
                                else
                                {
                                    clear.variable $script:MaintChecks
                                }
                            }
                            else
                            {
                                 WriteLog ("Not seen this NoServiceCheck before, starting Maintenance Window now for {0}" -f $checkMparams[1])
                                 $hourTop = (get-date).Minute
                                 $script:MaintChecks[$checkMparams[1]] = (get-date).AddMinutes(-($hourTop))
                                 continue
                            }
                        }
                        # end of maintenance hold   
                    }
                }
            }
            WriteLog ("Checking service {0}" -f $checkparams[1])

            $winsrv = Get-Service -Name $checkparams[1]
            if ($winsrv.Status -eq 'Stopped')
            {
                writeLog ("!! Service {0} is stopped" -f $checkparams[1])
                if ($script:ServiceChecks.ContainsKey($checkparams[1]))
                {
                    $restarttime = $script:ServiceChecks[$checkparams[1]].AddSeconds($duration)
                    writeLog "Seen this service before; restart time is $restarttime"
                    if ($restarttime -lt (get-date))
                    {
                        writeLog (" -> Starting service {0}" -f $checkparams[1])
                        $winsrv.Start()
                    }
                }
                else
                {
                    writeLog "Not seen this service before, setting restart time -1 hour"
                    $script:ServiceChecks[$checkparams[1]] = (get-date).AddHours(-1)
                }
            }
            elseif ('StartPending', 'Running' -contains $winsrv.Status)
            {
                writeLog "  -Service is running, updating last seen time"
                $script:ServiceChecks[$checkparams[1]] = get-date
            }
        }
    }
}

function XymonTerminalServicesSessionsCheck
{
    # this function relies on data from XymonWho - should be called after XymonWho
    WriteLog "Executing XymonTerminalServicesSessionsCheck"

    # config: terminalservicessessions:<yellowthreshold>:<redthreshold>
    # thresholds are number of free sessions - so alert when only x sessions free
    $script:clientlocalcfg_entries.keys | where { $_ -match '^(?:ts|terminalservices)sessions:(\d+):(\d+)' } |`
        foreach {
            try
            {
                $maxSessions = Get-ItemProperty -ErrorAction:Stop `
                    -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'`
                    -Name MaxInstanceCount | select -ExpandProperty MaxInstanceCount
            }
            catch
            {
                WriteLog "Failed to get max sessions from CurrentControlSet registry: $_"
                $maxSessions = 0xffffffffL 
            }

            $maxSessionMsg = ''
            if ($maxSessions -eq 0xffffffffL)
            {
                # try group policy key
                try
                {
                    $maxSessions = Get-ItemProperty -ErrorAction:Stop `
                        -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services'`
                        -Name MaxInstanceCount | select -ExpandProperty MaxInstanceCount
                }
                catch
                {
                    WriteLog "Failed to get max sessions from Group Policy registry: $_"
                    return
                }
            }

            if ($maxSessions -eq 0xffffffffL)
            {
                $maxSessionMsg = "Max sessions not set (probably not an RDS server)"
                WriteLog $maxSessionMsg
                $maxSessions = 2
            }

            $yellowThreshold = $matches[1]
            $redThreshold = $matches[2]

            $activeSessions = $script:usersessions | where { $_ -match 'Active' } | measure | 
                select -ExpandProperty Count

            $freeSessions = $maxSessions - $activeSessions

            WriteLog "sessions: active: $activeSessions maximum: $maxSessions free: $freeSessions"
            WriteLog "thresholds: yellow: $yellowThreshold red: $redThreshold"

            $alertColour = 'green'

            if ($freeSessions -le $redThreshold)
            {
                $alertColour = 'red'
            }
            elseif ($freeSessions -le $yellowThreshold)
            {
                $alertColour = 'yellow'
            }

            $outputtext = (('<img src="{0}{1}.gif" alt="{1}" ' +`
                            'height="16" width="16" border="0">' +`
                            'sessions: active: {2} maximum: {3} free: {4}. {7}<br>yellow alert = {5} free, red = {6} free.<br>') `
                            -f $script:XymonSettings.servergiflocation, $alertColour, `
                            $activeSessions, $maxSessions, $freeSessions, $yellowThreshold, $redThreshold, $maxSessionMsg)
            
            $outputtext = (get-date -format G) + '<br><h2>Terminal Services Sessions</h2>' + $outputtext
            $output = ('status {0}.tssessions {1} {2}' -f $script:clientname, $alertColour, $outputtext)
            WriteLog "Terminal Services Sessions: sending $output"
            XymonSend $output $script:XymonSettings.serversList
        }
}

function XymonActiveDirectoryReplicationCheck
{
    WriteLog "Executing XymonActiveDirectoryReplicationCheck"
    if ($script:clientlocalcfg_entries.keys -contains 'adreplicationcheck')
    {
        $status = repadmin /showrepl * /csv
        $results = @(ConvertFrom-Csv -InputObject $status)

        $alertColour = 'green'
    
        $failcount = ($results | where { $_.'Last Failure Time' -gt $_.'Last Success Time' }).Length
        if ($failcount -gt 0)
        {
            $alertColour = 'red'
        }
        else
        {
            $failcount = 'none'
        }
        
        $outputtext = (('<img src="{0}{1}.gif" alt="{1}" ' +`
                        'height="16" width="16" border="0">' +`
                        'Failing replication contexts: {2}<br>red alert = more than zero.<br>') `
                        -f $script:XymonSettings.servergiflocation, $alertColour, `
                        $failcount)
        $outputtext = (get-date -format G) + '<br><h2>Active Directory Replication</h2>' + $outputtext
        $outputtext += '<br/>'

        $outputtable = ($results | select 'Source DSA', `
            'Naming Context', 'Destination DSA', 'Number of Failures', `
            'Last Failure Time', 'Last Success Time', 'Last Failure Status'`
             | ConvertTo-Html -Fragment)

        $outputtable = $outputtable -replace '<table>', '<table style="font-size: 10pt">'

        $outputtext += $outputtable
        $output = ('status {0}.adreplication {1} {2}' -f $script:clientname, $alertColour, $outputtext)
        WriteLog "Active Directory Replication: sending status $alertColour"
        XymonSend $output $script:XymonSettings.serversList
    }
}

function XymonProcessRuntimeCheck
{
    WriteLog 'Executing XymonProcessRuntimeCheck'
    
    # config: processruntime:<process name>:<yellow elapsed threshold>:<red elapsed threshold>
    # thresholds in minutes

    $groupColour = 'green'

    $outputHeader = (get-date -format G) + "<br><h3>Process Run Time Check</h3><pre>"
    $output = ''

    $script:clientlocalcfg_entries.keys | where { $_ -match '^proc(?:ess)?runtime:(.+):(\d+):(\d+)' } | `
        foreach {
            $processName = $matches[1]
            $yellowThreshold = $matches[2]
            $redThreshold = $matches[3]
            $alertColour = 'green'
            $headerColour = 'green'

            $script:procs | where { $_.XymonProcessName -eq $processName } | foreach {
                if ($_.ElapsedSinceStart -gt $redThreshold)
                {
                    $alertColour = 'red'
                    $headerColour = 'red'
                    $groupcolour = 'red'
                }
                elseif ($_.ElapsedSinceStart -gt $yellowThreshold)
                {
                    $alertColour = 'yellow'
                }
                if ($groupcolour -eq 'green' -and $alertcolour -eq 'yellow')
                {
                    $groupcolour = 'yellow'
                }
                if ($headerColour -eq 'green' -and $alertColour -eq 'yellow')
                {
                    $headerColour = 'yellow'
                }

                WriteLog "Process $($_.XymonProcessName) running for $($_.ElapsedSinceStart) minutes: $alertcolour"

                $startTime = Get-Date -Date $_.StartTime -uformat '%Y-%m-%d %H:%M:%S'
                $processLine = "{0,8} {1,-35} {2,-19} {3,7:F0} {4} {5}" -f $_.Id, $_.Owner, `
                     $startTime, $_.ElapsedSinceStart, $_.XymonProcessName, $_.CommandLine

                $output += '<img src="{2}{0}.gif" alt="{0}" height="16" width="16" border="0">{1}<br>' `
                    -f $alertcolour, $processLine, $script:XymonSettings.servergiflocation
            }

            $outputHeader += ('<img src="{1}{0}.gif" alt="{0}" height="16" width="16" border="0">' + `
                'Process: {2}  Yellow alert after {3} minutes, Red alert after {4} minutes<br>') `
                -f $headerColour, $script:XymonSettings.servergiflocation, `
                    $processName, $yellowThreshold, $redThreshold
        }

    if ($output -ne '')
    {
        $output += '</pre>'
        $outputHeader += '<br><span style="margin-left: 16px;">{0,8} {1,-35} {2,19} {3,7} {4} {5}</span><br>' `
            -f "PID", "User", 'Start Time', 'Elapsed', "Name", "Command"
    }
    $output = $outputHeader + $output

    WriteLog "Sending output for procruntime"
    $outputXymon = ('status {0}.procruntime {1} {2}' -f $script:clientname, $groupcolour, $output)
    XymonSend $outputXymon $script:XymonSettings.serversList
    WriteLog 'XymonProcessRuntimeCheck finished'
}

function XymonProcessExternalData
{
    WriteLog 'Executing XymonProcessExternalData'

    if (Test-Path $script:XymonSettings.externaldatalocation)
    {
        $files = Get-ChildItem $script:XymonSettings.externaldatalocation

        if ($files -ne $null)
        {
            foreach ($f in $files)
            {
                # external filenames
                # it appears that BBWin ignores external files containing a dot '.'?
                # so replicate that behaviour
                if ($f.Name -match '\.')
                {
                    continue
                }
                # a valid filename is either just the test name: testname
                # or testname^hostname, to allow sending results from a different 
                # named host
                if ($f.Name -match '^([\w-]+)(?:\^([\S]+))?$')
                {
                    $testName = $matches[1]
                    $hostName = $matches[2]
                
                    if ($hostName -eq $null)
                    {
                        $hostName = $script:clientname
                    }

                    # attempt to open the file with an exclusive lock
                    # if we cannot, the file may be being updated by a running job, so
                    # we will ignore it until the next poll
                    WriteLog "Attempting to process external file $($f.FullName)"
                    try
                    {
                        $statusFile = [System.IO.File]::Open($f.FullName, 'Open', 'Read', 'None')
                        $reader = New-Object System.IO.StreamReader($statusFile)
                        $statusFileContent = $reader.ReadToEnd()
                        $reader.Close()
                        $statusFile.Close()
                    }
                    catch
                    {        
                        # if this file is locked or other errors, skip and go to the next one
                        if ($_ -like '*The process cannot access the file*because it is being used by another process*')
                        {
                            WriteLog "External file $($f.Name) is locked by another process, skipping"
                        }
                        else
                        {
                            WriteLog "External file $($f.Name) error accessing file, skipping: $_"
                        }
                        continue
                    }

                    # match:
                    # colour ($matches[1])
                    # optionally + and any non-space chars ($matches[2])
                    # space
                    # remainder ($matches[3])
                    if ($statusFileContent -match '^(red|yellow|green|clear)(?:\+([^ ]+))? ([\s\S]+)$')
                    {
                        $groupColour = $matches[1]
                        $lifeSpan = $matches[2]
                        $statusMessage = $matches[3]

                        $msg = 'status'
                        if ($lifeSpan -ne $null -and $lifeSpan -ne '')
                        {
                            $msg += "+$lifeSpan"
                        }
                        $msg += (' {0}.{1} {2} {3}' -f $hostName, $testName, $groupColour, $statusMessage)
                        
                        WriteLog "Sending Xymon message for file $($f.Name) - test $($testName), host $($hostName)"
                        XymonSend $msg $script:XymonSettings.serversList
                    }
                    elseif ($statusFileContent -match '^usermsg ')
                    {
                        $msg = $statusFileContent
                        WriteLog "Sending Xymon usermsg"
                        XymonSend $msg $script:XymonSettings.serversList
                    }
                    else
                    {
                        WriteLog "External File: $($f.Name) - format not recognised"
                        WriteLog "Contents of file:`n$statusFileContent"
                    }
                    WriteLog "Deleting file $($f.Name)"
                    Remove-Item $f.FullName -Force
                }
                else
                {
                    WriteLog "Invalid filename $($f.Name)"
                }
            }
        }
        else
        {
            WriteLog "No files in $($script:XymonSettings.externaldatalocation), nothing to do"
        }
    }
    else
    {
        WriteLog "External data path $($script:XymonSettings.externaldatalocation) does not exist"
    }
    WriteLog 'XymonProcessExternalData finished'
}

# replicate Linux client behaviour
# include items from 'local' folder in client data, if present
# no validation is done on the file content - it's just included
# in the client data with [local:<filename>] tags
function XymonProcessLocalData
{
    WriteLog 'Executing XymonProcessLocalData'

    if (Test-Path $script:XymonSettings.localdatalocation)
    {
        $files = Get-ChildItem $script:XymonSettings.localdatalocation

        if ($files -ne $null)
        {
            foreach ($f in $files)
            {
                # attempt to open the file with an exclusive lock
                # if we cannot, the file may be being updated by a running job, so
                # we will ignore it until the next poll
                WriteLog "Attempting to process local file $($f.FullName)"

                $statusFileContent = ''

                try
                {
                    $statusFile = [System.IO.File]::Open($f.FullName, 'Open', 'Read', 'None')
                    $reader = New-Object System.IO.StreamReader($statusFile)
                    $statusFileContent = $reader.ReadToEnd()
                    $reader.Close()
                    $statusFile.Close()
                }
                catch
                {        
                    # if this file is locked or other errors, skip and go to the next one
                    if ($_ -like '*The process cannot access the file*because it is being used by another process*')
                    {
                        WriteLog "Local file $($f.Name) is locked by another process, skipping"
                    }
                    else
                    {
                        WriteLog "Local file $($f.Name) error accessing file, skipping: $_"
                    }
                    continue
                }

                if ($statusFileContent -ne '')
                {
                    $heading = "[local:$($f.Name)]"
                    $heading
                    $statusFileContent
                }

                WriteLog "Deleting file $($f.Name)"
                Remove-Item $f.FullName -Force
            }
        }
        else
        {
            WriteLog "No files in $($script:XymonSettings.localdatalocation), nothing to do"
        }

    }
    else
    {
        WriteLog "Local data path $($script:XymonSettings.localdatalocation) does not exist, nothing to do"
    }

    WriteLog 'XymonProcessLocalData finished'
}

# from http://poshcode.org/1054
function Remove-Diacritics([string]$String) 
{
    $objD = $String.Normalize([Text.NormalizationForm]::FormD)
    $sb = New-Object Text.StringBuilder
    for ($i = 0; $i -lt $objD.Length; $i++) 
    {
        $c = [Globalization.CharUnicodeInfo]::GetUnicodeCategory($objD[$i])
        if($c -ne [Globalization.UnicodeCategory]::NonSpacingMark) 
        {
            [void]$sb.Append($objD[$i])
        }
    }
    return("$sb".Normalize([Text.NormalizationForm]::FormC))
}

function DecryptHttpServerPassword
{
    $serverPassword = $script:XymonSettings.serverHttpPassword
    if ($serverPassword -like '{SecureString}*')
    {
        WriteLog '  Decrypting serverHttpPassword'
        $serverPass = ($serverPassword -replace '^{SecureString}', '')
        try
        {
            $securePass = ConvertTo-SecureString -String $serverPass
            $tempCred = New-Object System.Management.Automation.PSCredential 'N/A', $securePass
            $serverPassword = $tempCred.GetNetworkCredential().Password
        }
        catch
        {
            WriteLog "Failed to decrypt serverHttpPassword: $_"
            $serverPassword = ''
        }
    }
    return $serverPassword
}

function XymonSendViaHttp($msg, $filePath)
{
    WriteLog 'Executing XymonSendViaHttp'

    $script:XymonSettings.serverUrl.Split(" ") | ForEach {
        $url = $_
        if ($url -notmatch '^https?://')
        {
            WriteLog "  ERROR: invalid server Url, check config: $url"
            return ''
        }

        WriteLog "  Using url $url"
        $encodedAuth = ''
        if ($script:XymonSettings.serverHttpUsername -ne '')
        {
            $serverHttpPassword = DecryptHttpServerPassword
            $authString = ('{0}:{1}' -f $script:XymonSettings.serverHttpUsername, `
                $serverHttpPassword)
        
            $encodedAuth = [System.Convert]::ToBase64String(`
                [System.Text.Encoding]::GetEncoding('ISO-8859-1').GetBytes($authString))


            WriteLog "  Using username $($script:XymonSettings.serverHttpUsername)"
        }

        if ($url -match '^https://')
        {
            [Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
            try
            {
                [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
            }
            catch
            {
                WriteLog "Error setting TLS options (old version of .NET?): $_"
                return $false
            }
        }

        # AXI: verwijderen van ^M, dit stuurt de procs check volledig in de war
        $msg = $msg.Replace("`r","")

        # no Invoke-RestMethod before Powershell 3.0
        $request = [System.Net.HttpWebRequest]::Create($url)
        $request.Method = 'POST'
        $request.Timeout = $script:XymonSettings.serverHttpTimeoutMs
        if ($encodedAuth -ne '')
        {
            $request.Headers.Add('Authorization', "Basic $encodedAuth")
        }

        # $body = [byte[]][char[]]$msg
        $body = [text.encoding]::ascii.getbytes($msg)
        $bodyStream = $request.GetRequestStream()
        $bodyStream.Write($body, 0, $body.Length)

        WriteLog "  Connecting to $($url), body length $($body.Length), timeout $($script:XymonSettings.serverHttpTimeoutMs)ms"
        try
        {
            $response = $request.GetResponse()
        }
        catch
        {
            WriteLog "  Exception connecting to $($url):`n$($_)"
            return ''
        }
        
        $statusCode = [int]($response.StatusCode)
        if ($response.StatusCode -ne [System.Net.HttpStatusCode]::OK)
        {
            WriteLog "  FAILED, HTTP response code: $($response.StatusCode) ($statusCode)"
            return ''
        }

        $responseStream = $response.GetResponseStream()
        $readStream = New-Object System.IO.StreamReader $responseStream
        $output = $readStream.ReadToEnd()
        WriteLog "  Received $($output.Length) bytes from server"
        $script:LastTransmissionMethod = 'HTTP'

    }
    WriteLog 'XymonSendViaHttp finished'
    return $output
}

function XymonSend($msg, $servers, $filePath)
{
    $saveresponse = 1   # Only on the first server
    $outputbuffer = ""

    if ($script:XymonSettings.serverUrl -ne '')
    {
        $outputBuffer = XymonSendViaHttp $msg $filePath

        $line = ($msg -split [environment]::newline)[0]
        $line = $line -replace '[\t|\s]+', ' '
        if  ($line -match '(download) (.*$)' ) 
        {
            if ($filePath -eq $null -or $filePath -eq "") 
            {
                # save it locally with the same name
                $filePath = split-path -leaf $matches[2]
            }

            # Save in unix format so the hash is the same as on the (Linux) xymon server
            Set-Content $filePath ([byte[]][char[]] "$outputBuffer") -Encoding Byte -NoNewLine
        }
    }
    else
    {
        switch ($script:XymonSettings.XymonAcceptUTF8) 
        {
            1 {
                WriteLog 'Using UTF8 encoding'
                $MessageEncoder = New-Object System.Text.UTF8Encoding
            }
            2 {
                WriteLog 'Using "pure" ASCII encoding with remove diacritics etc'
                $MessageEncoder = New-Object System.Text.ASCIIEncoding
                # remove diacritics
                $msg = Remove-Diacritics -String $msg
                # convert non-break spaces to normal spaces
                $msg = $msg.Replace([char]0x00a0,' ')
            }
            default { 
                WriteLog 'Using "original" ASCII encoding' 
                $MessageEncoder = New-Object System.Text.ASCIIEncoding
            }
        }
        foreach ($srv in $servers) 
        {
            $srvparams = $srv.Split(":")
            # allow for server names that may resolve to multiple A records
            $srvIPs = & {
                $local:ErrorActionPreference = "SilentlyContinue"
                $srvparams[0] | %{[system.net.dns]::GetHostAddresses($_)} | %{ $_.IPAddressToString}
            }
            if ($srvIPs -eq $null) 
            { # no IP addresses could be looked up
                Write-Error -Category InvalidData ("No IP addresses could be found for host: " + $srvparams[0])
            } 
            else 
            {
                if ($srvparams.Count -gt 1) 
                {
                    $srvport = $srvparams[1]
                } 
                else 
                {
                    $srvport = 1984
                }
                foreach ($srvip in $srvIPs) 
                {
                    WriteLog "Connecting to host $srvip"

                    $saveerractpref = $ErrorActionPreference
                    $ErrorActionPreference = "SilentlyContinue"
                    $socket = new-object System.Net.Sockets.TcpClient
                    $socket.Connect($srvip, $srvport)
                    $ErrorActionPreference = $saveerractpref
                    if(! $? -or ! $socket.Connected ) 
                    {
                        $errmsg = $Error[0].Exception
                        WriteLog "ERROR: Cannot connect to host $srv ($srvip) : $errmsg"
                        Write-Error -Category OpenError "Cannot connect to host $srv ($srvip) : $errmsg"
                        continue;
                    }
                    $socket.sendTimeout = 500
                    $socket.NoDelay = $true

                    $stream = $socket.GetStream()
                    
                    $sent = 0
                    foreach ($line in $msg) 
                    {
                        # Convert data as appropriate
                        try
                        {
                            $sent += $socket.Client.Send($MessageEncoder.GetBytes($line.Replace("`r","") + "`n"))
                        }
                        catch
                        {
                            WriteLog "ERROR: $_"
                        }
                    }
                    WriteLog "Sent $sent bytes to server"

                    if ($saveresponse-- -gt 0) 
                    {
                        $socket.Client.Shutdown(1)  # Signal to Xymon we're done writing.

                        $bytes = 0
                        $line = ($msg -split [environment]::newline)[0]
                        $line = $line -replace '[\t|\s]+', ' '
                        if  ($line -match '(download) (.*$)' ) 
                        {
                            if ($filePath -eq $null -or $filePath -eq "") 
                            {
                                # save it locally with the same name
                                $filePath = split-path -leaf $matches[2]
                            }
                            $buffer = new-object System.Byte[] 2048;
                            $fileStream = New-Object System.IO.FileStream($filePath, [System.IO.FileMode]'Create', [System.IO.FileAccess]'Write');

                            do
                            {
                                $read = $null;
                                while($stream.DataAvailable -or $read -eq $null) 
                                {
                                    $read = $stream.Read($buffer, 0, 2048);
                                    if ($read -gt 0) 
                                    {
                                        $fileStream.Write($buffer, 0, $read);
                                        $bytes += $read
                                    }
                                }
                            } while ($read -gt 0);
                            $fileStream.Close();
                            WriteLog "Wrote $bytes bytes from server to $filePath"
                        } 
                        else 
                        {
                            $s = new-object system.io.StreamReader($stream,"ASCII")

                            start-sleep -m 200  # wait for data to buffer
                            try
                            {
                                $outputBuffer = $s.ReadToEnd()
                                WriteLog "Received $($outputBuffer.Length) bytes from server"
                            }
                            catch
                            {
                                WriteLog "ERROR: $_"
                            }
                        }
                    } # saveresponse-- -gt 0
                    $socket.Close()
                    $script:LastTransmissionMethod = 'TCP'
                } # foreach ($srvip in $srvIPs)
            } # else of if ($srvIPs -eq $null) 
        } # foreach $srv in $servers
    }
    $outputbuffer
}

function XymonClientConfig($cfglines)
{
    if ($cfglines -eq $null -or $cfglines -eq "") { return }

    # Convert to Windows-style linebreaks
    $script:clientlocalcfg = $cfglines.Split("`n")

    # overwrite local cached config with this version if 
    # remote config is enabled
    $configmode = ''
    if ($script:XymonSettings.clientremotecfgexec -ne 0)
    {
        WriteLog "Using new remote config, saving locally"
        $clientlocalcfg >$script:XymonSettings.clientconfigfile
        $configmode = 'remote'
    }
    else
    {
        WriteLog "Using local config only (if one exists), clientremotecfgexec = 0"
        $configmode = 'localonly'
    }

    # Parse the config - always uses the local file (which may contain
    # config from remote)
    if (test-path -PathType Leaf $script:XymonSettings.clientconfigfile) 
    {
        # make sure the config always contains something
        $script:clientlocalcfg_entries = @{ '_configmode_' = $configmode }
        $lines = get-content $script:XymonSettings.clientconfigfile
        $currentsection = ''
        $eventlogswantedSeen = 0
        foreach ($l in $lines)
        {
            # change this to recognise new config items
            if ($l -match '^eventlog:' -or $l -match '^servicecheck:' `
                -or $l -match '^dir:' -or $l -match '^file:' `
                -or $l -match '^dirsize:' -or $l -match '^dirtime:' `
                -or $l -match '^log' -or $l -match '^clientversion:' `
                -or $l -match '^eventlogswanted' `
                -or $l -match '^servergifs:' `
                -or $l -match '^(?:ts|terminalservices)sessions:' `
                -or $l -match '^adreplicationcheck' `
                -or $l -match '^ifstat:' `
                -or $l -match '^ports:' `
                -or $l -match '^repeattest:' `
                -or $l -match '^proc(?:ess)?runtime:' `
                -or $l -match '^external:' `
                -or $l -match '^xymonlogsend' `
                -or $l -match '^xymonlogarchive' `
                -or $l -match '^slimmode' `
                -or $l -match '^noservicecheck:' `
                -or $l -match '^enablediskpart' `
                -or $l -match '^maxloop' `
                -or $l -match '^slowscanrate' `
                -or $l -match '^config' `
                )
            {
                WriteLog "Found a command: $l"
                $currentsection = $l
                # merging for eventlog include/ignore
                if (-not ($script:clientlocalcfg_entries.ContainsKey($currentsection)))
                {
                    $script:clientlocalcfg_entries[$currentsection] = @()
                }
            }
            elseif ($l -ne '')
            {
                $script:clientlocalcfg_entries[$currentsection] += $l
            }
        }

        # re-parse slimmode config to make it easier
        if ($script:clientlocalcfg_entries.ContainsKey('slimmode'))
        {
            $slimConfig = @{}
            $script:clientlocalcfg_entries.slimmode | `
               foreach { $i = ($_ -split ':'); $slimConfig[$i[0]] = $i[1] }

            $script:clientlocalcfg_entries.slimmode = $slimConfig

            ('sections', 'services', 'processes') | foreach `
            {
                if ($script:clientlocalcfg_entries.slimmode.ContainsKey($_))
                {
                    $script:clientlocalcfg_entries.slimmode.$_ = `
                        ($script:clientlocalcfg_entries.slimmode.$_ -split ',')
                }
            }
        }
        # parse maxloop if it's there (add if not)
        $maxloop = @($script:clientlocalcfg_entries.keys | `
            where { $_ -match '^maxloop:([0-9]+)$' })
        if ($maxloop.length -gt 1)
        {
            WriteLog 'ERROR: more than one maxloop directive in config!'
        }
        elseif ($maxloop.Length -eq 1)
        {
            $script:maxloop = [int]$matches[1]
        }
        else
        {
            $script:maxloop = 0
        }
        # parse slowscanrate if it's there (add if not)
        $slowscanrate = @($script:clientlocalcfg_entries.keys | `
            where { $_ -match '^slowscanrate:([0-9]+)$' })
        if ($slowscanrate.length -gt 1)
        {
            WriteLog 'ERROR: more than one slowscanrate directive in config!'
        }
        elseif ($slowscanrate.Length -eq 1)
        {
            $script:slowscanrate = [int]$matches[1]
        }
        else
        {
            $script:slowscanrate = 72
        }
    }
    WriteLog "Cached config now contains: "
    WriteLog ($script:clientlocalcfg_entries.keys -join ', ')

    # special handling for servergifs
    $gifpath = @($script:clientlocalcfg_entries.keys | where { $_ -match '^servergifs:(.+)$' })
    if ($gifpath.length -eq 1)
    {
        $script:XymonSettings.servergiflocation = $matches[1]
    }
}

function XymonReportConfig
{
    # exclude serverHttpPassword from output
    $settings = (($script:XymonSettings | Out-String) -split [System.Environment]::NewLine) | `
        where { $_ -notmatch '^serverHttpPassword' }

    "[XymonConfig]"
    "XymonSettings"
    $settings
    ""
    "HaveCmd"
    $HaveCmd
    foreach($v in @("XymonClientVersion", "clientname" )) {
        ""; "$v"
        (Get-Variable $v).Value
    }
    "[XymonPSClientInfo]"
    "Collection number: $($script:collectionnumber)"
    "Last transmission method: $($script:LastTransmissionMethod)"
    $script:thisXymonProcess    

    #get-process -id $PID
    #"[XymonPSClientThreadStats]"
    #(get-process -id $PID).Threads
}

function XymonClientSections([boolean] $isSlowScan)
{
    XymonManageConfigs
    # maybe move XymonManageExternals to slow scan tasks
    XymonManageExternals
    XymonExecuteExternals $isSlowScan $loopcount

    XymonClientVersion
    XymonUname
    XymonCpu
    XymonDisk
    XymonMemory
    XymonMsgs
    XymonProcs

    $includeSections = @('Netstat', 'Ports', 'IPConfig', 'Route', 'Ifstat', 'Who', 'Users')
    if ($script:clientlocalcfg_entries.ContainsKey('slimmode'))
    {
        $includeSections = @()
        if ($script:clientlocalcfg_entries.slimmode.ContainsKey('sections'))
        {
            WriteLog "Slimmode: including sections $($script:clientlocalcfg_entries.slimmode.sections)"
            $includeSections += $script:clientlocalcfg_entries.slimmode.sections
        }
    }

    if ($includeSections -contains 'Netstat') { XymonNetstat }
    if ($includeSections -contains 'Ports') { XymonPorts }
    if ($includeSections -contains 'IPConfig') { XymonIPConfig }
    if ($includeSections -contains 'Route') { XymonRoute }
    if ($includeSections -contains 'Ifstat') { XymonIfstat }

    XymonSvcs
    XymonDir
    XymonFileCheck
    XymonLogCheck
    XymonUptime
    if ($includeSections -contains 'Who') { XymonWho }
    if ($includeSections -contains 'Users') { XymonUsers }

    if ($script:XymonSettings.EnableWMISections -eq 1)
    {
        XymonWMIOperatingSystem
        XymonWMIComputerSystem
        XymonWMIBIOS
        XymonWMIProcessor
        XymonWMIMemory
        XymonWMILogicalDisk
    }

    XymonServiceCheck
    XymonDirSize
    XymonDirTime
    XymonTerminalServicesSessionsCheck
    XymonActiveDirectoryReplicationCheck
    XymonProcessRuntimeCheck
    XymonProcessExternalData
    XymonProcessLocalData

    $XymonIISSitesCache
    $XymonWMIQuickFixEngineeringCache
    $XymonWMIProductCache

    XymonReportConfig
}

function XymonClientInstall([string]$scriptname)
{
    # client install re-written to use NSSM
    # also to remove any existing service first
    
    XymonClientUnInstall

    & "$xymondir\nssm.exe" install `"$xymonsvcname`" `"$PSHOME\powershell.exe`" -ExecutionPolicy RemoteSigned -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File `"`"`"$scriptname`"`"`"
    # "
}

function XymonClientUnInstall()
{
    if ((Get-Service -ea:SilentlyContinue $xymonsvcname) -ne $null)
    {
        Stop-Service $xymonsvcname
        $service = Get-WmiObject -Class Win32_Service -Filter "Name='$xymonsvcname'"
        $service.delete() | out-null

        Remove-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\$xymonsvcname\* -Recurse -ErrorAction SilentlyContinue
    }
}

function ExecuteSelfUpdate([string]$newversion)
{
    $oldversion = $MyInvocation.ScriptName

    WriteLog "Upgrading $oldversion to $newversion"

    # test newversion
    # copy oldversion as backup
    # copy newversion to correct name
    # remove newversion file
    # re-start service - by exiting, NSSM will notice the process has ended and will automatically restart it

    $Process = powershell.exe -File $newversion ping | Out-String

    if ( $Process -like "*xymond *" ) {
        WriteLog "New version is working"

        # Make backup of old script
        copy-item "$oldversion" "$oldversion$version" -force

        copy-item "$newversion" "$oldversion" -force
        remove-item "$newversion"

        WriteLog "Sending final log and restarting service..."
        XymonLogSend
        exit

    } else {
        WriteLog "ERROR! New version is not working"
        WriteLog $Process
    }
}

# XymonDownloadFromFile used when a file path is used instead of a URL
function XymonDownloadFromFile([string]$downloadPath, [string]$destinationFilePath)
{
    WriteLog "XymonDownloadFromFile - Downloading $downloadPath to $destinationFilePath"
    if (!(Test-Path $downloadPath))
    {
        WriteLog "File $downloadPath cannot be found - aborting"
        return $false
    }

    WriteLog "Copying $downloadPath to $destinationPath"
    try
    {
        Copy-Item  $downloadPath $destinationFilePath -Force
    }
    catch 
    {
        WriteLog "Error copying file: $_"
        return $false
    }
    return $true
}

function XymonDownloadFromURL([string]$downloadURL, [string]$destinationFilePath)
{
    $downloadURL = $downloadURL.Trim()
    WriteLog "XymonDownloadFromURL - Downloading $downloadURL to $destinationFilePath"
    $client = New-Object System.Net.WebClient
    try
    {
        # for self-signed certificates, turn off cert validation
        # TODO: make this a config option
        # TODO: at some point, deprecate tls1.1 & 1.0
        [Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
        if ($downloadURL -match '^https://')
        {
            try
            {
                [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
            }
            catch
            {
                WriteLog "Error setting TLS options (old version of .NET?): $_"
                return $false
            }
        }
        $client.DownloadFile($downloadURL, $destinationFilePath)
    }
    catch
    {
        WriteLog "Error downloading: $_"
        return $false
    }
    return $true
}

function XymonDownloadFromServer([string]$ServerPath, [string]$destinationFilePath)
{
    $ServerPath = $ServerPath.Trim()
    WriteLog "XymonDownloadFromServer - Downloading $ServerPath to $destinationFilePath"
    $message = "download $ServerPath"
    try
    {
        # should work transparently through any intermediate proxies
        XymonSend $message $script:XymonSettings.serversList $destinationFilePath
    }
    catch
    {
        WriteLog "Error downloading: $_"
        return $false
    }
    return $true
}

function GetHashValueForFile([string] $filename, [string] $hashAlgorithm)
{
    $hash = [System.Security.Cryptography.HashAlgorithm]::Create($hashAlgorithm)
    $stream = ([System.IO.StreamReader]$filename).BaseStream
    $fileHash = -join ($hash.ComputeHash($stream) | foreach { '{0:x2}' -f $_ } )
    $stream.Close()
    return $fileHash
}

function XymonCheckUpdate
{
    WriteLog "Executing XymonCheckUpdate"
    $updates = @($script:clientlocalcfg_entries.keys | `
        where { $_ -match '^clientversion:(\d+\.\d+):(.+?)(?::(MD5|SHA1|SHA256):([0-9a-f]+))?$' })
    if ($updates.length -gt 1)
    {
        WriteLog "ERROR: more than one clientversion directive in config!"
    }
    elseif ($updates.length -eq 1)
    {
        # $matches[1] = the new version number
        # $matches[2] = the place to look for new version file
        # $matches[3] = (optional) hash type
        # $matches[4] = (optional) hash value

        if ($Version -lt $matches[1])
        {
            WriteLog "Running version $Version; config version $($matches[1]); attempting upgrade"

            # $matches[2] can be either a http[s] URL, bb fake URL or a file path
            $updatePath = $matches[2]
            $updateFile = "xymonclient_$($matches[1]).ps1"
            $hashAlgorithm = $matches[3]
            $hashRequired = $matches[4]
            $destination = Join-Path -Path $xymondir -ChildPath $updateFile

            $result = $false
            if ($updatePath -match '^http')
            {
                $updateURL = $updatePath.Trim()
                if ($updateURL -notmatch '/$')
                {
                    $updateURL += '/'
                }
                $URL = "{0}{1}" -f $updateURL, $updateFile
                $destination = Join-Path -Path $xymondir -ChildPath $updateFile
                $result = XymonDownloadFromURL $URL $destination
            }
            elseif ($updatePath -match '^bb' -or $updatePath -match '^xymon')
            {
                $ServerPath = $updatePath.Trim()
                $ServerPath = $ServerPath -creplace '^[^:]*:/*',''
                if ($ServerPath -notmatch '/$')
                {
                    $ServerPath += '/'
                }
                $URL = "{0}{1}" -f $ServerPath, $updateFile
                $destination = Join-Path -Path $xymondir -ChildPath $updateFile
                $result = XymonDownloadFromServer $URL $destination
            }
            else
            {
                # not http, not bb - maybe a file path?
                $updateSource = Join-Path $updatePath $updateFile
                $result = XymonDownloadFromFile $updateSource $destination
            }

            if ($result)
            {
                $newversion = Join-Path $xymondir $updateFile
                if ($hashAlgorithm -ne $null -and $hashAlgorithm -ne "")
                {
                    WriteLog "$($hashAlgorithm) hash specified, testing update file"
                    $fileHash = ''
                    try
                    {
                        $fileHash = GetHashValueForFile -filename $newversion -hashAlgorithm $hashAlgorithm
                    }
                    catch
                    {
                        WriteLog "Update directive specifies hash, but error calculating hash: $_"
                        WriteLog "Update cancelled"
                        Remove-Item $newversion
                        return
                    }

                    if ($fileHash -ne $hashRequired)
                    {
                        WriteLog "Update: update file hash mismatch (calculated $fileHash should be $hashRequired)"
                        WriteLog "Update cancelled"
                        Remove-Item $newversion
                        return
                    }
                    else
                    {
                        WriteLog "Update file hash matches expected value, update can proceed"
                    }
                }

                WriteLog "Launching update"
                ExecuteSelfUpdate $newversion
            }
        }
        else
        {
            WriteLog "Update: Running version $Version; config version $($matches[1]); doing nothing"
        }
    }
    else
    {
        # no clientversion directive
        WriteLog "Update: No clientversion directive in config, nothing to do"
    }
}

function DownloadAndVerify([string] $URI, [string] $name, [string] $path, `
    [string] $hashAlgorithm, [string] $hashRequired)
{
    if (!(Test-Path $path))
    {
        New-Item -ItemType directory -Path $path
    }

    $tempName = "$($name)_new"
    $destination = Join-Path -Path $path -ChildPath $tempName

    $result = $false
    if ($URI -match '^http')
    {
        $result = XymonDownloadFromURL $URI $destination
    }
    elseif ($URI -match '^bb' -or $URI -match '^xymon')
    {
        $URI = $URI -creplace '^[^:]*:/*',''
        $result = XymonDownloadFromServer $URI $destination
    }
    else
    {
        # not http, not bb - maybe a file path?
        $result = XymonDownloadFromFile $URI $destination
    }

    if ($result -and $hashAlgorithm -and $hashAlgorithm -ne $null)
    {
        WriteLog "$($hashAlgorithm) hash specified, testing destination file"
        $fileHash = ''
        try
        {
            $fileHash = GetHashValueForFile -filename $destination -hashAlgorithm $hashAlgorithm
        }
        catch
        {
            WriteLog "Error calculating hash: $_"
            $result = $false
        }

        if ($result)
        {
            if ($fileHash -ne $hashRequired)
            {
                $result = $false
                WriteLog "File hash mismatch (calculated $fileHash should be $hashRequired)"
            }
            else
            {
                WriteLog "Downloaded file hash matches expected value, can proceed"
            }
        }
        if (!$result)
        {
            WriteLog "Removing failed download $destination"
            Remove-Item $destination
        }
    }
    if ($result)
    {
        $originalFile = Join-Path -Path $path -ChildPath $name
        if (Test-Path $originalFile)
        {
            WriteLog "Deleting original file $originalFile"
            Remove-Item -Force $originalFile
        }
        WriteLog "Renaming $destination to $originalFile"
        Move-Item -Force $destination $originalFile
    }
    return $result
}

function XymonManageConfigs
{
    WriteLog "Executing XymonManageConfigs"
    $Configs = @($script:clientlocalcfg_entries.keys | `
        where { $_ -match '^config:' })

    foreach ($config in $Configs)
    {

        if ($config -match '^config:(.+?)(?:\|(MD5|SHA1|SHA256)\|([0-9a-f]+))?$')
        {
            # $matches[1] = URL location
            # $matches[2] = optional hash type
            # $matches[3] = optional hash value

            ($ConfigURI, $ConfighashAlgorithm, $ConfighashRequired) = $matches[1..3]

            $ConfigName = $ConfigURI.SubString($ConfigURI.LastIndexOf('/') + 1)

            if ( $ConfigName -eq '$ClientName.ini' ) {
               $ConfigName = $script:clientname + ".ini"
               $ConfigBaseURI = $ConfigURI.SubString(0,$ConfigURI.LastIndexOf('/') + 1)
               $ConfigURI = $ConfigBaseURI + $ConfigName
               WriteLog "Changing config file name to $ConfigName"
            }

            $FullName = Join-Path $script:XymonSettings.configlocation $ConfigName

            $downloadFlag = $false

            WriteLog "Checking $FullName"

            # check to see if we have the matching version
            if (Test-Path $FullName)
            {
                if ($ConfighashAlgorithm -ne $null -and $ConfighashRequired -ne $null)
                {
                    WriteLog "Config file found, $ConfigName - testing against hash"
                    try
                    {
                        $fileHash = GetHashValueForFile -filename $FullName -hashAlgorithm $ConfighashAlgorithm
                    }
                    catch
                    {
                        WriteLog "Error calculating hash for file: $_"
                    }
                    if ($fileHash -ne $ConfighashRequired)
                    {
                        WriteLog "Existing script hash mismatch (calculated $fileHash should be $ConfighashRequired)"
                        # hash mismatch, need to update via download 
                        $downloadFlag = $true
                    }
                } else {
                    WriteLog "Configuration file $ConfigName found, but no hash to check against so downloading again"
                    $downloadFlag = $true
                }
            }
            else
            {
                WriteLog "Configuration file $FullName not found"
                $downloadFlag = $true
            }

            if ($downloadFlag)
            {
                WriteLog "Configuration file script $ConfigName not found or requires update, downloading"
 
                try
                {
                    $result = DownloadAndVerify -URI $ConfigURI -name $ConfigName `
                        -path $script:XymonSettings.configlocation `
                        -hashAlgorithm $ConfighashAlgorithm -hashRequired $ConfighashRequired
                    
                }
                catch
                {
                    WriteLog "Error downloading $ConfigName, ignoring"
                    WriteLog "Error was: $_"
                }
            }
        }
        else
        {
            WriteLog "Configuration directive does not match expected format: $config"
        }
    } # foreach ... configs
    WriteLog 'XymonManageConfigs finished'
}

function XymonManageExternals
{
    WriteLog "Executing XymonManageExternals"
    $externalConfig = @($script:clientlocalcfg_entries.keys | `
        where { $_ -match '^external:' })
    $script:externals = @()

    foreach ($external in $externalConfig)
    {
        if ($external -match '^external:(?:(\d+):)?(slowscan|everyscan|scan\|\d+):(sync|async):(.+?)(?:\|(MD5|SHA1|SHA256)\|([0-9a-f]+))?(?:\|(.+)\|(.+))?$')
        {
            # $matches[1] = priority (optional) 0-99
            # $matches[2] = slowscan/everyscan
            # $matches[3] = sync/async
            # $matches[4] = URL / file location
            # $matches[5] = optional hash type
            # $matches[6] = optional hash value
            # $matches[7] = optional process
            # $matches[8] = optional arguments

            ($priority, $executionFrequency, $executionMethod, $externalURI, `
             $hashAlgorithm, $hashRequired, $process, $arguments) = $matches[1..8]

            if ($externalURI -match '^(http|bb|xymon)')
            {
                $externalScriptName = $externalURI.SubString($externalURI.LastIndexOf('/') + 1)
            }
            else
            {
                $externalScriptName = Split-Path -Leaf $externalURI
            }
            $externalFullName = Join-Path $script:XymonSettings.externalscriptlocation $externalScriptName
            if ($arguments -ne $null)
            {
                $arguments = $arguments -replace '{script}', $externalFullName
                $arguments = $arguments -replace '{scriptdir}', $script:XymonSettings.externalscriptlocation
            }
            if ($priority -eq $null)
            {
                $priority = 99
            }
            if ($process -eq $null)
            {
                $process = $externalFullName
            }
            $externalInfo = @{ Fullname = $externalFullName; `
                ExecutionFrequency = $executionFrequency; `
                ExecutionMethod = $executionMethod; 
                ProcessName = $process; 
                Arguments = $arguments;
                Priority = $priority }
            $externalObj = New-Object -Type PSObject -Property $externalInfo
            $downloadFlag = $false

            WriteLog "Checking $externalFullName"

            # check to see if we have the matching version
            if (Test-Path $externalFullName)
            {
                WriteLog "External script $externalScriptName found"
                if ($hashAlgorithm -ne $null -and $hashRequired -ne $null)
                {
                    WriteLog "External script $externalScriptName - testing against hash"
                    try
                    {
                        $fileHash = GetHashValueForFile -filename $externalFullName -hashAlgorithm $hashAlgorithm
                    }
                    catch
                    {
                        WriteLog "Error calculating hash for external: $_"
                    }
                    if ($fileHash -ne $hashRequired)
                    {
                        WriteLog "Existing script hash mismatch (calculated $fileHash should be $hashRequired)"
                        # hash mismatch, need to update via download 
                        $downloadFlag = $true
                    }
                }
                if (!$downloadFlag)
                {
                    WriteLog "Success, adding/updating external $externalScriptName in execution plan"
                    $script:externals += $externalObj
                }
            }
            else
            {
                WriteLog "External $externalFullName not found"
                # external does not exist, need to download
                $downloadFlag = $true
            }

            if ($downloadFlag)
            {
                WriteLog "External script $externalScriptName not found or requires update, downloading from $externalURI"
                try
                {
                    $result = DownloadAndVerify -URI $externalURI -name $externalScriptName `
                        -path $script:XymonSettings.externalscriptlocation `
                        -hashAlgorithm $hashAlgorithm -hashRequired $hashRequired
                    
                    if ($result)
                    {
                        WriteLog "Success, adding/updating external $externalScriptName in execution plan"
                        $script:externals += $externalObj
                    }
                }
                catch
                {
                    WriteLog "Error downloading $externalScriptName, ignoring (will not be executed)"
                    WriteLog "Error was: $_"
                }
            }
        }
        else
        {
            WriteLog "external directive does not match expected format: $external"
        }
    } # foreach ... externals
    WriteLog 'XymonManageExternals finished'
}

function XymonExecuteExternals ([boolean] $isSlowscan, [int] $loopcount)
{
    WriteLog 'Executing XymonExecuteExternals'
    $env:clientname = $script:clientname

    if (!(Test-Path $script:XymonSettings.externaldatalocation))
    {
        New-Item -ItemType directory -Path $script:XymonSettings.externaldatalocation
    }

    $script:externals | Sort-Object Priority, ExecutionMethod | foreach {
        WriteLog "External: $($_.ExecutionFrequency) - $($_.FullName)"

        [bool] $execute = $true

        if (!$isSlowscan -and $_.ExecutionFrequency -eq 'slowscan')
        {
            WriteLog 'Skipping execution, this is not a slow scan'
            $execute = $false
        }

        if ($_.ExecutionFrequency -match '^scan\|(\d+)' ) {
            $rest = $loopcount % $Matches[1]
            if ( $loopcount % $Matches[1] -eq 0 )
            {
               WriteLog "Execution custom scan: $loopcount % $($Matches[1]) = $rest"
            } else {
               WriteLog "Skipping execution custom scan: $loopcount % $($Matches[1]) = $rest"
               $execute = $false
            }
        }

        if ( $execute -eq $true) {
            try
            {
                $process = $_.ProcessName
                $arguments = $_.Arguments
                if ($arguments -ne $null)
                {
                    WriteLog "Executing $process with arguments $arguments"
                    $extpid = Start-Process -PassThru `
                        -WindowStyle Hidden `
                        -WorkingDirectory $script:XymonSettings.externalscriptlocation `
                        $process $arguments
                }
                else
                {
                    WriteLog "Executing $process with no arguments"
                    $extpid = Start-Process -PassThru `
                        -WindowStyle Hidden `
                        -WorkingDirectory $script:XymonSettings.externalscriptlocation `
                        $process
                }
                WriteLog "Process $($extpid.Id) started"

                if ($_.ExecutionMethod -eq 'sync')
                {
                    WriteLog "Synchronous external: waiting for process $($extpid.Id) to complete"
                    $extpid | Wait-Process
                    WriteLog "Process $($extpid.Id) completed"
                }
                else
                {
                    WriteLog "Asynchronous: not waiting for process $($extpid.Id)"
                }
            }
            catch
            {
                WriteLog "Error executing: $_"
            }
        }
    }

    WriteLog 'XymonExecuteExternals finished'
}

function WriteLog([string]$message)
{
    $datestamp = get-date -format 'yyyy-MM-dd HH:mm:ss.fff'
    add-content -Path $script:XymonSettings.clientlogfile -Value "$datestamp  $message"
    Write-Host "$datestamp  $message"
}

function RotateLog([string]$logfile)
{
    $retain = $script:XymonSettings.clientlogretain
    if ($retain -gt 99)
    {
        $retain = 99
    }
    if ($retain -gt 0)
    {
        WriteLog "Rotating logfile $logfile"
        if (Test-Path $logfile)
        {
            $lastext = "{0:00}" -f $retain
            if (Test-Path "$logfile.$lastext")
            {
                WriteLog "Removing $logfile.$lastext"
                Remove-Item -Force "$logfile.$lastext"
            }

            (($retain - 1) .. 1) | foreach {
                # pad 1 -> 01 etc
                $ext = "{0:00}" -f $_
                if (Test-Path "$logfile.$ext")
                {
                    # pad 1 -> 01, 2 -> 02 etc
                    $newext = "{0:00}" -f ($_ + 1)
                    WriteLog "Renaming $logfile.$ext to $logfile.$newext"
                    Move-Item -Force "$logfile.$ext" "$logfile.$newext"
                }
            }

            if (Test-Path $logfile)
            {
                WriteLog "Finally: Renaming $logfile to $logfile.01"
                Move-Item -Force $logfile "$logfile.01"
            }
        }
    }
}

function RepeatTests([string] $content)
{
    if (@($script:clientlocalcfg_entries.Keys -like 'repeattest*').Length -eq 0)
    {
        WriteLog "RepeatTests: nothing to do!"
        return
    }

    WriteLog 'Executing RepeatTests'

    $lines = $content -split [environment]::newline
    $capturelines = $false
    $capturedSection = ''

    foreach ($line in $lines)
    {
        if ($line -match '^\[([^\]]+)\]$')
        {
            $currentSection = $matches[1]
            # found a new section - if we were previously capturing lines from the 
            # previous section, write out any repeat sections and reset
            if ($capturelines)
            {
                $capturelines = $false
                # we were capturing lines - check for alerts and send to Xymon
                $regex = "^repeattest:$($capturedSection):(.+)"
                $script:clientlocalcfg_entries.keys | where { $_ -match $regex } | foreach {
                    $newsection = $matches[1]
                    $outputHeader = @()
                    $outputHeader += (get-date -format G) + "<br><h2>$newsection</h2>"                
                    $groupcolour = 'green'
                    # check for triggers
                    if ($script:clientlocalcfg_entries[$_] -ne $null)
                    {
                        foreach ($trigger in $script:clientlocalcfg_entries[$_])
                        {
                            $alertcolour = 'green'
                            $alertLines = @()
                            if ($trigger -match '^trigger:([a-z]+):(.+)$')
                            {
                                $triggerAlertcolour = $Matches[1]
                                $triggerRegex = $Matches[2]
                                foreach ($line in $capturedlines)
                                {
                                    if ($line -match $triggerRegex)
                                    {
                                        $alertcolour = $triggerAlertcolour
                                        $alertLines += "matches `"$line`""
                                    }
                                }
                                if ($alertLines.Length -eq 0)
                                {
                                    $alertLine = 'no match'
                                }
                                else
                                {
                                    $alertLine = $alertLines -join '<br>'
                                }
                                $outputHeader += ('<img src="{3}{0}.gif" alt="{0}" height="16" width="16" border="0"> {1} {2}<br>' `
                                    -f $alertcolour, $trigger, $alertLine, $script:XymonSettings.servergiflocation)
                                if ($groupcolour -eq 'green' -and $alertcolour -eq 'yellow')
                                {
                                    $groupcolour = 'yellow'
                                }
                                elseif ($alertcolour -eq 'red')
                                {
                                    $groupcolour = 'red'
                                }
                            }
                        }
                    }

                    $outputHeader += '<br>'
                    $output = ($outputHeader -join "`n")
                    $output += ($capturedlines -join '<br>')
                    # repeat the test by sending to Xymon
                    WriteLog "Sending repeated test: $newsection"
                    $outputXymon = ('status {0}.{1} {2} {3}' -f $script:clientname, $newsection, $groupcolour, $output)
                    XymonSend $outputXymon $script:XymonSettings.serversList
                }
            }
            $capturedlines = @()
            $capturedSection = $currentSection -replace '\\', '\\'
            $regex = "^repeattest:$($capturedSection):(.+)"
            # check to see if the new section is one we want to repeat
            $script:clientlocalcfg_entries.keys | where { $_ -match $regex } | foreach {
                $capturelines = $true
            }
        }
        elseif ($capturelines)
        {
            $capturedlines += $line
        }
    }
    WriteLog 'RepeatTests finished'
}

function XymonLogSend()
{
    if (@($script:clientlocalcfg_entries.Keys -like 'xymonlogarchive*').Length -gt 1)
    {
        WriteLog "XymonLogArchive: disabling, more than one xymonlogarchive directive in config"
    }
    elseif (@($script:clientlocalcfg_entries.Keys -like 'xymonlogarchive*').Length -eq 0)
    {
        WriteLog 'XymonLogArchive: disabling, no entry found in config file'
    }
    else
    {
        # Keeping older logs in directory $OldSubDirectory for $RententionInDays days
        # Default values:
   
        $script:clientlocalcfg_entries.Keys | where { $_ -match '^xymonlogarchive:(.*):(.*)$' } | foreach {
            $OldSubDirectory  = $Matches[1]
            $RententionInDays = $Matches[2]
        }

        if ( $OldSubDirectory -ne $null -and $RententionInDays -ne $null ) {
            WriteLog "XymonLogArchive: rotate logs: $RententionInDays days @ directory $OldSubDirectory"

            # Format of the old logfile
            $DateTimeFormat   = "yyyy-MM-dd_HHmmss"

            $S = Get-Item -LiteralPath $script:XymonSettings.clientlogfile

            # Make sure the directory for the old log files exists
            $DestinationPath = Join-Path -Path $S.DirectoryName -ChildPath $OldSubDirectory
            If (! (Test-Path -LiteralPath $DestinationPath) ) {
               $Null = New-Item -Path $DestinationPath -Type Directory -Force
            }

            # Copy logfile
            $Destination = Join-Path -Path $DestinationPath -ChildPath ('{0}_{1}{2}' -F $S.BaseName, ((Get-Date).ToString($DateTimeFormat)), $S.Extension)
            Copy-Item -Path $script:XymonSettings.clientlogfile -Destination $Destination -Force

            # Cleanup old files
            Get-ChildItem -LiteralPath $DestinationPath -File -Filter ($Format -F $S.BaseName, '*',$S.Extension) | ? LastWriteTime -le ((Get-Date).AddDays(-$RententionInDays)) | Remove-Item -ErrorAction SilentlyContinue



            $S = Get-Item -LiteralPath $script:lastcollectfile

            # Make sure the directory for the old log files exists
            $DestinationPath = Join-Path -Path $S.DirectoryName -ChildPath $OldSubDirectory
            If (! (Test-Path -LiteralPath $DestinationPath) ) {
               $Null = New-Item -Path $DestinationPath -Type Directory -Force
            }

            # Copy logfile
            $Destination = Join-Path -Path $DestinationPath -ChildPath ('{0}_{1}{2}' -F $S.BaseName, ((Get-Date).ToString($DateTimeFormat)), $S.Extension)
            Copy-Item -Path $script:lastcollectfile -Destination $Destination -Force

            # Cleanup old files
            Get-ChildItem -LiteralPath $DestinationPath -File -Filter ($Format -F $S.BaseName, '*',$S.Extension) | ? LastWriteTime -le ((Get-Date).AddDays(-$RententionInDays)) | Remove-Item -ErrorAction SilentlyContinue

        } else {
            WriteLog "XymonLogArchive: rotate logs: error in format of setting!"
        }
    }


    # special handling for xymonlog
    $markslowscan = 'green'
    if (@($script:clientlocalcfg_entries.Keys -like 'xymonlogsend*').Length -gt 1)
    {
        WriteLog "XymonLogSend: more than one xymonlogsend directive in config!"
        $markslowscan = 'yellow'
    }
    elseif (@($script:clientlocalcfg_entries.Keys -like 'xymonlogsend*').Length -eq 0)
    {
        WriteLog 'XymonLogSend: nothing to do!'
        return
    }
    else
    {
        $XymonLogSendConfig = @($script:clientlocalcfg_entries.Keys | where { $_ -match '^xymonlogsend:(.*)$' })

        # parameter should be 'xymonlogsend:<slow colour>:<restart colour>'
        # <restart colour> not mandatory
        $checkparams = $XymonLogSendConfig -split ':'
        # should maybe check these are valid xymon colours red, yellow, clear

        if ($($script:collectionnumber) -eq 1 )
        {
            if ($checkparams.length -ge 3)
            {
                $markslowscan = $checkparams[2]
            }
        }
        elseif ($($script:loopcount) -eq 0)
        {
            if ($checkparams.length -ge 2)
            {
                $markslowscan = $checkparams[1]
            }
        }
    }

    WriteLog 'XymonLogSend - sending log'

    $log = ((get-content $script:XymonSettings.clientlogfile) -join "`n")
    $log = [System.Web.HttpUtility]::HtmlEncode($log)

    $output = (get-date -format G) + '<br><h2>Xymon client log</h2><pre>' 
    $output += $log
    $output += '</pre>'

    $outputXymon = ('status {0}.{1} {2} {3}' -f $script:clientname, 'xymonlog', $markslowscan, $output)
    XymonSend $outputXymon $script:XymonSettings.serversList

    WriteLog 'XymonLogSend - finished'
}

##### Main code #####
$script:thisXymonProcess = get-process -id $PID
$script:thisXymonProcess.PriorityClass = "High"
$hasargs = $false
if ($args -ne $null)
{
    $hasargs = $true
}
XymonConfig $hasargs
$ret = 0
# check for install/set/unset/config/start/stop for service management
if($args -eq "Install") {
    XymonClientInstall $MyInvocation.MyCommand.Definition
    $ret=1
}
if ($args -eq "uninstall")
{
    XymonClientUnInstall
    $ret=1
}
if($args[0] -eq "config") {
    "XymonPSClient config:`n"
    $XymonCfgLocation
    "Settable Params and values:"
    foreach($param in $script:XymonSettings | gm -memberType NoteProperty,Property) {
        if($param.Name -notlike "PS*") {
            $val = $script:XymonSettings.($param.Name)
            if($val -is [Array]) {
                $out = [string]::join(" ",$val)
            } else {
                $out = $val.ToString()
            }
            "    {0}={1}" -f $param.Name,$out
        }
    }
    return
}
if($args -eq "Start") {
    if((get-service $xymonsvcname).Status -ne "Running") { start-service $xymonsvcname }
    return
}
if($args -eq "Stop") {
    if((get-service $xymonsvcname).Status -eq "Running") { stop-service $xymonsvcname }
    return
}
if($args -eq "ping") {
    $output = XymonSend "ping" $script:XymonSettings.serversList
    $output
    return
}
if($ret) {return}
if($args -ne $null) {
    "Usage: "+ $MyInvocation.MyCommand.Definition +" install | uninstall | start | stop | config "
    return
}

# assume no other args, so run as normal

# elevate our priority to configured setting
$script:thisXymonProcess.PriorityClass = $script:XymonSettings.ClientProcessPriority

# ZB: read any cached client config
if (Test-Path -PathType Leaf $script:XymonSettings.clientconfigfile)
{
    $cfglines = (get-content $script:XymonSettings.clientconfigfile) -join "`n"
    XymonClientConfig $cfglines
}

$script:lastcollectfile = join-path $script:XymonSettings.clientlogpath 'xymon-lastcollect.txt'
$running = $true
$script:collectionnumber = (0 -as [long])
$loopcount = Get-Random -Maximum ($script:slowscanrate - 1)

AddHelperTypes

while ($running -eq $true) {
    # log file setup/maintenance
    RotateLog $script:lastcollectfile
    RotateLog $script:XymonSettings.clientlogfile
    Set-Content -Path $script:XymonSettings.clientlogfile `
        -Value "$clientname - $XymonClientVersion"

    $script:collectionnumber++
    $loopcount++ 
    $UTCstr = get-date -Date ((get-date).ToUniversalTime()) -uformat '%Y-%m-%d %H:%M:%S'
    WriteLog "UTC date/time: $UTCstr"
    WriteLog "This is collection number $($script:collectionnumber), loopcount $loopcount"
    WriteLog "Next 'slow scan' is when loopcount reaches $($script:slowscanrate)"
    if ($script:maxloop -gt 0)
    {
        WriteLog "XymonPSClient service will restart when loopcount greater than $($script:maxloop)"
    }
    else
    {
        WriteLog 'XymonPSClient is configured to never automatically restart'
    }

    $starttime = Get-Date
    $slowscan = $false
    
    if ($loopcount -eq $script:slowscanrate) { 
        $loopcount = 0
        $slowscan = $true
        
        WriteLog "Doing slow scan tasks: $loopcount -eq $($script:slowscanrate)"

        WriteLog "Executing XymonWMIQuickFixEngineering"
        $XymonWMIQuickFixEngineeringCache = XymonWMIQuickFixEngineering
        WriteLog "Executing XymonWMIProduct"
        $XymonWMIProductCache = XymonWMIProduct
        WriteLog "Executing XymonIISSites"
        $XymonIISSitesCache = XymonIISSites
        if ($script:XymonSettings.EnableDiskPart -eq 1 `
            -or $script:clientlocalcfg_entries.ContainsKey('enablediskpart'))
        {
            $script:diskpartData = XymonDiskPart
        }
        else
        {
            $script:diskpartData = ''
        }

        WriteLog "Slow scan tasks completed."
    }

    XymonCollectInfo $slowscan
    
    WriteLog "Performing main and optional tests and building output..."
    $clout = "client $($clientname).$($script:XymonSettings.clientsoftware) $($script:XymonSettings.clientclass) XymonPS" | 
        Out-String
    $clsecs = XymonClientSections $slowscan | Out-String
    $localdatetime = Get-Date
    $clout += XymonDate | Out-String
    $clout += XymonClock | Out-String
    $clout +=  $clsecs
    
    #XymonReportConfig >> $script:XymonSettings.clientlogfile
    WriteLog "Main and optional tests finished."
    
    WriteLog "Sending to server"
    Set-Content -path $script:lastcollectfile -value $clout
        
    $newconfig = XymonSend $clout $script:XymonSettings.serversList

    RepeatTests $clout

    XymonClientConfig $newconfig
    [GC]::Collect() # run every time to avoid memory bloat
    
    #maybe check for update - only happens after a slow scan, when loopcount = 0
    if ($slowscan)
    {
        XymonCheckUpdate
    }

    $delay = ($script:XymonSettings.loopinterval - (Get-Date).Subtract($starttime).TotalSeconds)
    if ($script:collectionnumber -eq 1)
    {
        # if this is the very first collection, make the second collection happen sooner
        # than the normal delay - this is because CPU usage is not collected on the 
        # first run
        $delay = 30
    }
    WriteLog "Status: maxloop: $($script:maxloop) collection number: $($script:collectionnumber)"
    if ($script:maxloop -gt 0 -and $script:collectionnumber -gt $script:maxloop)
    {
        # restart service by exiting, NSSM will restart it
        WriteLog "Maximum collections reached: collection $($script:collectionnumber), maxloop $($script:maxloop): restarting service..."
        XymonLogSend
        exit
    }
    WriteLog "Delaying until next run: $delay seconds"
    XymonLogSend
    if ($delay -gt 0) { sleep $delay }
}


More information about the Xymon mailing list