Search code examples
wpfpowershellxamlprocesskill-process

Kill process based upon process Id powershell - xaml listview


I have a xaml listview with a button. The listview shows all processes that have powershell* or pwsh* in them. I want to be able to kill any of the processes by clicking a button but so far nothing I've tried has worked. Here is my code so far:

[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$input = @'
<Window 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:PowershellListView"
        Title="End Processes" Height="450" Width="870">
    <Grid>
        <Button Name="btnEndProc" Content="End Process" HorizontalAlignment="Left" Height="45" Margin="112,341,0,0" VerticalAlignment="Top" Width="110"/>
        <ListView Name="listdisk" HorizontalAlignment="Left" Height="200" Margin="12,107,-140,0" VerticalAlignment="Top" Width="840">
           <ListView.View>
                <GridView>
                    <GridViewColumn Header="Process ID" DisplayMemberBinding ="{Binding ProcessID}" Width="100"/>
                    <GridViewColumn Header="Name" DisplayMemberBinding ="{Binding Name}" Width="255"/>
                    <GridViewColumn Header="Handle Count" DisplayMemberBinding ="{Binding HandleCount}" Width="105"/>
                    <GridViewColumn Header="Working Set Size" DisplayMemberBinding ="{Binding Workingsetsize}" Width="100"/>
                    <GridViewColumn Header="Virtual Size" DisplayMemberBinding ="{Binding VirtualSize}" Width="105"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>
'@


#region code
[xml]$xaml = $input
$input = $input -replace '^window.*', '<window' -replace 'mc:ignorable="d"','' -replace "x:N",'N' -replace 'x:Class="\S+"',''

#Read XAML
$xmlreader=(New-Object System.Xml.XmlNodeReader $xaml)
$xamlForm=[Windows.Markup.XamlReader]::Load($xmlReader)
$xaml.SelectNodes("//*[@Name]") | ForEach-Object -Process {
    Set-variable -name ($_.Name) -Value $xamlForm.Findname($_.Name)
    }
#endregion code

$cim = Get-CimInstance -ClassName Win32_Process -Filter "Name like 'Powershell%' or Name like 'pwsh%'" 
$procids = Get-CimInstance -ClassName Win32_Process -Filter "Name like 'Powershell%' or Name like 'pwsh%'" | select-object -expandproperty ProcessId

#region XAML Controls

#endregion
$xamlForm.activate()
$listdisk.ItemsSource = $cim

$process = Get-Process -id $cim
write-host $process

$xamlForm.showdialog() | out-null

$btnEndProc.add_click({
    write-host "$listdisk.items.CurrentItem.ProcessId"
    $listdisk.items.currentitem.Processid.remove
    [System.Windows.Forms.Application]::DoEvents()
    })

As you can see, at the end I was just doing some testing with the write-hosts but they're coming up empty. $procids is returning process ids.

Help please. I've been working on this for days.


Solution

    • Instead of $listdisk.items.currentitem.Processid.remove, use
      Stop-Process -Force -Id $listdisk.items.currentitem.Processid in order to kill the process corresponding to the currently selected list item showing the running PowerShell and PowerShell ISE processes.

      • Note: Stop-Process forcefully kills (terminates) the target process.

        • If you want to give the process a chance to shut down gracefully - which it may refuse, however - you can use
          taskkill.exe /pid $listdisk.items.currentitem.Processid instead.
      • Also, Stop-Process terminates only the targeted process, not the entire process tree, i.e. not also any child processes (and their children) that may have been launched by it.

        • While powershell.exe and pwsh.exe themselves ensures that any child processes are also terminated, the PowerShell ISE does not do that.

        • To ensure that the entire process tree is terminated, make direct use of the underlying .NET APIs:

          [System.Diagnostics.Process]::GetProcessById($PID).Kill($true)
          
    • However, to ensure that $listdisk.items.currentitem truly reflects the currently selected list item, your XAML's <ListView> element is missing the IsSynchronizedWithCurrentItem="True" attribute.

    • A few asides:

      • Remove the "..." around the Write-Host argument (or use Write-Host "$($listdisk.items.CurrentItem.ProcessId)", though you do not need explicit stringification int his case; inside "...", expressions such as property access require enclosure in $(...).

      • [System.Windows.Forms.Application]::DoEvents() relates to WinForms, not WPF, so it doesn't apply here - nor do you need equivalent WPF functionality here, given that killing a process executes quickly and therefore doesn't interfere with GUI event handling.

      • Your code mistakenly places $btnEndProc.add_click() after the .ShowDialog() call, where it wouldn't have any effect, given that .ShowDialog() shows the window modally (in a blocking fashion).


    Below is a self-contained, streamlined, corrected form of your code that uses Stop-Process to kill the selected process.
    However, to be safe it uses the -WhatIf common parameter to preview the operation. Remove -WhatIf once you're sure the the correct process is being targeted.

    Add-Type -AssemblyName PresentationFramework
    
    [xml] $xaml = @'
    <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:PowershellListView"
            Title="End Processes" Height="450" Width="870">
        <Grid>
            <Button Name="btnEndProc" Content="End Process" HorizontalAlignment="Left" Height="45" Margin="112,341,0,0" VerticalAlignment="Top" Width="110"/>
            <!-- 
              NOTE THE REQUIRED USE OF IsSynchronizedWithCurrentItem="True" 
              Also, to keep this demo simple, SelectionMode="Single" ensures that only ONE process can be selected at time.
            -->
            <ListView Name="listProcesses" HorizontalAlignment="Left" Height="200" Margin="12,107,-140,0" VerticalAlignment="Top" Width="840" IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
               <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Process ID" DisplayMemberBinding ="{Binding ProcessID}" Width="100"/>
                        <GridViewColumn Header="Name" DisplayMemberBinding ="{Binding Name}" Width="255"/>
                        <GridViewColumn Header="Handle Count" DisplayMemberBinding ="{Binding HandleCount}" Width="105"/>
                        <GridViewColumn Header="Working Set Size" DisplayMemberBinding ="{Binding Workingsetsize}" Width="100"/>
                        <GridViewColumn Header="Virtual Size" DisplayMemberBinding ="{Binding VirtualSize}" Width="105"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </Grid>
    </Window>
    '@ -replace '^window.*', '<window' -replace 'mc:ignorable="d"', '' -replace "x:N", 'N' -replace 'x:Class="\S+"', ''
    
    
    # Parse the XAML and create PowerShell variables named for each control.
    $xmlreader = (New-Object System.Xml.XmlNodeReader $xaml)
    $xamlForm = [Windows.Markup.XamlReader]::Load($xmlReader)
    $xaml.SelectNodes("//*[@Name]") | ForEach-Object -Process {
      Set-variable -name $_.Name -Value $xamlForm.Findname($_.Name)
    }
    
    # Get the processes of interest...
    # NOTE: Using a [System.Collections.ArrayList] instance allows later modification of the list of processes, which
    #       in turn allows removing items from the list view.
    [System.Collections.ArrayList] $processes = Get-CimInstance -ClassName Win32_Process -Filter "Name like 'Powershell%' or Name like 'pwsh%'" 
    # ... and bind them to the list control.
    $listProcesses.ItemsSource = $processes
    
    $btnEndProc.add_click({
        $selectedProcess = $listProcesses.items.CurrentItem
        $selectedProcessId = $selectedProcess.ProcessId
        Write-Verbose -Verbose "Forcefully terminating process with ID $selectedProcessId..."
        # NOTE: -WhatIf  *previews* the operation. Remove `-WhatIf` and re-execute once you're sure the operation will do what you want.
        Stop-Process -Force -ID $selectedProcessId -WhatIf
        # Remove the terminated process from the list view and remove the selection.
        $listProcesses.items.SourceCollection.Remove($selectedProcess)
        $listProcesses.items.Refresh()
        $listProcesses.SelectedIndex = -1
      })
    
    # Enable the button only if a list item is selected.
    $listProcesses.add_SelectionChanged({
        $btnEndProc.IsEnabled = $this.SelectedIndex -ne -1
    })
    
    # Make sure that the window receives focus when it is first displayed.
    $xamlForm.add_Loaded({ $this.Activate() })
    
    # Show the WPF window modally (blocking call).
    $null = $xamlForm.ShowDialog()