0

Anyone can help me fix my code below. I am displaying a log file in the textbox, but the textbox wont update automatically when the log file is updated. I tried using a timer but it wont work since it will refresh the whole form and the input file name will be reset too, so it will return a null value. Please go to "viewComo" function.

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()


#2. Instantiate a Form Object
#-----------------------------------------------------------------------------------------
$Form_MAIN = New-Object system.Windows.Forms.Form;
$Form_MAIN.ClientSize  = New-Object System.Drawing.Point(1024,1000);
$Form_MAIN.StartPosition = "manual";
$Form_MAIN.Location = New-Object System.Drawing.Size(600,300);
$Form_MAIN.BackColor = [System.Drawing.ColorTranslator]::FromHtml("#6699CC");
$Form_MAIN.text = "DataCenter Operation Applications";
$Form_MAIN.TopMost = $false;
$Form_MAIN.AutoSize = $false;
$Form_MAIN.AutoScale = $false;
$Form_MAIN.MaximizeBox = $false;

#3. Build the Form Components
#-----------------------------------------------------------------------------------------
$MainMenu = New-Object System.Windows.Forms.MenuStrip;

$Menu_File = New-Object System.Windows.Forms.ToolStripMenuItem("File");
$SubMenu_Open = New-Object System.Windows.Forms.ToolStripMenuItem("Open");
$SubMenu_Save = New-Object System.Windows.Forms.ToolStripMenuItem("Save");
$SubMenu_Exit = New-Object System.Windows.Forms.ToolStripMenuItem("Exit");

$Menu_DCApp = New-Object System.Windows.Forms.ToolStripMenuItem("DCApp");
$SubMenu_Reports_Checker = New-Object System.Windows.Forms.ToolStripMenuItem("Reports_Checker");
$SubMenu_Reports_Transfer = New-Object System.Windows.Forms.ToolStripMenuItem("Reports_Transfer");
$SubMenu_COB_Monitoring = New-Object System.Windows.Forms.ToolStripMenuItem("COB_Monitoring");
$SubMenu_AD_AccountLock_Checker = New-Object      System.Windows.Forms.ToolStripMenuItem("AD_AccountLock_Checker");

$Menu_Help = New-Object System.Windows.Forms.ToolStripMenuItem("Help");
$Menu_About = New-Object System.Windows.Forms.ToolStripMenuItem("About");

$GB_Top = New-Object system.Windows.Forms.Groupbox;
$GB_Top.height = 330;
$GB_Top.width = 990;
$GB_Top.text = "Como Checker, Please input the Service name below";
$GB_Top.location = New-Object System.Drawing.Point(15,30);
$GB_Top.Font = New-Object System.Drawing.Font('Calibri',12);


$TB_Top_Input = New-Object system.Windows.Forms.TextBox;
$TB_Top_Input.multiline = $true;
$TB_Top_Input.width = 840;
$TB_Top_Input.height = 30;
$TB_Top_Input.location = New-Object System.Drawing.Point(10,25);
$TB_Top_Input.Font = New-Object System.Drawing.Font('Calibri',10);


$TB_Top_Button = New-Object system.Windows.Forms.Button;
$TB_Top_Button.Text = "Search COMO";
$TB_Top_Button.width = 124;
$TB_Top_Button.height = 30;
$TB_Top_Button.location = New-Object System.Drawing.Point(850,25);
$TB_Top_Button.Add_Click({checkComoFiles})

$RB_LabelQuery = New-Object System.Windows.Forms.Label;
$RB_LabelQuery.Text = "Choose the Range to query:";
$RB_LabelQuery.AutoSize  = $true;
$RB_LabelQuery.width = 104;
$RB_LabelQuery.height = 10;
$RB_LabelQuery.location = New-Object System.Drawing.Point(10,55);
$RB_LabelQuery.Font = New-Object System.Drawing.Font('Callibri',9);

$RB_Option1 = New-Object system.Windows.Forms.RadioButton;
$RB_Option1.Text = "Last 1 Hour";
$RB_Option1.AutoSize  = $true;
$RB_Option1.width = 104;
$RB_Option1.height = 10;
$RB_Option1.location = New-Object System.Drawing.Point(170,55);
$RB_Option1.Font = New-Object System.Drawing.Font('Callibri',9);



$RB_Option2 = New-Object system.Windows.Forms.RadioButton;
$RB_Option2.Text = "Last 3 Hours";
$RB_Option2.AutoSize = $true;
$RB_Option2.width = 104;
$RB_Option2.height = 10;
$RB_Option2.location = New-Object System.Drawing.Point(270,55);
$RB_Option2.Font = New-Object System.Drawing.Font('Callibri',9);


$RB_Option3 = New-Object system.Windows.Forms.RadioButton;
$RB_Option3.Text = "Last 24 Hours";
$RB_Option3.AutoSize = $true;
$RB_Option3.width = 104;
$RB_Option3.height = 10;
$RB_Option3.location = New-Object System.Drawing.Point(360,55);
$RB_Option3.Font = New-Object System.Drawing.Font('Callibri',9);

$RB_LabelOutput = New-Object System.Windows.Forms.Label;
$RB_LabelOutput.Text = "Result:";
$RB_LabelOutput.AutoSize  = $true;
$RB_LabelOutput.width = 104;
$RB_LabelOutput.height = 10;
$RB_LabelOutput.location = New-Object System.Drawing.Point(10,85);
$RB_LabelOutput.Font = New-Object System.Drawing.Font('Calibri',12);


#$TB_Top_Output = New-Object system.Windows.Forms.TextBox;
#$TB_Top_Output.multiline = $true;
#$TB_Top_Output.width = 970;
#$TB_Top_Output.height = 200;
#$TB_Top_Output.location = New-Object System.Drawing.Point(10,110);

$dataGridView = New-Object System.Windows.Forms.DataGridView
$dataGridView.Size=New-Object System.Drawing.Size(970,200)
$dataGridView.Location = New-Object System.Drawing.Point(10,110); 
$dataGridView.BackgroundColor = "White";

#$form.Controls.Add($dataGridView)
$dataGridView.ColumnCount = 1
$dataGridView.ColumnHeadersVisible = $true
$dataGridView.Columns[0].Name = "Como Name"
$datagridview.Columns[0].Width = 300;
$datagridview.Font = New-Object System.Drawing.Font('Courier New',9);
$datagridview.Add_CellMouseDoubleClick({viewComoGrid})


$GB_Bottom = New-Object system.Windows.Forms.Groupbox;
$GB_Bottom.height = 600;
$GB_Bottom.width = 990;
$GB_Bottom.text = "View COMO";
$GB_Bottom.location = New-Object System.Drawing.Point(15,380);
$GB_Bottom.Font = New-Object System.Drawing.Font('Calibri',12);


$TB_Bottom_Input = New-Object system.Windows.Forms.TextBox;
$TB_Bottom_Input.multiline = $true;
$TB_Bottom_Input.width = 840;
$TB_Bottom_Input.height = 30;
$TB_Bottom_Input.location = New-Object System.Drawing.Point(10,20);
$TB_Bottom_Input.Font = New-Object System.Drawing.Font('Calibri',10);


$TB_Bottom_Button = New-Object system.Windows.Forms.Button;
$TB_Bottom_Button.Text = "View COMO";
$TB_Bottom_Button.width = 124;
$TB_Bottom_Button.height = 30;
$TB_Bottom_Button.location = New-Object System.Drawing.Point(850,20);
$TB_Bottom_Button.Font = New-Object System.Drawing.Font('Calibri',10);
$TB_Bottom_Button.Add_Click({viewComo})

$RB_LabelOutput_Bottom = New-Object System.Windows.Forms.Label;
$RB_LabelOutput_Bottom.Text = "Result:";
$RB_LabelOutput_Bottom.AutoSize  = $true;
$RB_LabelOutput_Bottom.width = 104;
$RB_LabelOutput_Bottom.height = 10;
$RB_LabelOutput_Bottom.location = New-Object System.Drawing.Point(10,55);
$RB_LabelOutput_Bottom.Font = New-Object System.Drawing.Font('Calibri',12);


$TB_Top_Output_Bottom = New-Object system.Windows.Forms.TextBox;
$TB_Top_Output_Bottom.multiline = $true;
$TB_Top_Output_Bottom.size = New-Object System.Drawing.Size(970,510) 
$TB_Top_Output_Bottom.ReadOnly = $True
#$TB_Top_Output_Bottom.width = 970;
#$TB_Top_Output_Bottom.height = 510;
$TB_Top_Output_Bottom.location = New-Object System.Drawing.Size(10,80);
$TB_Top_Output_Bottom.ScrollBars = 'Both'
$TB_Top_Output_Bottom.Font = New-Object System.Drawing.Font('Courier New',9);


#4. Add the Components to the Group Boxes
#-----------------------------------------------------------------------------------------
$GB_Top.controls.AddRange(@($RB_LabelQuery, $RB_Option1,$RB_Option2,$RB_Option3,$TB_Top_Input,     $TB_Top_Button, $RB_LabelOutput, $dataGridView));
$GB_Bottom.controls.AddRange(@($L_Bottom_Output,$B_Enter, $TB_Bottom_Input, $TB_Bottom_Button,   $RB_LabelOutput_Bottom, $TB_Top_Output_Bottom  ));


#4. Add the Components to the Form
#-----------------------------------------------------------------------------------------
$Menu_File.DropDownItems.Add($SubMenu_Open);
$Menu_File.DropDownItems.Add($SubMenu_Save);
$Menu_File.DropDownItems.Add($SubMenu_Exit);

$Menu_DCApp.DropDownItems.Add($SubMenu_Reports_Checker);
$Menu_DCApp.DropDownItems.Add($SubMenu_Reports_Transfer);
$Menu_DCApp.DropDownItems.Add($SubMenu_COB_Monitoring);
$Menu_DCApp.DropDownItems.Add($SubMenu_AD_AccountLock_Checker);

$MainMenu.Items.Add($Menu_File);
$MainMenu.Items.Add($Menu_DCApp);  
$MainMenu.Items.Add($Menu_Help);
$MainMenu.Items.Add($Menu_About);

$Form_MAIN.controls.AddRange(@($MainMenu,$TB_Output));

#4.1 Add the Group Boxes to the Form
#-----------------------------------------------------------------------------------------
$Form_MAIN.controls.AddRange(@($GB_Top,$GB_Bottom));


#5. Code the Event Handlers
#-----------------------------------------------------------------------------------------
$SubMenu_Open.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  Open File."; })
$SubMenu_Save.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  Save File."; })
$SubMenu_Exit.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  Exit Program."; })

$SubMenu_Reports_Checker.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  Reports_Checker"; })
$SubMenu_Reports_Transfer.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  Reports_Transfer"; })
$SubMenu_COB_Monitoring.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  COB_Monitoring"; })
$SubMenu_AD_AccountLock_Checker.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  AD_AccountLock_Checker"; })

$Menu_DCApp.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  Options!"; })
$Menu_Help.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  Help me!"; })
$Menu_About.Add_Click({ $TB_Top_Output_Bottom.Text = "`r`n  All about ME."; })

function checkComoFiles {

$servicename= $TB_Top_Input.Text
$dataGridView.Rows.Clear()
$TB_Top_Input.Clear()

if ($RB_Option1.Checked) {
  $hours = 1

$ResultQuery = Get-ChildItem -Path C:\Users\user\Downloads\ -File | Where-Object {($_.LastWriteTime - gt (Get-Date (Get-Date).AddHours(-$hours))) } |
ForEach-Object { $_ | Select-String -List -Pattern $servicename  -SimpleMatch  |
        Select-Object -first 1 -ExpandProperty FileName}

 }


if ($RB_Option2.Checked) {
 $hours = 3

$ResultQuery = Get-ChildItem -Path C:\Users\user\Downloads\ -File | Where-Object {($_.LastWriteTime -gt (Get-Date (Get-Date).AddHours(-$hours))) } |
ForEach-Object { $_ | Select-String -List -Pattern $servicename  -SimpleMatch  |
        Select-Object -first 1 -ExpandProperty FileName}

}


if ($RB_Option3.Checked) {
 $hours = 24

$ResultQuery = Get-ChildItem -Path C:\Users\user\Downloads\ -File | Where-Object {($_.LastWriteTime -gt (Get-Date (Get-Date).AddHours(-$hours))) } |
ForEach-Object { $_ | Select-String -List -Pattern $servicename  -SimpleMatch  |
        Select-Object -first 1 -ExpandProperty FileName}

}

$dataGridView.Columns[0].Name = $servicename

$rows = @($ResultQuery)
foreach ($row in $rows)
{    
$dataGridView.Rows.Add($row)
}


}


Function viewComo { 

$comoName = $TB_Bottom_Input.Text
$openComolive = Get-Content C:\Users\user\Downloads\$comoName  
$TB_Top_Output_Bottom.Lines = $openComolive
$TB_Bottom_Input.Clear()

}

Function viewComoGrid {

$rowIndex = $datagridview.CurrentRow.Index
$columnIndex = $datagridview.CurrentCell.ColumnIndex

#Write-Host $rowIndex
#Write-Host $columnIndex 
#Write-Host $datagridview.Rows[$rowIndex].Cells[0].value
#Write-Host $datagridview.Rows[$rowIndex].Cells[$columnIndex].value

$comoNameGrid = $datagridview.Rows[$rowIndex].Cells[0].value
$openComoliveGrid = Get-Content C:\Users\user\Downloads\$comoNameGrid  

  $TB_Top_Output_Bottom.Lines = $openComoliveGrid
  $TB_Bottom_Input.Clear()

  }
#6. Display Form
#-----------------------------------------------------------------------------------------
$Form_Main.ShowDialog();

Im trying to use below timer in my above code but it wont work since the whole form will be refresh

$timer = new-object Windows.Forms.Timer
$timer.Interval=10000
$timer.add_Tick({$TB_Top_Output_Bottom.Lines = Get-Content C:\Users\user\Downloads\$logname | Out-String;     $TB_Top_Output_Bottom.Refresh()})
$timer.Start()
mklement0
  • 382,024
  • 64
  • 607
  • 775

2 Answers2

1

Original Version:

This isn't exactly intended to be an answer!
I think mklement0's answer is probably a safer way of getting the job done. But I remember reading about FileSystemWatcher some years ago, and having never used it before, wanted to give it a try.

Found this C# FileSystemWatcher answer, and figured out how to recreate the work in PowerShell.

Found this interesting use of SynchronizingObject for System.Timers.Timer that appears to allow Timer's Elapsed event to to run in the same thread as a control or form, and discovered that FileSystemWatcher also has a SynchronizingObject Property.

This seems to work flawlessly when editing and saving MyLogFile.TXT with NotePad.exe, but when I load MyLogFile.TXT in VSCode, the script either crashes or stops working. I think VSCode is locking the file and preventing the script from reading it, but I'm not really sure.

I would like to stress that this is an experiment, and this is outside my experience, use with with caution.

Basic code for setting up a Form for testing FileSystemWatcher:

using namespace System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms
[Application]::EnableVisualStyles()

$FilePathToWatch = $PSScriptRoot
$FileNameToWatch = "MyLogFile.TXT"
$FilePathNameToWatch = Join-Path -Path $FilePathToWatch -ChildPath $FileNameToWatch

$Form_MAIN = [Form]@{
    AutoSize = $false
    AutoScale = $false
    BackColor = '0x6699CC'
    ClientSize  = "1024,1000"
    Location = "600,300"
    MaximizeBox = $false
    StartPosition = "manual"
    Text            = "DataCenter Operation Applications"
    Topmost         = $true
}

$TextBox_Output = [TextBox]@{
    Anchor = 'Top, Left, Bottom, Right'
    Location = '12, 12'
    Multiline = $true
    Name = 'TextBox_Output'
    Size = "$($Form_MAIN.ClientSize.Width - 24), $($Form_MAIN.ClientSize.Height - 24)"
    Text = Get-Content -Raw $FilePathNameToWatch
}
$Form_Main.Controls.Add($TextBox_Output)

Code for setting up FileSystemWatcher, ending with ShowDialog() to open the form, $watcher.Dispose() to, as mklement0 pointed out, to stop $watcher from continuing to fire after form closes.:

[IO.FileSystemWatcher]$watcher = [IO.FileSystemWatcher]@{
    Path = $FilePathToWatch
    NotifyFilter = [IO.NotifyFilters]::LastWrite
    Filter = $FileNameToWatch
    SynchronizingObject = $TextBox_Output
}
$watcher.Add_Changed({
    $TextBox_Output.Text = Get-Content -Raw $FilePathNameToWatch
})
$watcher.EnableRaisingEvents = $true

$null = $Form_Main.ShowDialog()
$watcher.Dispose()

UPDATED Version:

Walk through of changes from above code, full code listed in order in following sections:

Add function GetLogFileContent for safely reading the file, or returning an empty string when the file doesn't exist.

using namespace System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms
[Application]::EnableVisualStyles()

$FilePathToWatch = $PSScriptRoot
$FileNameToWatch = "MyLogFile.TXT"
$FilePathNameToWatch = Join-Path -Path $FilePathToWatch -ChildPath $FileNameToWatch

function GetLogFileContent {
    param ( [Parameter(Mandatory = $true, Position = 0)][string]$FilePathName )
    if(Test-Path -PathType Leaf -LiteralPath $FilePathName) { Get-Content -Raw $FilePathName } else { '' }
}

Added code for Form's FormClosing event to shutdown $watcher so it no longer is firing events and properly disposed.

$Form_MAIN = [Form]@{
    AutoSize = $false
    AutoScale = $false
    BackColor = '0x6699CC'
    ClientSize  = "1024,1000"
    Location = "600,300"
    MaximizeBox = $false
    StartPosition = "manual"
    Text            = "DataCenter Operation Applications"
    Topmost         = $true
}
$Form_MAIN.Add_FormClosing({
    $watcher.Dispose()
})

The TextBox Text property is assigned results of call to GetLogFileContent function.

$TextBox_Output = [TextBox]@{
    Anchor = 'Top, Left, Bottom, Right'
    Location = '12, 12'
    Multiline = $true
    Name = 'TextBox_Output'
    Size = "$($Form_MAIN.ClientSize.Width - 24), $($Form_MAIN.ClientSize.Height - 24)"
    Text = GetLogFileContent $FilePathNameToWatch
}
$Form_Main.Controls.Add($TextBox_Output)

$watcher's NotifyFilter property now set for checking for both FileName and LastWrite. Just a reminder, SynchronizingObject set to the TextBox so it can be updated on the same thread.

[IO.FileSystemWatcher]$watcher = [IO.FileSystemWatcher]@{
    Path = $FilePathToWatch
    NotifyFilter = [IO.NotifyFilters]::FileName -bor [IO.NotifyFilters]::LastWrite
    Filter = $FileNameToWatch
    SynchronizingObject = $TextBox_Output
}

Added Deleted and Renamed events to $watcher to catch deletions and renames, and using GetLogFileContent function to populate the textbox Text property.

$watcher.Add_Changed({
    $TextBox_Output.Text = GetLogFileContent $FilePathNameToWatch
})
$watcher.Add_Deleted({
    $TextBox_Output.Text = ''
})
$watcher.Add_Renamed({
    $TextBox_Output.Text = if($_.Name -eq $FileNameToWatch) { GetLogFileContent $FilePathNameToWatch } else { '' }
})

Watcher.Dispose() removed from end of code (taken care of in FormClosing event).

$watcher.EnableRaisingEvents = $true
$null = $Form_Main.ShowDialog()
Darin
  • 1,423
  • 1
  • 10
  • 12
  • This code seems nice too since it will not consume more resources than using a timer. Guys thank you very much for sharing your knowledge, I'm looking to update my code using this more proficient coding style. Both of you are good mklement0 and Darin – user16800441 Feb 13 '23 at 10:47
  • @user16800441, you are welcome, but keep in mind I only committed myself to learning PowerShell a little over a year ago. And still making mistakes that I'm borderline embarrassed about, such as [this one](https://stackoverflow.com/a/75380037/4190564) that Santiago helped me with. With this in mind, I hope someone who has been deep into PS for 5+ years will come by and point out any problems with this code. – Darin Feb 13 '23 at 13:44
  • 1
    @mklement0, thank you for looking at the code and for helping make it better! – Darin Feb 13 '23 at 15:14
  • @user16800441, FYI: Added an updated version that can deal with situations where the log file is deleted or renamed. – Darin Feb 14 '23 at 02:47
0

Note:

  • This answer addresses the question as asked.

  • Darin's helpful answer shows an alternative approach that doesn't unconditionally, periodically re-read the log file (based on a timer), but uses an event-based file-system watcher to only update re-read the file if and when it changes.

    • This is more elegant and generally preferable, except if the log file is updated very frequently, in which case you'd need a throttling mechanism (which the timer-based approach implicitly provides, though one could be implemented with the file-watcher approach too).

There is no obvious problem with your approach:

  • The System.Windows.Forms.Timer type fires its events on the GUI thread and therefore allows modifying the form state.

  • While script blocks ({ ... }) that are passed as event delegates in .add_<eventName>() calls run in a child scope of the script, thanks to PowerShell's dynamic scoping you can still read the variables from the script scope.

  • Since WinForms is in control of the event loop when you show a form modally with .ShowDialog(), it is sufficient to assign new content to the .Lines property of your text-box control: the control should refresh automatically (and even an explicit .Refresh() call should not make the whole form refresh).

The following is a self-contained proof of concept:

  • A background job is started that writes the current timestamp to a given file every second.

  • The WinForms code uses a timer event to read that file and update its multi-line text box control with it.

using namespace System.Windows.Forms
using namespace System.Drawing

Add-Type -AssemblyName System.Windows.Forms

# Create the form.
$form = [Form] @{
  Text = "Textbox Timer-Based Refresh Demo"
  Size = [Size]::new(380,200)
  StartPosition = "CenterScreen"
}

# Create the textbox and add it to the form.
$form.Controls.AddRange(@(
  ($textBox = [TextBox] @{
    Location = [Point]::new(10, 10)
    Size = [Size]::new(320, 90)
    MultiLine = $true
  })
))

# Create a timer that fires every second, and
# reads the then-current file content.
$timer = [Timer]::new()
$timer.InterVal = 1000
$timer.add_Tick({
  $textBox.Lines = Get-Content -Raw $logName
})
$timer.Start()

# The log file to read.
$logName = 't.txt'

# Create a background job that updates the log file every second.
$jb = Start-Job {
  while ($true) {
    Get-Date > "$using:PWD/$using:logName"
    Start-Sleep 1
  }
}

# Show the form modally.
$null = $form.ShowDialog()

# Clean up
$timer.Dispose()
$jb | Remove-Job -Force
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thanks for your help and explanation!, I have tried your code and it is working fine. Let me first understand/ dissect your code. – user16800441 Feb 13 '23 at 05:12
  • 1
    @mklement0, I added an answer, not as a better alternative yours, but more of as a way to document an experimental alternative that uses FileSystemWatcher to update a control on a form. This code is all new to me, just playing around to see what does, and does not, work. So please feel free to comment on it, alter it, improve it, etc... – Darin Feb 13 '23 at 07:24
  • @user16800441, Darin's approach is more elegant and may be better _except_ if the log file is frequently updated in short intervals, which could cause a lot of re-reading of the file. In the latter case, and if near-realtime updates aren't necessary, your timer-based approach may be better. – mklement0 Feb 13 '23 at 15:02
  • 1
    @user16800441, reading mklement0's last comment triggered a memory of [-Tail parameter of Get-Content](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-content?view=powershell-7.3#example-4-get-the-last-line-of-a-text-file). You have to do a little more work by reading several lines at the end and verifying how many lines are newly added, but, if it is a very large log file, this could reduce read time considerably. – Darin Feb 13 '23 at 16:10
  • @Darin, it's probably simpler to use the `-Wait` switch, which checks for new content in one-second intervals. But that would require running `Get-Content -Wait` in a background job, which the timer event could then check for new output with `Receive-Job` and append that to the text box (the case where the log file gets deleted must also be handled). Good point about calling `$watcher.Dispose()` as early as possible. – mklement0 Feb 13 '23 at 17:01
  • @mklement0, FYI: Updated version handles deletions and renames. – Darin Feb 14 '23 at 02:05