-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
359 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ChromeProfile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[submodule "lib/WebSocket.ahk"] | ||
path = lib/WebSocket.ahk | ||
url = https://github.com/G33kDude/WebSocket.ahk.git | ||
[submodule "lib/AutoHotkey-JSON"] | ||
path = lib/AutoHotkey-JSON | ||
url = https://github.com/G33kDude/AutoHotkey-JSON.git | ||
branch = boolean |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
class Chrome | ||
{ | ||
static DebugPort := 9222 | ||
|
||
; Escape a string in a manner suitable for command line parameters | ||
CliEscape(Param) | ||
{ | ||
return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """" | ||
} | ||
|
||
__New(ProfilePath:="", URL:="about:blank", ChromePath:="", DebugPort:="") | ||
{ | ||
if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D")) | ||
throw Exception("The given ProfilePath does not exist") | ||
this.ProfilePath := ProfilePath | ||
|
||
; TODO: Perform a more rigorous search for Chrome | ||
if (ChromePath == "") | ||
FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath | ||
if !FileExist(ChromePath) | ||
throw Exception("Chrome could not be found") | ||
this.ChromePath := ChromePath | ||
|
||
if (DebugPort != "") | ||
{ | ||
this.DebugPort := Round(DebugPort) | ||
if (this.DebugPort <= 0) ; TODO: Support DebugPort of 0 | ||
throw Exception("DebugPort must be a positive integer") | ||
} | ||
|
||
; TODO: Support an array of URLs | ||
Run, % this.CliEscape(ChromePath) | ||
. " --remote-debugging-port=" this.DebugPort | ||
. (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "") | ||
. (URL ? " " this.CliEscape(URL) : "") | ||
} | ||
|
||
GetTabs() | ||
{ | ||
http := ComObjCreate("WinHttp.WinHttpRequest.5.1") | ||
http.open("GET", "http://127.0.0.1:" this.DebugPort "/json") | ||
http.send() | ||
return this.Jxon_Load(http.responseText) | ||
} | ||
|
||
GetTab(Index:=0) | ||
{ | ||
; TODO: Filter pages by type before returning an indexed page | ||
if (Index > 0) | ||
return new this.Tab(this.GetTabs()[Index]) | ||
|
||
for Index, Tab in this.GetTabs() | ||
if (Tab.type == "page") | ||
return new this.Tab(Tab) | ||
} | ||
|
||
class Tab | ||
{ | ||
Connected := False | ||
ID := 0 | ||
Responses := [] | ||
|
||
__New(wsurl) | ||
{ | ||
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 | ||
} | ||
|
||
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(Chrome.Jxon_Dump({"id": ID | ||
, "method": DomainAndMethod, "params": Params})) | ||
|
||
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",, Chrome.Jxon_Dump(response.error)) | ||
|
||
return response.result | ||
} | ||
|
||
Evaluate(JS) | ||
{ | ||
response := this.Call("Runtime.evaluate", | ||
( LTrim Join | ||
{ | ||
"expression": JS, | ||
"objectGroup": "console", | ||
"includeCommandLineAPI": Chrome.Jxon_True(), | ||
"silent": Chrome.Jxon_False(), | ||
"returnByValue": Chrome.Jxon_False(), | ||
"userGesture": Chrome.Jxon_True(), | ||
"awaitPromise": Chrome.Jxon_False() | ||
} | ||
)) | ||
|
||
if (response.exceptionDetails) | ||
throw Exception(response.result.description,, Chrome.Jxon_Dump(response.exceptionDetails)) | ||
|
||
return response.result | ||
} | ||
|
||
WaitForLoad(DesiredState:="complete", Interval:=100) | ||
{ | ||
while this.Evaluate("document.readyState").value != DesiredState | ||
Sleep, %Interval% | ||
} | ||
|
||
Event(EventName, Event) | ||
{ | ||
; Called from WebSocket | ||
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 := Chrome.Jxon_Load(Event.data) | ||
if this.responses.HasKey(data.ID) | ||
this.responses[data.ID] := data | ||
} | ||
else if (EventName == "Close") | ||
{ | ||
this.Disconnect() | ||
} | ||
} | ||
|
||
Disconnect() | ||
{ | ||
if !this.Connected | ||
return | ||
|
||
this.Connected := False | ||
this.ws.Delete("Parent") | ||
this.ws.Disconnect() | ||
|
||
BoundKeepAlive := this.BoundKeepAlive | ||
SetTimer, %BoundKeepAlive%, Delete | ||
this.Delete("BoundKeepAlive") | ||
} | ||
|
||
#Include %A_LineFile%\..\lib\WebSocket.ahk\WebSocket.ahk | ||
} | ||
|
||
#Include %A_LineFile%\..\lib\AutoHotkey-JSON\Jxon.ahk | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
#NoEnv | ||
SetBatchLines, -1 | ||
|
||
|
||
; --- Create a new chrome instance --- | ||
|
||
FileCreateDir, ChromeProfile | ||
ChromeInst := new Chrome("ChromeProfile", "https://autohotkey.com/") | ||
|
||
|
||
; --- Connect to the active tab --- | ||
|
||
Tab := ChromeInst.GetTab() | ||
|
||
|
||
; --- Perform JavaScript injection --- | ||
|
||
Loop | ||
{ | ||
InputBox, JS,, | ||
( LTrim | ||
Enter some JavaScript to be run in the tab | ||
|
||
For example: | ||
alert('hi'); | ||
window.location = "https://p.ahkscript.org/"; | ||
) | ||
|
||
if ErrorLevel | ||
break | ||
|
||
try | ||
Result := Tab.Evaluate(JS) | ||
catch e | ||
{ | ||
MsgBox, % "Exception encountered in " e.What ":`n`n" | ||
. e.Message "`n`n" | ||
. "Specifically:`n`n" | ||
. Chrome.Jxon_Dump(Chrome.Jxon_Load(e.Extra), "`t") | ||
|
||
continue | ||
} | ||
|
||
MsgBox, % "Result: " Chrome.Jxon_Dump(Result) | ||
} | ||
|
||
ExitApp | ||
return | ||
|
||
|
||
#include ../Chrome.ahk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
#NoEnv | ||
SetBatchLines, -1 | ||
|
||
|
||
; --- Create a new chrome instance --- | ||
|
||
FileCreateDir, ChromeProfile | ||
ChromeInst := new Chrome("ChromeProfile") | ||
|
||
|
||
; --- Connect to the active tab --- | ||
|
||
Tab := ChromeInst.GetTab() | ||
|
||
|
||
; --- Navigate to the pastebin --- | ||
|
||
Tab.Call("Page.navigate", {"url": "https://p.ahkscript.org"}) | ||
Tab.WaitForLoad() | ||
|
||
|
||
; --- Manipulation via DOM --- | ||
|
||
; Find the root node | ||
RootNode := Tab.Call("DOM.getDocument").root | ||
|
||
; Find and change the name element | ||
NameNode := Tab.Call("DOM.querySelector", {"nodeId": RootNode.nodeId, "selector": "input[name=name]"}) | ||
Tab.Call("DOM.setAttributeValue", {"nodeId": NameNode.NodeId, "name": "value", "value": "ChromeBot"}) | ||
|
||
; Find and change the description element | ||
DescNode := Tab.Call("DOM.querySelector", {"nodeId": RootNode.nodeId, "selector": "input[name=desc]"}) | ||
Tab.Call("DOM.setAttributeValue", {"nodeId": DescNode.NodeId, "name": "value", "value": "Pasted with ChromeBot"}) | ||
|
||
|
||
; --- Manipulation via JavaScript --- | ||
|
||
Tab.Evaluate("editor.setValue('test');") | ||
Tab.Evaluate("document.querySelector('input[type=submit]').click();") | ||
Tab.WaitForLoad() | ||
|
||
MsgBox, % Tab.Evaluate("window.location.href").value | ||
|
||
Tab.Call("Browser.close") | ||
ExitApp | ||
return | ||
|
||
|
||
#include ../Chrome.ahk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,73 @@ | ||
# Chrome.ahk | ||
# Chrome.ahk | ||
|
||
Automate Google Chrome using native AutoHotkey. | ||
|
||
|
||
## How it works | ||
|
||
Chrome offers a WebSocket based API they call the **Chrome DevTools Protocol**. This API is what allows web development tools to build integrations, and tools such as Selenium to perform their automation. The protocol's documentation describes a plethora of exciting endpoints accessible using this library, and can be found at the link below. | ||
|
||
https://chromedevtools.github.io/devtools-protocol/ | ||
|
||
|
||
## Advantages | ||
|
||
* **No external dependencies such as Selenium are required** | ||
* Chrome can be automated even when running in headless mode | ||
* Launching in headless mode is not currently supported by this library | ||
* Chrome consistently benchmarks better than Internet Explorer | ||
* Chrome offers extensions which provide unique opportunities for interaction | ||
* Automate your Chromecast | ||
* Connect to remote servers with FoxyProxy and update web based configs | ||
* Manage your password vault with LastPass | ||
* Many features are available that would be difficult to replicate in Internet Explorer | ||
* `Page.printToPDF` | ||
* `Page.captureScreenshot` | ||
* Geolocation spoofing | ||
|
||
|
||
## Limitations | ||
|
||
* Chrome **must** be started in debug mode | ||
* If chrome is already running out of debug mode, it must either be **closed and reopened** or **launched again under a new profile** that isn't already running | ||
* **You cannot attach to an existing non-debug session** | ||
* Less flexible than Internet Explorer's COM interface | ||
* Cannot pass function references for callbacks | ||
|
||
|
||
## Using this Library | ||
|
||
To start using this library you need to create an instance of the class `Chrome`. `Chrome`'s constructor accepts four optional parameters: | ||
|
||
1. **ProfilePath** - This is the path, relative to the working directory, that your Chrome user profile is located. If an empty folder is given, chrome will generate a new user profile in it. **When this parameter is omitted, Chrome will be launched under the default user profile.** However, if chrome is already running under that user profile out of debug mode, this will fail. Because of this, **it is recommended to always launch Chrome under an alternate user profile.** | ||
2. **URL** - The page that chrome should initially be opened to. Pass an empty string to open Chrome's homepage. **When this parameter is omitted, Chrome will be opened to `about:blank`.** | ||
3. **ChromePath** - The path to find the Chrome executable file. **When this parameter is omitted, Chrome will be launched from the path in its start menu entry.** | ||
4. **DebugPort** - The network port to communicate with Chrome over. **When this parameter is omitted, port `9222` will be used** as specified in the Chrome DevTools Protocol documentation. | ||
|
||
Once an instance of the class `Chrome` has been created, Google Chrome will be launched. To connect to the newly opened page call `TabInstance := ChromeInstance.GetTab()`. Afterward, use `Tab.Call()` to call protocol endpoints, and `Tab.Evaluate()` to execute JavaScript. | ||
|
||
```AutoHotkey | ||
#Include Chrome.ahk | ||
; Create an instance of the Chrome class using | ||
; the folder ChromeProfile to store the user profile | ||
FileCreateDir, ChromeProfile | ||
ChromeInst := new Chrome("ChromeProfile") | ||
; Connect to the newly opened tab and navigate to another website | ||
; Note: If your first action is to navigate away, it may be just as | ||
; effective to provide the target URL when instantiating the Chrome class | ||
Tab := ChromeInst.GetTab() | ||
Tab.Call("Page.navigate", {"url": "https://autohotkey.com/"}) | ||
Tab.WaitForLoad() | ||
; Execute some JavaScript | ||
Tab.Evaluate("alert('Hello World!');") | ||
; Close the browser (note: this closes *all* pages/tabs) | ||
Tab.Call("Browser.close") | ||
ExitApp | ||
return | ||
``` | ||
|
||
**You can find more sample code showing how to use this library in the Examples folder.** |
Submodule AutoHotkey-JSON
added at
189957
Submodule WebSocket.ahk
added at
8a0c1f