Wakan Tanka
Wakan Tanka

Reputation: 8042

how to debug .NET framework exceptions in powershell

I have working code which will change tray icon depending on notepad process existence.

Set-StrictMode -Version Latest

Add-Type -AssemblyName System.Windows.Forms    
Add-Type -AssemblyName System.Drawing

function Test-Notepad {
    [bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}

function menu1clickEvent ($label) {
    # Build Form object
    $Form = New-Object System.Windows.Forms.Form
        $Form.Text = "My Form"
        $Form.Size = New-Object System.Drawing.Size(200,200)
        $Form.StartPosition = "CenterScreen"
        $Form.Topmost = $True
        $Form.Controls.Add($Label)               # Add label to form
        $form.ShowDialog()| Out-Null             # Show the Form
}

function menu4clickEvent ($icon) {
    $icon.Visible = $false
    [System.Windows.Forms.Application]::Exit()
}

function create_taskbar_menu($label){
    # Create menu item
    $MenuItem1 = New-Object System.Windows.Forms.MenuItem
        $MenuItem1.Text = "Menu Item 1"

    # Create menu item
    $MenuItem2 = New-Object System.Windows.Forms.MenuItem
        $MenuItem2.Text = "Exit"

    # Add menu items to context menu
    $contextmenu = New-Object System.Windows.Forms.ContextMenu
        $Main_Tool_Icon.ContextMenu = $contextmenu
        $Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem1)
        $Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem2)

    # Add click events
    $MenuItem1.add_Click({menu1clickEvent $label})
    $MenuItem2.add_Click({menu4clickEvent $Main_Tool_Icon})
}

# Add assemblies for WPF and Mahapps
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')    | out-null
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework')   | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')          | out-null
[System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | out-null

# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:/WINDOWS/system32/notepad.exe")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:/WINDOWS/system32/resmon.exe")

# create tray icon
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "Icon Text"
$Main_Tool_Icon.Icon = if (Test-Notepad) { $onlineIcon } else { $offlineIcon }
$Main_Tool_Icon.Visible = $true

# Build Label object
$Label = New-Object System.Windows.Forms.Label
    $Label.Name = "labelName"
    $Label.AutoSize = $True

# Initialize the timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
$timer.Add_Tick({
    if ($Label){
        $Label.Text, $Main_Tool_Icon.Icon = if (Test-Notepad) {
            "Notepad is running", $onlineIcon
        } else {
            "Notepad is NOT running", $offlineIcon
        }
    }
})
$timer.Start()

create_taskbar_menu $label

# ERROR:
# create_taskbar_menu2 $label $Main_Tool_Icon


# Make PowerShell Disappear - Thanks Chrissy
$windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
$asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
$null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0)

# Use a Garbage colection to reduce Memory RAM
# https://dmitrysotnikov.wordpress.com/2012/02/24/freeing-up-memory-in-powershell-using-garbage-collector/
# https://learn.microsoft.com/fr-fr/dotnet/api/system.gc.collect?view=netframework-4.7.2
[System.GC]::Collect()

# Create an application context for it to all run within - Thanks Chrissy
# This helps with responsiveness, especially when clicking Exit - Thanks Chrissy
$appContext = New-Object System.Windows.Forms.ApplicationContext
try
{
    [System.Windows.Forms.Application]::Run($appContext)    
}
finally
{
    foreach ($component in $timer, $Main_Tool_Icon, $offlineIcon, $onlineIcon, $appContext)
    {
        # The following test returns $false if $component is
        # $null, which is really what we're concerned about
        if ($component -is [System.IDisposable])
        {
            $component.Dispose()
        }
    }

    Stop-Process -Id $PID
}

My goal is to refactor create_taskbar_menu method to use passed variables and not the global one, here is my code:

function create_taskbar_menu2($label, $trayIcon){
    # Create menu item
    $MenuItem1 = New-Object System.Windows.Forms.MenuItem
        $MenuItem1.Text = "Menu Item 1"

    # Create menu item
    $MenuItem2 = New-Object System.Windows.Forms.MenuItem
        $MenuItem2.Text = "Exit"

    # Add menu items to context menu
    $contextmenu = New-Object System.Windows.Forms.ContextMenu

    # Set icton properties
    $trayIcon.ContextMenu = $contextmenu
    $trayIcon.contextMenu.MenuItems.AddRange($MenuItem1)
    $trayIcon.contextMenu.MenuItems.AddRange($MenuItem2)

    # Add click events
    $MenuItem1.add_Click({menu1clickEvent $label})
    $MenuItem2.add_Click({menu4clickEvent $trayIcon})
}

I call this method as create_taskbar_menu2 $label $Main_Tool_Icon instead of create_taskbar_menu $label. Problem is that after thisrefactor my application crashes, when I click on taskbar exit button. Following message appears:

Unhandled exception has occurred in your application. If you click Continue, the application will ignore this error and attempt to continue. If you click Quit, the application will close immediately.

The variable '$trayIcon' cannot be retrieved because it has not been set.

I run my code from cmd.exe as powershell -f script.ps1 And I get nothing to console. What I'm, doing wrong and how can I debug such .NET Framework exceptions?

PS: here is log which I can copy from dialog error message box (it is a bit longer so I've pasted it on pastebin)

Upvotes: 0

Views: 412

Answers (1)

mclayton
mclayton

Reputation: 9975

This answer doesn't address your question, but it does fix your problem :-)...

Inside your create_taskbar_menu2 function, change:

$MenuItem2.add_Click({menu4clickEvent $trayIcon})

to

$MenuItem2.add_Click({
        menu4clickEvent $trayIcon
    }.GetNewClosure()
)

I think what's happening is that the callback ScriptBlock - i.e. { menu4clickEvent $trayIcon } - is being executed in a separate scope to your function, and there's no variable called $trayIcon in scope when the ScriptBlock gets executed.

The call to GetNewClosure() creates a new scriptblock and "binds" (for want of a better word - not sure of the exact terminology) the current value of the function's $trayIcon parameter to the $trayIcon variable inside the ScriptBlock's scope, so now when your callback gets executed, $trayIcon has the value you'd expect it to have.

To see a smaller example of this in action, compare these two scripts:

Set-StrictMode -Version "Latest";

function Get-MyCallback1
{
    param( $x )
    return { write-host $x }
}

$callback = Get-MyCallback1 "aaa"
$callback.Invoke()

which fails with:

Exception calling "Invoke" with "0" argument(s): "The variable '$x' cannot be retrieved because it has not been set."
At line:1 char:1
+ $callback.Invoke()
+ ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : RuntimeException

versus

Set-StrictMode -Version "Latest";

function Get-MyCallback2
{
    param( $x )
    return { write-host $x }.GetNewClosure()
}

$callback = Get-MyCallback2 "aaa"
$callback.Invoke()

which outputs:

aaa

As for debugging PowerShell, I've never tried it, but may try what @Lex Li suggested - attach a debugger (e.g. using Visual Studio -> Debug -> Attach to Process...) to the running instance of powershell.exe and see if it triggers debugging when your exception happens...

Upvotes: 1

Related Questions