Reputation: 51
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
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
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
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.
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
1 LoggingEvent... NotStarted False ...
2 Monitor Log BackgroundJob Running True localhost ...
PS C:\temp>
2021-07-22 13:42:10, Info MOUPG Mapped Global progress: [10%]
Upgrade stage is at 10% progress
2021-07-22 13:42:10, Info MOUPG Mapped Global progress: [20%]
Upvotes: 0
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
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