Introduction

Internet Explorer classic (IE in the following) was based on ActiveX technology. It was very easy to automate IE for tasks like Webscraping or testing from OLE-aware programming languages like VBA. But Microsoft will end support for IE in the near future and wants users to move to newer browsers like Microsoft Edge.

Microsoft Edge is no longer based on ActiveX technology. Microsoft seems uninterested in creating a drop-in replacement for the IE OLE Object. There are libraries that try to fill this gap using Selenium, see Seleniumbasic as an example. But this requires the installation of a Webdriver, which might not be feasible in some environments. The following solution needs no additional software, apart from a Chrome-based browser.

Keep in mind, that all running Edge procceses must be terminated before running the code. Otherwise the tabs are opened in the currently running process, not the one that has been started and subsequent communication between VBA and Edge fails.

CDP Protocol

The code uses the Chrome Devtools Protocol (CDP) to communicate with the browser. A full documentation of the protocol can be found here . The code implements only a very narrow set of functions:

  • Basic functions to set up the communication channel
  • Navigation to a url
  • Evaluate arbitrary JavaScript expressions in the context of a page and return the result
  • But these functions should suffice to do basic Webscraping. The main code is as follows:

    VB.NET
    ' This is an example of how to use the classes Sub runedge() ' Start Browser Dim objBrowser As clsEdge Set objBrowser = New clsEdge Call objBrowser.start ' Attach to any ("") or a specific page Call objBrowser.attach( " " ) ' navigate Call objBrowser.navigate( " https://google.de" ) Call objBrowser.waitCompletion ' evaluate javascript Call objBrowser.jsEval( " alert(""hi"")" ) ' fill search form (textbox is named q) Call objBrowser.jsEval( " document.getElementsByName(""q"")[0].value=""automate edge vba""" ) ' run search Call objBrowser.jsEval( " document.getElementsByName(""q"")[0].form.submit()" ) ' wait till search has finished Call objBrowser.waitCompletion ' click on codeproject link Call objBrowser.jsEval( " document.evaluate("".//h3[text()='Automate Chrome / Edge using VBA - CodeProject']"", document).iterateNext().click()" ) Call objBrowser.waitCompletion Dim strVotes As String ' if a javascript expression evaluates to a plain type it is passed back to VBA strVotes = objBrowser.jsEval( " ctl00_RateArticle_VountCountHist.innerText" )     MsgBox ( " finish! Vote count is " & strVotes)     objBrowser.closeBrowser End Sub

    The class clsEdge implements the CDP protocol. The CDP protocol is a message-based protocol. Messages are encoded as JSON. To generate and parse JSON, the code uses the VBA-JSON library from here .

    Low-Level Communication with Pipes

    The low-level access to the CDP protocol is avaible by two means: Either Edge starts a small Webserver on a specific port or via pipes. The Webserver lacks any security features. Any user on the computer has access to the webserver. This may pose no risks on single user computers or dedicated virtual containers. But if the process is run on a terminal server with more than one user, this is not acceptable. That's why the code uses pipes to communicate with Edge.

    Edge uses the third file descriptor (fd) for reading messages and the fourth fd for writing messages. Passing fds from a parent process to child process is common under Unix, but not under Windows. The WinApi call to create a child process ( CreateProcess ) allows to setup pipes for the three common fds ( stdin , stdout , stderr ) using the STARTUPINFO structure, see CreateProcessA function (processthreadsapi.h) and STARTUPINFOA structure (processthreadsapi.h) . Other fds cannot be passed to the child process.

    In order to set up the fourth and fifth fds, one must use an undocumented feature of the Microsoft Visual C Runtime ( MSVCRT ): If an application is compiled with Microsoft C, than one can pass the pipes using the lpReserved2 parameter of the STARTUPINFO structure. See " Undocumented CreateProcess " for more details (scroll down the page).

    The structure that can be passed in lpReserved2 is defined in the module modExec .

    VB.NET
    Public Type STDIO_BUFFER number_of_fds As Long crt_flags( 0 To 4 ) As Byte os_handle( 0 To 4 ) As LongPtr End Type

    The structure is defined to pass five fds in the os_handle array. The values for the crt_flags array can be obtained from https://github.com/libuv/libuv/blob/v1.x/src/win/process-stdio.c . The fields of the struct must lie contiguously in memory (packed). VBA aligns struct fields to 4 byte boundaries (on 32-bit systems). That's why a second struct with raw types is defined.

    VB.NET
    Public Type STDIO_BUFFER2 number_of_fds As Long raw_bytes( 0 To 24 ) As Byte End Type

    After populating the STDIO_BUFFER struct , the content is copied using MoveMemory to the STDIO_BUFFER2 struct. The size of 25 bytes is enought to hold crt_flags (5 bytes) and the pointers (20 bytes).

    History

  • 8 th July, 2021: Initial version
  • 18 th August 2021, added support for 64bit Office
  • 3 rd November 2021, some minor improvements
  • ' http://ww3.cad.de/foren/ubb/Forum226/HTML/000794.shtml Private Declare Function CreateToolhelpSnapshot Lib "kernel32" Alias "CreateToolhelp32Snapshot" ( _ ByVal lFlgas As Long, _ ByVal lProcessID As Long) As Long Private Declare Function TerminateProcess Lib "kernel32" ( _ ByVal hProcess As Long, _ ByVal uExitCode As Long) As Long Private Declare Function OpenProcess Lib "kernel32" ( _ ByVal dwDesiredAccess As Long, _ ByVal bInheritHandle As Long, _ ByVal dwProcessId As Long) As Long Private Declare Function ProcessFirst Lib "kernel32" Alias "Process32First" ( _ ByVal hSnapshot As Long, _ ByRef uProcess As PROCESSENTRY32) As Long Private Declare Function ProcessNext Lib "kernel32" Alias "Process32Next" ( _ ByVal hSnapshot As Long, _ ByRef uProcess As PROCESSENTRY32) As Long Private Declare Function CloseHandle Lib "kernel32" ( _ ByVal hObject As Long) As Long Private Type PROCESSENTRY32 dwSize As Long cntUsage As Long th32ProcessID As Long th32DefaultHeapID As Long th32ModuleID As Long cntThreads As Long th32ParentProcessID As Long pcPriClassBase As Long dwflags As Long szexeFile As String * 260 End Type Private Const PROCESS_TERMINATE = &H1 Private Const GW_HWNDNEXT As Long = 2 Public Sub KillEXE(ByVal strEXEName As String) Dim lngHandle As Long, retVal As Long, hTask As Long, lResult As Long Dim pe32current As PROCESSENTRY32 lngHandle = CreateToolhelpSnapshot(2&, 0&) If lngHandle <> 0 Then pe32current.dwSize = Len(pe32current) retVal = ProcessFirst(lngHandle, pe32current) Do While Not (retVal = 0) If InStr(1, pe32current.szexeFile, strEXEName, vbTextCompare) > 0 Then hTask = OpenProcess(PROCESS_TERMINATE, 0&, pe32current.th32ProcessID) lResult = TerminateProcess(hTask, 1&) lResult = CloseHandle(hTask) Exit Sub End If retVal = ProcessNext(lngHandle, pe32current) CloseHandle lngHandle End If End Sub Public Function IsEXErunning(ByVal strEXEName As String) Dim lngHandle As Long, retVal As Long, hTask As Long, lResult As Long Dim pe32current As PROCESSENTRY32 lngHandle = CreateToolhelpSnapshot(2&, 0&) If lngHandle <> 0 Then pe32current.dwSize = Len(pe32current) retVal = ProcessFirst(lngHandle, pe32current) Do While Not (retVal = 0) If InStr(1, pe32current.szexeFile, strEXEName, vbTextCompare) > 0 Then hTask = OpenProcess(PROCESS_TERMINATE, 0&, pe32current.th32ProcessID) ' lResult = TerminateProcess(hTask, 1&) lResult = CloseHandle(hTask) IsEXErunning = True Exit Function End If retVal = ProcessNext(lngHandle, pe32current) CloseHandle lngHandle End If End Function Public Sub test() Dim Response As VbMsgBoxResult If IsEXErunning( " msedge.exe" ) Then Response = MsgBox( " Achtung, MS EDGE läuft bereits." & vbCrLf & vbCrLf & " Soll der Browser jetzt geschlossen werden?" , vbYesNo) If Response = vbNo Then Exit Sub Call KillEXE( " msedge.exe" ) End If End If End Sub
    2) So before the actual call: "Set objBrowser = New clsEdge" we make another query if EDGE is already running:
    Dim Response As VbMsgBoxResult If IsEXErunning( " msedge.exe" ) Then Response = MsgBox( " Achtung, MS EDGE läuft bereits." & vbCrLf & vbCrLf & " Soll der Browser jetzt geschlossen werden?" , vbYesNo) If Response = vbNo Then Exit Function Call KillEXE( " msedge.exe" ) End If End If ' Start Browser Set objBrowser = New clsEdge
    Sign In · View Thread
    Would you please teach me how to use these codes? I still have "Error PeekNamedPipe in readProcCDP Pin".
    Sign In · View Thread I still have this:"Error PeekNamedPipe in readProcCDP"
    I have used your code but nothing changes.
    Sign In · View Thread I included the files and pasted the sample code. I also added the reference to the Scripting for Dictionary. When I try to run the sample code I get an error.
    Edge is opened without problem but than the execution of
    Quote:
    Call objBrowser.attach("")

    leads to the error
    Quote:
    Runetime: 901; Error PeekNamedPipe in readProcCDP

    Sign In · View Thread For me a similiar approach worked. Before opening Edge I kill all running instances with this little function (this takes some seconds but I never had that error again):
    Function Kill_Edges() Dim myWmi As Object , getProc As Variant Dim myProcArr As Variant, tarProc As Variant Set myWmi = GetObject( " winmgmts:" ) getProc = " select * from win32_process where name='msedge.exe'" Set myProcArr = myWmi.ExecQuery(getProc) On Error Resume Next For Each tarProc In myProcArr tarProc.Terminate 0 End Function
    Sign In · View Thread is it possible to attach to edge that already opened without run objBrowser.start() ?

    modified 3-Mar-22 3:07am.

    Sign In · View Thread Just want update base on my experience so far. I think there is no way to attach edge already oppened.
    I also have another issue on automate edge by this code or selenium. The issue is when we found website that blocked automate then probably we will found "Bad response status 400" on some action POST.
    Appreciate for anyone that can share the problem solving for above issue.
    Sign In · View Thread Hello, Chris K23
    "- enable logging" in strcall string seems to be used for log. After deleted, console windows disappears.
    Will it affect the function of the program? Hope to get your reply,Thanks.

    modified 14-Feb-22 19:49pm.

    Sign In · View Thread I removed it also and everything works as expected.
    By the way, does anybody see something in that black window? If this is the log window, I never see any text showing up...
    Sign In · View Thread to hide that window you can set as below on clsEdge
    strCall = """C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"" --remote-debugging-pipe --disable-automation --disable-logging"
    Sign In · View Thread Your code works perfectly thank you so much..
    Iam very new to this so sorry if this is basic question
    Where can i find the exact syntax for other commands (like finding element by id, class or other parameters) .
    because i tried something like
    Call objBrowser.jsEval("document.getElementsByClassName(""xxxx"")[0].form.submit()")
    but it is not working. Please help
    Sign In · View Thread The easiest way to try out commands is the JavaScript console in the developer tools. I think it even has syntax completion.

    Sign In · View Thread If objRes.Item( " result" ).Item( " result" ).Item( " type" ) = " string" Or objRes.Item( " result" ).Item( " result" ).Item( " type" ) = " boolean" Or objRes.Item( " result" ).Item( " result" ).Item( " type" ) = " number" Then jsEval = objRes.Item( " result" ).Item( " result" ).Item( " value" ) End If But if he wants to use "getElementsByTagName", this will give him a collection of objects. Is there a way for the jsEval function to return a collection of objects from the CDP message?
    Sign In · View Thread With the current code:no. If you need to work with non-elementary objects like DOM elements you could code your logic in JavaScript as a workaround. The JavaScript code could then be evaluated in the browser using jseval and return the plain resulting object back to VBA.

    Sign In · View Thread Or you could do it the long way around as I do.
    I use jsEval to get the innerHTML of the main document and then set it
    to MSHTML.HTMLDoc (in Excel VBA).
    I can then use the normal getElementById, getElementsByName etc...
    This is in Excel (rough example)
    Dim webDoc As MSHTML.HTMLDocument ' Later in the code when I need it Set webDoc = objBrowser.jsEval("document.getElementById(' myid ' ).innerHTML;") ' at this point you should be able to use the the following webDoc.getElementById webDoc.getElementsByName
    I have gotten Option List Elements, pretty much anything I need.
    Sign In · View Thread IF you opened Edge with the provided code (through CDP protocol) in the first place, then yes!
    It's the serialize/deserialize functions that will do the trick. You can read previous posts to learn more about it.
    Sign In · View Thread