1

I am trying to search for a string in multiple text files to trigger an event. The log file is being actively added to by a program. The following script successfully achieves that goal, but it only works for one text file at a time:

$PSDefaultParameterValues = @{"Get-Date:format"="yyyy-MM-dd HH:mm:ss"}

Get-Content -path "C:\Log 0.txt" -Tail 1 -Wait | ForEach-Object { If ($_ -match 'keyword') {  

Write-Host "Down : $_" -ForegroundColor Green

Add-Content "C:\log.txt" "$(get-date) down"

Unfortunately it means I have to run 3 instances of this script to search the 3 log files (C:\log 0.txt, C:\log 1.txt and C:'log 2.txt).

What I want to do is run one powershell script to search for that string across all three text files and not three.

I tried using a wildcard in the path ("C:\log*.txt)

I also tried adding a foreach loop:

$PSDefaultParameterValues = @{"Get-Date:format"="yyyy-MM-dd HH:mm:ss"}

$LogGroup = ('C:\log 0.txt', 'C:\Log 1.txt', 'C:\Log 2.txt')


ForEach ($log in $LogGroup) {


Get-Content $log -Tail 1 -Wait | ForEach-Object { If ($_ -match 'keyword') {  

Write-Host "Down: $_" -ForegroundColor Green

Add-Content -path "C:\log.txt" "$(get-date) down"
Add-Content -path "C:\log.txt" "$(get-date) down"

}

       
        }
       }

This got me no errors but it also didn't work.

I saw others use Get-ChildItem instead of Get-Content but since this worked with one file... shouldn't it work with multiple? I assume it's my lack of scripting ability. Any help would be appreciated. Thanks.

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
Sceptersax
  • 83
  • 1
  • 9
  • Looks like `Get-Content` allows for multiple paths in the `-Path` parameter. Have you tried `Get-Content $LogGroup -Tail 1 -Wait` to get the tail of all the files? – TheMadTechnician Jul 18 '22 at 23:55
  • @TheMadTechnician that would work only with the first file in the array unfortunately. `-Wait` blocks the thread. He needs to do this in 3 separated runspaces if using `-Wait` – Santiago Squarzon Jul 18 '22 at 23:57
  • @TheMadTechnician yes, i have tried that. Thanks – Sceptersax Jul 18 '22 at 23:59
  • @Sceptersax when does the script stop? Or what's the condition to make it stop? I can show you how it can be done with multithreading but there is some missing info to share the code – Santiago Squarzon Jul 19 '22 at 00:00
  • @SantiagoSquarzon It doesn't stop. It continues to look for the same keyword indicating a down state for the program. Eventually, I am trying to incorporate the script into universal dashboard to let me know the state of several programs and a few servers in a dashboard environment. – Sceptersax Jul 19 '22 at 00:03
  • @SantiagoSquarzon So maybe i need to describe the situation a bit better but this program spits out logs into 3 different logs starting with textlog 0. Once it fills that log to a specific amount of kb, it then moves to textlog 1 then textlog 2. Then it writes over textlog 0 and starts all over. – Sceptersax Jul 19 '22 at 00:07
  • @SantiagoSquarzon So the -wait parameter is what's giving me trouble then? – Sceptersax Jul 19 '22 at 00:08
  • It is possible to `-Wait` for the 3 files at the same time with Runspaces but I don't see a condition in your script to ever stop the Runspaces, meaning they will run indefinitely until the parent process (the one spawning the Runspaces) is terminated. Is that OK ? – Santiago Squarzon Jul 19 '22 at 00:09
  • @SantiagoSquarzon The end goal is to update a dashboard on the condition of a down state. I was also incorporating a batch file to be triggered in the event of the down state that would kill and run the program once again as a keep alive. But I still want the dashboard to know the current state of the program im monitoring even after the restart. Considering all of this.. what do you think? – Sceptersax Jul 19 '22 at 00:14
  • @SantiagoSquarzon to answer your question, I think having it run in 3 seperate runspaces is acceptable. I just have no idea how to accomplish this – Sceptersax Jul 19 '22 at 00:19
  • Since the log files need to be continuously monitoring for a specific string across 3 text files with no break in sight, I'm wondering if maybe I should first combine the running logs into 1 log somehow? Then, I just need 2 scripts running? One script to combine and the other to search the string perhaps? – Sceptersax Jul 19 '22 at 01:08

1 Answers1

1

This is how you can apply the same logic you already have for one file but for multiple logs at the same time, the concept is to spawn as many PowerShell instances as log paths there are in the $LogGroup array. Each instance is assigned and will be monitoring 1 log path and when the keyword is matched it will append to the main log file.

The instances are assigned the same RunspacePool, this help us initialize all with a SemaphoreSlim instance which help us ensure thread safety (only 1 thread can write to the main log at a time).

using namespace System.Management.Automation.Runspaces
using namespace System.Threading

# get the log files here
$LogGroup = ('C:\log 0.txt', 'C:\Log 1.txt', 'C:\Log 2.txt')
# this help us write to the main log file in a thread safe manner
$lock     = [SemaphoreSlim]::new(1, 1)

# define the logic used for each thread, this is very similar to the
# initial script except for the use of the SemaphoreSlim
$action = {
    param($path)

    $PSDefaultParameterValues = @{ "Get-Date:format" = "yyyy-MM-dd HH:mm:ss" }
    Get-Content $path -Tail 1 -Wait | ForEach-Object {
        if($_ -match 'down') {
            # can I write to this file?
            $lock.Wait()
            try {
                Write-Host "Down: $_ - $path" -ForegroundColor Green
                Add-Content "path\to\mainLog.txt" -Value "$(Get-Date) Down: $_ - $path"
            }
            finally {
                # release the lock so other threads can write to the file
                $null = $lock.Release()
            }
        }
    }
}

try {
    $iss = [initialsessionstate]::CreateDefault2()
    $iss.Variables.Add([SessionStateVariableEntry]::new('lock', $lock, $null))
    $rspool = [runspacefactory]::CreateRunspacePool(1, $LogGroup.Count, $iss, $Host)
    $rspool.ApartmentState = [ApartmentState]::STA
    $rspool.ThreadOptions  = [PSThreadOptions]::UseNewThread
    $rspool.Open()

    $res = foreach($path in $LogGroup) {
        $ps = [powershell]::Create($iss).AddScript($action).AddArgument($path)
        $ps.RunspacePool = $rspool
        @{
            Instance    = $ps
            AsyncResult = $ps.BeginInvoke()
        }
    }

    # block the main thread
    do {
        $id = [WaitHandle]::WaitAny($res.AsyncResult.AsyncWaitHandle, 200)
    }
    while($id -eq [WaitHandle]::WaitTimeout)
}
finally {
    # clean all the runspaces
    $res.Instance.ForEach('Dispose')
    $rspool.ForEach('Dispose')
}
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Wow. Right on. I am heading home for the day. I can't wait to try it tomorrow! Thanks for the effort. I'll let you know how it works out for me tomorrow. Much appreciated. – Sceptersax Jul 19 '22 at 01:22
  • @Sceptersax np, I tested it with 3 test files and it works pretty well. Just ensure you're always using the Full Paths of the logs and inside the `$action` scriptblock, in the `Add-Content` line ensure that the main log is a full path too. – Santiago Squarzon Jul 19 '22 at 01:29
  • Santiago Squarzon, you nailed it. It's a beautiful script. It will take me a solid month just to understand what you did exactly... but thank you for the comments! I will be studying this. Definitely some high level scripting. I couldn't help but try it out before bed and i'm glad i did :) – Sceptersax Jul 19 '22 at 05:29
  • Just one last thing Santiago Squarzon, at the moment, I only have an if statement for a downstate... can I just add an "elseif" clause after the first "try" that searches the 3 logs for a keyword to determine the "up state"? Or would that somehow break this code? Not asking for extra work here. I'm happy the way it is. I'm just always trying to improve. Thanks again. – Sceptersax Jul 19 '22 at 05:50
  • @Sceptersax yeah you definitely can. But the same code to wait and release should be repeated. You could use a function for that to not repeat the code. And thank you for the kind words. It was my pleasure – Santiago Squarzon Jul 19 '22 at 11:33
  • @Sceptersax you also wait() bedore the match conditions and release() after the conditions. That shouldn't be an issue too – Santiago Squarzon Jul 19 '22 at 12:01
  • Ok great will try that. Thanks again. I've tested your code several times so far and it works like a charm btw. – Sceptersax Jul 19 '22 at 15:26
  • @Sceptersax glad to hear it. I love multithreading so yours was a nice and enjoyable question ;) – Santiago Squarzon Jul 19 '22 at 15:30
  • 1
    HaHa. I like a good challenge too but in this, i'm out of my depth. you're the man. take care my friend. – Sceptersax Jul 19 '22 at 15:40
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246587/discussion-between-sceptersax-and-santiago-squarzon). – Sceptersax Jul 19 '22 at 16:05