[Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post your working scripts, libraries and tools for AHK v1.1 and older
eleet
Posts: 5
Joined: 24 Apr 2022, 04:25

[Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by eleet » 24 Apr 2022, 05:29

edge.ahk
I am sharing modified script version of chrome.ahk for Edge as chrome is not allowed in our organization and Edge is the only option there.
Original post for Chrome is viewtopic.php?t=42890

Automate Edge using native AutoHotkey.

Edge must be started in debug mode:
Edit Edge shortcut properties and add to Target: --remote-debugging-port=9222
For example: "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --profile-directory=Default --remote-debugging-port=9222
To check if Edge was opened in debug mode open: http://localhost:9222/json/version
The opened page should show some data like: "Browser": "Edg/...

Example script:

Code: Select all

F2::
ifWinActive, ahk_class Chrome_WidgetWin_1  ;if Edge active
{
	#Include C:\Users\someuser\Desktop\scripts\edge.ahk ;change it to your script location
	if (Edges := Edge.FindInstances()){
		EdgeInst := {"base": Edge, "DebugPort": Edges.MinIndex()}	; or if you know the port:  EdgeInst := {"base": Edge, "DebugPort": 9222}
	}
	else {
		msgbox That didn't work. Please check if Edge is running in debug mode.`n(use, for example, http://localhost:9222/json/version )
		return
	}
	; --- Connect to the page ---
	if(Pwb:=EdgeInst.GetPage(1)){ ;connect to active tab
                Pwb.Evaluate("alert('connected');") ;execute js and alert test text in active tab
		title:=Pwb.Evaluate("document.title").Value ;get title from active tab using js
		url:=Pwb.Evaluate("document.location.href").Value ;get location from active tab using js
                msgbox, active Edge tab Title and Url: %title% %url%
         }
        Pwb.Disconnect()
}
Exit
To connect to inactive tab use functions like: GetPageByURL(Url) or GetPageByTitle(title) etc.

Sometimes function WaitWebPageLoad(Pwb) does not work as expected so I wrote custom function which waits for certain page DOM element to load. It maybe useful:

Case use examle:

Code: Select all

CheckIfElementExistEDGE(Pwb,"document.querySelector('#LabelTitle').innerText",5,"Names","Table with label 'Names' did not appear. Do you want to wait for it to appear?")
or

Code: Select all

CheckIfElementExistEDGE(Pwb,"document.title")

Code: Select all

CheckIfElementExistEDGE(Pwb,element,sectowait=5,property="null",custom_alert="default"){
	now:=A_TickCount
	Loop ;wait the page to update
	{
		timeout := A_TickCount-now
		if(timeout > (sectowait*1000)){ ;timout 
			if (custom_alert="default"){
				custom_alert="Timeout passed %sectowait%s.`nCould not find DOM element:`n%element%`nDo you want to wait %sectowait%s again?"
			}
			MsgBox, 4,, %custom_alert%
			IfMsgBox Yes
				now:=A_TickCount
			else{
				Pwb.Disconnect()
				Exit
			}
		}
		if (property="null"){	
			try{
				if (Pwb.Evaluate("try{let testifloaded=()=>(" element "==" property ")?'0':'1'; testifloaded();}catch{}").value="1") ;checking if DOM element loaded with JS
					break
			}
		}
		else{
			try{
				if (Pwb.Evaluate(element).value=property)
					break
			}
		}	
		Sleep 100
	}
}
Attachments
edge.ahk
(21.76 KiB) Downloaded 788 times

silentNinja
Posts: 1
Joined: 10 Jun 2022, 10:34

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by silentNinja » 10 Jun 2022, 10:44

Dude thanks for sharing this! This is just what I needed!

My company uses an outdated cloud based call center app that doesn't function correctly in Chrome so every now and then it has connections issues which crashes my AutoHotKey script that is using the Chrome debugging library.

After hours of digging into the Call Center application's JavaScript code, I found it was using an outdated XMLHttpRequest library that only works in Edge/IE apparently (According to the notes left by the developer xD).

I am hoping once I switch to Edge and modify my script, it resolves my AutoHotKey freezing issue.

gregster
Posts: 9002
Joined: 30 Sep 2013, 06:48

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by gregster » 10 Jun 2022, 11:03

Perhaps, but since Edge is now Chromium-based (that's the reason why a slightly modified Chrome.ahk works with it), I guess it could have similar problems. But it's worth a try.

User avatar
GabeCalderon
Posts: 7
Joined: 19 Aug 2022, 01:24

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by GabeCalderon » 19 Aug 2022, 01:58

@eleet
Thank you very much for posting this customized library. :dance: :clap:

I have personally used it for the past two months-ish to automate functionality on an online question bank + wrote a lot of custom stuff to handle Microsoft's "Read Aloud" feature on web pages which was pretty tricky. Tricky because the built-in feature creates its own "sandboxed" document object within the tab session and does not respond to javascript from the page's Dev Tools console. I would be happy to share more about that Microsoft "Read Aloud" feature automation using AHK if anyone was interested in what tricks I found.

I was working on evaluating JS with your library, and I tried setting the Timeout parameter to something other than 30 seconds, but then I realized that my custom Timeout integer was never being set whenever I used the parameter inside of the ".Evaluate(JS, Timeout)." I believe I have found the potential error/bug? Note that I was using AHK v1.1.34.02 on a Windows 11 machine for any reference.

The current code is:

Code: Select all

Evaluate(JS, Timeout:=30)
{
   response := this.Call("Runtime.evaluate",
   ( LTrim Join
   {
      "expression": JS,
      "objectGroup": "console",
      "includeCommandLineAPI": Edge.Jxon_True(),
      "silent": Edge.Jxon_False(),
      "returnByValue": Edge.Jxon_False(),
      "userGesture": Edge.Jxon_True(),
      "awaitPromise": Edge.Jxon_False()
   }
   ), Timeout)
   
   if (response.exceptionDetails)
      throw Exception(response.result.description, -1
         , Edge.Jxon_Dump({"Code": JS
         , "exceptionDetails": response.exceptionDetails}))
   
   return response.result
}
The "Call" function has a 2nd additional optional parameter "WaitForResponse:=True" that comes before Timeout.

Code: Select all

Call(DomainAndMethod, Params:="", WaitForResponse:=True, Timeout:=30)
The WaitForResponse parameter was never set in the "Call" inside of "Evaluate," therefore, I believe the revised code could be:

Code: Select all

Evaluate(JS, Timeout:=30)
{
   response := this.Call("Runtime.evaluate",
   ( LTrim Join
   {
      "expression": JS,
      "objectGroup": "console",
      "includeCommandLineAPI": Edge.Jxon_True(),
      "silent": Edge.Jxon_False(),
      "returnByValue": Edge.Jxon_False(),
      "userGesture": Edge.Jxon_True(),
      "awaitPromise": Edge.Jxon_False()
   }
   ), WaitForResponse:=True, Timeout:=Timeout)
   
   if (response.exceptionDetails)
      throw Exception(response.result.description, -1
         , Edge.Jxon_Dump({"Code": JS
         , "exceptionDetails": response.exceptionDetails}))
   
   return response.result
}

tester
Posts: 84
Joined: 10 Jun 2021, 23:03

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by tester » 04 Sep 2022, 03:02

Hi,

Thanks for the library. It seems Edge opens an initial page with the message "Welcome to Microsoft Edge, the best performing browser on Windows" with options for the user to choose when launched for the first time with a new profile directory. I'm looking for a way to disable it.

Any ideas?

Edit: Never mind, The "--no-first-run" option seems to work.

Code: Select all

EdgeInst := new Edge(A_ScriptDir "\EdgeProfile",, "--no-first-run")

eleet
Posts: 5
Joined: 24 Apr 2022, 04:25

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by eleet » 01 Dec 2022, 10:30

I noticed that sometimes Edge can't be opened in debug mode due to previous sessions still hanging in the memory. It can be spotted by clicking ctrl+shift+esc. Therefore, all the processes of edge should be killed before launching in debug mode. To overcome the issue one might create cmd file like "edgekill.cmd" and paste bellow commands. It will kill all the processes upon running and launch the browser in debug mode.

Taskkill /IM msedge.exe /F
cd "C:\Program Files (x86)\Microsoft\Edge\Application\"
start /b msedge.exe --profile-directory-Default --remote-debugging-port=9222

User avatar
labrint
Posts: 379
Joined: 14 Jun 2017, 05:06
Location: Malta

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by labrint » 03 Mar 2023, 01:28

Dear All,

When running the edge for the first time 2 or 3 pages come up that ask for edge preferences, to allow or not allow. How do we get past those programmatically?

It looks like this:
image.png
image.png (249.46 KiB) Viewed 6003 times
Really good job, I managed to use it to automate some logins and data extractions.

Thanks

locoleos
Posts: 4
Joined: 27 Mar 2023, 06:57

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by locoleos » 30 Mar 2023, 06:38

This is lovely. Is it important that the remote debugging be running on port 9222 specifically? Because with my orgs' rules, imma gonna have to set it manually via edge flags instead of by modifying the shortcut, and Im not seeing a way to specify the port in there.

eleet
Posts: 5
Joined: 24 Apr 2022, 04:25

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by eleet » 02 Apr 2023, 01:53

No you can change port to which ever you like. Just give it a try.

eleet
Posts: 5
Joined: 24 Apr 2022, 04:25

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by eleet » 02 Apr 2023, 02:05

labrint wrote:
03 Mar 2023, 01:28
Dear All,

When running the edge for the first time 2 or 3 pages come up that ask for edge preferences, to allow or not allow. How do we get past those programmatically?

It looks like this:

image.png

Really good job, I managed to use it to automate some logins and data extractions.

Thanks
I guess you can click the button with Autohotkey ControlClick command, by detecting id using autohotkey winspy tool , can't you?

User avatar
labrint
Posts: 379
Joined: 14 Jun 2017, 05:06
Location: Malta

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by labrint » 08 Apr 2023, 02:15

Hi, Microsoft edge update is not allowing the program to work anymore.

Does anybody have the newer library?

____________________________________________________________________

Edit: Issue fixed using the latest Chrome.ahk from @geek as well as using the @malcev 's patch to the update as follows:

Add . " --remote-allow-origins=*" below . " --remote-debugging-port=" this.DebugPort

Code: Select all

		. " --remote-debugging-port=" this.DebugPort
		. " --remote-allow-origins=*"
Source: viewtopic.php?f=6&t=42890&start=620

Thank you @geek and @malcev . What a great code!

Special thanks to @HeXaDeCiMaToR since I would not have found what im looking for without your post in another thread: viewtopic.php?t=114872

The following is the updated Edge.ahk code:

Code: Select all

; Copyright GeekDude 2021
; https://github.com/G33kDude/Chrome.ahk
class Edge
{
	static DebugPort := 9222
	
	/*
		Escape a string in a manner suitable for command line parameters
	*/
	CliEscape(Param)
	{
		return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
	}
	
	/*
		Finds instances of msedge in debug mode and the ports they're running
		on. If no instances are found, returns a false value. If one or more
		instances are found, returns an associative array where the keys are
		the ports, and the values are the full command line texts used to start
		the processes.
		
		One example of how this may be used would be to open msedge on a
		different port if an instance of msedge is already open on the port
		you wanted to used.
		
		```
		; If the wanted port is taken, use the largest taken port plus one
		DebugPort := 9222
		if (Edges := Edge.FindInstances()).HasKey(DebugPort)
			DebugPort := Edges.MaxIndex() + 1
		EdgeInst := new Edge(ProfilePath,,,, DebugPort)
		```
		
		Another use would be to scan for running instances and attach to one
		instead of starting a new instance.
		
		```
		if (Edges := Edge.FindInstances())
			EdgeInst := {"base": Edge, "DebugPort": Edges.MinIndex()}
		else
			EdgeInst := new Edge(ProfilePath)
		```
	*/
	FindInstances()
	{
		static Needle := "--remote-debugging-port=(\d+)"
		Out := {}
		for Item in ComObjGet("winmgmts:")
			.ExecQuery("SELECT CommandLine FROM Win32_Process"
			. " WHERE Name = 'msedge.exe'")
			if RegExMatch(Item.CommandLine, Needle, Match)
				Out[Match1] := Item.CommandLine
		return Out.MaxIndex() ? Out : False
	}
	
	/*
		ProfilePath - Path to the user profile directory to use. Will use the standard if left blank.
		URLs        - The page or array of pages for Edge to load when it opens
		Flags       - Additional flags for msedge when launching
		EdgePath  - Path to msedge.exe, will detect from start menu when left blank
		DebugPort   - What port should Edge's remote debugging server run on
	*/
	__New(ProfilePath:="EdgeProfile", URLs:="about:blank", Flags:="", EdgePath:="", DebugPort:="")
	{
		if (ProfilePath == "")
			throw Exception("Need a profile directory")
		; Verify ProfilePath
		if (!InStr(FileExist(ProfilePath), "D"))
		{
			FileCreateDir, %ProfilePath%
			if (ErrorLevel = 1)
				throw Exception("Failed to create the profile directory")
		}
		this.ProfilePath := ProfilePath
		
		; Verify EdgePath
		if (EdgePath == "")
			; By using winmgmts to get the path of a shortcut file we fix an edge case where the path is retreived incorrectly
			; if using the ahk executable with a different architecture than the OS (using 32bit AHK on a 64bit OS for example)
			 try EdgePath := ComObjGet("winmgmts:").ExecQuery("Select * from Win32_ShortcutFile where Name=""" StrReplace(A_StartMenuCommon "\Programs\Microsoft Edge.lnk", "\", "\\") """").ItemIndex(0).Target
			; FileGetShortcut, %A_StartMenuCommon%\Programs\Microsoft Edge.lnk, EdgePath
		if (EdgePath == "")
			RegRead, EdgePath, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe
		if !FileExist(EdgePath)
			throw Exception("Edge could not be found")
		this.EdgePath := EdgePath
		
		; Verify DebugPort
		if (DebugPort != "")
		{
			if DebugPort is not integer
				throw Exception("DebugPort must be a positive integer")
			else if (DebugPort <= 0)
				throw Exception("DebugPort must be a positive integer")
			this.DebugPort := DebugPort
		}
		
		; Escape the URL(s)
		for Index, URL in IsObject(URLs) ? URLs : [URLs]
			URLString .= " " this.CliEscape(URL)
		
		Run, % this.CliEscape(EdgePath)
		. " --remote-debugging-port=" this.DebugPort
		. " --remote-allow-origins=*"
		. (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
		. (Flags ? " " Flags : "")
		. URLString
		,,, OutputVarPID
		this.PID := OutputVarPID
	}
	
	/*
		End Edge by terminating the process.
	*/
	Kill()
	{
		Process, Close, % this.PID
	}
	
	/*
		Queries msedge for a list of pages that expose a debug interface.
		In addition to standard tabs, these include pages such as extension
		configuration pages.
	*/
	GetPageList()
	{
		http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
		StartTime := A_TickCount
		while (A_TickCount-StartTime < 10*1000)
		{
			; It is easy to fail here because "new msedge()" takes a long time to execute.
			; Therefore, it will be tried again and again within 10 seconds until it succeeds or timeout.
			try
			{
				http.Open("GET", "http://127.0.0.1:" this.DebugPort "/json", true)
				http.Send()
				http.WaitForResponse(-1)
				if (http.Status = 200)
					break
			}
			Sleep, 50
		}
		return this.Jxon_Load(http.responseText)
	}
	
	/*
		Returns a connection to the debug interface of a page that matches the
		provided criteria. When multiple pages match the criteria, they appear
		ordered by how recently the pages were opened.
		
		Key        - The key from the page list to search for, such as "url" or "title"
		Value      - The value to search for in the provided key
		MatchMode  - What kind of search to use, such as "exact", "contains", "startswith", or "regex"
		Index      - If multiple pages match the given criteria, which one of them to return
		Timeout    - Maximum number of seconds to wait for the page connection
		fnCallback - A function to be called whenever message is received from the page
	*/
	GetPageBy(Key, Value, MatchMode:="exact", Index:=1, Timeout:=30, fnCallback:="", fnClose:="")
	{
		Count := 0
		for n, PageData in this.GetPageList()
		{
			if (((MatchMode = "exact" && PageData[Key] = Value) ; Case insensitive
			|| (MatchMode = "contains" && InStr(PageData[Key], Value))
			|| (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1)
			|| (MatchMode = "regex" && PageData[Key] ~= Value))
			&& ++Count == Index)
				return new this.Page(PageData.webSocketDebuggerUrl, Timeout, fnCallback, fnClose)
		}
	}
	
	/*
		Shorthand for GetPageBy("url", Value, "startswith")
	*/
	GetPageByURL(Value, MatchMode:="startswith", Index:=1, Timeout:=30, fnCallback:="", fnClose:="")
	{
		return this.GetPageBy("url", Value, MatchMode, Index, Timeout, fnCallback, fnClose)
	}
	
	/*
		Shorthand for GetPageBy("title", Value, "startswith")
	*/
	GetPageByTitle(Value, MatchMode:="startswith", Index:=1, Timeout:=30, fnCallback:="", fnClose:="")
	{
		return this.GetPageBy("title", Value, MatchMode, Index, Timeout, fnCallback, fnClose)
	}
	
	/*
		Shorthand for GetPageBy("type", Type, "exact")
		
		The default type to search for is "page", which is the visible area of
		a normal Edge tab.
	*/
	GetPage(Index:=1, Type:="page", Timeout:=30, fnCallback:="", fnClose:="")
	{
		return this.GetPageBy("type", Type, "exact", Index, Timeout, fnCallback, fnClose)
	}
	
	/*
		Connects to the debug interface of a page given its WebSocket URL.
	*/
	class Page
	{
		Connected := False
		ID := 0
		Responses := []
		
		/*
			wsurl      - The desired page's WebSocket URL
			timeout    - Maximum number of seconds to wait for the page connection
			fnCallback - A function to be called whenever message is received
			fnClose    - A function to be called whenever the page connection is lost
		*/
		__New(wsurl, timeout:=30, fnCallback:="", fnClose:="")
		{
			this.fnCallback := fnCallback
			this.fnClose := fnClose
			; Here is no waiting for a response so no need to add a timeout
			this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)
			
			; TODO: Throw exception on invalid objects
			if IsObject(wsurl)
				wsurl := wsurl.webSocketDebuggerUrl
			
			wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
			this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
			this.ws.__New(wsurl, timeout)
			
			; The timeout here is perhaps duplicated with the previous line
			StartTime := A_TickCount
			while !this.Connected
			{
				if (A_TickCount-StartTime > timeout*1000)
					throw Exception("Page connection timeout")
				else
					Sleep, 50
			}
		}
		
		/*
			Calls the specified endpoint and provides it with the given
			parameters.
			
			DomainAndMethod - The endpoint domain and method name for the
				endpoint you would like to call. For example:
				PageInst.Call("Browser.close")
				PageInst.Call("Schema.getDomains")
			
			Params - An associative array of parameters to be provided to the
				endpoint. For example:
				PageInst.Call("Page.printToPDF", {"scale": 0.5 ; Numeric Value
					, "landscape": Edge.Jxon_True() ; Boolean Value
					, "pageRanges: "1-5, 8, 11-13"}) ; String value
				PageInst.Call("Page.navigate", {"url": "https://autohotkey.com/"})
			
			WaitForResponse - Whether to block until a response is received from
				Edge, which is necessary to receive a return value, or whether
				to continue on with the script without waiting for a response.
			
			Timeout - Maximum number of seconds to wait for a response.
		*/
		Call(DomainAndMethod, Params:="", WaitForResponse:=True, Timeout:=30)
		{
			if !this.Connected
				throw Exception("Not connected to tab")
			
			; Use a temporary variable for ID in case more calls are made
			; before we receive a response.
			ID := this.ID += 1
			this.ws.Send(Edge.Jxon_Dump({"id": ID
			, "params": Params ? Params : {}
			, "method": DomainAndMethod}))
			
			if !WaitForResponse
				return
			
			; Wait for the response
			this.responses[ID] := False
			StartTime := A_TickCount
			while !this.responses[ID]
			{
				if (A_TickCount-StartTime > Timeout*1000)
					throw Exception(DomainAndMethod " response timeout")
				else
					Sleep, 50
			}
			
			; Get the response, check if it's an error
			response := this.responses.Delete(ID)
			if (response.error)
				throw Exception("Edge indicated error in response",, Edge.Jxon_Dump(response.error))
			
			return response.result
		}
		
		/*
			Run some JavaScript on the page. For example:
			
			PageInst.Evaluate("alert(""I can't believe it's not IE!"");")
			PageInst.Evaluate("document.getElementsByTagName('button')[0].click();")
		*/
		Evaluate(JS, Timeout:=30)
		{
			response := this.Call("Runtime.evaluate",
			( LTrim Join
			{
				"expression": JS,
				"objectGroup": "console",
				"includeCommandLineAPI": Edge.Jxon_True(),
				"silent": Edge.Jxon_False(),
				"returnByValue": Edge.Jxon_False(),
				"userGesture": Edge.Jxon_True(),
				"awaitPromise": Edge.Jxon_False()
			}
			), Timeout)
			
			if (response.exceptionDetails)
				throw Exception(response.result.description, -1
					, Edge.Jxon_Dump({"Code": JS
					, "exceptionDetails": response.exceptionDetails}))
			
			return response.result
		}
		
		/*
			Waits for the page's readyState to match the DesiredState.
			
			DesiredState - The state to wait for the page's ReadyState to match
			Interval     - How often it should check whether the state matches
			Timeout      - Maximum number of seconds to wait for the page's ReadyState to match
		*/
		WaitForLoad(DesiredState:="complete", Interval:=100, Timeout:=30)
		{
			StartTime := A_TickCount
			;while this.Evaluate("document.readyState").value != DesiredState
			;while this.Evaluate("document.readyState").value != DesiredState && this.Evaluate("document.readyState").value !=4
			while this.Evaluate("document.readyState").value != DesiredState
			{
				if (A_TickCount-StartTime > Timeout*1000)
					throw Exception("Wait for page " DesiredState " timeout")
				else
					Sleep, Interval
			}
		}
		
		/*
			Internal function triggered when the script receives a message on
			the WebSocket connected to the page.
		*/
		Event(EventName, Event)
		{
			; If it was called from the WebSocket adjust the class context
			if this.Parent
				this := this.Parent
			
			; TODO: Handle Error events
			if (EventName == "Open")
			{
				this.Connected := True
				BoundKeepAlive := this.BoundKeepAlive
				SetTimer, %BoundKeepAlive%, 15000
			}
			else if (EventName == "Message")
			{
				data := Edge.Jxon_Load(Event.data)
				
				; Run the callback routine
				fnCallback := this.fnCallback
				if (newData := %fnCallback%(data))
					data := newData
				
				if this.responses.HasKey(data.ID)
					this.responses[data.ID] := data
			}
			else if (EventName == "Close")
			{
				this.Disconnect()
				fnClose := this.fnClose
				%fnClose%(this)
			}
		}
		
		/*
			Disconnect from the page's debug interface, allowing the instance
			to be garbage collected.
			
			This method should always be called when you are finished with a
			page or else your script will leak memory.
		*/
		Disconnect()
		{
			if !this.Connected
				return
			
			this.Connected := False
			this.ws.Delete("Parent")
			this.ws.Disconnect()
			
			BoundKeepAlive := this.BoundKeepAlive
			SetTimer, %BoundKeepAlive%, Delete
			this.Delete("BoundKeepAlive")
		}
		
		class WebSocket
		{
			__New(WS_URL, Timeout:=30)
			{
				static wb
				
				; Need IE10+
				RegRead, OutputVar, HKLM, Software\Microsoft\Internet Explorer, svcVersion
				if (StrSplit(OutputVar, ".")[1] < 10)
					throw Exception("Connect to a WebSocket server need IE10+")
				
				; Create an IE instance
				Gui, +hWndhOld
				Gui, New, +hWndhWnd
				this.hWnd := hWnd
				Gui, Add, ActiveX, vWB, Shell.Explorer
				Gui, %hOld%: Default
				
				; Write an appropriate document
				WB.Navigate("about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
				. "content='IE=edge'><body></body>")
				StartTime := A_TickCount
				while (WB.ReadyState < 4)
				{
					if (A_TickCount-StartTime > Timeout*1000)
						throw Exception("Connect to a WebSocket server timeout")
					else
						Sleep, 50
				}
				this.document := WB.document
				
				; Add our handlers to the JavaScript namespace
				this.document.parentWindow.ahk_savews := this._SaveWS.Bind(this)
				this.document.parentWindow.ahk_event := this._Event.Bind(this)
				this.document.parentWindow.ahk_ws_url := WS_URL
				
				; Add some JavaScript to the page to open a socket
				; Here is the JS code, no need to add a timeout
				Script := this.document.createElement("script")
				Script.text := "ws = new WebSocket(ahk_ws_url);`n"
				. "ws.onopen = function(event){ ahk_event('Open', event); };`n"
				. "ws.onclose = function(event){ ahk_event('Close', event); };`n"
				. "ws.onerror = function(event){ ahk_event('Error', event); };`n"
				. "ws.onmessage = function(event){ ahk_event('Message', event); };"
				this.document.body.appendChild(Script)
			}
			
			; Called by the JS in response to WS events
			_Event(EventName, Event)
			{
				this["On" EventName](Event)
			}
			
			; Sends data through the WebSocket
			Send(Data)
			{
				this.document.parentWindow.ws.send(Data)
			}
			
			; Closes the WebSocket connection
			Close(Code:=1000, Reason:="")
			{
				this.document.parentWindow.ws.close(Code, Reason)
			}
			
			; Closes and deletes the WebSocket, removing
			; references so the class can be garbage collected
			Disconnect()
			{
				if this.hWnd
				{
					this.Close()
					Gui, % this.hWnd ": Destroy"
					this.hWnd := False
				}
			}
		}
	}
	
	Jxon_Load(ByRef src, args*)
	{
		static q := Chr(34)
		
		key := "", is_key := false
		stack := [ tree := [] ]
		is_arr := { (tree): 1 }
		next := q . "{[01234567890-tfn"
		pos := 0
		while ( (ch := SubStr(src, ++pos, 1)) != "" )
		{
			if InStr(" `t`n`r", ch)
				continue
			if !InStr(next, ch, true)
			{
				ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
				col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
				
				msg := Format("{}: line {} col {} (char {})"
				,   (next == "")      ? ["Extra data", ch := SubStr(src, pos)][1]
				: (next == "'")     ? "Unterminated string starting at"
				: (next == "\")     ? "Invalid \escape"
				: (next == ":")     ? "Expecting ':' delimiter"
				: (next == q)       ? "Expecting object key enclosed in double quotes"
				: (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
				: (next == ",}")    ? "Expecting ',' delimiter or object closing '}'"
				: (next == ",]")    ? "Expecting ',' delimiter or array closing ']'"
				: [ "Expecting JSON value(string, number, [true, false, null], object or array)"
				, ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
				, ln, col, pos)
				
				throw Exception(msg, -1, ch)
			}
			
			is_array := is_arr[obj := stack[1]]
			
			if i := InStr("{[", ch)
			{
				val := (proto := args[i]) ? new proto : {}
				is_array? ObjPush(obj, val) : obj[key] := val
				ObjInsertAt(stack, 1, val)
				
				is_arr[val] := !(is_key := ch == "{")
				next := q . (is_key ? "}" : "{[]0123456789-tfn")
			}
			
			else if InStr("}]", ch)
			{
				ObjRemoveAt(stack, 1)
				next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
			}
			
			else if InStr(",:", ch)
			{
				is_key := (!is_array && ch == ",")
				next := is_key ? q : q . "{[0123456789-tfn"
			}
			
			else ; string | number | true | false | null
			{
				if (ch == q) ; string
				{
					i := pos
					while i := InStr(src, q,, i+1)
					{
						val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
						static end := A_AhkVersion<"2" ? 0 : -1
						if (SubStr(val, end) != "\")
							break
					}
					if !i ? (pos--, next := "'") : 0
						continue
					
					pos := i ; update pos
					
					val := StrReplace(val,    "\/",  "/")
					, val := StrReplace(val, "\" . q,    q)
					, val := StrReplace(val,    "\b", "`b")
					, val := StrReplace(val,    "\f", "`f")
					, val := StrReplace(val,    "\n", "`n")
					, val := StrReplace(val,    "\r", "`r")
					, val := StrReplace(val,    "\t", "`t")
					
					i := 0
					while i := InStr(val, "\",, i+1)
					{
						if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
							continue 2
						
						; \uXXXX - JSON unicode escape sequence
						xxxx := Abs("0x" . SubStr(val, i+2, 4))
						if (A_IsUnicode || xxxx < 0x100)
							val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
					}
					
					if is_key
					{
						key := val, next := ":"
						continue
					}
				}
				
				else ; number | true | false | null
				{
					val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
					
					; For numerical values, numerify integers and keep floats as is.
					; I'm not yet sure if I should numerify floats in v2.0-a ...
					static number := "number", integer := "integer"
					if val is %number%
					{
						if val is %integer%
							val += 0
					}
					; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
					; SOMETIMES return strings due to certain optimizations. Since it
					; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
					else if (val == "true" || val == "false")
						val := %value% + 0
					; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
					; as it would raise an exception in AHK_H(overriding built-in var)
					else if (val == "null")
						val := ""
					; any other values are invalid, continue to trigger error
					else if (pos--, next := "#")
						continue
					
					pos += i-1
				}
				
				is_array? ObjPush(obj, val) : obj[key] := val
				next := obj==tree ? "" : is_array ? ",]" : ",}"
			}
		}
		
		return tree[1]
	}
	
	Jxon_Dump(obj, indent:="", lvl:=1)
	{
		static q := Chr(34)
		
		if IsObject(obj)
		{
			static Type := Func("Type")
			if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "")
				throw Exception("Object type not supported.", -1, Format("<Object at 0x{:p}>", &obj))
			
			prefix := SubStr(A_ThisFunc, 1, InStr(A_ThisFunc, ".",, 0))
			fn_t := prefix "Jxon_True",  obj_t := this ? %fn_t%(this) : %fn_t%()
			fn_f := prefix "Jxon_False", obj_f := this ? %fn_f%(this) : %fn_f%()
			
			if (&obj == &obj_t)
				return "true"
			else if (&obj == &obj_f)
				return "false"
			
			is_array := 0
			for k in obj
				is_array := k == A_Index
			until !is_array
			
			static integer := "integer"
			if indent is %integer%
			{
				if (indent < 0)
					throw Exception("Indent parameter must be a postive integer.", -1, indent)
				spaces := indent, indent := ""
				Loop % spaces
					indent .= " "
			}
			indt := ""
			Loop, % indent ? lvl : 0
				indt .= indent
			
			this_fn := this ? Func(A_ThisFunc).Bind(this) : A_ThisFunc
			lvl += 1, out := "" ; Make #Warn happy
			for k, v in obj
			{
				if IsObject(k) || (k == "")
					throw Exception("Invalid object key.", -1, k ? Format("<Object at 0x{:p}>", &obj) : "<blank>")
				
				if !is_array
					out .= ( ObjGetCapacity([k], 1) ? %this_fn%(k) : q . k . q ) ;// key
				.  ( indent ? ": " : ":" ) ; token + padding
				out .= %this_fn%(v, indent, lvl) ; value
				.  ( indent ? ",`n" . indt : "," ) ; token + indent
			}
			
			if (out != "")
			{
				out := Trim(out, ",`n" . indent)
				if (indent != "")
					out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1)
			}
			
			return is_array ? "[" . out . "]" : "{" . out . "}"
		}
		
		; Number
		else if (ObjGetCapacity([obj], 1) == "")
			return obj
		
		; String (null -> not supported by AHK)
		if (obj != "")
		{
			obj := StrReplace(obj,  "\",    "\\")
			, obj := StrReplace(obj,  "/",    "\/")
			, obj := StrReplace(obj,    q, "\" . q)
			, obj := StrReplace(obj, "`b",    "\b")
			, obj := StrReplace(obj, "`f",    "\f")
			, obj := StrReplace(obj, "`n",    "\n")
			, obj := StrReplace(obj, "`r",    "\r")
			, obj := StrReplace(obj, "`t",    "\t")
			
			static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]"
			while RegExMatch(obj, needle, m)
				obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0])))
		}
		
		return q . obj . q
	}
	
	Jxon_True()
	{
		static obj := {}
		return obj
	}
	
	Jxon_False()
	{
		static obj := {}
		return obj
	}
}


Jxon_Load(ByRef src, args*)
{
	static q := Chr(34)

	key := "", is_key := false
	stack := [ tree := [] ]
	is_arr := { (tree): 1 }
	next := q . "{[01234567890-tfn"
	pos := 0
	while ( (ch := SubStr(src, ++pos, 1)) != "" )
	{
		if InStr(" `t`n`r", ch)
			continue
		if !InStr(next, ch, true)
		{
			ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
			col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))

			msg := Format("{}: line {} col {} (char {})"
			,   (next == "")      ? ["Extra data", ch := SubStr(src, pos)][1]
			  : (next == "'")     ? "Unterminated string starting at"
			  : (next == "\")     ? "Invalid \escape"
			  : (next == ":")     ? "Expecting ':' delimiter"
			  : (next == q)       ? "Expecting object key enclosed in double quotes"
			  : (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
			  : (next == ",}")    ? "Expecting ',' delimiter or object closing '}'"
			  : (next == ",]")    ? "Expecting ',' delimiter or array closing ']'"
			  : [ "Expecting JSON value(string, number, [true, false, null], object or array)"
			    , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
			, ln, col, pos)

			throw Exception(msg, -1, ch)
		}

		is_array := is_arr[obj := stack[1]]

		if i := InStr("{[", ch)
		{
			val := (proto := args[i]) ? new proto : {}
			is_array? ObjPush(obj, val) : obj[key] := val
			ObjInsertAt(stack, 1, val)
			
			is_arr[val] := !(is_key := ch == "{")
			next := q . (is_key ? "}" : "{[]0123456789-tfn")
		}

		else if InStr("}]", ch)
		{
			ObjRemoveAt(stack, 1)
			next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
		}

		else if InStr(",:", ch)
		{
			is_key := (!is_array && ch == ",")
			next := is_key ? q : q . "{[0123456789-tfn"
		}

		else ; string | number | true | false | null
		{
			if (ch == q) ; string
			{
				i := pos
				while i := InStr(src, q,, i+1)
				{
					val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
					static end := A_AhkVersion<"2" ? 0 : -1
					if (SubStr(val, end) != "\")
						break
				}
				if !i ? (pos--, next := "'") : 0
					continue

				pos := i ; update pos

				  val := StrReplace(val,    "\/",  "/")
				, val := StrReplace(val, "\" . q,    q)
				, val := StrReplace(val,    "\b", "`b")
				, val := StrReplace(val,    "\f", "`f")
				, val := StrReplace(val,    "\n", "`n")
				, val := StrReplace(val,    "\r", "`r")
				, val := StrReplace(val,    "\t", "`t")

				i := 0
				while i := InStr(val, "\",, i+1)
				{
					if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
						continue 2

					; \uXXXX - JSON unicode escape sequence
					xxxx := Abs("0x" . SubStr(val, i+2, 4))
					if (A_IsUnicode || xxxx < 0x100)
						val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
				}

				if is_key
				{
					key := val, next := ":"
					continue
				}
			}

			else ; number | true | false | null
			{
				val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
			
			; For numerical values, numerify integers and keep floats as is.
			; I'm not yet sure if I should numerify floats in v2.0-a ...
				static number := "number", integer := "integer"
				if val is %number%
				{
					if val is %integer%
						val += 0
				}
			; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
			; SOMETIMES return strings due to certain optimizations. Since it
			; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
				else if (val == "true" || val == "false")
					val := %value% + 0
			; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
			; as it would raise an exception in AHK_H(overriding built-in var)
				else if (val == "null")
					val := ""
			; any other values are invalid, continue to trigger error
				else if (pos--, next := "#")
					continue
				
				pos += i-1
			}
			
			is_array? ObjPush(obj, val) : obj[key] := val
			next := obj==tree ? "" : is_array ? ",]" : ",}"
		}
	}

	return tree[1]
}


thaihoa3189
Posts: 35
Joined: 23 Mar 2022, 22:10

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by thaihoa3189 » 12 May 2023, 21:47

This library is not working. I started Edge in debug mode, then connected to the opened Edge tab but failed. Even though I've done it so many times in every way

eleet
Posts: 5
Joined: 24 Apr 2022, 04:25

Re: [Library] Edge.ahk - Automate Edge using native AutoHotkey. No Selenium!

Post by eleet » 28 Jun 2023, 01:24

Well actually it works great.
Did you start your edge with edge.bat file containing text (with adjusted Edge path to your system):

Code: Select all

Taskkill /IM msedge.exe /F
cd "C:\Program Files (x86)\Microsoft\Edge\Application\"
start /b msedge.exe --profile-directory-Default --remote-debugging-port=9222 --remote-allow-origins=*
Did you check url http://localhost:9222/json/version? Did it show something like bellow, to confirm if EDGE is running in debug mode:
{
"Browser": "Edg/114.0.1823.58",
"Protocol-Version": "1.3",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.58",
"V8-Version": "11.4.19.20",
"WebKit-Version": "537.36 (@913ab82f672fa157e03cb56700e4b132a0a04a31)",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/012a7a2b-e410-410e-8793-650f3c182c20"
}

Did you adjust path to your script while testing? What error it was showing while running script and failing?

Post Reply

Return to “Scripts and Functions (v1)”