Parsing Large Text Files Eventually Leading to Memory and Performance Issues - powershell

I'm attempting to work with large text files (500 MB - 2+ GB) that contain multi line events and sending them out VIA syslog. The script I have so far seems to work well for quite a while, but after a while it's causing ISE (64 bit) to not respond and use up all system memory.
I'm also curious if there's a way to improve the speed as the current script only sends to syslog at about 300 events per second.
Example Data
START--random stuff here
more random stuff on this new line
more stuff and things
START--some random things
additional random things
blah blah
START--data data more data
START--things
blah data
Code
Function SendSyslogEvent {
$Server = '1.1.1.1'
$Message = $global:Event
#0=EMERG 1=Alert 2=CRIT 3=ERR 4=WARNING 5=NOTICE 6=INFO 7=DEBUG
$Severity = '10'
#(16-23)=LOCAL0-LOCAL7
$Facility = '22'
$Hostname= 'ServerSyslogEvents'
# Create a UDP Client Object
$UDPCLient = New-Object System.Net.Sockets.UdpClient
$UDPCLient.Connect($Server, 514)
# Calculate the priority
$Priority = ([int]$Facility * 8) + [int]$Severity
#Time format the SW syslog understands
$Timestamp = Get-Date -Format "MMM dd HH:mm:ss"
# Assemble the full syslog formatted message
$FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message
# create an ASCII Encoding object
$Encoding = [System.Text.Encoding]::ASCII
# Convert into byte array representation
$ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
# Send the Message
$UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
}
$LogFiles = Get-ChildItem -Path E:\Unzipped\
foreach ($File in $LogFiles){
$EventCount = 0
$global:Event = ''
switch -Regex -File $File.fullname {
'^START--' { #Regex to find events
if ($global:Event) {
# send previous events' lines to syslog
write-host "Send event to syslog........................."
$EventCount ++
SendSyslogEvent
}
# Current line is the start of a new event.
$global:Event = $_
}
default {
# Event-interior line, append it.
$global:Event += [Environment]::NewLine + $_
}
}
# Process last block.
if ($global:Event) {
# send last event's lines to syslog
write-host "Send last event to syslog-------------------------"
$EventCount ++
SendSyslogEvent
}
}

There are a couple of real-bad things in your script, but before we get to that let's have a look at how you can parameterize your syslog function.
Parameterize your functions
Scriptblocks and functions in powershell support optionally typed parameter declarations in the aptly named param-block.
For the purposes if this answer, let's focus exclusively on the only thing that ever changes when you invoke the current function, namely the message. If we turn that into a parameter, we'll end up with a function definition that looks more like this:
function Send-SyslogEvent {
param(
[string]$Message
)
$Server = '1.1.1.1'
$Severity = '10'
$Facility = '22'
# ... rest of the function here
}
(I took the liberty of renaming it to PowerShell's characteristic Verb-Noun command naming convention).
There's a small performance-benefit to using parameters rather than global variables, but the real benefit here is that you're going to end up with clean and correct code, which will save you a headache for the rest.
IDisposable's
.NET is a "managed" runtime, meaning that we don't really need to worry about resource-management (allocating and freeing memory for example), but there are a few cases where we have to manage resources that are external to the runtime - such as network sockets used by an UDPClient object for example :)
Types that depend on these kinds of external resources usually implement the IDisposable interface, and the golden rule here is:
Who-ever creates a new IDisposable object should also dispose of it as soon as possible, preferably at latest when exiting the scope in which it was created.
So, when you create a new instance of UDPClient inside Send-SyslogEvent, you should also ensure that you always call $UDPClient.Dispose() before returning from Send-SyslogEvent. We can do that with a set of try/finally blocks:
function Send-SyslogEvent {
param(
[string]$Message
)
$Server = '1.1.1.1'
$Severity = '10'
$Facility = '22'
$Hostname= 'ServerSyslogEvents'
try{
$UDPCLient = New-Object System.Net.Sockets.UdpClient
$UDPCLient.Connect($Server, 514)
$Priority = ([int]$Facility * 8) + [int]$Severity
$Timestamp = Get-Date -Format "MMM dd HH:mm:ss"
$FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message
$Encoding = [System.Text.Encoding]::ASCII
$ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
$UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
}
finally {
# this is the important part
if($UDPCLient){
$UDPCLient.Dispose()
}
}
}
Failing to dispose of IDisposable objects is one of the surest way to leak memory and cause resource contention in the operating system you're running on, so this is definitely a must, especially for performance-sensitive or frequently invoked code.
Re-use instances!
Now, I showed above how you should handle disposal of the UDPClient, but another thing you can do is re-use the same client - you'll be connecting to the same syslog host every single time anyway!
function Send-SyslogEvent {
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[Parameter(Mandatory = $false)]
[System.Net.Sockets.UdpClient]$Client
)
$Server = '1.1.1.1'
$Severity = '10'
$Facility = '22'
$Hostname= 'ServerSyslogEvents'
try{
# check if an already connected UDPClient object was passed
if($PSBoundParameters.ContainsKey('Client') -and $Client.Available){
$UDPClient = $Client
$borrowedClient = $true
}
else{
$UDPClient = New-Object System.Net.Sockets.UdpClient
$UDPClient.Connect($Server, 514)
}
$Priority = ([int]$Facility * 8) + [int]$Severity
$Timestamp = Get-Date -Format "MMM dd HH:mm:ss"
$FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message
$Encoding = [System.Text.Encoding]::ASCII
$ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
$UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
}
finally {
# this is the important part
# if we "borrowed" the client from the caller we won't dispose of it
if($UDPCLient -and -not $borrowedClient){
$UDPCLient.Dispose()
}
}
}
This last modification will allow us to create the UDPClient once and re-use it over and over again:
# ...
$SyslogClient = New-Object System.Net.Sockets.UdpClient
$SyslogClient.Connect($SyslogServer, 514)
foreach($file in $LogFiles)
{
# ... assign the relevant output from the logs to $message, or pass $_ directly:
Send-SyslogEvent -Message $message -Client $SyslogClient
# ...
}
Use a StreamReader instead of a switch!
Finally, if you want to minimize allocations while slurping the files, for example use File.OpenText() to create a StreamReader to read the file line-by-line:
$SyslogClient = New-Object System.Net.Sockets.UdpClient
$SyslogClient.Connect($SyslogServer, 514)
foreach($File in $LogFiles)
{
try{
$reader = [System.IO.File]::OpenText($File.FullName)
$msg = ''
while($null -ne ($line = $reader.ReadLine()))
{
if($line.StartsWith('START--'))
{
if($msg){
Send-SyslogEvent -Message $msg -Client $SyslogClient
}
$msg = $line
}
else
{
$msg = $msg,$line -join [System.Environment]::NewLine
}
}
if($msg){
# last block
Send-SyslogEvent -Message $msg -Client $SyslogClient
}
}
finally{
# Same as with UDPClient, remember to dispose of the reader.
if($reader){
$reader.Dispose()
}
}
}
This is likely going to be faster than the switch, although I doubt you'll see much improvement to the memory foot-print - simply because identical strings are interned in .NET (they're basically cached in a big in-memory pool).
Inspecting types for IDisposable
You can test if an object implements IDisposable with the -is operator:
PS C:\> $reader -is [System.IDisposable]
True
Or using Type.GetInterfaces(), as suggested by the TheIncorrigible1
PS C:\> [System.Net.Sockets.UdpClient].GetInterfaces()
IsPublic IsSerial Name
-------- -------- ----
True False IDisposable
I hope the above helps!

Here's an example of a way to switch over a file one line at a time.
get-content file.log | foreach {
switch -regex ($_) {
'^START--' { "start line is $_"}
default { "line is $_" }
}
}
Actually, I don't think switch -file is a problem. It seems to be optimized not to use too much memory according to "ps powershell" in another window. I tried it with a one gig file.

Related

When running a command in powershell how can I prepend a date/time for all output on stdout/stderr?

Is it possible in powershell when running a script to add a date prefix to all log output?
I know that it would be possible to do something like:
Write-Host "$(Get-Date -format 'u') my log output"
But I dont want to have to call some function for each time we output a line. Instead I want to modify all output when running any script or command and have the time prefix for every line.
To insert a date in front of all output, that is stdout, stderr and the PowerShell-specific streams, you can use the redirection operator *>&1 to redirect (merge) all streams of a command or scriptblock, pipe to Out-String -Stream to format the stream objects into lines of text and then use ForEach-Object to process each line and prepend the date.
Let me start with a simple example, a more complete solution can be found below.
# Run a scriptblock
&{
# Test output to all possible streams, using various formatting methods.
# Added a few delays to test if the final output is still streaming.
"Write $($PSStyle.Foreground.BrightGreen)colored`ntext$($PSStyle.Reset) to stdout"
Start-Sleep -Millis 250
[PSCustomObject]#{ Answer = 42; Question = 'What?' } | Format-Table
Start-Sleep -Millis 250
Get-Content -Path not-exists -EA Continue # produce a non-terminating error
Start-Sleep -Millis 250
Write-Host 'Write to information stream'
Start-Sleep -Millis 250
Write-Warning 'Write to warning stream'
Start-Sleep -Millis 250
Write-Verbose 'Write to verbose stream' -Verbose
Start-Sleep -Millis 250
$DebugPreference = 'Continue' # To avoid prompt, needed for Windows Powershell
Write-Debug 'Write to debug stream'
} *>&1 | Out-String -Stream | ForEach-Object {
# Add date in front of each output line
$date = Get-Date -Format "yy\/MM\/dd H:mm:ss"
foreach( $line in $_ -split '\r?\n' ) {
"$($PSStyle.Reset)[$date] $line"
}
}
Output in PS 7.2 console:
Using Out-String we use the standard PowerShell formatting system to have the output look normally, as it would appear without redirection (e. g. things like tables stay intact). The -Stream parameter is crucial to keep the streaming output behaviour of PowerShell. Without this parameter, output would only be received once the whole scriptblock has completed.
While the output already looks quite nice, there are some minor issues:
The verbose, warning and debug messages are not colored as usual.
The word "text" in the 2nd line should be colored in green. This isn't working due to the use of $PSStyle.Reset. When removed, the colors of the error message leak into the date column, which looks far worse. It can be fixed, but it is not trivial.
The line wrapping isn't right (it wraps into the date column in the middle of the output).
As a more general, reusable solution I've created a function Invoke-WithDateLog that runs a scriptblock, captures all of its output, inserts a date in front of each line and outputs it again:
Function Invoke-WithDateLog {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock] $ScriptBlock,
[Parameter()]
[string] $DateFormat = '[yy\/MM\/dd H:mm:ss] ',
[Parameter()]
[string] $DateStyle = $PSStyle.Foreground.BrightBlack,
[Parameter()]
[switch] $CatchExceptions,
[Parameter()]
[switch] $ExceptionStackTrace,
[Parameter()]
[Collections.ICollection] $ErrorCollection
)
# Variables are private so they are not visible from within the ScriptBlock.
$private:ansiEscapePattern = "`e\[[0-9;]*m"
$private:lastFmt = ''
& {
if( $CatchExceptions ) {
try { & $scriptBlock }
catch {
# The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
if( $null -ne $ErrorCollection ) {
$null = $ErrorCollection.Add( $_ )
}
# Write as regular output, colored like an error message.
"`n" + $PSStyle.Formatting.Error + "EXCEPTION ($($_.Exception.GetType().FullName)):`n $_" + $PSStyle.Reset
# Optionally write stacktrace. Using the -replace operator we indent each line.
Write-Debug ($_.ScriptStackTrace -replace '^|\r?\n', "`n ") -Debug:$ExceptionStackTrace
}
}
else {
& $scriptBlock
}
} *>&1 | ForEach-Object -PipelineVariable record {
# Here the $_ variable is either:
# - a string in case of simple output
# - an instance of one of the System.Management.Automation.*Record classes (output of Write-Error, Write-Debug, ...)
# - an instance of one of the Microsoft.PowerShell.Commands.Internal.Format.* classes (output of a Format-* cmdlet)
if( $_ -is [System.Management.Automation.ErrorRecord] ) {
# The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
if( $null -ne $ErrorCollection ) {
$null = $ErrorCollection.Add( $_ )
}
}
$_ # Forward current record
} | Out-String -Stream | ForEach-Object {
# Here the $_ variable is always a (possibly multiline) string of formatted output.
# Out-String doesn't add any ANSI escape codes to colorize Verbose, Warning and Debug messages,
# so we have to do it by ourselfs.
$overrideFmt = switch( $record ) {
{ $_ -is [System.Management.Automation.VerboseRecord] } { $PSStyle.Formatting.Verbose; break }
{ $_ -is [System.Management.Automation.WarningRecord] } { $PSStyle.Formatting.Warning; break }
{ $_ -is [System.Management.Automation.DebugRecord] } { $PSStyle.Formatting.Debug; break }
}
# Prefix for each line. It resets the ANSI escape formatting before the date.
$prefix = $DateStyle + (Get-Date -Format $DateFormat) + $PSStyle.Reset
foreach( $line in $_ -split '\r?\n' ) {
# Produce the final, formatted output.
$prefix + ($overrideFmt ?? $lastFmt) + $line + ($overrideFmt ? $PSStyle.Reset : '')
# Remember last ANSI escape sequence (if any) of current line, for cases where formatting spans multiple lines.
$lastFmt = [regex]::Match( $line, $ansiEscapePattern, 'RightToLeft' ).Value
}
}
}
Usage example:
# To differentiate debug and verbose output from warnings
$PSStyle.Formatting.Debug = $PSStyle.Foreground.Yellow
$PSStyle.Formatting.Verbose = $PSStyle.Foreground.BrightCyan
Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace {
"Write $($PSStyle.Foreground.Green)colored`ntext$($PSStyle.Reset) to stdout"
[PSCustomObject]#{ Answer = 42; Question = 'What?' } | Format-Table
Get-Content -Path not-exists -EA Continue # produce a non-terminating error
Write-Host 'Write to information stream'
Write-Warning 'Write to warning stream'
Write-Verbose 'Write to verbose stream' -Verbose
Write-Debug 'Write to debug stream' -Debug
throw 'Critical error'
}
Output in PS 7.2 console:
Notes:
The code requires PowerShell 7+.
The date formatting can be changed through parameters -DateFormat (see formatting specifiers) and -DateStyle (ANSI escape sequence for coloring).
Script-terminating errors such as created by throwing an exception or using Write-Error -EA Stop, are not logged by default. Instead they bubble up from the scriptblock as usual. You can pass parameter -CatchExceptions to catch exceptions and log them like regular non-terminating errors. Pass -ExceptionStackTrace to also log the script stacktrace, which is very useful for debugging.
Scripted cmdlets such as this one don't set the automatic variable $? and also don't add errors to the automatic $Error variable when an error is written via Write-Error. Neither the common parameter -ErrorVariable works. To still be able to collect error information I've added parameter -ErrorCollection which can be used like this:
$scriptErrors = [Collections.ArrayList]::new()
Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace -ErrorCollection $scriptErrors {
Write-Error 'Write to stderr' -EA Continue
throw 'Critical error'
}
if( $scriptErrors ) {
# Outputs "Number of errors: 2"
"`nNumber of errors: $($scriptErrors.Count)"
}
The objects generated by Write-Host already come with a timestamp, you can use Update-TypeData to override the .ToString() Method from the InformationRecord Class and then redirect the output from the Information Stream to the Success Stream.
Update-TypeData -TypeName System.Management.Automation.InformationRecord -Value {
return $this.TimeGenerated.ToString('u') + $this.MessageData.Message.PadLeft(10)
} -MemberType ScriptMethod -MemberName ToString -Force
'Hello', 'World', 123 | Write-Host 6>&1

Redirect/Capture Write-Host output even with -NoNewLine

The function Select-WriteHost from an answer to another Stackoverflow question (see code below) will redirect/capture Write-Host output:
Example:
PS> $test = 'a','b','c' |%{ Write-Host $_ } | Select-WriteHost
a
b
c
PS> $test
a
b
c
However, if I add -NoNewLine to Write-Host, Select-WriteHost will ignore it:
PS> $test = 'a','b','c' |%{ Write-Host -NoNewLine $_ } | Select-WriteHost
abc
PS> $test
a
b
c
Can anyone figure out how to modify Select-WriteHost (code below) to also support -NoNewLine?
function Select-WriteHost
{
[CmdletBinding(DefaultParameterSetName = 'FromPipeline')]
param(
[Parameter(ValueFromPipeline = $true, ParameterSetName = 'FromPipeline')]
[object] $InputObject,
[Parameter(Mandatory = $true, ParameterSetName = 'FromScriptblock', Position = 0)]
[ScriptBlock] $ScriptBlock,
[switch] $Quiet
)
begin
{
function Cleanup
{
# Clear out our proxy version of write-host
remove-item function:\write-host -ea 0
}
function ReplaceWriteHost([switch] $Quiet, [string] $Scope)
{
# Create a proxy for write-host
$metaData = New-Object System.Management.Automation.CommandMetaData (Get-Command 'Microsoft.PowerShell.Utility\Write-Host')
$proxy = [System.Management.Automation.ProxyCommand]::create($metaData)
# Change its behavior
$content = if($quiet)
{
# In quiet mode, whack the entire function body,
# simply pass input directly to the pipeline
$proxy -replace '(?s)\bbegin\b.+', '$Object'
}
else
{
# In noisy mode, pass input to the pipeline, but allow
# real Write-Host to process as well
$proxy -replace '(\$steppablePipeline\.Process)', '$Object; $1'
}
# Load our version into the specified scope
Invoke-Expression "function ${scope}:Write-Host { $content }"
}
Cleanup
# If we are running at the end of a pipeline, we need
# to immediately inject our version into global
# scope, so that everybody else in the pipeline
# uses it. This works great, but it is dangerous
# if we don't clean up properly.
if($pscmdlet.ParameterSetName -eq 'FromPipeline')
{
ReplaceWriteHost -Quiet:$quiet -Scope 'global'
}
}
process
{
# If a scriptblock was passed to us, then we can declare
# our version as local scope and let the runtime take
# it out of scope for us. It is much safer, but it
# won't work in the pipeline scenario.
#
# The scriptblock will inherit our version automatically
# as it's in a child scope.
if($pscmdlet.ParameterSetName -eq 'FromScriptBlock')
{
. ReplaceWriteHost -Quiet:$quiet -Scope 'local'
& $scriptblock
}
else
{
# In a pipeline scenario, just pass input along
$InputObject
}
}
end
{
Cleanup
}
}
PS: I tried inserting -NoNewLine to the line below (just to see how it would react) however, its producing the exception, "Missing function body in function declaration"
Invoke-Expression "function ${scope}:Write-Host { $content }"
to:
Invoke-Expression "function ${scope}:Write-Host -NoNewLine { $content }"
(Just to recap) Write-Host is meant for host, i.e. display / console output only, and originally couldn't be captured (in-session) at all. In PowerShell 5, the ability to capture Write-Host output was introduced via the information stream, whose number is 6, enabling techniques such as redirection 6>&1 in order to merge Write-Host output into the success (output) stream (whose number is 1), where it can be captured as usual.
However, due to your desire to use the -NoNewLine switch across several calls, 6>&1 by itself is not enough, because the concept of not emitting a newline only applies to display output, not to distinct objects in the pipeline.
E.g., in the following call -NoNewLine is effectively ignored, because there are multiple Write-Host calls producing multiple output objects (strings) that are captured separately:
'a','b','c' | % { Write-Host $_ -NoNewline } 6>&1
Your Select-WriteHost function - necessary in PowerShell 4 and below only - would have the same problem if you adapted it to support the -NoNewLine switch.
An aside re 6>&1: The strings that Write-Host invariably outputs are wrapped in [System.Management.Automation.InformationRecord] instances, due to being re-routed via the information stream. In display output you will not notice the difference, but to get the actual string you need to access the .MessageData.Message property or simply call .ToString().
There is no general solution I am aware of, but situationally the following may work:
If you know that the code of interest uses only Write-Host -NoNewLine calls:
Simply join the resulting strings after the fact without a separator to emulate -NoNewLine behavior:
# -> 'abc'
# Note: Whether or not you use -NoNewLine here makes no difference.
-join ('a','b','c' | % { Write-Host -NoNewLine $_ })
If you know that all instances of Write-Host -NoNewLine calls apply only to their respective pipeline input, you can write a simplified proxy function that collects all input up front and performs separator-less concatenation of the stringified objects:
# -> 'abc'
$test = & {
# Simplified proxy function
function Write-Host {
param([switch] $NoNewLine)
if ($MyInvocation.ExpectingInput) { $allInput = $Input }
else { $allInput = $args }
if ($NoNewLine) { -join $allInput.ForEach({ "$_" }) }
else { $allInput.ForEach({ "$_" }) }
}
# Important: pipe all input directly.
'a','b','c' | Write-Host -NoNewLine
}

handle error from external console application in powershell script

my powershell script calls a third party console application which uses custom commands. I want powershell to try to run that console applications command but if an error is returned (not by the powershell script but the external console app) which contains a specific string then run another command instead. If not just move onto the next instruction in the script.
What would be the best way of handling that, so basically:
if command1 returns "error1" then run command2. if command 1 does not return error1 skip command2 and move down the script.
You can call and catch errors of native applications in many ways.
Some examples:
1. Most easy with no process handling, no distinguishing between success and error.
$nativeAppFilePath = 'ping.exe'
# parameters as collection. Parameter-Value pairs with a space in between must be splitted into two.
$nativeAppParam= #(
'google.com'
'-n'
'5'
)
# using powershell's call operator '&'
$response = & $nativeAppFilePath $nativeAppParam
$response
2. Easy, same as 1., but distinguishing between success and error possible.
$nativeAppFilePath = 'ping.exe'
# parameters as collection. Parameter-Value pairs with a space in between must be splitted into two.
$nativeAppParam= #(
'google2.com'
'-n'
'5'
)
# using powershell's call operator '&' and redirect the error stream to success stream
$nativeCmdResult = & $nativeAppFilePath $nativeAppParam 2>&1
if ($LASTEXITCODE -eq 0) {
# success handling
$nativeCmdResult
} else {
# error handling
# even with redirecting the error stream to the success stream (above)..
# $LASTEXITCODE determines what happend if returned as not "0" (depends on application)
Write-Error -Message "$LASTEXITCODE - $nativeCmdResult"
}
! Now two more complex snippets, which doesn't work with "ping.exe" (but most other applications), because "ping" doesn't raise error events.
3. More complex with process handling, but still process blocking until the application has been finished.
$nativeAppProcessStartInfo = #{
FileName = 'ping.exe' # either OS well-known as short name or full path
Arguments = #(
'google.com'
'-n 5'
)
RedirectStandardOutput = $true # required to catch stdOut stream
RedirectStandardError = $true # required to catch stdErr stream
UseShellExecute = $false # required to redirect streams
CreateNoWindow = $true # does what is says (background work only)
}
try {
$nativeApp= [System.Diagnostics.Process]#{
EnableRaisingEvents = $true
StartInfo = $nativeAppProcessStartInfo
}
[void]$nativeApp.Start()
# Warning: As soon as the buffer gets full, the application could stuck in a deadlock. Then you require async reading
# see: https://stackoverflow.com/a/7608823/13699968
$stdOut = $nativeApp.StandardOutput.ReadToEnd()
$stdErr = $nativeApp.StandardError.ReadToEnd()
# To avoid deadlocks with synchronous read, always read the output stream first and then wait.
# see: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?redirectedfrom=MSDN&view=net-5.0#remarks
$nativeApp.WaitForExit()
if ($stdOut.Result) {
# success handling
$stdOut.Result
}
if ($stdErr.Result) {
# error handling
$stdErr.Result
}
} finally {
$nativeApp.Dispose()
}
4. The most complex with realtime output & reaction, capturing, and so on...
This time with wmic.exe and a nonsense parameter as example.
$appFilePath = 'wmic.exe'
$appArguments = #(
'someNonExistentArgument'
)
$appWorkingDirPath = ''
# handler for events of process
$eventScriptBlock = {
# $Event is an automatic variable. Only existent in scriptblocks used with Register-ObjectEvent
# received app output
$receivedAppData = $Event.SourceEventArgs.Data
# Write output as stream to console in real-time (without -stream parameter output will produce blank lines!)
# (without "Out-String" output with multiple lines at once would be displayed as tab delimited line!)
Write-Host ($receivedAppData | Out-String -Stream)
<#
Insert additional real-time processing steps here.
Since it is in a different scope, variables changed in this scope will not get changed in parent scope
and scope "$script:" will not work as well. (scope "$global:" would work but should be avoided!)
Modify/Enhance variables "*MessageData" (see below) before registering the event to modify such variables.
#>
# add received data to stringbuilder definded in $stdOutEventMessageData and $stdErrEventMessageData
$Event.MessageData.Data.AppendLine($receivedAppData)
}
# MessageData parameters for events of success stream
$stdOutEventMessageData = #{
# useful for further usage after application has been exited
Data = [System.Text.StringBuilder]::new()
# add additional properties necessary in event handler scriptblock above
}
# MessageData parameters for events of error stream
$stdErrEventMessageData = #{
# useful for further usage after application has been exited
Data = [System.Text.StringBuilder]::new()
# add additional properties necessary in event handler scriptblock above
}
#######################################################
#region Process-Definition, -Start and Event-Subscriptions
#------------------------------------------------------
try {
$appProcessStartInfo = #{
FileName = $appFilePath
Arguments = $appArguments
WorkingDirectory = $appWorkingDirPath
RedirectStandardOutput = $true # required to catch stdOut stream
RedirectStandardError = $true # required to catch stdErr stream
# RedirectStandardInput = $true # only useful in some circumstances. Didn't find any use yet, but mentioned in: https://stackoverflow.com/questions/8808663/get-live-output-from-process
UseShellExecute = $false # required to redirect streams
CreateNoWindow = $true # does what is says (background work only)
}
$appProcess = [System.Diagnostics.Process]#{
EnableRaisingEvents = $true
StartInfo = $appProcessStartInfo
}
# to obtain available events of an object / type, read the event members of it: "Get-Member -InputObject $appProcess -MemberType Event"
$stdOutEvent = Register-ObjectEvent -InputObject $appProcess -Action $eventScriptBlock -EventName 'OutputDataReceived' -MessageData $stdOutEventMessageData
$stdErrEvent = Register-ObjectEvent -InputObject $appProcess -Action $eventScriptBlock -EventName 'ErrorDataReceived' -MessageData $stdErrEventMessageData
[void]$appProcess.Start()
# async reading
$appProcess.BeginOutputReadLine()
$appProcess.BeginErrorReadLine()
while (!$appProcess.HasExited) {
# Don't use method "WaitForExit()"! This will not show the output in real-time as it blocks the output stream!
# using "Sleep" from System.Threading.Thread for short sleep times below 1/1.5 seconds is better than
# "Start-Sleep" in terms of PS overhead/performance (Test it yourself)
[System.Threading.Thread]::Sleep(250)
# maybe timeout ...
}
} finally {
if (!$appProcess.HasExited) {
$appProcess.Kill() # WARNING: Entire process gets killed!
}
$appProcess.Dispose()
if ($stdOutEvent -is [System.Management.Automation.PSEventJob]) {
Unregister-Event -SourceIdentifier $stdOutEvent.Name
}
if ($stdErrEvent -is [System.Management.Automation.PSEventJob]) {
Unregister-Event -SourceIdentifier $stdErrEvent.Name
}
}
#------------------------------------------------------
#endregion
#######################################################
$stdOutText = $stdOutEventMessageData.Data.ToString() # final output for further usage
$stdErrText = $stdErrEventMessageData.Data.ToString() # final errors for further usage

Is this code a Keylogger? What does it do?

Due to Windows10 task manager I have a powershell.exe running which is continously consuming 8% CPU and blocking 64MB of RAM. After inspecting my Windows event log I found a pipeline event (800) with subsequent code:
Add-Type -AssemblyName System.Core
function Run-Server() {
param([string]$h);
$b = New-Object byte[] 8;
$p = New-Object System.IO.Pipes.AnonymousPipeClientStream -ArgumentList #([System.IO.Pipes.PipeDirection]::In, $h);
if ($p) {
$l = $p.Read($b, 0, 8); while ($l -gt 7) {
$c = [System.BitConverter]::ToInt32($b, 0); $l = System.BitConverter]::ToInt32($b, 4);
$t = $null; if ($l -gt 0) {
$t1 = New-Object byte[] $l;
$l = $p.Read($t1, 0, $t1.Length);
$t = [System.Text.Encoding]::UTF8.GetString($t1, 0, $l) }
if ($c -eq 1) { Invoke-Expression $t } elseif ($c -eq 9) { break } $l = $p.Read($b, 0, 8) }
$p.Dispose()
}
} Run-Server -h 728
I'm working in a corporate environment and I'm not a Powershell expert, but it seems as the script is catching byte by byte and make a string out of it? Do you have any idea what this script could be used for? Do you think it can cause the given indication of 8% CPU and 64MB RAM usage?
I formatted the code, changed the variable names and added some comments to make it easier to understand:
Add-Type -AssemblyName System.Core
function Run-Server() {
param(
[string]$h
);
$buffer = New-Object byte[] 8;
# Creates an annonymous pipe
$pipe = New-Object System.IO.Pipes.AnonymousPipeClientStream -ArgumentList #([System.IO.Pipes.PipeDirection]::In, $h);
if ($pipe) {
# Read up to 8 bytes from the pipe
$readBytes = $pipe.Read($buffer,0, 8); #(byte[] buffer, int offset, int count);
# if it managed to read 8 bytes
while ($readBytes -gt 7) {
# Seems the sender is sending some kind of 'command' or instruction.
# If command is '1' means execute the rest as a script
# If command is '9' means terminate
$command = [System.BitConverter]::ToInt32($buffer,0);
# Seems that in position 4 it sends how big the text will be
$textSize = [System.BitConverter]::ToInt32($buffer,4); # ToInt32 (byte[] value, int startIndex);
# based on the $textSize, read the full message and convert it to string ($text)
$text = $null;
if ($readBytes -gt 0) {
$text1 = New-Object byte[] $textSize;
$readBytes = $pipe.Read($text1, 0, $text1.Length);
$text = [System.Text.Encoding]::UTF8.GetString($text1, 0, $readBytes)
}
if ($command -eq 1) {
# Scary! execute the text string that came from the pipe
Invoke-Expression $text
}
elseif ($command -eq 9) {
break
}
$readBytes = $pipe.Read($buffer,0, 8)
}
$pipe.Dispose()
}
}
Run-Server -h 728
Infor about pipe: AnonymousPipeClientStream Class
That codes creates an In pipe with handle 728 and receives a script from another process, then it executes the script
Some details:
The first message seems to be a kind of command ($c) and an indication of how big the script will be ($l)
Then it reads a second message of size ($l) and, if command == 1, it executes the second message as if it would be a powershell script: Invoke-Expression $t (scary!)
Folks, I'm from Snow Software and can confirm that this is a legit code executed by Snow Inventory Agent to run PowerShell scripts that are deployed with the agents for gathering more advanced information about the device and certain apps installed on it. It does indeed run the anonymous pipe and send the Powershell code as text sourced from the encrypted script files that are deployed together with the agent. The gathered data is used by Snow Software and Technology Asset Management product suite and is deployed by large organizations to optimize technology spend, get visibility, and manageability of the technology assets.
Let me know if you have more questions!
I happened to run into the same issue. After some digging through my system (grep), I found
out that the offending code occurs in an executable 'snowagent.exe'. As far as I can tell it is used by our (company) IT department to get an inventory of the applications installed on my machine, and maybe more.
As such, I conclude that it is at least not a big issue (virus/malware). Still, if I am
hampered by it (i.e. eating away 13% CPU), I just kill it.
gr M.

powershell - get file, parse it and if "value=" < X then function sendMail

i have a text file that is automatically downloaded and updated with Task Scheduler.
It looks like this:
C:\PrintLog\GetUsageReportFromPrinterWebService\10.22.17.102:58:<input type="hidden" name="AVAILABELBLACKTONER" value="60">
C:\PrintLog\GetUsageReportFromPrinterWebService\192.167.10.140:58:<input type="hidden" name="AVAILABELBLACKTONER" value="80">
C:\PrintLog\GetUsageReportFromPrinterWebService\192.167.14.128:58:<input type="hidden" name="AVAILABELBLACKTONER" value="80">
I would like to:
delete "C:\PrintLog\GetUsageReportFromPrinterWebService\" and "input type="hidden" name="AVAILIBLETONER""
replace that IP adress with printer name (example 10.51.17.122:58 equals HP-01, 192.168.10.150:58 equals HP-02 etc)
check if "value=" is smaller than 20 and if so than send an email with function sendMail
It doesn't matter what is in that email (after that I will check it manually with webservice anyway).
I just need this as an remainder/alerter that some printer is getting low on toner, so I am not forced to manually check that txt file everyday(I most certainly would forget that :) ). These printers are offsite so I need to know in advance that the printer is low.
Note1: There are empty lines and spaces at the beginning of that txt
Note2: And no, there is no send report via email when low on toner to be configured on those printers (I double checked).
Note3: using C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
I guess point one and two are optional. The third one is important for me, I googled for something similar but just got lost as everyone wanted something little bit different.
Thanks
Here's another possibility (I think it requires Powershell 4 because of Send-Mailmessage, but I could be wrong):
#requires -version 4.0
$logs = Get-Content "F:\scripts\ParseLog\file.log"
$warning = $false
$lowPrinters = ""
# Mail Settings
$Subject = "Low Toner Warning"
$To = "printeradmin#contoso.com"
$From = "admin#contoso.com"
$SMTPServer = "smtp.contoso.com"
$Priority = "High"
$printerList = #{
"10.51.17.122:58" = "HP-01";
"192.168.10.150:58" = "HP-02";
"10.22.17.102:58" = "HP-03";
"192.167.10.140:58" = "HP-04";
}
foreach ( $log in $logs ) {
if ( $log -match '^C:\\PrintLog\\GetUsageReportFromPrinterWebService\\([^:]+:[^:]+):.*value="(.*)">$' ) {
$printer = $Matches[1]
$toner = [int]$Matches[2]
}
if( $toner -lt 20 ) {
$warning = $true
if( $printerList.ContainsKey( $printer ) ) {
$printerName = $printerList[ $printer ]
} else {
$printerName = $printer
}
$lowPrinters += "Printer {0} has a low toner level of {1}.`n" -f $printerName, $toner
}
}
if( $warning ) {
Send-MailMessage -From $From -To $To -Subject $Subject -body $lowPrinters -SmtpServer $SMTPServer
}
Along about line 8 we setup some stuff for sending email. Starting at line 15, we build a hash table mapping printer IPs/Ports with Printer Names (since printer queues aren't always listed in DNS, I decided to use a hash table instead). On line 23, we use a regular expression to grab the ip and port, and the toner value by using the -match operator. Stuff grabbed by the regex is stored in an array called $Matches.
As an example, you could do something like this:
$filePath = "C:\updatedFile.txt"
$prefix = "C:\PrintLog\GetUsageReportFromPrinterWebService\"
$lines = Get-Content $filePath |Where-Object {$_.Trim() -like "$($prefix)*"}
foreach($line in $lines)
{
$content = $line.Trim().Substring($prefix.Length)
$parts = $content -split ":"
$inputML = [xml]"$($parts[2])</input>"
$inputValue = [int]$inputML.input.value
if($inputValue -lt 20)
{
$printer = [System.Net.DNS]::GetHostByAddress($parts[0]).HostName
<#
Your sendMail call in here
#>
}
}
You remove the "C:\PrintLog\GetUsageReportFromPrinterWebService\" part with Substring(), split it into 3 pieces, and then parse to last part as an xml element, giving much easier access to the value attribute
The IP to Printer name part will only work if you have reverse DNS in place already