Skip to content

Commit bf1fd25

Browse files
committed
Initial support for communicating with browser using websocket instead of directly with pipe
1 parent dee4fc0 commit bf1fd25

File tree

6 files changed

+299
-45
lines changed

6 files changed

+299
-45
lines changed

src/Functions.bas

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Public Type SECURITY_ATTRIBUTES
5656
End Type
5757

5858

59-
Public Type STARTUPINFO
59+
Public Type STARTUP_INFO
6060
cb As Long
6161
lpReserved As LongPtr
6262
lpDesktop As LongPtr
@@ -180,4 +180,49 @@ Cleanup:
180180
TerminateProcess = TerminateProcess Or (Not processFound) ' successfully terminated process or no process found
181181
End Function
182182

183+
Public Function SpawnProcess(cmdLine As String) As Boolean
184+
Dim proc As PROCESS_INFORMATION
185+
Dim startupInfo As STARTUP_INFO
186+
Dim sa As SECURITY_ATTRIBUTES
187+
Dim hStdOutRd As LongPtr, hStdOutWr As LongPtr
188+
Dim hStdInRd As LongPtr, hStdInWr As LongPtr
189+
190+
' initialize to default security attributes
191+
sa.nLength = Len(sa)
192+
sa.bInheritHandle = 1&
193+
sa.lpSecurityDescriptor = 0&
194+
195+
' First we create all 3 default pipes, stdin, stdout, stderr (we reuse stdout for stderr)
196+
If CreatePipe(hStdInRd, hStdInWr, sa, 0) = 0 Then
197+
Debug.Print "Error creating pipe for stdin"
198+
Exit Function
199+
End If
200+
If CreatePipe(hStdOutRd, hStdOutWr, sa, 0) = 0 Then
201+
Debug.Print "Error creating pipe for stdout/stderr"
202+
Exit Function
203+
End If
204+
205+
206+
With startupInfo
207+
.cb = Len(startupInfo)
208+
.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
209+
.hStdOutput = hStdOutWr
210+
.hStdInput = hStdInRd
211+
.hStdError = hStdOutWr
212+
.wShowWindow = vbNormal
213+
.cbReserved2 = 0&
214+
.lpReserved2 = 0&
215+
End With
216+
183217

218+
If CreateProcessA(0&, cmdLine, sa, sa, 1&, NORMAL_PRIORITY_CLASS, 0&, 0&, startupInfo, proc) = 0 Then
219+
Debug.Print "Error spawning " & cmdLine
220+
End If
221+
222+
' We close the sides of the handles that we dont need anymore (child process side of pipes)
223+
Call CloseHandle(hStdOutWr)
224+
Call CloseHandle(hStdInRd)
225+
226+
' assume success
227+
SpawnProcess = True
228+
End Function

src/WinHttpCommon.bas

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,18 @@ Public Declare PtrSafe Function WinHttpSendRequest Lib "winhttp" ( _
209209
ByVal dwTotalLength As Long, _
210210
ByVal dwContext As Long _
211211
) As Long
212+
213+
Public Declare PtrSafe Function WinHttpQueryDataAvailable Lib "winhttp" ( _
214+
ByVal hRequest As LongPtr, _
215+
ByVal lpdwNumberOfBytesAvailable As LongPtr _
216+
) As Long
217+
218+
Public Declare PtrSafe Function WinHttpReadData Lib "winhttp" ( _
219+
ByVal hRequest As LongPtr, _
220+
ByRef pvBuffer As Any, _
221+
ByVal dwBufferLength As Long, _
222+
ByRef pdwBytesRead As LongPtr _
223+
) As Long
212224

213225
Public Declare PtrSafe Function WinHttpReceiveResponse Lib "winhttp" ( _
214226
ByVal hRequest As LongPtr, _

src/clsCDP.cls

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ Public sessionId As String
3939
' which browser did we connect to
4040
Public browserProgram As browserType
4141

42-
' link to browser for sending and receiving protocol messages
42+
' link to browser for sending and receiving protocol messages, only 1 of these (objBrowser, wsBrowser) should be valid
4343
Private objBrowser As clsProcess
44+
Private wsBrowser As clsWebSocket
4445

4546
' every message sent over Chrome Developer Protocol has an id, this is id of last message we sent
4647
Private lngLastID As Long
@@ -82,22 +83,24 @@ Private Function searchNull() As Long
8283
End Function
8384

8485

85-
' read in any pending data from connected browser pipe/soccket and append to buffer
86+
' read in any pending data from connected browser pipe/socket and append to buffer
8687
' nonblocking, will return immediately if nothing new to add
8788
Private Sub readRawMessageData()
88-
Dim intRes As Long
89-
Dim strRes As String
90-
91-
' read in all data currently sent from browser to us
92-
intRes = 1
93-
Do Until intRes < 1
94-
DoEvents
95-
intRes = objBrowser.readProcCDP(strRes)
96-
97-
If intRes > 0 Then
98-
strBuffer = strBuffer & strRes
99-
End If
100-
Loop
89+
Dim errorText As String, strResult As String
90+
91+
If Not objBrowser Is Nothing Then
92+
strResult = objBrowser.GetMessageData(errorText)
93+
ElseIf Not wsBrowser Is Nothing Then
94+
strResult = wsBrowser.GetMessageUTF8()
95+
errorText = wsBrowser.errorText
96+
Else
97+
Debug.Print "readRawMessageData() - Error: no implementation to communicate with browser available!"
98+
Stop
99+
End If
100+
101+
If errorText = "None" Then
102+
strBuffer = strBuffer & strResult
103+
End If
101104
End Sub
102105

103106
' store and retrieve messages from a queue event processing or delayed response handling
@@ -132,6 +135,7 @@ End Function
132135
' event handlers are called and if returns true event not queued
133136
' queues and returns any message found
134137
Public Function peakMessage() As Dictionary
138+
On Error GoTo ErrHandler
135139
Dim lngNullCharPos As Long
136140

137141
' get any new data if available since last call
@@ -186,6 +190,11 @@ messageHandled:
186190
End If
187191

188192
DoEvents
193+
Exit Function
194+
ErrHandler:
195+
Debug.Print "peakMessage() - Error: " & Err.description
196+
Stop
197+
Resume
189198
End Function
190199

191200
' sends a CDP message to browser
@@ -207,7 +216,7 @@ Private Function sendMessage(ByVal strMessage As String, Optional ByVal nowait A
207216
' sometimes edge writes to stdout
208217
' we clear stdout here, too.
209218
Dim ignored As String
210-
objBrowser.readProcSTD ignored
219+
'objBrowser.readProcSTD ignored
211220

212221
' We add the currentID and sessionID to the message (assume flat messages and sessionId required)
213222
strMessage = left(strMessage, Len(strMessage) - 1)
@@ -219,7 +228,16 @@ Private Function sendMessage(ByVal strMessage As String, Optional ByVal nowait A
219228
strMessage = strMessage & ", ""id"":" & lngLastID & "}" & vbNullChar
220229

221230
' write message to browser
222-
If objBrowser.writeProc(strMessage) = 0 Then
231+
Dim errorSending As Boolean
232+
If Not objBrowser Is Nothing Then
233+
errorSending = (objBrowser.writeProc(strMessage) = 0)
234+
ElseIf Not wsBrowser Is Nothing Then
235+
errorSending = Not wsBrowser.SendMessageUTF8(strMessage)
236+
Else
237+
errorSending = True
238+
Debug.Print "Warning objBrowser and wsBrowser are both nothing, unable to communicate with browser!"
239+
End If
240+
If errorSending Then
223241
Debug.Print "-----"
224242
Debug.Print "Failed to write CDP message!"
225243
Debug.Print strMessage
@@ -381,56 +399,109 @@ End Function
381399
' This method starts up the browser
382400
' It will attempt to terminate the browser if found to already be running (user is prompted prior to abort)
383401
' If autoAttach is True then after connection to browser established will initiate attach call automatically
402+
' if useWebSocket is True then will connect to browser websocket on localhost port 9222, otherwise connects directly via a pipe
403+
' if useWebSocket is True and useExistingBrowser then does not (kill and) spawn browser before connecting, ignored if useWebSocket is false
384404
' Warning! if autoAttach is True then the url must exactly match and may fail if browser changes expected url unless partialMatch is True
385405
' Returns True if successfully initialized browser, False otherwise
386-
Public Function launch(Optional url As String = vbNullString, Optional autoAttach As Boolean = True, Optional partialMatch As Boolean = True) As Boolean
406+
Public Function launch( _
407+
Optional url As String = vbNullString, _
408+
Optional autoAttach As Boolean = True, _
409+
Optional partialMatch As Boolean = True, _
410+
Optional useWebSocket As Boolean = False, _
411+
Optional useExistingBrowser As Boolean = False _
412+
) As Boolean
387413
' ensure browser is not already running (kill it if it is)
388-
If Not TerminateProcess(ProcessName:="msedge.exe", PromptBefore:=True) Then
389-
' abort if browser is already running and failed to kill it or user elected not to terminate
390-
Exit Function
414+
If Not (useWebSocket And useExistingBrowser) Then
415+
If Not TerminateProcess(ProcessName:="msedge.exe", PromptBefore:=True) Then
416+
' abort if browser is already running and failed to kill it or user elected not to terminate
417+
Exit Function
418+
End If
391419
End If
392420

393-
Set objBrowser = New clsProcess
394-
395421
Dim strCall As String
396422
' --remote-debugging-pipe allow communicating with Edge
397423
' --remote-debugging-port=9222 alternative for communication via TCP
398424
' --enable-automation can be omitted for Edge to avoid displaying "being controlled banner", but necessary for Chrome
399425
' --disable-infobars removes messages such as "being controlled by automation" banners, ignored by Edge, used by Chrome
400426
' --enable-logging allows viewing additional connection details, but causes opening of console windows
401427
' --user-data-dir=c:\temp\fakeEdgeUser to open in different profile, and therefore session than current user, but screws up because Admin managed user so don't use
402-
strCall = """C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"" --remote-debugging-pipe " & url
428+
429+
' only one of objBrowser or wsBrowser can be valid, depending how we are connecting initialize the correct one
430+
If useWebSocket Then
431+
Set objBrowser = Nothing
432+
Set wsBrowser = New clsWebSocket
433+
strCall = """C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"" --remote-debugging-port=9222 " & url
434+
If Not useExistingBrowser Then
435+
If Not SpawnProcess(strCall) Then Exit Function
436+
End If
437+
438+
' give it a bit to startup
439+
Sleep 1
440+
441+
' get the path to the browser target websocket - we hard code only connecting to localhost port 9222
442+
Dim wsPath As String
443+
444+
' we get the path (really full link) by connecting via HTTP to http://localhost:9222/json/version - note 9222 is same as specified on cmdline
445+
' and extracting the information from the webSocketDebuggerUrl field of the returned JSON
446+
' eg. "ws://localhost:9222/devtools/browser/f875efa4-b4ee-4c35-848b-73bc384a32bb"
447+
wsPath = "/devtools/browser/f875efa4-b4ee-4c35-848b-73bc384a32bb"
448+
wsPath = wsBrowser.HttpGetMessage("localhost", 9222, "/json/version")
449+
Dim versionInfo As Dictionary
450+
Set versionInfo = JsonConverter.ParseJson(wsPath)
451+
wsPath = versionInfo("webSocketDebuggerUrl")
452+
wsPath = Right(wsPath, Len(wsPath) - Len("ws://localhost:9222"))
453+
454+
' connect to the browser target websocket
455+
With wsBrowser
456+
.protocol = "ws://"
457+
.server = "localhost"
458+
.port = 9222
459+
.path = wsPath ' this one changes with each time browser is started
460+
If Not .Connect() Then Exit Function ' False
461+
End With
462+
Else
463+
Set objBrowser = New clsProcess
464+
Set wsBrowser = Nothing
465+
strCall = """C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"" --remote-debugging-pipe " & url
466+
467+
Dim intRes As Integer
468+
intRes = objBrowser.init(strCall)
469+
If intRes <> 0 Then
470+
'Call Err.Raise(-99, , "error start browser")
471+
Exit Function ' False
472+
End If
473+
474+
' give it a bit to startup
475+
Sleep 1
476+
End If
477+
478+
' for now we only support Edge, but eventually we will add the logic to spawn/recognize Chrome and FireFox along with Edge
403479
browserProgram = browserType.Edge
404480

405-
Dim intRes As Integer
406-
407-
intRes = objBrowser.init(strCall)
408-
409-
If intRes <> 0 Then
410-
'Call Err.Raise(-99, , "error start browser")
411-
Exit Function ' False
412-
End If
413-
414-
Call Sleep
481+
' initialize message id used to track message responses
415482
lngLastID = 1
416483

484+
' if connected via pipe we need to clear pipe from any data written during startup
417485
Dim strRes As String
418-
419486
intRes = 0
420487

421488
Dim intCounter As Integer
422489
intCounter = 0
423490

424-
Do Until intRes > 0 Or intCounter > 1000
491+
Do Until intRes > 0 Or intCounter > 1000 Or useWebSocket
425492
intRes = objBrowser.readProcSTD(strRes)
426493
DoEvents
427494
Call Sleep(0.1)
428495
intCounter = intCounter + 1
429496
Loop
430497

431498
' automatically attach to requested page or any page if no specific one requested
499+
' WARNING if we didn't spawn browser then url may not match any page Target!
432500
If autoAttach Then
433-
If Me.attach(url, partialMatch:=partialMatch) = vbNullString Then Exit Function ' failed to attach on launch
501+
If Me.attach(url, partialMatch:=partialMatch) = vbNullString Then
502+
If useWebSocket Then wsBrowser.Disconnect
503+
Exit Function ' failed to attach on launch
504+
End If
434505
End If
435506

436507
' assume success

src/clsProcess.cls

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ Private hCDPInRd As LongPtr
2424

2525
Public Function init(strExec As String) As Integer
2626
Dim proc As PROCESS_INFORMATION
27-
Dim start As STARTUPINFO
27+
Dim start As STARTUP_INFO
2828
Dim sa As SECURITY_ATTRIBUTES
2929
Dim hReadPipe As LongPtr, hWritePipe As LongPtr
3030
Dim L As Long, result As Long, bSuccess As Long
31-
Dim Buffer As String
31+
Dim buffer As String
3232
Dim k As Long
3333

3434
Dim pipes As STDIO_BUFFER
@@ -191,6 +191,34 @@ Public Function readProcSTD(ByRef strData As String) As Integer
191191

192192
End Function
193193

194+
' reads all pending data from connected browser pipe and returns
195+
' nonblocking, will return immediately if nothing new to add
196+
' On success (no error) then errorText will set to "None"
197+
' returns "" if error or no data available
198+
' otherwise will return all data currently available to be read (full message)
199+
Public Function GetMessageData(ByRef errorText As String) As String
200+
Dim intRes As Long, strRes As String
201+
202+
' read in all data currently sent from browser to us
203+
intRes = 1
204+
Do Until intRes < 1
205+
DoEvents
206+
intRes = readProcCDP(strRes)
207+
208+
If intRes > 0 Then
209+
GetMessageData = GetMessageData & strRes
210+
End If
211+
Loop
212+
213+
If intRes >= -1 Then
214+
errorText = "None"
215+
Else
216+
errorText = "Error reading from pipe"
217+
GetMessageData = vbNullString
218+
End If
219+
End Function
220+
221+
194222
' This functions sends a CDP message to edge
195223
Public Function writeProc(ByVal strData As String) As Integer
196224
Dim lngWritten As Long

0 commit comments

Comments
 (0)