无法停止 powershell 中的低级别挂钩(c# 挂钩)

Can't stop low level hooks in powershell (c# hook)

提问人:pfeidewigo100 提问时间:11/4/2023 更新时间:11/5/2023 访问量:75

问:

在与 Chat GPT 和 Bing Chat 争论了一个晚上之后,我现在转向 stackoverflow 的(希望)更聪明的人。我的问题是:我在 c# 中实现了低级钩子。我订阅 powershell 类中的事件,然后在 GUI 中使用这些事件。订阅事件并启动钩子工作得很好,但是,我无法从我的类或 GUI 程序中停止钩子。目前我在想,每当我按下“f4”时,钩子都应该停止,但我总是收到“无法在空值表达式上调用方法”,我真的不明白为什么它会是空值或我将如何解决它。这是我的代码,我认为没有必要展示钩子或 GUI 的实现,但请告诉我其他情况。

class InputRecorder {
    [KeyboardHookExample.KeyboardHook] $kh
    [Ikst.MouseHook.MouseHook] $mh
    [System.Windows.Forms.ListView] $list

    InputRecorder() {
        $this.kh = New-Object KeyboardHookExample.KeyboardHook
        $this.mh = New-Object Ikst.MouseHook.MouseHook

        # Store the reference to the class instance
        $self = $this

        # Define the event handler for KeyDown
        $self.kh.add_KeyDown({
            param($sender, $e)
            $vkCode = $sender.vkCode
            Write-Host $vkCode
            if ($vkCode -eq 115) {
                $self.kh.Stop()
                $self.mh.Stop()
            }

            $charCode = [Win32.NativeMethods]::MapVirtualKey($vkCode, 2)
            $char = [char]$charCode

            Write-Host $char
        })

        # Define the event handler for LeftButtonDown
        $self.mh.add_LeftButtonDown({
            param($sender, $e)
            $mousePosition = $sender.pt
            $y = $mousePosition.y
            $x = $mousePosition.x
            $item = New-Object System.Windows.Forms.ListViewItem
            $item.ToolTipText = $global:dict["LeftClickCode"] -f $x, $y
            $item.Text = $global:dict["LeftClickDescription"] -f $x, $y
            $CMDList.Items.Add($item)
        })

        # Start the keyboard and mouse hooks
        $self.kh.Start()
        $self.mh.Start()
    }
    [System.Collections.Concurrent.ConcurrentBag[string]] getList() {
        return $this.list
    }

    [void] StopHooks() {
        $this.mh.Stop()
        $this.kh.Stop()
    }
}
C# PowerShell 挂钩 低级

评论


答:

3赞 mklement0 11/4/2023 #1

问题在于方法局部$self变量在脚本块内部不可用,用作传递给 .add_KeyDown() 方法的事件委托

虽然期望 PowerShell 的动态范围也应用于 PowerShell 的自定义是可以理解的,但事实并非如此

作为事件委托从类内部传递给 .NET 方法的脚本块

  • 看不到 enclosing 方法的局部变量 - 除非您通过调用 显式捕获它们。GetNewClosure() 在脚本块上。

    • 在没有的情况下,它所看到的是来自类定义作用域的变量,即在类外部定义的变量,在定义类的作用域中(以及来自该作用域的祖先作用域),因为它在作用域的孙子作用域中运行。[1].GetNewClosure()
  • 认为它指的是封闭类的实例,因为 - 不幸的是 - 事件委托中的自动$this变量隐藏了 -level 定义,而是引用了事件发送方$thisclass$this

    • 而且,由于类内部使用 of 是访问类的实例变量(属性)的唯一方法,因此后者也会被遮蔽。$this

有两种解决方案选项

  • 每种方法的基础上(使您的尝试成功):

    • 定义一个方法局部变量,例如,然后调用从同一方法传递给 .NET 方法的每个事件委托脚本块,这允许您访问该脚本块中的方法局部变量。$self.GetNewClosure()

    • 但是,请注意,这将使脚本块无法访问类定义作用域中的变量。.GetNewClosure()

  • 最好是在级别(不需要特定于方法的代码):

    • 用于访问手头的类实例,即(Get-Variable -Scope 1 -ValueOnly this)$this

    • 这样就不需要特定于方法的逻辑,并且还保留了对类定义作用域中变量的访问(如果需要)。


以下独立示例代码演示了这两种方法:

  • 为了便于可视化,创建了一个 WinForms 表单(范围问题同样适用),其中包含两个按钮,其事件处理程序演示了上述任一方法。

  • 单击任一按钮都会使用不同的文本更新窗体的标题文本,并且这样做的能力意味着已从事件处理程序脚本块内部成功获取了对封闭实例的引用。class

重要:

  • 在通过脚本文件调用以下代码之前,请务必执行以下命令:

    Add-Type -ErrorAction Stop -AssemblyName System.Windows.Forms
    
  • 不幸的是,这是必要的,因为(至少 PowerShell 7.4.0)定义中引用的任何 .NET 类型都必须在分析(加载)脚本之前加载到会话中 - 有关背景信息,请参阅此答案class

# IMPORTANT: 
#  * Run 
#      Add-Type -AssemblyName System.Windows.Forms
#    BEFORE invoking this script.

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

class FormWrapper {
  [Form] $form

  FormWrapper() {
    # Create a form with two buttons that update the form's caption (title text).
    $this.form = [Form] @{ Text = "Sample"; Size = [Size]::new(360, 90); StartPosition = 'CenterScreen' }
    $this.form.Controls.AddRange(@(
        [Button] @{
          Name = 'button1'
          Location = [Point]::new(30, 10); Size = [Size]::new(130, 30)
          Text = "With Get-Variable"
        }
        [Button] @{
          Name = 'button2'
          Location = [Point]::new(190, 10); Size = [Size]::new(130, 30)
          Text = "With GetNewClosure"
        }
      ))

    # Get-Variable approach.
    $this.form.Controls['button1'].add_Click({
        param($sender, [System.EventArgs] $evtArgs)
        # Obtain the shadowed $this variable value to refer to
        # this class instance and access its $form instance variable (property)
        (Get-Variable -Scope 1 -ValueOnly this).form.Text = 'Button clicked (Get-Variable)'
      })

    # Local variable + .GetNewClosure() approach.
    # Define a method-local $self variable to cache the value of $this.
    $self = $this
    # !! YOU MUST CALL .GetNewClosure() on the event-delegate script
    # !! block to capture the local $self variable
    $this.form.Controls['button2'].add_Click({
        param($sender, [System.EventArgs] $evtArgs)
        # Use the captured local $self variable to refer to this class instance.
        $self.form.Text = 'Button clicked (.GetNewClosure)'
      }.GetNewClosure())
  
  }

  ShowDialog() {
    # Display the dialog modally
    $null = $this.form.ShowDialog()
  }

}

# Instantiate the class and show the form.
[FormWrapper]::new().ShowDialog()

[1] 直接父作用域是手头的类实例,但 (a) 似乎没有方法级作用域(因此无法访问方法局部变量)和 (b) 在实例级别,唯一定义的变量是 $this,它是隐藏的,如后面所述(任何实例变量(属性)都必须通过$this访问

评论

0赞 pfeidewigo100 11/5/2023
这正是我一直在寻找的答案类型。易于理解,我只是喜欢你也提供了一个例子。谢谢。我选择了 GetNewClosure,它就像我想要完成的任务的魅力!
0赞 mklement0 11/5/2023
很高兴听到它,@pfeidewigo100;别客气。写一个例子帮助我加深了自己的理解。
0赞 pfeidewigo100 11/6/2023
一些更新(如果你有兴趣的话)。正如我之前所说,使用 GetNewClosure 的解决方案工作得很好。但是,我注意到(到目前为止)每当我按空格键或回车键时,都会创建一个新的事件处理程序 (?)。因此,如果我启动钩子,然后按空格键或回车键,下一个事件将被记录两次,如果我再次按空格键/回车键,则记录三次。只有这两个按钮造成了这个问题,它们甚至不在我实际侦听和执行操作的键之列......
0赞 mklement0 11/6/2023
这很奇怪,@pfeidewigo100 - 我没有解释。基于的解决方案也会发生这种情况吗?Get-Variable
0赞 pfeidewigo100 11/7/2023
这确实看起来很奇怪。我无法让 Get-Variable 解决方案工作,所以我不知道。无论我选择什么范围,它都不会找到“这个”。我还尝试打印示波器中的所有变量,但找不到任何有用的东西。