WebSocket client (Implemented via winhttp)

Post your working scripts, libraries and tools.
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

WebSocket client (Implemented via winhttp)

16 Jan 2023, 08:51

This is a websocket client implemented with winhttp, used to connect to the websocket server.

Usage

Code: Select all

ws := WebSocket(wss_or_ws_url, {
	message: (self, data) => FileAppend(data '`n', '*', 'utf-8'),
	close: (self, status, reason) => FileAppend(status ' ' reason '`n', '*', 'utf-8')
})
ws.sendText('hello'), Sleep(100)
ws.send(0, Buffer(10), 10), Sleep(100)
Download
crocodile
Posts: 98
Joined: 28 Dec 2020, 13:41

Re: WebSocket client (Implemented via winhttp)

23 Jan 2023, 03:38

Sorry for the off-topic comment. I saw the method you posted here to call the "vscode-autohotkey2-lsp server" format code. Can you use this WebSocket() instead of the stdio method? That way different processes can call it via WebSocket().
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

Re: WebSocket client (Implemented via winhttp)

24 Jan 2023, 09:33

@crocodile
If you want to use a socket to connect to lsp server, you need to use a socket to bind the port and listen, rather than a websocket. The two are different.
lexikos
Posts: 9635
Joined: 30 Sep 2013, 04:07
Contact:

Re: WebSocket client (Implemented via winhttp)

22 Jul 2023, 03:48

I am using this to control a device on my LAN, but I had to make one modification to get it to work. The device requires SSL, even though it can't possibly return a valid certificate. There didn't appear to be any way around this with the IE-based WebSocket, but the following line added to connect() allows this script to work:

Code: Select all

DllCall('Winhttp\WinHttpSetOption', 'ptr', hRequest, 'uint', 31, 'uint*', 0x3300, 'uint', 4, 'int')
This ignores certificate errors, so shouldn't be default behaviour. It would be helpful to make this an option somehow.

I also reduced the timeout on connection, but the script still seemed to take 4-5 seconds minimum. For that I tried a few things:

Code: Select all

DllCall('Winhttp\WinHttpSetTimeouts', 'ptr', hSession, 'int', 500, 'int', 500, 'int', 500, 'int', 500, 'int')
DllCall('Winhttp\WinHttpSetTimeouts', 'ptr', hRequest, 'int', 500, 'int', 500, 'int', 500, 'int', 500, 'int')
DllCall('Winhttp\WinHttpSetOption', 'ptr', hRequest, 'uint', 3, 'uint*', 500, 'uint', 4, 'int')
They all had the same result; reducing the timeout from around 60 seconds to around 5.

Perhaps the script could allow for these options via properties of the second parameter, or provide a callback to modify the request prior to sending.

I intended to use the timeout to detect that the device is turned off, so I can toggle it on/off by combining WebSocket with wake on LAN.

Thank you for the useful script!
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

Re: WebSocket client (Implemented via winhttp)

26 Jul 2023, 08:43

lexikos wrote:
22 Jul 2023, 03:48
Perhaps the script could allow for these options via properties of the second parameter, or provide a callback to modify the request prior to sending.
I added the TimeOut parameter, as well as the open event that fires after the request is opened.
The required handles can be obtained through the HINTERNETs array, this.HINTERNETs := [hSession, hConnect, hRequest, hWebSocket].
teadrinker
Posts: 4368
Joined: 29 Mar 2015, 09:41
Contact:

Re: WebSocket client (Implemented via winhttp)

04 Aug 2023, 18:54

Hi, @thqby
thqby wrote: the open event that fires after the request is opened
I found that the open event is fired regardless of whether the url is valid or invalid:

Code: Select all

ws := WebSocket('bla-bla', {
    open: (*) => MsgBox('open')
})
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

Re: WebSocket client (Implemented via winhttp)

05 Aug 2023, 00:09

When there is no protocol header, the default is ws://, so ws://bla-bla should be a valid url.
User avatar
Xeo786
Posts: 760
Joined: 09 Nov 2015, 02:43
Location: Karachi, Pakistan

Re: WebSocket client (Implemented via winhttp)

16 Aug 2023, 08:45

I used you library I manage to connect to Chrome's DebuggerUrl but I failed to receive any event. may be I did something wrong

here is my code

Code: Select all

chrome := Rufaydium()
page := Chrome.NewSession()
page.Url := "https://nowsecure.nl" 
page.CDPCall("Network.enable",Map())

ws := WebSocket(
	"ws://" Page.debuggerAddress "/devtools/page/" Page.currentTab, 
		{
			message: (self, data) => PageMessage,
			text: (self, data) => PageMessage,
 			close: (self, status, reason) => PageClose
	 	}
	)

PageMessage(*)
{
	tooltip "message event"
}

PageClose(*)
{
	tooltip "Page close Event"
}
there is a same working example in Rufaydium AHKV1 where I manage to received tab close event, here
Update 1:
I just debugged step by step and when I close chrome tab manually it is generating the event and I manage to grab it in debugger but it in side the WS class not triggering the callback func, I must be doing something wrong.
image.png
image.png (125.77 KiB) Viewed 2833 times
Update 2:
Now I get it these are not events I need to litrally pass function

following code words

Code: Select all

ws := WebSocket(
	"ws://" Page.debuggerAddress "/devtools/page/" Page.currentTab, 
		{
			message: (self, data) => PageMessage(self, data),
 			close: (self, status, reason) => PageClose(self, status, reason)
	 	}
	)
PageMessage(*)
{
	tooltip "message event"
}

PageClose(*)
{
	tooltip "Page close Event"
}
BTW Thanks thqby for this awsome WS cient
"When there is no gravity, there is absolute vacuum and light travel with no time" -Game changer theory
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

Re: WebSocket client (Implemented via winhttp)

17 Aug 2023, 08:38

I'm using this class to run vscode commands without having to send keys (or having to define keys for commands that don't have assigned keys). It's super fast with unnoticeable latencies.

VSCode doesn't even have to be active.

The limitation is that you need to run different websocket server at a different port for each (simultaneously open) new workspace.

Just make an elementary vscode extension and have it run a websocket server at say port 8980:

Code: Select all

  export async function activate(context: vscode.ExtensionContext) {
	console.log('Your extension "command-runner-ahk" is now active!');

	// Check if the WebSocket server is already running
	const isRunning = await isWebSocketServerRunning(8980);
	if (!isRunning) {
	    // Set up WebSocket server
	    wss = new WebSocket.Server({ port: 8980 });

	            wss.on('connection', (ws: WebSocket) => {
	                console.log('Client connected');

	                ws.on('message', (message: string) => {

	                    // Execute the command in the active workspace
	    						vscode.commands.executeCommand(message.toString()).then(() => {
	    						    console.log(`Executed command: ${message}`);
	    						}, (error) => {
	    						    console.error(`Error executing command: ${message}`, error);
	    						});
	                });

	                ws.on('close', () => {
	                    console.log('Client disconnected');
	                });
	            });

	}
  }
Then in AHK

Code: Select all

ws := WebSocket("ws://localhost:8980", { message: (self, data) => FileAppend(data '`n', '*', 'utf-8'), close: (self, status, reason) => FileAppend(status ' ' reason '`n', '*', 'utf-8') })
ws.Send("search.action.focusNextSearchResult")
ws.Send("fileutils.removeFile")
ws.Send("editor.action.revealDefinition")

neogna2
Posts: 598
Joined: 15 Sep 2016, 15:44

Re: WebSocket client (Implemented via winhttp)

18 Sep 2023, 10:52

I'm trying to reproduce the x64 mcode embedded in WebSocket.ahk
Base64 SIPsSEyL0kGB+AAACAB0CUGB+AAAAAR1MEiLAotSGEyJTCQwRTPJQYH4AAAIAEiJTCQoSYtKCEyNRCQgQQ+UwUiJRCQgQf9SEEiDxEjD
which converts to HEX

Code: Select all

4883EC484C8BD24181F80000080074094181F8000000047530488B028B52184C894C24304533C94181F80000080048894C2428498B4A084C8D442420410F94C1488944242041FF52104883C448C3
I tried taking the c++ source provided in WebSocket.ahk and added a windows.h header line and ran it in godbolt with settings C++ , x64 msvc v19.latest and flags /FAc /O2 like this https://www.godbolt.org/z/7479Yab6d
It compiles without error. Godbolt outputs this mcode

Code: Select all

4883EC48488B05000000004833C448894424384C8BD24181F80000080074094181F8000000047530488B028B52184C894C24304533C94181F80000080048894C2428498B4A084C8D442420410F94C1488944242041FF5210488B4C24384833CCE8000000004883C448C3
which differs from the above. If I use the godbolt mcode (after Base64 conversion) in WebSocket.ahk then it causes thqby's Chrome.ahk to silently fail.

@thqby can you describe how to generate the mcode you used? In another thread you used McodeGen.ahk and MSVC2019 to generate mcode. Is that the case here too?

I think it is an advantage if AutoHotkey scripts and libraries that use mcode not only provide the mcode but also add details on how to produce that mcode (compiler details).
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

Re: WebSocket client (Implemented via winhttp)

18 Sep 2023, 11:19

I forgot what tool was used to generate it, but it is highly likely that it is also this website

00060 e8 00 00 00 00 call __security_check_cookie
Security checks need to be disabled to avoid generating similar code.
https://www.godbolt.org/z/461hn5EM7
neogna2
Posts: 598
Joined: 15 Sep 2016, 15:44

Re: WebSocket client (Implemented via winhttp)

18 Sep 2023, 11:51

I didn't know that, thanks! With flags /FAc /O2 /GC- I get this
https://www.godbolt.org/z/E16cYjYKr
from which we can parse hex mcode

Code: Select all

4883EC484C8BD24181F80000080074094181F8000000047530488B028B52184C894C24304533C94181F80000080048894C2428498B4A084C8D442420410F94C1488944242041FF52104883C448C3
which when converted to Base64 matches the WebSocket.ahk source x64 mcode.

https://learn.microsoft.com/en-us/cpp/build/reference/gs-buffer-security-check?view=msvc-170
/GS (Buffer Security Check) .... /GS is on by default. If you expect your application to have no security exposure, use /GS-
edit: adding this for completion: with x86 msvc v19.latest and the same flags the output mcode (after Base64 conversion) matches the x86 mcode in WebSocket.ahk. So all mcode successfully reproduced.
https://www.godbolt.org/z/xEca7W79x

Code: Select all

8B54240C83EC0C81FA00000800740881FA0000000475358B4C24148B018904248B442410894424048B44241C8944240833C081FA000008000F94C0508D44240450FF710C8B4108FF7104FFD083C40CC21400
Consider adding godbolt short URLs next to the c++ source in the script.
bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: WebSocket client (Implemented via winhttp)

02 Nov 2023, 19:43

Great library!

In case someone else is running into the same problem, when I call a function F in the body of the message eventlistener, sometimes this error will occur:

"Message": "(0x8001010D) An outgoing call cannot be made since the application is dispatching an input-synchronous call."

I was able to solve this problem by replacing the direct function call to F by setTimer, even though I'm not exactly sure why it helped.

This is the code in case anyone is curious (requries v2.1+)

Code: Select all

nodeWSServer(msgObj, callback){
	static callbackMap := Map()
	static correlationIDLast := 0
	static wss := WebSocket("ws://localhost:8271", {
		message: (self, data) {
			replyObj := JSON.parse(data)
			id := replyObj["__correlationID__"]
			replyObj.Delete("__correlationID__")
			if callbackMap.Has(id){
				callbackFunc := callbackMap.Get(id)
				; ↓ this won't work: Error: (0x8001010D) An outgoing call cannot be made since the application is dispatching an input-synchronous call
				; callbackFunc(replyObj) 
				; ↓ this works
				SetTimer(()=>callbackFunc(replyObj), -1)
				callbackMap.Delete(id)
			}
		},
		close: (self, status, reason) {
			tt(status ' ' reason '`n')
		}
	})
	msgObj.__correlationID__ := correlationIDLast++
	callbackMap.Set(msgObj.__correlationID__, callback)
	wss.sendText(JSON.stringify(msgObj))
}
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

Re: WebSocket client (Implemented via winhttp)

02 Nov 2023, 20:24

Is your callback a com object from another thread or process?
The data of websocket is accepted by other thread and synchronized to ahk thread with sendmessage.
Therefore, the callback function is called in the window procedure of ahk.
But in the window process, the outgoing call of com objects is restricted to avoid possible deadlock.
bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: WebSocket client (Implemented via winhttp)

03 Nov 2023, 18:02

thqby wrote:
02 Nov 2023, 20:24
Is your callback a com object from another thread or process?
The data of websocket is accepted by other thread and synchronized to ahk thread with sendmessage.
Therefore, the callback function is called in the window procedure of ahk.
But in the window process, the outgoing call of com objects is restricted to avoid possible deadlock.
Thanks for the explanation. Yes, you are quite right!
The callback isn't itself a com object from another process, but in the course of its execution it invokes a function in a child script created using LoadScript (from ahk2_lib). I imagine that must be the source of the problem.
bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: WebSocket client (Implemented via winhttp)

05 Nov 2023, 06:24

@thqby Would you happen to know if there's a way to receive very long messages, possibly in the MB range? I found that when the length of an incoming message exceeds 8k, the on message handler will not fire, and the websocket instance becomes unable to receive any other message, even short ones.

I changed this line in the source code

Code: Select all

this.HINTERNETs := [], this.async := !!Async, this.cache.Size := 8192, this.url := Url
to

Code: Select all

this.HINTERNETs := [], this.async := !!Async, this.cache.Size := 1048576, this.url := Url

This does *help* in that messages between 8k and 32k can now be received. However, 32k (32768) still seems a hard limit, and setting the cache size above 32k doesn't seem to have any effect.

(I'm using 'ws' module of nodejs to communicate between node and AHK. I seem able to *send* message of any length (i.e. sending messages from ahk to node), the problem is only with *receiving* (i.e. receiving node's (very long) messages in ahk).
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

Re: WebSocket client (Implemented via winhttp)

05 Nov 2023, 07:13

Maybe you're receiving binary data, and binary data is returned via the onData callback function.

Code: Select all

WebSocket(wsurl, {
    data: (this, buf) {
    	; ...
    }
}
bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: WebSocket client (Implemented via winhttp)

05 Nov 2023, 07:25

thqby wrote:
05 Nov 2023, 07:13
Maybe you're receiving binary data, and binary data is returned via the onData callback function.

Code: Select all

WebSocket(wsurl, {
    data: (this, buf) {
    	; ...
    }
}
Thanks thqby. I'm only sending json strings (text frames) back and forth. I tried the onData callback and it's not firing. The message handler fires when the incoming text message from node is smaller than 8k bytes (on the default setting).

I tried googling 'websocket 8192' and it turns out some people had similar issues in other environments. Oh well ...

https://stackoverflow.com/questions/35567020/websocket-client-on-linux-cuts-off-response-after-8192-bytes
https://github.com/TooTallNate/Java-WebSocket/issues/525
https://stackoverflow.com/questions/32309727/what-is-the-maximum-size-of-data-that-can-be-passed-through-a-websocket
User avatar
thqby
Posts: 427
Joined: 16 Apr 2021, 11:18
Contact:

Re: WebSocket client (Implemented via winhttp)

05 Nov 2023, 09:00

@bonobo
Messages over 1m can be received using synchronous mode.
bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: WebSocket client (Implemented via winhttp)

05 Nov 2023, 18:42

thqby wrote:
05 Nov 2023, 09:00
@bonobo
Messages over 1m can be received using synchronous mode.
Thanks thqby, that's good to know.

I did some further testing and added this line under case 1,3 (in static WEBSOCKET_STATUSCHANGE(wp, lp, msg, hwnd)). It appears that when the incoming message is long, a UTF-8 message fragment (case 3) is received at first (with value of dwBytesTransferred at 32764 in my case), but then the remaining message fragments are never processed, so the onMessage handler (in case 2) is never called eventually. On my machine the Tooltip only shows once.

Code: Select all

case 1, 3:	
	Tooltip(JSON.stringify([eBufferType, dwBytesTransferred, rec.Size, offset])) ; ⟵
	rec.Size += dwBytesTransferred
       , DllCall('RtlMoveMemory', 'ptr', rec.Ptr + offset, 'ptr', ws.cache, 'uint', dwBytesTransferred)
       , offset += dwBytesTransferred

Return to “Scripts and Functions (v2)”

Who is online

Users browsing this forum: No registered users and 16 guests