Powershell client maintainer
list Stef Coene
Hi, Who is the current maintainer of the Windows powershell client? It lives in the sandbox tree of the of the xymon project, but I think it deserves some more credits :) I patched the client quit a lot and want to share the patches. Who can I contact fot this? Stef
list Zak Beck
Hi Stef That would be me, in a very lightweight sense of the word maintainer ?. I haven't updated the client for ages, although I do have some minor patches compared to sourceforge running in my environments. Share me some diffs and I'll try and figure out how we merge them. Thanks Zak Beck Accenture -----Original Message----- From: Xymon <xymon-bounces at xymon.com> On Behalf Of Stef Coene Sent: Friday, December 1, 2023 10:37 AM To: Xymon at xymon.com Subject: [External] [Xymon] Powershell client maintainer CAUTION: External email. Be cautious with links and attachments.
▸
Hi,
Who is the current maintainer of the Windows powershell client?
It lives in the sandbox tree of the of the xymon project, but I think it deserves some more credits :)
I patched the client quit a lot and want to share the patches.
Who can I contact fot this?
Stef
https://urldefense.com/v3/__ This message is for the designated recipient only and may contain privileged, proprietary, or otherwise confidential information. If you have received it in error, please notify the sender immediately and delete the original. Any other use of the e-mail by you is prohibited. Where allowed by local law, electronic communications with Accenture and its affiliates, including e-mail and instant messaging (including content), may be scanned by our systems for the purposes of information security, AI-powered support capabilities, and assessment of internal compliance with Accenture policy. Your privacy is important to us. Accenture uses your personal data only in compliance with data protection laws. For further information on how Accenture processes your personal data, please see our privacy statement at https://www.accenture.com/us-en/privacy-policy. www.accenture.com
list Stef Coene
Hi, I will clean them up and send them to the xymon-dev mailinglist. Stef
▸
On 2023-12-01 14:16, Beck, Zak wrote:Hi Stef That would be me, in a very lightweight sense of the word maintainer ?. I haven't updated the client for ages, although I do have some minor patches compared to sourceforge running in my environments. Share me some diffs and I'll try and figure out how we merge them. Thanks Zak Beck Accenture -----Original Message----- From: Xymon <xymon-bounces at xymon.com> On Behalf Of Stef Coene Sent: Friday, December 1, 2023 10:37 AM To: Xymon at xymon.com Subject: [External] [Xymon] Powershell client maintainer CAUTION: External email. Be cautious with links and attachments. Hi, Who is the current maintainer of the Windows powershell client? It lives in the sandbox tree of the of the xymon project, but I think it deserves some more credits :) I patched the client quit a lot and want to share the patches. Who can I contact fot this? Stef https://urldefense.com/v3/__ This message is for the designated recipient only and may contain privileged, proprietary, or otherwise confidential information. If you have received it in error, please notify the sender immediately and delete the original. Any other use of the e-mail by you is prohibited. Where allowed by local law, electronic communications with Accenture and its affiliates, including e-mail and instant messaging (including content), may be scanned by our systems for the purposes of information security, AI-powered support capabilities, and assessment of internal compliance with Accenture policy. Your privacy is important to us. Accenture uses your personal data only in compliance with data protection laws. For further information on how Accenture processes your personal data, please see our privacy statement at https://www.accenture.com/us-en/privacy-policy. www.accenture.com
list Stef Coene
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 backup3452,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" -force3458,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 <user-ce4a2c883f75@xymon.invalid>
# Copyright (C) 2010 David Baldwin
# Copyright (c) 2014-2019 Accenture (user-aada0fa38bf8@xymon.invalid)
# 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 user-aada0fa38bf8@xymon.invalid"
# 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 }
}
list Stef Coene
Hi, I have a new version available with additional bug fixes and enhancements. See attached files. Additional changes: Return $false if XymonSendViaHttp has an error If DecryptHttpServerPassword returns an error, returns $null and abort XymonSendViaHttp Catch error in DownloadFile and write error to the log Also return $false Use output from XymonSend as return value in XymonDownloadFromServer This can be $false if there is an error in XymonSend Allow multiple clientversion download lines in configuration file Test if the file exist when downloading a file before overwriting the old file Stef
▸
On 2023-12-15 13:31, Stef Coene wrote: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,88
▸
# 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 #
# If DecryptHttpServerPassword returns an error, returns $null and abort XymonSendViaHttp
#
# Return $false if XymonSendViaHttp has an error
#
# Catch error in DownloadFile and write error to the log
# Als return $false
#
# Make slowscanrate a random number during startup
#
# Use output from XymonSend as return value in XymonDownloadFromServer
# This can be $false if there is an error in XymonSend
#
# Allow multiple clientversion download lines in configuration file
#
# Test if the file exist when downloading a file before overwriting the old file
43c101 < $Version = '2.42' ---
$Version = '2.437'
47c105,111
▸
< $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
}
1002d1065 < SetIfNot $script:XymonSettings slowscanrate 72 # repeats of main loop before collecting slowly changing information again 1020a1084
$configdir = Join-Path $xymondir 'etc'
1023a1088
SetIfNot $script:XymonSettings configlocation $configdir
2854c2919,2925
▸
< 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"2985c3056 < $serverPassword = '' ---
$serverPassword = $null
2991c3062 < function XymonSendViaHttp($msg) ---
function XymonSendViaHttp($msg, $filePath)
2995,3000c3066,3073
▸
< $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"
WriteLog 'XymonSendViaHttp finished'
return $false
}3002,3008c3075,3088
▸
< 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
if ( $serverHttpPassword -eq $null )
{
WriteLog 'XymonSendViaHttp finished'
return $false
}
else
{
$authString = ('{0}:{1}' -f $script:XymonSettings.serverHttpUsername, `
$serverHttpPassword)3010,3011c3090,3091
< $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))3012a3093,3095
WriteLog " Using username $($script:XymonSettings.serverHttpUsername)"
}
}3014,3015c3097,3110
▸
< 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?): $_"
WriteLog 'XymonSendViaHttp finished'
return $false
}
}3017,3018c3112,3129 < if ($url -match '^https://';)
▸
< {
--- # 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"3021c3132 < [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" ---
$response = $request.GetResponse()
3025c3136,3137 < WriteLog "Error setting TLS options (old version of .NET?): $_" ---
WriteLog " Exception connecting to $($url):`n$($_)"
WriteLog 'XymonSendViaHttp finished'3028,3052d3139
▸
< }
<
< # 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
< $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 ''
< }
3054,3059c3141,3147
▸
< $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)"
WriteLog 'XymonSendViaHttp finished'
return $false
}3061,3065c3149,3153
▸
< $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'3066a3155
}
3074c3163 < $outputbuffer = "" ---
$outputBuffer = ""
3078c3167,3184 < $outputBuffer = XymonSendViaHttp $msg ---
$outputBuffer = XymonSendViaHttp $msg $filePath
if ( $outputBuffer -ne $false)
▸
{
$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
}
}3214c3320 < $outputbuffer ---
$outputBuffer
3264a3371
-or $l -match '^xymonlogarchive' `
3268a3376,3377
-or $l -match '^slowscanrate' `
-or $l -match '^config' `3317a3427,3441
▸
# 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
}3357a3482
XymonManageConfigs
3360c3485 < XymonExecuteExternals $isSlowScan ---
XymonExecuteExternals $isSlowScan $loopcount
3449a3575,3576
# test newversion
# copy oldversion as backup3452,3453c3579,3581
▸
< # 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-String3455,3456c3583,3584
▸
< copy-item "$newversion" "$oldversion" -force
< remove-item "$newversion"
--- if ( $Process -like "*xymond *" ) {
WriteLog "New version is working"3458,3460c3586,3599
▸
< WriteLog "Sending final log and restarting service..."
< XymonLogSend
< exit
--- # 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
}3509c3648,3657 < $client.DownloadFile($downloadURL, $destinationFilePath) ---
try {
$client.DownloadFile($downloadURL, $destinationFilePath)
}
catch
{
$e = $_.Exception
WriteLog "Error during DownloadFile: $e.Message"
return $false
}3527c3675,3676 < XymonSend $message $script:XymonSettings.serversList $destinationFilePath ---
$output = XymonSend $message $script:XymonSettings.serversList $destinationFilePath
return $output3549,3560c3698
▸
< $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
--- $updates = @($script:clientlocalcfg_entries.keys | where { $_ -match '^clientversion:' })3562c3700,3702 < if ($Version -lt $matches[1]) ---
if ($updates.length -gt 0)
{
if ($updates.length -eq 1)3564c3704,3709 < WriteLog "Running version $Version; config version $($matches[1]); attempting upgrade" ---
WriteLog "Found 1 $($updates.length) clientversion lines:"
}
else
{
WriteLog "Found $($updates.length) clientversion liness:"
}3566,3571c3711,3716
▸
< # $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
--- $script:clientlocalcfg_entries.keys | where { $_ -match '^clientversion:(\d+\.\d+):(.+?)(?::(MD5|SHA1|SHA256):([0-9a-f]+))?$' } | foreach `
▸
{
# $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 value3573,3574c3718 < $result = $false < if ($updatePath -match '^http') ---
if ($Version -lt $matches[1])
3576,3593c3720,3726
▸
< $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')
< {
< $ServerPath = $updatePath.Trim()
< $ServerPath = $ServerPath -creplace '^[^:]*:/*',''
< if ($ServerPath -notmatch '/$')
< {
< $ServerPath += '/'
< }
< $URL = "{0}{1}" -f $ServerPath, $updateFile
---WriteLog "Running version $Version; config version $($matches[1]); attempting upgrade from $($matches[2])"
▸
# $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]3595,3602d3727
▸
< $result = XymonDownloadFromServer $URL $destination
< }
< else
< {
< # not http, not bb - maybe a file path?
< $updateSource = Join-Path $updatePath $updateFile
< $result = XymonDownloadFromFile $updateSource $destination
< }
3604,3607c3729,3730
▸
< if ($result)
< {
< $newversion = Join-Path $xymondir $updateFile
< if ($hashAlgorithm -ne $null)
--- $result = $false
if ($updatePath -match '^http')3609,3611c3732,3733 < WriteLog "$($hashAlgorithm) hash specified, testing update file" < $fileHash = '' < try ---
$updateURL = $updatePath.Trim()
if ($updateURL -notmatch '/$')3613c3735 < $fileHash = GetHashValueForFile -filename $newversion -hashAlgorithm $hashAlgorithm ---
$updateURL += '/'
3615c3737,3745 < catch
▸
--- $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 '/$')3617,3620c3747
▸
< WriteLog "Update directive specifies hash, but error calculating hash: $_"
< WriteLog "Update cancelled"
< Remove-Item $newversion
< return
---$ServerPath += '/'
3621a3749,3758
▸
$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
}3623c3760,3767 < if ($fileHash -ne $hashRequired) ---
if ($result)
{
$newversion = Join-Path $xymondir $updateFile
$fileSize = (Get-Item $newversion).Length
# Failed http download results in 1 byte file
if ( $fileSize -lt 2 )3625,3628c3769,3770
▸
< WriteLog "Update: update file hash mismatch (calculated $fileHash should be $hashRequired)"
< WriteLog "Update cancelled"
< Remove-Item $newversion
< return
--- WriteLog "Error: downloaded file is only $fileSize byte"
exit3632c3774,3804
▸
< WriteLog "Update file hash matches expected value, update can proceed"
--- 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 $newversion3635,3637d3806 < < WriteLog "Launching update" < ExecuteSelfUpdate $newversion 3639,3642c3808,3811
▸
< }
< else
< {
< WriteLog "Update: Running version $Version; config version $($matches[1]); doing nothing"
--- else
{
WriteLog "Update: Running version $Version; config version $($matches[1]) from $($matches[2]); doing nothing"
}3647d3815 < # no clientversion directive 3668c3836
▸
< elseif ($URI -match '^bb')
---elseif ($URI -match '^bb' -or $URI -match '^xymon')
3679c3847
▸
< if ($result -and $hashAlgorithm -ne $null)
---if ($result -and $hashAlgorithm -and $hashAlgorithm -ne $null)
3713,3714c3881 < $originalFile = Join-Path -Path $path -ChildPath $name < if (Test-Path $originalFile) ---
if (Test-Path $destination)
3716,3717c3883,3894
▸
< WriteLog "Deleting original file $originalFile"
< Remove-Item -Force $originalFile
--- $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
}
else
{
WriteLog "Error: new file $destination not found"3719,3720d3895 < WriteLog "Renaming $destination to $originalFile" < Move-Item -Force $destination $originalFile 3724a3900,3988
▸
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'
}
3734c3998 < 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]+))?(?:\|(.+)\|(.+))?$')
3748c4012 < if ($externalURI -match '^(http|bb)') ---
if ($externalURI -match '^(http|bb|xymon)')
3818c4082
▸
< WriteLog "External script $externalScriptName not found or requires update, downloading"
---WriteLog "External script $externalScriptName not found or requires update, downloading from $externalURI"
3846c4110 < function XymonExecuteExternals([boolean] $isSlowscan) ---
function XymonExecuteExternals ([boolean] $isSlowscan, [int] $loopcount)
3848a4113,4114
$env:clientname = $script:clientname
3852a4119
3854a4122,4124
[bool] $execute = $true
3857a4128
$execute = $false
3859,3860c4130,4142
▸
< 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) {3882c4164 < ---
3899a4182
4048a4332,4393
▸
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!"
}
}
4145a4491,4495
if($args -eq "ping") {
$output = XymonSend "ping" $script:XymonSettings.serversList
$output
return
}4164c4514 < $lastcollectfile = join-path $script:XymonSettings.clientlogpath 'xymon-lastcollect.txt' ---
$script:lastcollectfile = join-path $script:XymonSettings.clientlogpath 'xymon-lastcollect.txt'
4167c4517,4522 < $loopcount = ($script:XymonSettings.slowscanrate - 1) ---
if ( $script:slowscanrate -gt 0 ) {
$loopcount = Get-Random -Maximum ($script:slowscanrate)
} else {
$loopcount = 0
}4173c4528 < RotateLog $lastcollectfile ---
RotateLog $script:lastcollectfile
4182,4183c4537,4538
▸
< 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)"4195,4196c4550,4553
▸
<
< if ($loopcount -eq $script:XymonSettings.slowscanrate) {
---
if ($loopcount -eq $script:slowscanrate) {
WriteLog "Doing slow scan tasks: $loopcount -eq $($script:slowscanrate)"
4200,4201d4556 < WriteLog "Doing slow scan tasks" < 4236c4591
▸
< 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 <user-ce4a2c883f75@xymon.invalid> # Copyright (C) 2010 David Baldwin # Copyright (c) 2014-2019 Accenture (user-aada0fa38bf8@xymon.invalid) # 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 #
# Changes for 2.437:
# Return $false if XymonSendViaHttp has an error
# If DecryptHttpServerPassword returns an error, returns $null and abort XymonSendViaHttp
#
# Catch error in DownloadFile and write error to the log
# Also return $false
#
# Use output from XymonSend as return value in XymonDownloadFromServer
# This can be $false if there is an error in XymonSend
#
# Allow multiple clientversion download lines in configuration file
#
# Test if the file exist when downloading a file before overwriting the old file
▸
# -----------------------------------------------------------------------------------
# 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.437'
▸
$XymonClientVersion = "${Id}: xymonclient.ps1 $Version 2019-03-11 user-aada0fa38bf8@xymon.invalid" # 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 = $null
▸
}
}
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"
WriteLog 'XymonSendViaHttp finished'
return $false
}
WriteLog " Using url $url"
$encodedAuth = ''
if ($script:XymonSettings.serverHttpUsername -ne '')
{
$serverHttpPassword = DecryptHttpServerPassword
if ( $serverHttpPassword -eq $null )
{
WriteLog 'XymonSendViaHttp finished'
return $false
}
else
▸
{ $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?): $_"
WriteLog 'XymonSendViaHttp finished'
▸
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$($_)"
WriteLog 'XymonSendViaHttp finished'
return $false
▸
}
$statusCode = [int]($response.StatusCode)
if ($response.StatusCode -ne [System.Net.HttpStatusCode]::OK)
{
WriteLog " FAILED, HTTP response code: $($response.StatusCode) ($statusCode)"
WriteLog 'XymonSendViaHttp finished'
return $false
▸
}
$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
if ( $outputBuffer -ne $false)
▸
{ $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 } }
try {
$client.DownloadFile($downloadURL, $destinationFilePath)
}
catch
{
$e = $_.Exception
WriteLog "Error during DownloadFile: $e.Message"
return $false
▸
}
}
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
$output = XymonSend $message $script:XymonSettings.serversList $destinationFilePath
return $output
▸
}
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:' })
if ($updates.length -gt 0)
{
if ($updates.length -eq 1)
{
WriteLog "Found 1 $($updates.length) clientversion lines:"
}
else
{
WriteLog "Found $($updates.length) clientversion liness:"
}
$script:clientlocalcfg_entries.keys | where { $_ -match '^clientversion:(\d+\.\d+):(.+?)(?::(MD5|SHA1|SHA256):([0-9a-f]+))?$' } | foreach `
▸
{
# $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 from $($matches[2])"
▸
# $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
$fileSize = (Get-Item $newversion).Length
# Failed http download results in 1 byte file
if ( $fileSize -lt 2 )
{
WriteLog "Error: downloaded file is only $fileSize byte"
exit
}
else
▸
{
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]) from $($matches[2]); doing nothing"
}
}
}
else
▸
{
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)
{
if (Test-Path $destination)
▸
{
$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
}
else
{
WriteLog "Error: new file $destination not found"
▸
}
}
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])
if ( $script:slowscanrate -gt 0 ) {
$loopcount = Get-Random -Maximum ($script:slowscanrate)
} else {
$loopcount = 0
▸
}
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) {
WriteLog "Doing slow scan tasks: $loopcount -eq $($script:slowscanrate)"
$loopcount = 0
$slowscan = $true
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 }
}