class Chrome
{
static DebugPort := 9222
/*
Escape a string in a manner suitable for command line parameters
*/
CliEscape(Param)
{
return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
}
FindInstance(exeName)
{
static Needle := "i)\Q" . exeName . "\E""?\s+--remote-debugging-port=(\d+)"
Out := {}
for k, PID in this.EnumProcessesByName(exeName)
cmd := this.GetCommandLine(PID).cmd
until found := RegExMatch(cmd, Needle, Match)
Return found ? {port: Match1, PID: PID} : ""
}
EnumProcessesByName(procName) {
local
if !DllCall("Wtsapi32\WTSEnumerateProcesses", Ptr, 0, UInt, 0, UInt, 1, PtrP, pProcessInfo, PtrP, count)
throw Exception("WTSEnumerateProcesses failed. A_LastError: " . A_LastError)
addr := pProcessInfo, PIDs := []
Loop % count {
if StrGet( NumGet(addr + 8) ) = procName
PID := NumGet(addr + 4, "UInt"), PIDs.Push(PID)
addr += A_PtrSize = 4 ? 16 : 24
}
DllCall("Wtsapi32\WTSFreeMemory", Ptr, pProcessInfo)
Return PIDs
}
GetCommandLine(PID, GetImagePath := false) {
local
static SetDebug := 0, PROCESS_QUERY_INFORMATION := 0x400, PROCESS_VM_READ := 0x10, STATUS_SUCCESS := 0
hProc := DllCall("OpenProcess", UInt, PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, Int, 0, UInt, PID, Ptr)
(A_Is64bitOS && DllCall("IsWow64Process", Ptr, hProc, UIntP, IsWow64))
if (!A_Is64bitOS || IsWow64)
PtrSize := 4, PtrType := "UInt", pPtr := "UIntP", offsetCMD := 0x40
else
PtrSize := 8, PtrType := "Int64", pPtr := "Int64P", offsetCMD := 0x70
hModule := DllCall("GetModuleHandle", "str", "Ntdll", Ptr)
if (A_PtrSize < PtrSize) { ; скрипт 32, целевой процесс 64
if !QueryInformationProcess := DllCall("GetProcAddress", Ptr, hModule, AStr, "NtWow64QueryInformationProcess64", Ptr)
failed := "NtWow64QueryInformationProcess64"
if !ReadProcessMemory := DllCall("GetProcAddress", Ptr, hModule, AStr, "NtWow64ReadVirtualMemory64", Ptr)
failed := "NtWow64ReadVirtualMemory64"
info := 0, szPBI := 48, offsetPEB := 8
}
else {
if !QueryInformationProcess := DllCall("GetProcAddress", Ptr, hModule, AStr, "NtQueryInformationProcess", Ptr)
failed := "NtQueryInformationProcess"
ReadProcessMemory := "ReadProcessMemory"
if (A_PtrSize > PtrSize) ; скрипт 64, целевой процесс 32
info := 26, szPBI := 8, offsetPEB := 0
else ; скрипт и целевой процесс одной битности
info := 0, szPBI := PtrSize * 6, offsetPEB := PtrSize
}
if failed {
DllCall("CloseHandle", Ptr, hProc)
MsgBox, Не удалось получить указатель на функцию %failed%
Return
}
VarSetCapacity(PBI, 48, 0)
if DllCall(QueryInformationProcess, Ptr, hProc, UInt, info, Ptr, &PBI, UInt, szPBI, UIntP, bytes) != STATUS_SUCCESS {
DllCall("CloseHandle", Ptr, hProc)
Return
}
pPEB := NumGet(&PBI + offsetPEB, PtrType)
DllCall(ReadProcessMemory, Ptr, hProc, PtrType, pPEB + PtrSize * 4, pPtr, pRUPP, PtrType, PtrSize, UIntP, bytes)
DllCall(ReadProcessMemory, Ptr, hProc, PtrType, pRUPP + offsetCMD, UShortP, szCMD, PtrType, 2, UIntP, bytes)
DllCall(ReadProcessMemory, Ptr, hProc, PtrType, pRUPP + offsetCMD + PtrSize, pPtr, pCMD, PtrType, PtrSize, UIntP, bytes)
VarSetCapacity(buff, szCMD, 0)
DllCall(ReadProcessMemory, Ptr, hProc, PtrType, pCMD, Ptr, &buff, PtrType, szCMD, UIntP, bytes)
obj := { cmd: StrGet(&buff, "UTF-16") }
if (GetImagePath && obj.cmd) {
DllCall(ReadProcessMemory, Ptr, hProc, PtrType, pRUPP + offsetCMD - PtrSize*2, UShortP, szPATH, PtrType, 2, UIntP, bytes)
DllCall(ReadProcessMemory, Ptr, hProc, PtrType, pRUPP + offsetCMD - PtrSize, pPtr, pPATH, PtrType, PtrSize, UIntP, bytes)
VarSetCapacity(buff, szPATH, 0)
DllCall(ReadProcessMemory, Ptr, hProc, PtrType, pPATH, Ptr, &buff, PtrType, szPATH, UIntP, bytes)
obj.path := StrGet(&buff, "UTF-16") . (IsWow64 ? " *32" : "")
}
DllCall("CloseHandle", Ptr, hProc)
Return obj
}
__New(ProfilePath:="", URLs:="about:blank", Flags:="", ChromePath:="", DebugPort:="")
{
; Verify ProfilePath
if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D"))
throw Exception("The given ProfilePath does not exist")
this.ProfilePath := ProfilePath
; Verify ChromePath
if (ChromePath == "")
FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath
if (ChromePath == "")
RegRead, ChromePath, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe
if !FileExist(ChromePath)
throw Exception("Chrome could not be found")
this.ChromePath := ChromePath
; 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(ChromePath)
. " --remote-debugging-port=" this.DebugPort
. (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
. (Flags ? " " Flags : "")
. URLString
,,, OutputVarPID
this.PID := OutputVarPID
}
/*
End Chrome by terminating the process.
*/
Kill()
{
Process, Close, % this.PID
}
/*
Queries chrome 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 < 30000)
{
try
{
http.Open("GET", "http://127.0.0.1:" this.DebugPort "/json/list", true)
http.Send()
http.WaitForResponse(-1)
if (http.Status = 200)
break
}
Sleep, 50
}
return LightJson.Parse(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
fnCallback - A function to be called whenever message is received from the page
*/
GetPageBy(Key, Value, MatchMode:="exact", Index:=1, fnCallback:="")
{
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, fnCallback)
}
}
/*
Shorthand for GetPageBy("url", Value, "startswith")
*/
GetPageByURL(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
{
return this.GetPageBy("url", Value, MatchMode, Index, fnCallback)
}
/*
Shorthand for GetPageBy("title", Value, "startswith")
*/
GetPageByTitle(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
{
return this.GetPageBy("title", Value, MatchMode, Index, fnCallback)
}
/*
Shorthand for GetPageBy("type", Type, "exact")
The default type to search for is "page", which is the visible area of
a normal Chrome tab.
*/
GetPage(Index:=1, Type:="page", fnCallback:="")
{
return this.GetPageBy("type", Type, "exact", Index, fnCallback)
}
/*
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
fnCallback - A function to be called whenever message is received
*/
__New(wsurl, fnCallback:="")
{
this.fnCallback := fnCallback
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)
while !this.Connected
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": LightJson.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
Chrome, which is necessary to receive a return value, or whether
to continue on with the script without waiting for a response.
*/
Call(DomainAndMethod, Params:="", WaitForResponse:=True)
{
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(LightJson.Stringify({"id": ID
, "params": Params ? Params : {}
, "method": DomainAndMethod}))
if !WaitForResponse
return
; Wait for the response
this.responses[ID] := False
while !this.responses[ID]
Sleep, 50
; Get the response, check if it's an error
response := this.responses.Delete(ID)
if (response.error)
throw Exception("Chrome indicated error in response",, LightJson.Stringify(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)
{
response := this.Call("Runtime.evaluate",
( LTrim Join
{
"expression": JS,
"objectGroup": "console",
"includeCommandLineAPI": LightJson.true,
"silent": LightJson.false,
"returnByValue": LightJson.false,
"userGesture": LightJson.true,
"awaitPromise": LightJson.false
}
))
if (response.exceptionDetails)
throw Exception(response.result.description,, LightJson.Stringify(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
*/
WaitForLoad(DesiredState:="complete", Interval:=100)
{
while this.Evaluate("document.readyState").value != DesiredState
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 := LightJson.Parse(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()
}
else if (EventName == "Error")
{
throw Exception("Websocket Error!")
}
}
/*
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)
{
static wb
; 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>")
while (WB.ReadyState < 4)
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
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
}
}
}
}
}
class LightJson
{
static JS := LightJson.GetJS(), true := {}, false := {}, null := {}
Parse(json, _rec := false) {
if !_rec
obj := this.Parse(this.JS.eval("(" . json . ")"), true)
else if !IsObject(json)
obj := json
else if this.JS.Object.prototype.toString.call(json) == "[object Array]" {
obj := []
Loop % json.length
obj.Push( this.Parse(json[A_Index - 1], true) )
}
else {
obj := {}
keys := this.JS.Object.keys(json)
Loop % keys.length {
k := keys[A_Index - 1]
obj[k] := this.Parse(json[k], true)
}
}
Return obj
}
Stringify(obj, indent := "") {
if indent|1 {
for k, v in ["true", "false", "null"]
if (obj = this[v])
Return v
if IsObject( obj ) {
isArray := true
for key in obj {
if IsObject(key)
throw Exception("Invalid key")
if !( key = A_Index || isArray := false )
break
}
for k, v in obj
str .= ( A_Index = 1 ? "" : "," ) . ( isArray ? "" : """" . k . """:" ) . this.Stringify(v, true)
Return str = "" ? "{}" : isArray ? "[" . str . "]" : "{" . str . "}"
}
else if !(obj*1 = "" || RegExMatch(obj, "^-?0|\s"))
Return obj
for k, v in [["\", "\\"], [A_Tab, "\t"], ["""", "\"""], ["/", "\/"], ["`n", "\n"], ["`r", "\r"], [Chr(12), "\f"], [Chr(8), "\b"]]
obj := StrReplace( obj, v[1], v[2] )
Return """" obj """"
}
sObj := this.Stringify(obj, true)
Return this.JS.eval("JSON.stringify(" . sObj . ",'','" . indent . "')")
}
GetJS() {
static Doc, JS
if !Doc {
Doc := ComObjCreate("htmlfile")
Doc.write("<meta http-equiv=""X-UA-Compatible"" content=""IE=9"">")
JS := Doc.parentWindow
( Doc.documentMode < 9 && JS.execScript() )
}
Return JS
}
}