I have this script that should initialize a context containing a form, a notify icon, a timer and some methods.
using module .\..\tm
# Start-ProcessMonitor function using ApplicationContext
function Start-ProcessMonitor {
param (
[string] $processNameToMonitor,
[int] $intervalInSec,
[string] $logFilePath = '',
[switch] $noGraphic
)
# Initialize your custom ApplicationContext
if ($logFilePath -ne '') {
$context = [CustomApplicationContext]::new($processNameToMonitor, $intervalInSec, $logFilePath)
} else {
$context = [CustomApplicationContext]::new($processNameToMonitor, $intervalInSec)
}
# Show form if graphic mode
if (!$noGraphic.IsPresent) {
$context.ShowForm() # Show the form if not in graphic mode
}
# Run the application using the custom ApplicationContext
[System.Windows.Forms.Application]::Run($context)
}
Start-ProcessMonitor -processNameToMonitor 'notepad' -intervalInSec 2
I don't understand why, once it reach the [System.Windows.Forms.Application]::Run($context), $context is null inside the class methods. To be more specific, the run triggers the timer loop. It runs crazy since the interval has been erased. I think it does nothing inside the Add_Tick block.
I'm quit in a learning progress and I can't figure out what's happening.
The class code is arround 300 lines, so I don't know if it's good to post it entirely.
EDIT : as the comment suggested it, here is the code for the class CustomApplicationContext. I think the issue comes from how I manage $context and the timer Add_Tick part.
# Define your custom ApplicationContext
class CustomApplicationContext : System.Windows.Forms.ApplicationContext {
[System.Windows.Forms.Form] $form
[System.Windows.Forms.NotifyIcon] $notifyIcon
[System.Windows.Forms.ListBox] $logListBox
[System.Windows.Forms.ContextMenu] $contextMenu
[System.Windows.Forms.MenuItem] $menuItemChangeProcessName
[System.Windows.Forms.MenuItem] $menuItemChangeCheckInterval
[System.Windows.Forms.MenuItem] $menuItemExit
[System.Windows.Forms.Timer] $timer
[System.Array] $previousProcesses
[System.Array] $processStartTime
[string] $logFilePath
[string] $processNameToMonitor
[string] $caption
[bool] $menuAskClosing
[int] $intervalInSec
CustomApplicationContext() {
$this.processNameToMonitor = "explorer"
$this.intervalInSec = "10"
$this.Init($this.processNameToMonitor, $this.intervalInSec)
}
CustomApplicationContext([string] $processNameToMonitor, [int] $intervalInSec) {
$this.logFilePath = "$env:TEMP\ProcessMonitor_$processNameToMonitor.log"
$this.Init($processNameToMonitor, $intervalInSec, $this.logFilePath)
}
CustomApplicationContext([string] $processNameToMonitor, [int] $intervalInSec, [string] $logFilePath) {
$this.Init($processNameToMonitor, $intervalInSec, $logFilePath)
}
[void] Init([string] $processNameToMonitor, [int] $intervalInSec, [string] $logFilePath) {
# Store parameters
$this.processNameToMonitor = $processNameToMonitor
$this.intervalInSec = $intervalInSec
$this.caption = "Process Monitor - {0} - {1} s" -f $processNameToMonitor, $intervalInSec
$this.menuAskClosing = $false
$this.previousProcesses = @()
$this.processStartTime = @{}
$this.logFilePath = $logFilePath
# Create a form
$this.form = New-Object System.Windows.Forms.Form
$this.form.Text = $this.caption
$this.form.Size = New-Object System.Drawing.Size(800, 300)
$this.form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$this.form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
# Create a list box to display the log
$this.logListBox = New-Object System.Windows.Forms.ListBox
$this.logListBox.Dock = [System.Windows.Forms.DockStyle]::Fill
$this.logListBox.SelectionMode = [System.Windows.Forms.SelectionMode]::MultiExtended
$this.logListBox.FormattingEnabled = $true
$this.logListBox.Font = "Courier"
$this.form.Controls.Add($this.logListBox)
# Create a NotifyIcon for the system tray
$this.notifyIcon = New-Object System.Windows.Forms.NotifyIcon
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$env:SystemRoot\System32\SHELL32.dll")
$this.notifyIcon.Icon = $icon
$this.notifyIcon.Text = $this.caption
$this.notifyIcon.Visible = $true
# Create a context menu to the NotifyIcon
$this.contextMenu = New-Object Windows.Forms.ContextMenu
# MenuItem to change the searched process name
$this.menuItemChangeProcessName = New-Object Windows.Forms.MenuItem
$this.menuItemChangeProcessName.Text = "Change Process Name"
# MenuItem to exit the application
$this.menuItemExit = New-Object Windows.Forms.MenuItem
$this.menuItemExit.Text = "Exit"
# MenuItem to change perdiode between 2 checks
$this.menuItemChangeCheckInterval = New-Object Windows.Forms.MenuItem
$this.menuItemChangeCheckInterval.Text = "Change Checking Interval"
# Complete context menu
$this.contextMenu.MenuItems.Add($this.menuItemChangeProcessName)
$this.contextMenu.MenuItems.Add($this.menuItemChangeCheckInterval)
$this.contextMenu.MenuItems.Add($this.menuItemExit)
# Add contexte menu to Tray Icon and Form
$this.notifyIcon.ContextMenu = $this.contextMenu
$this.form.ContextMenu = $this.contextMenu
# Create a timer to periodically check for processes
$this.timer = New-Object System.Windows.Forms.Timer
$this.timer.Interval = $this.intervalInSec * 1000
$this.timer.Add_Tick({
$this.TimerProcess()
})
# Handle the FormClosing event to dispose of resources
$this.form.Add_FormClosing({
if ($this.menuAskClosing) {
$this.timer.Stop()
$this.timer.Dispose()
$this.notifyIcon.Dispose()
$this.form.Dispose()
$this.ExitThread()
}
else {
$this.form.Hide()
$this.notifyIcon.ShowBalloonTip(2000, "Process Monitor", "The application has been minimized to the system tray.", [System.Windows.Forms.ToolTipIcon]::Info)
$_.Cancel = $true # Cancel the default close action
}
})
# Handle the FormKeyPress event to check for the shortcuts
$this.form.KeyPreview = $true
$this.form.Add_KeyDown({
if ($_.Control -and $_.KeyValue -eq 80) { # Check for Ctrl+P (ASCII code 80)
$this.menuItemChangeProcessName.PerformClick() # Trigger the menu item click event
}
if ($_.Control -and $_.KeyValue -eq 73) { # Check for Ctrl+I (ASCII code 73)
$this.menuItemChangeCheckInterval.PerformClick() # Trigger the menu item click event
}
if ($_.Control -and $_.KeyValue -eq 67) { # Check for Ctrl+C (ASCII code 67)
$this.CopySelectedItemsToClipboard()
}
if ($_.Control -and $_.KeyValue -eq 65) { # Check for Ctrl+A (ASCII code 65)
$this.SelectAllListItems()
}
})
# Handle the NotifyIcon Click event to show/hide the form
$this.notifyIcon.Add_Click({
if ($_.Button -ne [System.Windows.Forms.MouseButtons]::Left) {
return
}
if ($this.form.Visible) {
$this.form.Hide()
} else {
$this.form.Show()
$this.form.Activate()
}
})
# Handle the Change Process Name menu button click event
$this.menuItemChangeProcessName.Add_Click({
$this.ChangeProcessName($this.AskNewProcessName())
$this.ChangeCaption()
})
# Handle the Change Interval menu button click event
$this.menuItemChangeCheckInterval.Add_Click({
$this.ChangeInterval($this.AskNewInterval())
$this.ChangeCaption()
})
$this.menuItemExit.Add_Click({
$this.menuAskClosing = $true
$this.form.Close()
})
# Start the timer
$this.timer.Start()
}
###########################################################################
# Constructor - End #
###########################################################################
# Core process of the timer
[void] TimerProcess() {
$changedProcesses = $this.UpdateProcesses()
foreach ($changedProcess in $changedProcesses) {
$logEntry = $this.PrintLog($changedProcess)
$this.PrintInForm($logEntry)
$this.PrintInFile($logEntry)
}
}
# List the latest created and stoped processes
[Hashtable] UpdateProcesses() {
# Your logic for updating and logging processes here
# You can use $this.logListBox to update the list box and log to a file if needed
$processStatuses = @{}
$currentProcesses = Get-Process -Name $this.processNameToMonitor -ErrorAction SilentlyContinue
if ($currentProcesses -eq $null -and $this.previousProcesses.Count -eq 0) {
return $processStatuses
}
if ($currentProcesses -ne $null) {
$newProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "=>" }
$endedProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "<=" }
# Update the previous Xxxx processes
$this.previousProcesses = $currentProcesses
}
else {
$endedProcesses = $this.previousProcesses
$newProcesses = @()
$this.previousProcesses = @()
}
foreach ($newProcess in $newProcesses) {
$processStatuses += [PSCustomObject]@{
Status = "Started"
Process = $newProcess.Name
}
}
foreach ($endedProcess in $endedProcesses) {
$processStatuses += [PSCustomObject]@{
Status = "Ended"
Process = $endedProcess.Name
}
}
return $processStatuses
}
# Format the process info to log string
# Then send to printer with form or file
[string] PrintLog([PSCustomObject] $processInfo) {
$process = $processInfo.Process
$status = $processInfo.Status
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$startTime = $process.StartTime
if (-not $process.HasExited) {
$this.processStartTime[$process.Id] = $process.StartTime
}
else {
$startTime = $this.processStartTime[$process.Id]
$this.processStartTime.Remove($process.Id)
}
$processTime = ((Get-Date) - $startTime).ToString("hh\:mm\:ss")
$privateBytes = [int]($process.PrivateMemorySize64 / 1MB)
$logEntry = "[{0}] {1} | PID: {2,-6}| Status: {3,-8}| Run Time: {4}s | Private Bytes: {5,4} MB" -f $timestamp, $process.Name, $process.Id, $status, $processTime, $privateBytes
return $logEntry
}
# Print log entry in form
[void] PrintInForm([string] $logEntry) {
$this.logListBox.Items.Add($logEntry)
$this.logListBox.ClearSelected()
$this.logListBox.SelectedIndex = $this.logListBox.Items.Count - 1 # Focus on the last element
}
# Print log entry in file
[void] PrintInFile([string] $logEntry) {
if (-not (Test-Path($this.logFilePath)) -and $this.logFilePath -ne '') {
New-Item -Path $this.logFilePath -ItemType File
}
# Append the log entry to a file
$logEntry | Out-File -Append -FilePath $this.logFilePath
}
# Show dialog to input new process name to monitor
[string] AskNewProcessName(){
# Show a dialog to input the new process name
$title = "Change Process Name"
$msg = "Enter the new process name:"
$newProcessName = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title, $this.processNameToMonitor)
if (-not [string]::IsNullOrWhiteSpace($newProcessName)) {
return $newProcessName
}
return $this.processNameToMonitor
}
# Show dialog to input new check interval
[int] AskNewInterval(){
# Show a dialog to input the new periode
$title = "Change Checking Interval"
$msg = "Enter the new periode in secondes:"
$newInterval = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title, $this.intervalInSec)
if (![string]::IsNullOrEmpty($newInterval) -and [decimal]::TryParse($newInterval, [ref]$null) -and $newInterval -gt 0) {
return $newInterval
}
return $this.intervalInSec
}
# Set process name property and clear the previous processes pile
[void] ChangeProcessName([string] $newProcessName) {
if ($newProcessName -ne $this.processNameToMonitor){
$this.processNameToMonitor = $newProcessName
$this.previousProcesses = @()
}
}
# Set interval property and update timer interval
[void] ChangeInterval([int] $newInterval) {
if ($newInterval -ne $this.intervalInSec){
$this.intervalInSec = [int]$newInterval
$this.timer.Interval = $this.intervalInSec *1000
}
}
# Set caption of tray icon and form according to process name and interval properties
[void] ChangeCaption() {
$this.caption = "Process Monitor - $this.processNameToMonitor - $this.intervalInSec s"
$this.notifyIcon.Text = $this.form.Text = $this.caption
}
# Set clipboard with seleted lines of listbox
[void] CopySelectedItemsToClipboard() {
# Get the selected items
$selectedItems = $this.logListBox.SelectedItems
# Convert the selected items to a plain text string
$textToCopy = $selectedItems -join "`r`n"
# Copy the text to the clipboard
[System.Windows.Forms.Clipboard]::SetText($textToCopy)
}
# Select all lines of listbox
[void] SelectAllListItems() {
for ($i = 0; $i -lt $this.logListBox.Items.Count; $i++) {
$this.logListBox.SetSelected($i, $true)
}
}
# Show form
[void] ShowForm() {
$this.form.Show()
}
}