簡體   English   中英

method.Invoke(o, null); 只能從主體調用一次,不能從鍵盤處理程序中調用

[英]method.Invoke(o, null); can be called only once from the body and not from within the keyboard handler

我正在嘗試創建一個低級鍵盤處理程序,以便在檢測到特定鍵盤組合時使用反射啟動外部應用程序。

我可以直接啟動應用程序一次,沒有錯誤,但是從按鍵處理程序內部,應用程序在正確顯示MessageBox對話框后崩潰。

我正在將外部 dll 讀入一個字節數組,對於這個測試,您可以使用任何 .Net 應用程序,一個簡單的 Hello World win 表單應用程序將可以工作。

string filePath = @".\x.exe";
FileStream fs = new FileStream(filePath, FileMode.Open);
BinaryReader br = new BinaryReader(fs);
byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
fs.Close();
br.Close();

然后加載外部Assembly並通過反射執行它:

public Assembly a;
public MethodInfo method;
public object o;

a = Assembly.Load(bin);
method = a.EntryPoint;
o = a.CreateInstance(method.Name);
method.Invoke(o, null); 

這工作一次,但如果我重復上面的代碼再次執行它會失敗。

下面的代碼演示了第二次調用的失敗,如果你注釋掉,你可以看到Left + Down + A的鍵盤檢測失敗

即使您注釋掉程序根目錄中的兩個調用,應用程序也會在第一次檢測到鍵時意外關閉。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Text;
using System.Diagnostics;
using System.Reflection;
using System.IO;
using System.Windows.Input;

public class Program
{
    public static bool lctrlKeyPressed;
    public static bool f1KeyPressed;
    public static bool AKeyPressed;
    public static bool LeftKeyPressed;
    public static bool DownKeyPressed;

    public static Assembly a;
    // search for the Entry Point
    public static MethodInfo method;
    // create an istance of the Startup form Main method
    public static object o;

    [STAThread]
    public static void Main()
    {
        //Application.EnableVisualStyles();
        //Application.SetCompatibleTextRenderingDefault(false);

        string filePath = @".\x.exe";
        FileStream fs = new FileStream(filePath, FileMode.Open);
        BinaryReader br = new BinaryReader(fs);
        byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
        fs.Close();
        br.Close();

        //public Assembly a;
        //public MethodInfo method;
        //public object o;

        a = Assembly.Load(bin);
        method = a.EntryPoint;
        o = a.CreateInstance(method.Name);
        method.Invoke(o, null);

        a = Assembly.Load(bin);
        method = a.EntryPoint;
        o = a.CreateInstance(method.Name);
        method.Invoke(o, null);

        LowLevelKeyboardHook kbh = new LowLevelKeyboardHook();
        kbh.OnKeyPressed += kbh_OnKeyPressed;
        kbh.OnKeyUnpressed += kbh_OnKeyUnPressed;

        kbh.HookKeyboard();

        Application.Run();

        kbh.UnHookKeyboard();
    }

    public static void kbh_OnKeyPressed(object sender, Keys e)
    {
        if (e == Keys.A)
        {
            AKeyPressed = true;
        }
        else if (e == Keys.Left)
        {
            LeftKeyPressed = true;
        }
        else if (e == Keys.Down)
        {
            DownKeyPressed = true;
        }
        CheckKeyCombo();
    }

    public static void kbh_OnKeyUnPressed(object sender, Keys e)
    {
        if (e == Keys.A)
        {
            AKeyPressed = false;
        }
        else if (e == Keys.Left)
        {
            LeftKeyPressed = false;
        }
        else if (e == Keys.Down)
        {
            DownKeyPressed = false;
        }
    }

    public static void CheckKeyCombo()
    {
        if (AKeyPressed && LeftKeyPressed && DownKeyPressed)
        {
            MessageBox.Show("Left Key + Down Key + A Key + Pressed");
            
            //a = Assembly.Load(bin);
            method = a.EntryPoint;
            o = a.CreateInstance(method.Name);
            method.Invoke(o, null);
        }
    }
}

public class LowLevelKeyboardHook
{
    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;
    private const int WM_SYSKEYDOWN = 0x0104;
    private const int WM_KEYUP = 0x101;
    private const int WM_SYSKEYUP = 0x105;

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

    public event EventHandler<Keys> OnKeyPressed;
    public event EventHandler<Keys> OnKeyUnpressed;

    private LowLevelKeyboardProc _proc;
    private IntPtr _hookID = IntPtr.Zero;

    public LowLevelKeyboardHook()
    {
        _proc = HookCallback;
    }

    public void HookKeyboard()
    {
        _hookID = SetHook(_proc);
    }

    public void UnHookKeyboard()
    {
        UnhookWindowsHookEx(_hookID);
    }

    private IntPtr SetHook(LowLevelKeyboardProc proc)
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);

            OnKeyPressed.Invoke(this, ((Keys)vkCode));
        }
        else if (nCode >= 0 && wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP)
        {
            int vkCode = Marshal.ReadInt32(lParam);

            OnKeyUnpressed.Invoke(this, ((Keys)vkCode));
        }

        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
}

這段代碼有兩個相互競爭的問題:

  1. 您不能從Main方法連續兩次啟動應用程序。
  2. 您無法從鍵盤事件處理程序啟動應用程序。

您還沒有發布特定的異常,但是如果我為x.exe創建一個獨立的控制台應用程序,那么我可以復制第一個問題,它會引發此異常:

System.InvalidOperationException: '在應用程序中創建第一個 IWin32Window 對象之前,必須調用 SetCompatibleTextRenderingDefault。

x.exe應用程序中,我可以從 main 方法中刪除以下行以解決此問題。 但它暗示的是,應用程序第二次啟動時,前一次調用仍會加載到當前AppDomain 中

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    //Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
}

如果我們注釋掉測試加載器,則會檢測到第二個異常,如下所示:

System.InvalidOperationException: '在單個線程上啟動第二個消息循環不是有效的操作。 改用 Form.ShowDialog。

因此,即使我們沒有將應用程序兩次加載到同一上下文中的問題,問題是我們試圖在[STAThread]單線程隔間的范圍內啟動一個新的消息循環

如果您不需要對正在啟動的應用程序的引用,那么使用 ProcessStart 啟動外部進程可能會更簡單,這類似於從命令提示符 (CMD) 執行外部應用程序:

ProcessStartInfo ps = new ProcessStartInfo(filePath);
System.Diagnostics.Process.Start(ps);

這非常有效,而且比將程序集加載到內存中並反射到其中要簡單得多,除非您確實需要,否則我不會尋找更復雜的東西,這將適用於您現有的應用程序,並為其他非.網絡應用程序也是如此。

玩一下以下顯示幾個選項的代碼:

    private static string filePath;

    [STAThread]
    public static void Main()
    {
        //Application.EnableVisualStyles();
        //Application.SetCompatibleTextRenderingDefault(false);

        filePath = @".\x.exe";

        // This will start an instance and no wait for it to close
        StartProcess();

        // These will each wait for the launched process to end before moving on 
        WaitForProcess();
        WaitForProcess();

        LowLevelKeyboardHook kbh = new LowLevelKeyboardHook();
        kbh.OnKeyPressed += kbh_OnKeyPressed;
        kbh.OnKeyUnpressed += kbh_OnKeyUnPressed;

        kbh.HookKeyboard();

        Application.Run();

        kbh.UnHookKeyboard();
    }

    private static void StartProcess()
    {
        ProcessStartInfo ps = new ProcessStartInfo(filePath);
        System.Diagnostics.Process.Start(ps);
    }

    private static bool _InProgress = false;
    /// <summary>Launches the external process if it was not already launched, state is managed using <seealso cref="_InProgress"/></summary>
    /// <returns> true if the app was launched, false indicates the process was still waiting.</returns>
    private static bool WaitForProcess()
    {
        if (_InProgress) return false; // do nothing
        _InProgress = true;
        try
        {
            ProcessStartInfo ps = new ProcessStartInfo(filePath);
            System.Diagnostics.Process.Start(ps).WaitForExit();
            return true;
        }
        finally
        {
            _InProgress = false;
        }
    }

    public static void kbh_OnKeyPressed(object sender, Keys e)
    {
        if (e == Keys.A)
        {
            AKeyPressed = true;
        }
        else if (e == Keys.Left)
        {
            LeftKeyPressed = true;
        }
        else if (e == Keys.Down)
        {
            DownKeyPressed = true;
        }
        CheckKeyCombo();
    }

    public static void kbh_OnKeyUnPressed(object sender, Keys e)
    {
        if (e == Keys.A)
        {
            AKeyPressed = false;
        }
        else if (e == Keys.Left)
        {
            LeftKeyPressed = false;
        }
        else if (e == Keys.Down)
        {
            DownKeyPressed = false;
        }
    }

    public static void CheckKeyCombo()
    {
        if (AKeyPressed && LeftKeyPressed && DownKeyPressed)
        {
            MessageBox.Show("Left Key + Down Key + A Key + Pressed");

            //It's up to you if is makes sense to wait for a response or not.
            //StartProcess();
            
            // Wait will still raise the above message box if the other app is still running
            // But it wont't try to launch the app again
            // You can play around with the timing logic for what makes sense for you.
            WaitForProcess();
        }
    }
}

這超出了本文的范圍,但如果您確實需要使用反射來加載外部應用程序,則需要對這兩個應用程序進行更改。

以下是我在通讀后能夠卸載外部程序集的嘗試: https://www.west-wind.com/presentations/dynamicCode/DynamicCode.htm但第一個調用之后的每個調用仍然存在,所以它不是真的卸載根本。

這需要您將 dll 加載代碼放回啟動中,但將byte[] bin存儲為靜態字段,而不是本地方法。

    private static void LoadApp()
    {
        AppDomainSetup domainStarter = new AppDomainSetup();
        domainStarter.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

        AppDomain domain = AppDomain.CreateDomain("XternalApp", null, domainStarter);
        var assembly = domain.Load(bin);
        var cls = assembly.EntryPoint;
        //object instance = assembly.CreateInstance(domain.Load((assembly.FullName, cls.DeclaringType.FullName + "." + cls.Name);
        object result = cls.Invoke(null, null);

        result = null;
        //instance = null;
        cls = null;
        assembly = null;

        GC.Collect();

        AppDomain.Unload(domain);

        //GC.Collect();
    }

對原始應用程序進行這些更改會使上述LoadApp()方法起作用
即使來自鍵盤事件!

    static Form1 instance = null;
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    //[STAThread]
    [MTAThread]
    static void Main()
    {
        if (instance == null)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            instance = new Form1();

            Application.Run(instance);
        }
        else
        {
            // TODO: take this out, just here for effect
            MessageBox.Show("Already running!");
            instance = new Form1();
            instance.Show();
        }
    }

非常感謝大家的參與

基本上我打算讓 C# 代碼做的是這個

  1. X.exe 是由 JitBit Macro Recorder 創建的宏
  2. C# 讀入鍵盤按鈕按下
  3. 當按下一系列鍵盤按鈕時,它會運行 X.exe 宏

這是一種可怕的做事方式

但這是我現在要做的最好的事情

因此,在 C# 代碼中,我需要能夠盡快運行 X.exe

這就是我將它加載到內存中的原因

X.exe 也應該可以多次調用

這是我正在玩的游戲

C# 程序在我玩游戲時在后台運行

如果我按一定順序的鍵盤按鈕,C# 將運行 X.exe

這本質上只是 JitBit Macro Recorder 的一個宏

為什么我使用 Jitbit 宏記錄器?

因為它是一個非常快的宏引擎,而且運行良好

用我剛才說的

非常感謝對 C# 程序的任何進一步貢獻

真心感謝大家!!!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM