amazing-andrew / AutoHotkey.Interop

A wrapper to natively interact and embed autohotkey into your .net program.
GNU General Public License v2.0
187 stars 55 forks source link

SendPipeMessage unreliable when used in a hotkey function #9

Open evilC opened 7 years ago

evilC commented 7 years ago
using AutoHotkey.Interop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            //grab a copy of the AutoHotkey singleton instance
            var ahk = AutoHotkeyEngine.Instance;

            var ipcHandler = new Func<string, string>(fromAhk =>
            {
                Console.WriteLine("received message from ahk " + fromAhk);
                //System.Threading.Thread.Sleep(3000); //simulating lots of work
                return ".NET: I LIKE PIE!";
            });

            //the initalize pipes module only needs to be called once per application
            ahk.InitalizePipesModule(ipcHandler);

            ahk.ExecRaw(@"
            BindHotkey(hk, id){
                fn := Func(""HkEvent"").Bind(id, ""1"")
                hotkey, % hk, % fn

                fn := Func(""HkEvent"").Bind(id, ""0"")
                hotkey, % hk "" up"", % fn
            }

            HkEvent(id, state){
                SendPipeMessage(""HK EVENT! ID = "" id "", State = "" state)
            }

            ");

            ahk.ExecRaw(@"BindHotkey(""F12"", ""999"")");

            while (true)
            {
                Thread.Sleep(100);
            }
        }
    }
}

Sometimes a runtime error is thrown ("Functions cannot contain functions")
Sometimes none of the hotkeys work
Hotkeys NEVER fire as often as they should (Spam F12 and you do not see a console log for each press / release)
If you have a release hotkey, after about the 5th press/release, all hotkeys stop firing

evilC commented 7 years ago

It appears that the problem is to do with SendPipeMessage

            var ipcHandler = new Func<string, string>(fromAhk =>
            {
                Console.WriteLine("received message from ahk " + fromAhk);
                return ".NET: I LIKE PIE!";
            });

            ahk.ExecRaw(@"
                F11::
                    Tooltip, % ""HERE - "" A_TickCount
                    return

                F12::
                    SendPipeMessage(""HERE - "" A_TickCount)
                    return
            ");

The F11 hotkey displays a tooltip reliably on every press, but the F12 hotkey does not.

evilC commented 7 years ago

The problem appears to be due to the fact that SendPipeMessage is a two-way communication:

SendPipeMessage(strMessage) {
    global A__PIPECLIENT
    A__PIPECLIENT.write(strMessage)
    sleep, 100
    A__PIPECLIENT_RESULT := A__PIPECLIENT.read()
    sleep, 100
    return A__PIPECLIENT_RESULT
}

Those sleep 100s clearly are causing the problem.

If I comment out everything after A__PIPECLIENT.write(strMessage) and ignore the return value, I get callbacks for all hotkeys.

All I actually want is one-way communication from the script to the host C# code (ie an Action instead of a Func) I tried to change the code in this way, but had no joy.

I have been chatting to HotkeyIt about this, and he reckons this could also be done with ObjShare, although he hasn't done C# before and I have not got a clue how this all works. We'll keep beavering away, but if you are about then I could really do with some help on this one.

amazing-andrew commented 7 years ago

How does the program behave, if you just take the sleeps out ?

evilC commented 7 years ago

Yeah it seems OK if I take them out and don't try to do anything with a return value, but I don't know enough about it to know if it is safe to do so, or could be more performant without it.
HotkeyIt also thought it would be better to implement communication via ObjShare, but neither of use are really sure how to implement this in C# - any ideas?
https://github.com/HotKeyIt/ahkdll/blob/master/source/resources/reslib/ObjShare.ahk

amazing-andrew commented 7 years ago

The CSharp code acts as a Server by hosting the Named Pipe, used for communication. The AHK code acts as a client by making the connection only when it needs to send something.

Currently the named pipe server behaves like the following:

Overall the .NET and AHK processes are waiting on each other before they continue, so they block each other's threads. The sleeps are probably put in by me for testing purposes. Sometimes I find AHK is more stable when you give it some time around important pieces of code.

If you find that your system handles the calls without the two sleeps well, I don't see an issue with removing them. I agree it may perform better.

Also I've looked at ObjShare, and please correct me if i'm wrong. But it looks like a way to share an AHK object between AHK Threads. I'm not sure how this can translate sending a message to the hosting environment and returning a result back from it. Do you have an example in any programming language of the ObjShare being used to call a function in the hosting environment, outside of AHK and then return the result back to AHK? I can look at this and perhaps find a way to replicate it in CSharp.

evilC commented 7 years ago

Not got time to read this all right now, will get back to it, but for now here is an example of inter-thread communication using a function object wrapped with ObjShare:

Main thread launching second thread, passing callback func object wrapped with ObjShare:
https://github.com/evilC/UCR/blob/master/Classes/Profile.ahk#L106

Child thread getting function object:
https://github.com/evilC/UCR/blob/master/Threads/ProfileInputThread.ahk#L14

Child thread using function object:
https://github.com/evilC/UCR/blob/master/Threads/ProfileInputThread.ahk#L195

jaryn-kubik commented 7 years ago

So I've been toying around with this and the SendPipeMessage really is kinda unreliable. However from what I found out, since the ahkdll runs in the same process, you don't even need any sort of IPC for communicating, you can just directly call a C# delegate with DllCall.

Example:

void AHKCallback(string s)
{
   Console.WriteLine(s);
}

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void AHKDelegate([MarshalAs(UnmanagedType.LPWStr)]string s);

AHKDelegate ahkDelegate = AHKCallback;

void writeCrapInConsoleOnPressingA()
{
   IntPtr ptr = Marshal.GetFunctionPointerForDelegate(ahkDelegate);
   var ahk = AutoHotkeyEngine.Instance;
   ahk.ExecRaw(@"a::
      DllCall(" + ptr + @", ""Str"", ""whatever you wanna pass to AHKCallback"")
      return");
}
amazing-andrew commented 7 years ago

I like this method a lot. Should be simple and less hassles implementing this type communication between the two worlds.

evilC commented 7 years ago

So if we switched to this method, does that mean that all the pipes code could be removed?
I need to have lots of copies of AHK running - up to a minimum of about 6, but ideally as many as the user is willing to sacrifice memory for.
Why so many? Because I need one for "Bind Mode" (Has hotkeys to all keyboard and mouse buttons declared) which is used to detect which key the end-user wishes to use, then one copy of AHK per user profile.
This way, it is easy to turn on/off sets of hotkeys - you just Suspend/Unsuspend the copies of AHK as appropriate.
In order to handle "Shifted" inputs, my system uses child profiles - so these must be available pretty much instantly, and could contain hundreds of hotkeys - so in this case they need to be loaded and suspended so they can be activated at very short notice.
I managed to alter the code to not be a singleton, and in order to do so had to comment out a bunch of stuff to do with pipes.
Am hoping maybe switching to this method will also make this possible without having to have my own custom fork.