JAG
JAG

Reputation: 51

PowerShell script to monitor a log and output progress to another

I have a PowerShell script to upgrade our Windows 10 computers to newer feature updates. The script also logs certain steps as they progress so we can track them. However, the actual upgrade step takes a couple of hours and (currently) there's no visibility on how far along it is until it's finished. Here's what I have so far:

Get-Content -Path "C:\$WINDOWS.~BT\Sources\Panther\setupact.log" -Tail 0 -Wait | Where {$_ -Match "Mapped\ Global\ progress:\ \[10%]"}
Add-Content -Path "\\SERVER1\Logs\Upgrade.log" -Value "Upgrade stage is at 10% progress"
Get-Content -Path "C:\$WINDOWS.~BT\Sources\Panther\setupact.log" -Tail 0 -Wait | Where {$_ -Match "Mapped\ Global\ progress:\ \[20%]"}
Add-Content -Path "\\SERVER1\Logs\Upgrade.log" -Value "Upgrade stage is at 20% progress"

This works, in so far as Upgrade.log is updated when the script sees the "Mapped Global Progress: 10%" value appear in setupact.log, but the problem is it stops there and will not detect any further updates. I assume this is because of the -wait parameter added to the Get-Content command.

Does anyone know of a way (within PowerShell as I'd prefer not to use an external program) of monitoring the log as described but not experiencing the pause after the first match?

Thanks.

Upvotes: 2

Views: 1537

Answers (5)

JAG
JAG

Reputation: 51

This is updated code extrapolated from your latest answer:

Get-Content C:\temp\Test.log -Wait -Last 0 |
ForEach-Object {
    Switch -RegEx ($_)
    {
        Default {}
        "Mapped\ Global\ progress:\ \[10%]"
        {
            Write-Host "Upgrade stage is at 10% progress"
        }
        "Mapped\ Global\ progress:\ \[20%]"
        {
            Write-Host "Upgrade stage is at 20% progress"
        }
        "Mapped\ Global\ progress:\ \[30%]"
        {
            Write-Host "Upgrade stage is at 30% progress"
        }
        "Mapped\ Global\ progress:\ \[40%]"
        {
            Write-Host "Upgrade stage is at 40% progress"
        }
        "Mapped\ Global\ progress:\ \[50%]"
        {
            Write-Host "Upgrade stage is at 50% progress"
        }
        "Mapped\ Global\ progress:\ \[60%]"
        {
            Write-Host "Upgrade stage is at 60% progress"
        }
        "Mapped\ Global\ progress:\ \[70%]"
        {
            Write-Host "Upgrade stage is at 70% progress"
        }
        "Mapped\ Global\ progress:\ \[80%]"
        {
            Write-Host "Upgrade stage is at 80% progress"
        }
        "Mapped\ Global\ progress:\ \[90%]"
        {
            Write-Host "Upgrade stage is at 90% progress"
        }
        "Mapped\ Global\ progress:\ \[100%]"
        {
            Write-Host "Upgrade stage is at 100% progress"
            Break
        }
    }
}

Note that I have put a Break command after the upgrade stage is at 100% but it does not stop the loop processing. Do I need to place it elsewhere?

Thanks.

Upvotes: 0

Daniel
Daniel

Reputation: 5122

Try like this

Get-Content C:\temp\Test.log -Wait -Last 0 |
    ForEach-Object {
        switch -regex ($_) {
            '\[(\d+)%\]' {
                $match = $Matches[1]
                Write-Host "$match% Progress"
            }
            Default {  } # do nothing if line not matched
        }
    }

Based off of this answer

Upvotes: 1

JAG
JAG

Reputation: 51

I think I must be doing something wrong because I can't get it to work. This is how I'm testing it, perhaps that will clarify things? By the way, this is using your second script exactly as is, without any mods.

  1. I create a blank file called log.log in C:\temp
  2. I open a PowerShell window as a standard user and cd \temp
  3. I run Test.ps1 (your script) and the screen outputs the following:
Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
1      LoggingEvent...                 NotStarted    False                                ...
2      Monitor Log     BackgroundJob   Running       True            localhost            ...

PS C:\temp>
  1. I edit C:\temp\log.log and add the following line to it:
2021-07-22 13:42:10, Info                  MOUPG  Mapped Global progress: [10%]
  1. No output to the PowerShell window.
  2. I then save the file, but still no output.
  3. If I hit ENTER in the PowerShell window, I get the following output:
Upgrade stage is at 10% progress
  1. I add the following line to C:\temp\log.log:
2021-07-22 13:42:10, Info                  MOUPG  Mapped Global progress: [20%]
  1. I get no output.
  2. If I save it, no output.
  3. If I hit ENTER in PowerShell window, no output.

Upvotes: 0

Daniel
Daniel

Reputation: 5122

Here is a way you can do it with a .NET class written in C#. It uses a class called LogFileMonitor which we first define using Add-Type.

Once the type is loaded we can then create an instance of it. It's constructor takes in the path to the log that will be monitored and a TimeSpan for defining how often we want to check for updates in the log. In the code below it is set to every second.

Next we subscribe to one of the 2 available events: LineAdded or LinesAdded. I've chosen to go with LinesAdded which will raise only 1 event every second (or whatever our chosen timespan was) and return a list of lines containing all of the lines that have been added since the last check. We will check these individually in our action. (The other, LineAdded, will raise 1 event for every new line that is added and provide the single line. If 10 lines are added in the last second, 10 events will be raised each with 1 line. )

When subscribing we need to define the action we would like to perform when the event we are subscribing to is raised. This is where we check the line for a match and update the other log. I've decided to use a switch here with the -regex option. When any of our lines match one of the regex patterns we've listed then the scriptblock will be executed adding content to the other log (or whatever other action we define)

Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace CustomClasses
{
    public class LogFileMonitor
    {
        private DateTime _lastWriteTime;

        private int _lineCount;
        private TimeSpan _checkInterval = new TimeSpan(0, 0, 5);

        public string Path { get; }
        public bool Active { get; private set; } = false;

        public LogFileMonitor(string path)
        {
            if (String.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

            if (!File.Exists(path))
            {
                throw new FileNotFoundException("Could not find file", path);
            }

            Path = path;
            _lastWriteTime = File.GetLastWriteTime(path);
            _lineCount = File.ReadAllLines(path).Length;
        }

        public LogFileMonitor(string path, TimeSpan interval) : this(path)
        {
            _checkInterval = interval;
        }

        public event EventHandler<LineAddedEventArgs> LineAdded;
        public event EventHandler<LinesAddedEventArgs> LinesAdded;

        protected virtual void OnLineAdded(LineAddedEventArgs e)
        {
            EventHandler<LineAddedEventArgs> handler = LineAdded;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        protected virtual void OnLinesAdded(LinesAddedEventArgs e)
        {
            EventHandler<LinesAddedEventArgs> handler = LinesAdded;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        public virtual async Task StartMonitoring()
        {
            if (!Active)
            {
                Active = true;

                await Task.Run(() =>
                {
                    do
                    {
                        DateTime currentLastWriteTime = File.GetLastWriteTime(Path);
                        if (_lastWriteTime != currentLastWriteTime)
                        {
                            try
                            {
                                var lines = File.ReadAllLines(Path);
                                _lastWriteTime = currentLastWriteTime;

                                if (lines.Length > _lineCount)
                                {
                                    var linesAddedArgs = new LinesAddedEventArgs()
                                    {
                                        Lines = lines.Skip(_lineCount).ToList<string>()
                                    };
                                    OnLinesAdded(linesAddedArgs);

                                    int newLineNumber = 0;
                                    foreach (string line in lines.Skip(_lineCount))
                                    {
                                        var lineAddedArgs = new LineAddedEventArgs()
                                        {
                                            Line = line,
                                            LineNumber = _lineCount + ++newLineNumber
                                        };
                                        OnLineAdded(lineAddedArgs);
                                    }
                                    _lineCount = lines.Length;
                                }
                                else if (lines.Length < _lineCount)
                                {
                                    _lineCount = lines.Length;
                                }
                            }
                            catch (IOException)
                            {
                                // Log file likely being written to
                            }
                        }
                        Task.Delay((int)_checkInterval.TotalMilliseconds).Wait();
                    } while (Active);
                });

                Active = false;
            }
            else
            {
                throw new InvalidOperationException("Monitoring is already active.  Subscribe to 'LineAdded' Event.");
            }
        }

        public virtual void StopMonitoring()
        {
            Active = false;
        }
    }
}

public class LineAddedEventArgs : EventArgs
{
    public int LineNumber { get; set; }
    public string Line { get; set; }
    public DateTime Time { get; } = DateTime.Now;
}

public class LinesAddedEventArgs : EventArgs
{
    public List<string> Lines { get; set; }
    public DateTime Time { get; } = DateTime.Now;
}
'@

$logBeingMonitored = 'C:\$WINDOWS.~BT\Sources\Panther\setupact.log'
$monitor = New-Object CustomClasses.LogFileMonitor -ArgumentList @($logBeingMonitored, [timespan]::new(0, 0, 1))

$subscriber = Register-ObjectEvent -InputObject $monitor -EventName LinesAdded -SourceIdentifier MyScript -Action {
    $logProgress = '\\SERVER1\Logs\Upgrade.log'
    switch -regex ($eventargs.lines) {
        'Mapped\ Global\ progress:\ \[10%]' { 
            Add-Content -Path $logProgress -Value 'Upgrade stage is at 10% progress'
        }
        'Mapped\ Global\ progress:\ \[20%]' {
            Add-Content -Path $logProgress -Value 'Upgrade stage is at 20% progress'
        }
        Default {}
    } 
}

$task = $monitor.StartMonitoring()

Upvotes: 1

Daniel
Daniel

Reputation: 5122

Another way to do this without use of a custom .NET class and to use Get-Content -Wait the way that you are using it is to run Get-Content -Wait in a job where it will not block the main thread and use New-Event to create an event that we can react to whenever Get-Content discovers a new line.

Register-EngineEvent -SourceIdentifier 'LoggingEventHandler' -Action {
    $logProgress = 'c:\temp\Upgrade.log'
    switch -regex ($event.messagedata) {
        'Mapped\ Global\ progress:\ \[10%]' {
            'Upgrade stage is at 10% progress' | Out-Host
            Add-Content -Path $logProgress -Value 'Upgrade stage is at 10% progress'
        }
        'Mapped\ Global\ progress:\ \[20%]' {
            'Upgrade stage is at 20% progress' | Out-Host
            Add-Content -Path $logProgress -Value 'Upgrade stage is at 20% progress'
        }

        Default {}
    }
}

Start-Job -Name 'Monitor Log' -ScriptBlock {
    Register-EngineEvent -SourceIdentifier 'LoggingEventHandler' -Forward
    Get-Content -Path C:\temp\log.log -Wait | ForEach-Object { New-Event -SourceIdentifier 'LoggingEventHandler' -MessageData $_ }
}

Upvotes: 1

Related Questions