├── .gitattributes
├── .gitignore
├── CHANGELOG.md
├── Gopkg.lock
├── Gopkg.toml
├── LICENSE
├── README.md
├── api_overrides.go
├── autogcd.go
├── autogcd_test.go
├── conditionals.go
├── element.go
├── element_test.go
├── examples
└── googler
│ └── googler.go
├── settings.go
├── tab.go
├── tab_subscribers.go
├── tab_test.go
├── testdata
├── attributes.html
├── background.html
├── big_body.html
├── button.html
├── console_log.html
├── cookie1.html
├── cookie2.html
├── dblclick.html
├── events.html
├── frame_a.html
├── frame_b.html
├── frame_top.html
├── frameset.html
├── iframe.html
├── index.html
├── inner.html
├── input.html
├── invalidated.html
├── mouseover.html
├── prompt.html
├── redirect.html
├── redirect_target.html
├── script.html
├── script_inner.html
├── scroll.html
├── table.html
├── window_main.html
├── window_sub1.html
└── window_sub2.html
├── types.go
└── vendor
├── github.com
└── wirepair
│ └── gcd
│ ├── .gitattributes
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── Gopkg.lock
│ ├── Gopkg.toml
│ ├── LICENSE
│ ├── README.md
│ ├── chrome_target.go
│ ├── gcd.go
│ ├── gcdapi
│ ├── README.md
│ ├── accessibility.go
│ ├── animation.go
│ ├── applicationcache.go
│ ├── audits.go
│ ├── browser.go
│ ├── cachestorage.go
│ ├── console.go
│ ├── css.go
│ ├── database.go
│ ├── debugger.go
│ ├── deviceorientation.go
│ ├── dom.go
│ ├── domdebugger.go
│ ├── domsnapshot.go
│ ├── domstorage.go
│ ├── emulation.go
│ ├── fetch.go
│ ├── headlessexperimental.go
│ ├── heapprofiler.go
│ ├── indexeddb.go
│ ├── input.go
│ ├── inspector.go
│ ├── io.go
│ ├── layertree.go
│ ├── log.go
│ ├── memory.go
│ ├── network.go
│ ├── overlay.go
│ ├── page.go
│ ├── performance.go
│ ├── profiler.go
│ ├── runtime.go
│ ├── schema.go
│ ├── security.go
│ ├── serviceworker.go
│ ├── storage.go
│ ├── systeminfo.go
│ ├── target.go
│ ├── testing.go
│ ├── tethering.go
│ ├── tracing.go
│ └── version.go
│ └── gcdmessage
│ └── gcdmessage.go
└── golang.org
└── x
└── net
├── AUTHORS
├── CONTRIBUTORS
├── LICENSE
├── PATENTS
└── websocket
├── client.go
├── dial.go
├── hybi.go
├── server.go
└── websocket.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Windows image file caches
2 | Thumbs.db
3 | ehthumbs.db
4 |
5 | # Folder config file
6 | Desktop.ini
7 |
8 | # Recycle Bin used on file shares
9 | $RECYCLE.BIN/
10 |
11 | # Windows Installer files
12 | *.cab
13 | *.msi
14 | *.msm
15 | *.msp
16 |
17 | # Windows shortcuts
18 | *.lnk
19 |
20 | # =========================
21 | # Operating System Files
22 | # =========================
23 |
24 | # OSX
25 | # =========================
26 |
27 | .DS_Store
28 | .AppleDouble
29 | .LSOverride
30 |
31 | # Thumbnails
32 | ._*
33 |
34 | # Files that might appear on external disk
35 | .Spotlight-V100
36 | .Trashes
37 |
38 | # Directories potentially created on remote AFP share
39 | .AppleDB
40 | .AppleDesktop
41 | Network Trash Folder
42 | Temporary Items
43 | .apdisk
44 | debug.log
45 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Changelog (2018)
4 | - December 8th: Update to latest gcd / protocol.json file for 71.0.3578.80
5 | - April 24th: Updated to latest gcd / protocol.json file for 66.0.3359.117 for *stable* branch.
6 | - Feburary 22nd: Updated to latest gcd / protocol.json file for 66.0.3346.8. Added dep init/dep ensure to repository for package management.
7 | - January 17th: Updated to latest gcd / protocol.json file for 65.0.3322.3.
8 |
9 | ## Changelog (2017)
10 | - November 20th: Updated to latest gcd / protocol.json file for 64.0.3269.3. Navigate now exposes friendly error text.
11 | - October 30th: Updated to latest gcd / protocol.json file for 64.0.3251.0.
12 | - September 9th: Updated to latest gcd / protocol.json file for 62.0.3202.9. We are now bound to the browser version (latest dev channel). See [gcd](https://github.com/wirepair/gcd/) updates for more information. Note if you change the gcd channel, you'll probably end up breaking a lot of methods in autogcd, so be warned.
13 | - August 15th: Updated to the latest gcd / protocol.json file.
14 | - May: Updated to latest gcd / protocol.json file.
15 | - April: Updated to latest gcd / protocol.json file. Fixed unit tests to wait for stability a bit longer as node changes seem to take longer than before.
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | branch = "master"
6 | name = "github.com/wirepair/gcd"
7 | packages = [
8 | ".",
9 | "gcdapi",
10 | "gcdmessage"
11 | ]
12 | revision = "6ed90b36a93267191f45318344c02c34bf672144"
13 |
14 | [[projects]]
15 | branch = "master"
16 | name = "golang.org/x/net"
17 | packages = ["websocket"]
18 | revision = "610586996380ceef02dd726cc09df7e00a3f8e56"
19 |
20 | [solve-meta]
21 | analyzer-name = "dep"
22 | analyzer-version = 1
23 | inputs-digest = "a1a6bf0b7074b682aa653392bf970f4203ed59ebfd2798073d6fb0ae1524e80c"
24 | solver-name = "gps-cdcl"
25 | solver-version = 1
26 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 | #
22 | # [prune]
23 | # non-go = false
24 | # go-tests = true
25 | # unused-packages = true
26 |
27 |
28 | [[constraint]]
29 | branch = "master"
30 | name = "github.com/wirepair/gcd"
31 |
32 | [prune]
33 | go-tests = true
34 | unused-packages = true
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 isaac dawson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Automating Google Chrome Debugger (autogcd)
2 | Autogcd is a wrapper around the [gcd](https://github.com/wirepair/gcd/) library to enable automation of Google Chrome. It some what mimics the functionality offered by WebDriver but allows more low level access via the debugger service.
3 |
4 | # Changelog
5 | See the [CHANGELOG](https://github.com/wirepair/autogcd/tree/master/CHANGELOG.md).
6 |
7 | ## Dependencies
8 | autogcd requires [gcd](https://github.com/wirepair/gcd/), [gcdapi](https://github.com/wirepair/gcd/tree/master/gcdapi) and [gcdmessage](https://github.com/wirepair/gcd/tree/master/gcdmessage) packages.
9 |
10 | ## The API
11 | Autogcd is comprised of four components:
12 | * [autogcd.go](https://github.com/wirepair/autogcd/tree/master/autogcd.go) - The wrapper around gcd.Gcd.
13 | * [settings.go](https://github.com/wirepair/autogcd/tree/master/settings.go) - For managing startup of autogcd.
14 | * [tab.go](https://github.com/wirepair/autogcd/tree/master/tab.go) - Individual chrome tabs
15 | * [element.go](https://github.com/wirepair/autogcd/tree/master/element.go) - Elements that make up the page (includes iframes, #documents as well)
16 |
17 | ## API Documentation
18 | [Documentation](https://godoc.org/github.com/wirepair/autogcd/)
19 |
20 | ## Usage
21 | See the [examples](https://github.com/wirepair/autogcd/tree/master/examples) or the various testcases.
22 |
23 | ## Notes
24 | The chrome debugger service uses internal nodeIds for identifying unique elements/nodes in the DOM. In most cases you will not need to use this identifier directly, however if you plan on calling gcdapi related features you will probably need it. The most common example of when you'll need them is for getting access to a nested #document element inside of an iframe. To run query selectors on nested documents, the nodeId of the iframe #document must be known.
25 |
26 | ### Elements
27 | The Chrome Debugger by nature is far more asynchronous than WebDriver. It is possible to work with elements even though the debugger has not yet notified us of their existence. To deal with this, Elements can be in multiple states; Ready, NotReady or Invalid. Only certain features are available when an Element is in a Ready state. If an Element is Invalid, it should no longer be used and references to it should be discarded.
28 |
29 | ### Frames
30 | If you need to search elements (by id or by a selector) of a frame's #document, you'll need to get an Element reference that is the iframe's #document. This can be done by doing a tab.GetElementsBySelector("iframe"), iterating over the results and calling element.GetFrameDocumentNodeId(). This will return the internal document node id which you can then pass to tab.GetDocumentElementsBySelector(iframeDocNodeId, "#whatever").
31 |
32 | ### Windows
33 | The major limitation of using the Google Chrome Remote Debugger is when working with windows. Since each tab must have the debugger enabled, calls to window.open will open a new window prior to us being able to attach a debugger. To get around this, you'll need to get a list of tabs AutoGcd.GetAllTabs(), then call AutoGcd.RefreshTabList() which will connect each tab to an autogcd.Tab. You'd then need to reload the tab get begin working with it.
34 |
35 | ### Stability & Waiting
36 | There are a few ways you can test for stability or if an Element is ready. Element.WaitForReady() will not return until the debugger service has populated the element's information. If you are waiting for a page to stabilize, you can use the tab.WaitStable() method which won't return until it hasn't seen any DOM nodes being added/removed for a configurable (tab.SetStabilityTime(...)) amount of time.
37 |
38 | Finally, you can use the tab.WaitFor method, which takes a ConditionalFunc type and repeatedly calls it until it returns true, or times out.
39 |
40 | For example/simple ConditionalFuncs see the [conditionals.go](https://github.com/wirepair/autogcd/tree/master/conditionals.go) source. Of course you can use whatever you want as long as it matches the ConditionalFunc signature.
41 |
42 | ### Navigation Errors
43 | Unlike WebDriver, we can determine if navigation fails, *at least in chromium. After tab.Navigate(url), calling tab.DidNavigationFail() will return a true/false return value along with a string of the failure type if one did occur. It is strongly recommended you pass the following flags: --test-type, --ignore-certificate-errors on start up of autogcd if you wish to ignore certificate errors.
44 |
45 | \* This does not appear to work in chrome in windows or osx.
46 |
47 | ### Input
48 | Only a limited set of input functions have been implemented. Clicking and sending keys. You can use Element.SendKeys() or send the keys to whatever is focused by using Tab.SendKeys(). Only Enter ("\n"), Tab ("\t") and Backspace ("\b") were implemented, to use them, simply add them to your SendKeys argument Element.SendKeys("enter text hit enter\n") where \n will cause the enter key to be pressed.
49 |
50 | ### Listeners
51 | Four listener functions have been implemented, GetConsoleMessages, GetNetworkTraffic, GetStorageEvents, GetDOMChanges.
52 |
53 | #### GetConsoleMessages
54 | Pass in a ConsoleMessageFunc handler to begin receiving console messages from the tab. Use StopConsoleMessages to stop receiving them.
55 |
56 | #### GetNetworkTraffic
57 | Pass in either a NetworkRequestHandlerFunc, NetworkResponseHandlerFunc or NetworkFinishedHandlerFunc handler (or all three) to receive network traffic events. NetworkFinishedHandler should be used to signal your application that it's safe to get the response body of the request. While calling GetResponseBody *may* work from NetworkResponseHandlerFunc, it will in many cases fail as the debugger service isn't ready to return the data yet. Use StopNetworkTraffic to stop receiving them.
58 |
59 | #### GetStorageEvents
60 | Pass in a StorageFunc handler to recieve cleared, removed, added and updated storage events. Use StopStorageEvents to stop receiving them.
61 |
62 | #### GetDOMChanges
63 | Pass in a DomChangeHandlerFunc to receive various dom change events. Call it with a nil handler to stop receiving them.
64 |
65 | ## Calling gcd directly
66 | AutoGcd has not implemented all of the Google Chrome Debugger protocol methods and features because I don't see any point in wrapping a lot of them. However, you are not out of luck, all gcd components are bound to each Tab object. I'd suggest reviewing the gcdapi package if there is a particular component you wish to use. All components are bound to the Tab so it should be as simple as calling Tab.{component}.{method}.
67 |
68 | ### Overriding gcd
69 | Take a look at [api_overrides.go](https://github.com/wirepair/autogcd/tree/master/api_overrides.go) for an example of overriding gcd methods. In
70 | some cases the protocol.json specification is incorrect, in which case you may need to override specific methods. Since I designed the packages
71 | to use an intermediary gcdmessage package for requests and responses you're completely free to override anything necessary.
72 |
73 | ## Internals
74 | I'll admit, I do not fully like the design of the Elements. I have to track state updates very carefully and I chose to use sync.RWMutex locks. I couldn't see an obvious method of using channels to synchronize access to the DOMNodes. I'm very open to new architectures/designs if someone has a better method of keeping Element objects up to date as Chrome notifies autogcd of new values.
75 |
76 | As mentioned in the Elements section, Chrome Debugger Protocol is fully asynchronous. The debugger is only notified of elements when the page first loads (and even then only a few of the top level elements). It also occurs when an element has been modified, or when you request them with DOM.requestChildNodes. Autogcd tries to manage all of this for you, but there may be a case where you search for elements that chrome has not notified the debugger client yet. In this case the Element will be, in autogcd terminology, NotReady. This means you can sort of work with it because we know its nodeId but we may not know much else (even what type of node it is). Internally almost all chrome debugger methods take nodeIds.
77 |
78 | This package has been *heavily* tested in the real world. It was used to scan the top 1 million websites from Alexa. I found numerous goroutine leaks that have been subsequently fixed. After running my scan I no longer see any leaks. It should also be completely safe to kill the browser at any point and not have any runaway go routines since I have channels waiting for close messages at any point a channel is sending or receiving.
79 |
80 | ## Reporting Bugs & Requesting Features
81 | Found a bug? Great! Tell me what version of chrome/chromium you are using and how to reproduce and I'll get to it when I can. Keep in mind this is a side project for me. Same goes for new features. Patches obviously welcome.
82 |
83 |
84 | ## License
85 | The MIT License (MIT)
86 |
87 | Copyright (c) 2016 isaac dawson
88 |
89 | Permission is hereby granted, free of charge, to any person obtaining a copy
90 | of this software and associated documentation files (the "Software"), to deal
91 | in the Software without restriction, including without limitation the rights
92 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
93 | copies of the Software, and to permit persons to whom the Software is
94 | furnished to do so, subject to the following conditions:
95 |
96 | The above copyright notice and this permission notice shall be included in
97 | all copies or substantial portions of the Software.
98 |
99 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
100 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
101 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
102 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
103 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
104 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
105 | THE SOFTWARE.
--------------------------------------------------------------------------------
/api_overrides.go:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2017 isaac dawson
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 | */
24 |
25 | package autogcd
26 |
27 | import (
28 | "encoding/json"
29 | "github.com/wirepair/gcd"
30 | "github.com/wirepair/gcd/gcdapi"
31 | "github.com/wirepair/gcd/gcdmessage"
32 | )
33 |
34 | /*
35 | This is for overriding gcdapi calls that for some reason or another need parameters removed or they won't
36 | work.
37 | */
38 |
39 | // Evaluate - Evaluates expression on global object.
40 | // expression - Expression to evaluate.
41 | // objectGroup - Symbolic group name that can be used to release multiple objects.
42 | // includeCommandLineAPI - Determines whether Command Line API should be available during the evaluation.
43 | // silent - In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException
state.
44 | // contextId - Specifies in which execution context to perform evaluation. If the parameter is omitted the evaluation will be performed in the context of the inspected page.
45 | // returnByValue - Whether the result is expected to be a JSON object that should be sent by value.
46 | // generatePreview - Whether preview should be generated for the result.
47 | // userGesture - Whether execution should be treated as initiated by user in the UI.
48 | // awaitPromise - Whether execution should wait for promise to be resolved. If the result of evaluation is not a Promise, it's considered to be an error.
49 | // Returns - result - Evaluation result. exceptionDetails - Exception details.
50 | func overridenRuntimeEvaluate(target *gcd.ChromeTarget, expression string, objectGroup string, includeCommandLineAPI bool, silent bool, contextId int, returnByValue bool, generatePreview bool, userGesture bool, awaitPromise bool) (*gcdapi.RuntimeRemoteObject, *gcdapi.RuntimeExceptionDetails, error) {
51 | paramRequest := make(map[string]interface{}, 9)
52 | paramRequest["expression"] = expression
53 | paramRequest["objectGroup"] = objectGroup
54 | paramRequest["includeCommandLineAPI"] = includeCommandLineAPI
55 | paramRequest["silent"] = silent
56 | // only add context id if it's non-zero
57 | if contextId != 0 {
58 | paramRequest["contextId"] = contextId
59 | }
60 | paramRequest["returnByValue"] = returnByValue
61 | paramRequest["generatePreview"] = generatePreview
62 | paramRequest["userGesture"] = userGesture
63 | paramRequest["awaitPromise"] = awaitPromise
64 | resp, err := gcdmessage.SendCustomReturn(target, target.GetSendCh(), &gcdmessage.ParamRequest{Id: target.GetId(), Method: "Runtime.evaluate", Params: paramRequest})
65 | if err != nil {
66 | return nil, nil, err
67 | }
68 |
69 | var chromeData struct {
70 | Result struct {
71 | Result *gcdapi.RuntimeRemoteObject
72 | ExceptionDetails *gcdapi.RuntimeExceptionDetails
73 | }
74 | }
75 |
76 | if resp == nil {
77 | return nil, nil, &gcdmessage.ChromeEmptyResponseErr{}
78 | }
79 |
80 | // test if error first
81 | cerr := &gcdmessage.ChromeErrorResponse{}
82 | json.Unmarshal(resp.Data, cerr)
83 | if cerr != nil && cerr.Error != nil {
84 | return nil, nil, &gcdmessage.ChromeRequestErr{Resp: cerr}
85 | }
86 |
87 | if err := json.Unmarshal(resp.Data, &chromeData); err != nil {
88 | return nil, nil, err
89 | }
90 |
91 | return chromeData.Result.Result, chromeData.Result.ExceptionDetails, nil
92 | }
93 |
--------------------------------------------------------------------------------
/autogcd.go:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2017 isaac dawson
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 | */
24 |
25 | /*
26 | Autogcd - An automation interface for https://github.com/wirepair/gcd. Contains most functionality
27 | found in WebDriver and extends it to offer more low level features. This library was built due to
28 | WebDriver/Chromedriver also using the debugger service. Since it is not possible to attach to a Page's
29 | debugger twice, automating a custom extension with WebDriver turned out to not be possible.
30 |
31 | The Chrome Debugger by nature is far more asynchronous than WebDriver. It is possible to work with
32 | elements even though the debugger has not yet notified us of their existence. To deal with this, Elements
33 | can be in multiple states; Ready, NotReady or Invalid. Only certain features are available when an Element
34 | is in a Ready state. If an Element is Invalid, it should no longer be used and references to it should be
35 | discarded.
36 |
37 | Dealing with frames is also different than WebDriver. There is no SwitchToFrame, you simply pass in the frameId
38 | to certain methods that require it. You can lookup the these frame documents by finding frame/iframe Elements and
39 | requesting the document NodeId reference via the GetFrameDocumentNodeId method.
40 |
41 | Lastly, dealing with windows... doesn't really work since they open a new tab. A possible solution would be to monitor
42 | the list of tabs by calling AutoGcd.RefreshTabs() and doing a diff of known versus new. You could then do a Tab.Reload()
43 | to refresh the page. It is recommended that you clear cache on the tab first so it is possible to trap the various
44 | network events. There are other dirty hacks you could do as well, such as injecting script to override window.open,
45 | or rewriting links etc.
46 | */
47 | package autogcd
48 |
49 | import (
50 | "errors"
51 | "fmt"
52 | "os"
53 | "sync"
54 |
55 | "github.com/wirepair/gcd"
56 | )
57 |
58 | type AutoGcd struct {
59 | debugger *gcd.Gcd
60 | settings *Settings
61 | tabLock *sync.RWMutex
62 | tabs map[string]*Tab
63 | shutdown bool
64 | }
65 |
66 | // Creates a new AutoGcd based off the provided settings.
67 | func NewAutoGcd(settings *Settings) *AutoGcd {
68 | auto := &AutoGcd{settings: settings}
69 | auto.tabLock = &sync.RWMutex{}
70 | auto.tabs = make(map[string]*Tab)
71 | auto.debugger = gcd.NewChromeDebugger()
72 | auto.debugger.SetTerminationHandler(auto.defaultTerminationHandler)
73 | if len(settings.extensions) > 0 {
74 | auto.debugger.AddFlags(settings.extensions)
75 | }
76 |
77 | if len(settings.flags) > 0 {
78 | auto.debugger.AddFlags(settings.flags)
79 | }
80 |
81 | if settings.timeout > 0 {
82 | auto.debugger.SetTimeout(settings.timeout)
83 | }
84 |
85 | if len(settings.env) > 0 {
86 | auto.debugger.AddEnvironmentVars(settings.env)
87 | }
88 | return auto
89 | }
90 |
91 | // Default termination handling is to log, override with SetTerminationHandler
92 | func (auto *AutoGcd) defaultTerminationHandler(reason string) {
93 | fmt.Printf("chrome was terminated: %s\n", reason)
94 | }
95 |
96 | // Allow callers to handle chrome terminating.
97 | func (auto *AutoGcd) SetTerminationHandler(handler gcd.TerminatedHandler) {
98 | auto.debugger.SetTerminationHandler(handler)
99 | }
100 |
101 | // Starts Google Chrome with debugging enabled.
102 | func (auto *AutoGcd) Start() error {
103 | if auto.settings.connectToInstance {
104 | auto.debugger.ConnectToInstance(auto.settings.chromeHost, auto.settings.chromePort)
105 | } else {
106 | auto.debugger.StartProcess(auto.settings.chromePath, auto.settings.userDir, auto.settings.chromePort)
107 | }
108 |
109 | tabs, err := auto.debugger.GetTargets()
110 | if err != nil {
111 | return err
112 | }
113 | auto.tabLock.Lock()
114 | for _, tab := range tabs {
115 | t, err := open(tab)
116 | if err != nil {
117 | return err
118 | }
119 | auto.tabs[tab.Target.Id] = t
120 | }
121 | auto.tabLock.Unlock()
122 | return nil
123 | }
124 |
125 | // Closes all tabs and shuts down the browser.
126 | func (auto *AutoGcd) Shutdown() error {
127 | if auto.shutdown {
128 | return errors.New("AutoGcd already shut down.")
129 | }
130 |
131 | auto.tabLock.Lock()
132 | for _, tab := range auto.tabs {
133 | tab.close() // exit go routines
134 | auto.debugger.CloseTab(tab.ChromeTarget)
135 |
136 | }
137 | auto.tabLock.Unlock()
138 |
139 | if !auto.settings.connectToInstance {
140 | err := auto.debugger.ExitProcess()
141 | if auto.settings.removeUserDir == true {
142 | return os.RemoveAll(auto.settings.userDir)
143 | }
144 | return err
145 | }
146 |
147 | auto.shutdown = true
148 | return nil
149 | }
150 |
151 | // Refreshs our internal list of tabs and return all tabs
152 | func (auto *AutoGcd) RefreshTabList() (map[string]*Tab, error) {
153 |
154 | knownTabs := auto.GetAllTabs()
155 | knownIds := make(map[string]struct{}, len(knownTabs))
156 | for _, v := range knownTabs {
157 | knownIds[v.Target.Id] = struct{}{}
158 | }
159 | newTabs, err := auto.debugger.GetNewTargets(knownIds)
160 | if err != nil {
161 | return nil, err
162 | }
163 |
164 | auto.tabLock.Lock()
165 | for _, newTab := range newTabs {
166 | t, err := open(newTab)
167 | if err != nil {
168 | return nil, err
169 | }
170 | auto.tabs[newTab.Target.Id] = t
171 | }
172 | auto.tabLock.Unlock()
173 | return auto.GetAllTabs(), nil
174 | }
175 |
176 | // Returns the first "visual" tab.
177 | func (auto *AutoGcd) GetTab() (*Tab, error) {
178 | auto.tabLock.RLock()
179 | defer auto.tabLock.RUnlock()
180 | for _, tab := range auto.tabs {
181 | if tab.Target.Type == "page" {
182 | return tab, nil
183 | }
184 | }
185 | return nil, &InvalidTabErr{Message: "no Page tab types found"}
186 | }
187 |
188 | // Returns a safe copy of tabs
189 | func (auto *AutoGcd) GetAllTabs() map[string]*Tab {
190 | auto.tabLock.RLock()
191 | defer auto.tabLock.RUnlock()
192 | tabs := make(map[string]*Tab)
193 | for id, tab := range auto.tabs {
194 | tabs[id] = tab
195 | }
196 | return tabs
197 | }
198 |
199 | // Activate the tab in the chrome UI
200 | func (auto *AutoGcd) ActivateTab(tab *Tab) error {
201 | return auto.debugger.ActivateTab(tab.ChromeTarget)
202 | }
203 |
204 | // Activate the tab in the chrome UI, by tab id
205 | func (auto *AutoGcd) ActivateTabById(id string) error {
206 | tab, err := auto.tabById(id)
207 | if err != nil {
208 | return err
209 | }
210 | return auto.ActivateTab(tab)
211 | }
212 |
213 | // Creates a new tab
214 | func (auto *AutoGcd) NewTab() (*Tab, error) {
215 | target, err := auto.debugger.NewTab()
216 | if err != nil {
217 | return nil, &InvalidTabErr{Message: "unable to create tab: " + err.Error()}
218 | }
219 | auto.tabLock.Lock()
220 | defer auto.tabLock.Unlock()
221 |
222 | tab, err := open(target)
223 | if err != nil {
224 | return nil, err
225 | }
226 | auto.tabs[target.Target.Id] = tab
227 | return tab, nil
228 | }
229 |
230 | // Closes the provided tab.
231 | func (auto *AutoGcd) CloseTab(tab *Tab) error {
232 | tab.close() // kill listening go routines
233 |
234 | if err := auto.debugger.CloseTab(tab.ChromeTarget); err != nil {
235 | return err
236 | }
237 |
238 | auto.tabLock.Lock()
239 | defer auto.tabLock.Unlock()
240 |
241 | delete(auto.tabs, tab.Target.Id)
242 | return nil
243 | }
244 |
245 | // Closes a tab based off the tab id.
246 | func (auto *AutoGcd) CloseTabById(id string) error {
247 | tab, err := auto.tabById(id)
248 | if err != nil {
249 | return err
250 | }
251 | auto.CloseTab(tab)
252 | return nil
253 | }
254 |
255 | // Finds the tab by its id.
256 | func (auto *AutoGcd) tabById(id string) (*Tab, error) {
257 | auto.tabLock.RLock()
258 | tab := auto.tabs[id]
259 | auto.tabLock.RUnlock()
260 | if tab == nil {
261 | return nil, &InvalidTabErr{"unknown tab id " + id}
262 | }
263 | return tab, nil
264 | }
265 |
266 | func (auto *AutoGcd) GetChromeRevision() string {
267 | return auto.debugger.GetRevision()
268 | }
269 |
--------------------------------------------------------------------------------
/autogcd_test.go:
--------------------------------------------------------------------------------
1 | package autogcd
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "net"
9 | "net/http"
10 | "os"
11 | "os/exec"
12 | "runtime"
13 | "strings"
14 | "testing"
15 | "time"
16 | )
17 |
18 | var (
19 | testListener net.Listener
20 | testPath string
21 | testDir string
22 | testPort string
23 | testServerAddr string
24 | )
25 |
26 | var testStartupFlags = []string{"--test-type", "--ignore-certificate-errors", "--allow-running-insecure-content", "--disable-new-tab-first-run", "--no-first-run", "--disable-translate", "--safebrowsing-disable-auto-update", "--disable-component-update", "--safebrowsing-disable-download-protection"}
27 |
28 | func init() {
29 | switch runtime.GOOS {
30 | case "windows":
31 | flag.StringVar(&testPath, "chrome", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "path to chrome")
32 | flag.StringVar(&testDir, "dir", "C:\\temp\\", "user directory")
33 | case "darwin":
34 | flag.StringVar(&testPath, "chrome", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "path to chrome")
35 | flag.StringVar(&testDir, "dir", "/tmp/", "user directory")
36 | case "linux":
37 | flag.StringVar(&testPath, "chrome", "/usr/bin/chromium-browser", "path to chrome")
38 | flag.StringVar(&testDir, "dir", "/tmp/", "user directory")
39 | }
40 | flag.StringVar(&testPort, "port", "9222", "Debugger port")
41 | }
42 |
43 | func TestMain(m *testing.M) {
44 | flag.Parse()
45 | testServer()
46 | ret := m.Run()
47 | testCleanUp()
48 | os.Exit(ret)
49 | }
50 |
51 | func testCleanUp() {
52 | testListener.Close()
53 | }
54 |
55 | func TestStart(t *testing.T) {
56 | s := NewSettings(testPath, testRandomDir(t))
57 | s.SetStartTimeout(15)
58 | s.SetChromeHost("localhost")
59 | auto := NewAutoGcd(s)
60 | defer auto.Shutdown()
61 |
62 | if err := auto.Start(); err != nil {
63 | t.Fatalf("failed to start chrome: %s\n", err)
64 | }
65 | auto.SetTerminationHandler(nil)
66 | }
67 |
68 | func TestGetTabCheckVersion(t *testing.T) {
69 | var err error
70 | var tab *Tab
71 | var product string
72 |
73 | auto := testDefaultStartup(t)
74 | defer auto.Shutdown()
75 |
76 | tab, err = auto.GetTab()
77 | if err != nil {
78 | t.Fatalf("Error getting tab: %s\n", err)
79 | }
80 |
81 | if tab.Target.Type != "page" {
82 | t.Fatalf("Got tab but wasn't of type Page")
83 | }
84 |
85 | if _, product, _, _, _, err = tab.Browser.GetVersion(); err != nil {
86 | t.Fatalf("Error getting browser version information: %s\n", err)
87 | }
88 |
89 | if !strings.Contains(product, auto.GetChromeRevision()) {
90 | t.Fatalf("Error browser version: %s does not match gcd api version: %s\n", product, auto.GetChromeRevision())
91 |
92 | }
93 | }
94 |
95 | func TestNewTab(t *testing.T) {
96 | //var newTab *Tab
97 | auto := testDefaultStartup(t)
98 | defer auto.Shutdown()
99 |
100 | tabLen := len(auto.tabs)
101 | _, err := auto.NewTab()
102 | if err != nil {
103 | t.Fatalf("error creating new tab: %s\n", err)
104 | }
105 |
106 | if tabLen+1 != len(auto.tabs) {
107 | t.Fatalf("error created new tab but not reflected in our map")
108 | }
109 | }
110 |
111 | func TestCloseTab(t *testing.T) {
112 | var err error
113 | var newTab *Tab
114 | auto := testDefaultStartup(t)
115 | defer auto.Shutdown()
116 |
117 | tabLen := len(auto.tabs)
118 |
119 | newTab, err = auto.NewTab()
120 | if err != nil {
121 | t.Fatalf("error creating new tab: %s\n", err)
122 | }
123 |
124 | if tabLen+1 != len(auto.tabs) {
125 | t.Fatalf("error created new tab but not reflected in our map")
126 | }
127 |
128 | err = auto.CloseTab(newTab)
129 | if err != nil {
130 | t.Fatalf("error closing tab")
131 | }
132 |
133 | if _, err := auto.tabById(newTab.Target.Id); err == nil {
134 | t.Fatalf("error closed tab still in our map")
135 | }
136 | }
137 |
138 | func TestChromeTermination(t *testing.T) {
139 | auto := testDefaultStartup(t)
140 | doneCh := make(chan struct{})
141 | shutdown := time.NewTimer(time.Second * 4)
142 | timeout := time.NewTimer(time.Second * 10)
143 | terminatedHandler := func(reason string) {
144 | t.Logf("reason: %s\n", reason)
145 | doneCh <- struct{}{}
146 | }
147 |
148 | auto.SetTerminationHandler(terminatedHandler)
149 | for {
150 | select {
151 | case <-doneCh:
152 | goto DONE
153 | case <-shutdown.C:
154 | auto.Shutdown()
155 | case <-timeout.C:
156 | t.Fatalf("timed out waiting for termination")
157 | }
158 | }
159 | DONE:
160 | }
161 |
162 | func testDefaultStartup(t *testing.T) *AutoGcd {
163 | s := NewSettings(testPath, testRandomDir(t))
164 | s.RemoveUserDir(true)
165 | s.AddStartupFlags(testStartupFlags)
166 | s.SetDebuggerPort(testRandomPort(t))
167 | auto := NewAutoGcd(s)
168 | if err := auto.Start(); err != nil {
169 | t.Fatalf("failed to start chrome: %s\n", err)
170 | }
171 | auto.SetTerminationHandler(nil) // do not want our tests to panic
172 | return auto
173 | }
174 |
175 | func testHeadlessStartup(t *testing.T) *AutoGcd {
176 | s := NewSettings(testPath, testRandomDir(t))
177 | s.RemoveUserDir(true)
178 | s.AddStartupFlags(testStartupFlags)
179 | headlessFlags := []string{"--headless", "--hide-scrollbars"}
180 | s.AddStartupFlags(headlessFlags)
181 | s.SetDebuggerPort(testRandomPort(t))
182 | auto := NewAutoGcd(s)
183 | if err := auto.Start(); err != nil {
184 | t.Fatalf("failed to start chrome: %s\n", err)
185 | }
186 | auto.SetTerminationHandler(nil) // do not want our tests to panic
187 | return auto
188 | }
189 |
190 | func testServer() {
191 | testListener, _ = net.Listen("tcp", ":0")
192 | _, testServerPort, _ := net.SplitHostPort(testListener.Addr().String())
193 | testServerAddr = fmt.Sprintf("http://localhost:%s/", testServerPort)
194 | go http.Serve(testListener, http.FileServer(http.Dir("testdata")))
195 | }
196 |
197 | func testRandomPort(t *testing.T) string {
198 | l, err := net.Listen("tcp", ":0")
199 | if err != nil {
200 | t.Fatal(err)
201 | }
202 | _, randPort, _ := net.SplitHostPort(l.Addr().String())
203 | l.Close()
204 | return randPort
205 | }
206 |
207 | func testRandomDir(t *testing.T) string {
208 | dir, err := ioutil.TempDir(testDir, "autogcd")
209 | if err != nil {
210 | t.Fatalf("error getting temp dir: %s\n", err)
211 | }
212 | return dir
213 | }
214 |
215 | func testInstanceStartup(t *testing.T) (*AutoGcd, *exec.Cmd) {
216 | // ta := testDefaultStartup(t)
217 | port := testRandomPort(t)
218 | userDir := testRandomDir(t)
219 | flags := append(testStartupFlags, fmt.Sprintf("--remote-debugging-port=%s", port))
220 | flags = append(flags, fmt.Sprintf("--user-data-dir=%s", userDir))
221 | cmd := exec.Command(testPath, flags...)
222 | err := cmd.Start()
223 | if err != nil {
224 | log.Printf("start chrome ret err %+v", err)
225 | return nil, nil
226 | }
227 | s := NewSettings("", "")
228 |
229 | s.SetInstance("localhost", port)
230 |
231 | auto := NewAutoGcd(s)
232 | auto.Start()
233 | if err := auto.Start(); err != nil {
234 | t.Fatalf("failed to start chrome: %s\n", err)
235 | }
236 | auto.SetTerminationHandler(nil) // do not want our tests to panic
237 | return auto, cmd
238 | }
239 |
240 | func TestInstanceGetTab(t *testing.T) {
241 | var err error
242 | var tab *Tab
243 | auto, cmd := testInstanceStartup(t)
244 | defer func() { auto.Shutdown(); cmd.Process.Kill() }()
245 |
246 | tab, err = auto.GetTab()
247 | if err != nil {
248 | t.Fatalf("Error getting tab: %s\n", err)
249 | }
250 |
251 | if tab.Target.Type != "page" {
252 | t.Fatalf("Got tab but wasn't of type Page")
253 | }
254 | }
255 |
256 | func TestInstanceNewTab(t *testing.T) {
257 | //var newTab *Tab
258 | auto, cmd := testInstanceStartup(t)
259 | defer func() { auto.Shutdown(); cmd.Process.Kill() }()
260 |
261 | tabLen := len(auto.tabs)
262 | _, err := auto.NewTab()
263 | if err != nil {
264 | t.Fatalf("error creating new tab: %s\n", err)
265 | }
266 |
267 | if tabLen+1 != len(auto.tabs) {
268 | t.Fatalf("error created new tab but not reflected in our map")
269 | }
270 | }
271 |
272 | func TestInstanceCloseTab(t *testing.T) {
273 | var err error
274 | var newTab *Tab
275 | auto, cmd := testInstanceStartup(t)
276 | defer func() { auto.Shutdown(); cmd.Process.Kill() }()
277 |
278 | tabLen := len(auto.tabs)
279 |
280 | newTab, err = auto.NewTab()
281 | if err != nil {
282 | t.Fatalf("error creating new tab: %s\n", err)
283 | }
284 |
285 | if tabLen+1 != len(auto.tabs) {
286 | t.Fatalf("error created new tab but not reflected in our map")
287 | }
288 |
289 | err = auto.CloseTab(newTab)
290 | if err != nil {
291 | t.Fatalf("error closing tab")
292 | }
293 |
294 | if _, err := auto.tabById(newTab.Target.Id); err == nil {
295 | t.Fatalf("error closed tab still in our map")
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/conditionals.go:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2017 isaac dawson
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 | */
24 |
25 | package autogcd
26 |
27 | import (
28 | "strings"
29 | )
30 |
31 | // Returns true when the current url equals the equalsUrl
32 | func UrlEquals(tab *Tab, equalsUrl string) ConditionalFunc {
33 | return func(tab *Tab) bool {
34 | if url, err := tab.GetCurrentUrl(); err == nil && url == equalsUrl {
35 | return true
36 | }
37 | return false
38 | }
39 | }
40 |
41 | // Returns true when the current url contains the containsUrl
42 | func UrlContains(tab *Tab, containsUrl string) ConditionalFunc {
43 | return func(tab *Tab) bool {
44 | if url, err := tab.GetCurrentUrl(); err == nil && strings.Contains(url, containsUrl) {
45 | return true
46 | }
47 | return false
48 | }
49 | }
50 |
51 | // Returns true when the page title equals the provided equalsTitle
52 | func TitleEquals(tab *Tab, equalsTitle string) ConditionalFunc {
53 | return func(tab *Tab) bool {
54 | if pageTitle, err := tab.GetTitle(); err == nil && pageTitle == equalsTitle {
55 | return true
56 | }
57 | return false
58 | }
59 | }
60 |
61 | // Returns true if the searchTitle is contained within the page title.
62 | func TitleContains(tab *Tab, searchTitle string) ConditionalFunc {
63 | return func(tab *Tab) bool {
64 | if pageTitle, err := tab.GetTitle(); err == nil && strings.Contains(pageTitle, searchTitle) {
65 | return true
66 | }
67 | return false
68 | }
69 | }
70 |
71 | // Returns true when the element exists and is ready
72 | func ElementByIdReady(tab *Tab, elementAttributeId string) ConditionalFunc {
73 | return func(tab *Tab) bool {
74 | element, _, _ := tab.GetElementById(elementAttributeId)
75 | return (element != nil) && (element.IsReady())
76 | }
77 | }
78 |
79 | // Returns true when the element's attribute of name equals value.
80 | func ElementAttributeEquals(tab *Tab, element *Element, name, value string) ConditionalFunc {
81 | return func(tab *Tab) bool {
82 | if element.GetAttribute(name) == value {
83 | return true
84 | }
85 | return false
86 | }
87 | }
88 |
89 | // Returns true when a selector returns a valid list of elements.
90 | func ElementsBySelectorNotEmpty(tab *Tab, elementSelector string) ConditionalFunc {
91 | return func(tab *Tab) bool {
92 | eles, err := tab.GetElementsBySelector(elementSelector)
93 | if err == nil && len(eles) > 0 {
94 | return true
95 | }
96 | return false
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/examples/googler/googler.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "github.com/wirepair/autogcd"
6 | "io/ioutil"
7 | "log"
8 | "runtime"
9 | "time"
10 | )
11 |
12 | var (
13 | chromePath string
14 | userDir string
15 | chromePort string
16 | debug bool
17 | )
18 |
19 | // For an excellent list of command line switches see: http://peter.sh/experiments/chromium-command-line-switches/
20 | var startupFlags = []string{"--disable-new-tab-first-run", "--no-first-run", "--disable-translate"}
21 | var waitForTimeout = time.Second * 5
22 | var waitForRate = time.Millisecond * 25
23 |
24 | var navigationTimeout = time.Second * 10
25 |
26 | var stableAfter = time.Millisecond * 450
27 | var stabilityTimeout = time.Second * 2
28 |
29 | func init() {
30 | switch runtime.GOOS {
31 | case "windows":
32 | flag.StringVar(&chromePath, "chrome", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "path to chrome")
33 | flag.StringVar(&userDir, "dir", "C:\\temp\\", "user directory")
34 | case "darwin":
35 | flag.StringVar(&chromePath, "chrome", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "path to chrome")
36 | flag.StringVar(&userDir, "dir", "/tmp/", "user directory")
37 | case "linux":
38 | flag.StringVar(&chromePath, "chrome", "/usr/bin/chromium-browser", "path to chrome")
39 | flag.StringVar(&userDir, "dir", "/tmp/", "user directory")
40 | }
41 | flag.StringVar(&chromePort, "port", "9222", "Debugger port")
42 | flag.BoolVar(&debug, "debug", false, "Show debug DOM node event changes")
43 | }
44 |
45 | func main() {
46 | flag.Parse()
47 | settings := autogcd.NewSettings(chromePath, randUserDir())
48 | settings.RemoveUserDir(true) // clean up user directory after exit
49 | settings.AddStartupFlags(startupFlags) // disable new tab junk
50 |
51 | auto := autogcd.NewAutoGcd(settings) // create our automation debugger
52 | auto.Start() // start it
53 | defer auto.Shutdown()
54 |
55 | tab, err := auto.GetTab() // get the first visual tab
56 | if err != nil {
57 | log.Fatalf("error getting visual tab to work with")
58 | }
59 | configureTab(tab)
60 |
61 | if _, _, err := tab.Navigate("https://www.google.co.jp"); err != nil {
62 | log.Fatalf("error going to google: %s\n", err)
63 | }
64 | log.Printf("navigation complete")
65 |
66 | err = tab.WaitFor(waitForRate, waitForTimeout, autogcd.ElementByIdReady(tab, "lst-ib"))
67 | if err != nil {
68 | log.Println("timed out waiting for search field")
69 | }
70 |
71 | log.Printf("getting search element")
72 | ele, _, err := tab.GetElementById("lst-ib")
73 | if err != nil {
74 | log.Fatalf("error finding search element: %s\n", err)
75 | }
76 |
77 | log.Println("sending keys")
78 | err = ele.SendKeys("github gcd\n") // use \n to hit enter
79 | if err != nil {
80 | log.Fatalf("error sending keys to element: %s\n", err)
81 | }
82 |
83 | err = tab.WaitFor(waitForRate, waitForTimeout, autogcd.TitleContains(tab, "github gcd"))
84 | if err != nil {
85 | log.Println("timed out waiting for title to change to github gcd")
86 | }
87 |
88 | log.Println("waiting for stability")
89 | err = tab.WaitStable()
90 | if err != nil {
91 | log.Printf("stability timed out: %s\n", err)
92 | }
93 |
94 | log.Println("getting search elements")
95 | eles, err := tab.GetElementsBySelector("a")
96 | for _, ele := range eles {
97 | link := ele.GetAttribute("href")
98 | if link == "https://github.com/wirepair/gcd" {
99 | log.Println("found the best link there is")
100 | loadGcd(ele, tab)
101 | break
102 | }
103 | }
104 | log.Printf("Done")
105 | }
106 |
107 | // Set various timeouts
108 | func configureTab(tab *autogcd.Tab) {
109 | tab.SetNavigationTimeout(navigationTimeout) // give up after 10 seconds for navigating, default is 30 seconds
110 | tab.SetStabilityTime(stableAfter)
111 | if debug {
112 | domHandlerFn := func(tab *autogcd.Tab, change *autogcd.NodeChangeEvent) {
113 | log.Printf("change event %s occurred\n", change.EventType)
114 | }
115 | tab.GetDOMChanges(domHandlerFn)
116 | }
117 | }
118 |
119 | func loadGcd(ele *autogcd.Element, tab *autogcd.Tab) {
120 | log.Printf("clicking google link\n")
121 | err := ele.Click()
122 | if err != nil {
123 | log.Fatalf("error clicking google link: %s\n", err)
124 | }
125 | tab.WaitFor(waitForRate, waitForTimeout, autogcd.TitleContains(tab, "wirepair/gcd"))
126 | log.Printf("here we are, bask in its glory")
127 | time.Sleep(5 * time.Second)
128 | }
129 |
130 | func randUserDir() string {
131 | dir, err := ioutil.TempDir(userDir, "autogcd")
132 | if err != nil {
133 | log.Fatalf("error getting temp dir: %s\n", err)
134 | }
135 | return dir
136 | }
137 |
--------------------------------------------------------------------------------
/settings.go:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2017 isaac dawson
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 | */
24 |
25 | package autogcd
26 |
27 | import (
28 | "fmt"
29 | "time"
30 | )
31 |
32 | type Settings struct {
33 | connectToInstance bool
34 | timeout time.Duration // timeout for giving up on chrome starting and connecting to the debugger service
35 | chromePath string // path to chrome
36 | chromeHost string // can really only be localhost
37 | chromePort string // port to chrome debugger
38 | userDir string // the user directory to use
39 | removeUserDir bool // should we delete the user directory on shutdown?
40 | extensions []string // custom extensions to load
41 | flags []string // custom os.Environ flags to use to start the chrome process
42 | env []string // custom env vars for launching the process
43 | }
44 |
45 | // Creates a new settings object to start Chrome and enable remote debugging
46 | func NewSettings(chromePath, userDir string) *Settings {
47 | s := &Settings{}
48 | s.chromePath = chromePath
49 | s.chromePort = "9222"
50 | s.userDir = userDir
51 | s.removeUserDir = false
52 | s.extensions = make([]string, 0)
53 | s.flags = make([]string, 0)
54 | s.env = make([]string, 0)
55 | return s
56 | }
57 |
58 | //Set a instance to connect to other than start a new process
59 | func (s *Settings) SetInstance(host, port string) {
60 | s.chromeHost = host
61 | s.chromePort = port
62 | s.connectToInstance = true
63 | }
64 |
65 | // Can really only be localhost, but this may change in the future so support it anyways.
66 | func (s *Settings) SetChromeHost(host string) {
67 | s.chromeHost = host
68 | }
69 |
70 | func (s *Settings) AddEnvironmentVars(env []string) {
71 | s.env = append(s.env, env...)
72 | }
73 |
74 | // Sets the chrome debugger port.
75 | func (s *Settings) SetDebuggerPort(chromePort string) {
76 | s.chromePort = chromePort
77 | }
78 |
79 | // How long to wait for chrome to startup and allow us to connect.
80 | func (s *Settings) SetStartTimeout(timeout time.Duration) {
81 | s.timeout = timeout
82 | }
83 |
84 | // On Shutdown, deletes the userDir and files if true.
85 | func (s *Settings) RemoveUserDir(shouldRemove bool) {
86 | s.removeUserDir = true
87 | }
88 |
89 | // Adds custom flags when starting the chrome process
90 | func (s *Settings) AddStartupFlags(flags []string) {
91 | s.flags = append(s.flags, flags...)
92 | }
93 |
94 | // Adds a custom extension to launch with chrome. Note this extension MAY NOT USE
95 | // the chrome.debugger API since you can not attach debuggers to a Tab twice.
96 | func (s *Settings) AddExtension(paths []string) {
97 | for _, ext := range paths {
98 | s.extensions = append(s.extensions, fmt.Sprintf("--load-extension=%s", ext))
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tab_subscribers.go:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2017 isaac dawson
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 | */
24 |
25 | package autogcd
26 |
27 | import (
28 | "encoding/json"
29 | "github.com/wirepair/gcd"
30 | "github.com/wirepair/gcd/gcdapi"
31 | )
32 |
33 | func (t *Tab) subscribeTargetCrashed() {
34 | t.Subscribe("Inspector.targetCrashed", func(target *gcd.ChromeTarget, payload []byte) {
35 | select {
36 | case t.crashedCh <- "crashed":
37 | case <-t.exitCh:
38 | }
39 | })
40 | }
41 |
42 | func (t *Tab) subscribeTargetDetached() {
43 | t.Subscribe("Inspector.detached", func(target *gcd.ChromeTarget, payload []byte) {
44 | header := &gcdapi.InspectorDetachedEvent{}
45 | err := json.Unmarshal(payload, header)
46 | reason := "detached"
47 |
48 | if err == nil {
49 | reason = header.Params.Reason
50 | }
51 |
52 | select {
53 | case t.crashedCh <- reason:
54 | case <-t.exitCh:
55 | }
56 | })
57 | }
58 |
59 | // our default loadFiredEvent handler, returns a response to resp channel to navigate once complete.
60 | func (t *Tab) subscribeLoadEvent() {
61 | t.Subscribe("Page.loadEventFired", func(target *gcd.ChromeTarget, payload []byte) {
62 | if t.IsNavigating() {
63 | select {
64 | case t.navigationCh <- 0:
65 | case <-t.exitCh:
66 | }
67 |
68 | }
69 | })
70 | }
71 |
72 | func (t *Tab) subscribeFrameLoadingEvent() {
73 | t.Subscribe("Page.frameStartedLoading", func(target *gcd.ChromeTarget, payload []byte) {
74 | t.debugf("frameStartedLoading: %s\n", string(payload))
75 | if t.IsNavigating() {
76 | return
77 | }
78 | header := &gcdapi.PageFrameStartedLoadingEvent{}
79 | err := json.Unmarshal(payload, header)
80 | // has the top frame id begun navigating?
81 | if err == nil && header.Params.FrameId == t.GetTopFrameId() {
82 | t.setIsTransitioning(true)
83 | }
84 | })
85 | }
86 |
87 | func (t *Tab) subscribeFrameFinishedEvent() {
88 | t.Subscribe("Page.frameStoppedLoading", func(target *gcd.ChromeTarget, payload []byte) {
89 | t.debugf("frameStoppedLoading: %s\n", string(payload))
90 | if t.IsNavigating() {
91 | return
92 | }
93 | header := &gcdapi.PageFrameStoppedLoadingEvent{}
94 | err := json.Unmarshal(payload, header)
95 | // has the top frame id begun navigating?
96 | if err == nil && header.Params.FrameId == t.GetTopFrameId() {
97 | t.setIsTransitioning(false)
98 | }
99 | })
100 | }
101 |
102 | func (t *Tab) subscribeSetChildNodes() {
103 | // new nodes
104 | t.Subscribe("DOM.setChildNodes", func(target *gcd.ChromeTarget, payload []byte) {
105 | header := &gcdapi.DOMSetChildNodesEvent{}
106 | err := json.Unmarshal(payload, header)
107 | if err == nil {
108 | event := header.Params
109 | t.dispatchNodeChange(&NodeChangeEvent{EventType: SetChildNodesEvent, Nodes: event.Nodes, ParentNodeId: event.ParentId})
110 |
111 | }
112 | })
113 | }
114 |
115 | func (t *Tab) subscribeAttributeModified() {
116 | t.Subscribe("DOM.attributeModified", func(target *gcd.ChromeTarget, payload []byte) {
117 | header := &gcdapi.DOMAttributeModifiedEvent{}
118 | err := json.Unmarshal(payload, header)
119 | if err == nil {
120 | event := header.Params
121 | t.dispatchNodeChange(&NodeChangeEvent{EventType: AttributeModifiedEvent, Name: event.Name, Value: event.Value, NodeId: event.NodeId})
122 | }
123 | })
124 | }
125 |
126 | func (t *Tab) subscribeAttributeRemoved() {
127 | t.Subscribe("DOM.attributeRemoved", func(target *gcd.ChromeTarget, payload []byte) {
128 | header := &gcdapi.DOMAttributeRemovedEvent{}
129 | err := json.Unmarshal(payload, header)
130 | if err == nil {
131 | event := header.Params
132 | t.dispatchNodeChange(&NodeChangeEvent{EventType: AttributeRemovedEvent, NodeId: event.NodeId, Name: event.Name})
133 | }
134 | })
135 | }
136 | func (t *Tab) subscribeCharacterDataModified() {
137 | t.Subscribe("DOM.characterDataModified", func(target *gcd.ChromeTarget, payload []byte) {
138 | header := &gcdapi.DOMCharacterDataModifiedEvent{}
139 | err := json.Unmarshal(payload, header)
140 | if err == nil {
141 | event := header.Params
142 | t.dispatchNodeChange(&NodeChangeEvent{EventType: CharacterDataModifiedEvent, NodeId: event.NodeId, CharacterData: event.CharacterData})
143 | }
144 | })
145 | }
146 | func (t *Tab) subscribeChildNodeCountUpdated() {
147 | t.Subscribe("DOM.childNodeCountUpdated", func(target *gcd.ChromeTarget, payload []byte) {
148 | header := &gcdapi.DOMChildNodeCountUpdatedEvent{}
149 | err := json.Unmarshal(payload, header)
150 | if err == nil {
151 | event := header.Params
152 | t.dispatchNodeChange(&NodeChangeEvent{EventType: ChildNodeCountUpdatedEvent, NodeId: event.NodeId, ChildNodeCount: event.ChildNodeCount})
153 | }
154 | })
155 | }
156 | func (t *Tab) subscribeChildNodeInserted() {
157 | t.Subscribe("DOM.childNodeInserted", func(target *gcd.ChromeTarget, payload []byte) {
158 | //log.Printf("childNodeInserted: %s\n", string(payload))
159 | header := &gcdapi.DOMChildNodeInsertedEvent{}
160 | err := json.Unmarshal(payload, header)
161 | if err == nil {
162 | event := header.Params
163 | t.dispatchNodeChange(&NodeChangeEvent{EventType: ChildNodeInsertedEvent, Node: event.Node, ParentNodeId: event.ParentNodeId, PreviousNodeId: event.PreviousNodeId})
164 | }
165 | })
166 | }
167 | func (t *Tab) subscribeChildNodeRemoved() {
168 | t.Subscribe("DOM.childNodeRemoved", func(target *gcd.ChromeTarget, payload []byte) {
169 | header := &gcdapi.DOMChildNodeRemovedEvent{}
170 | err := json.Unmarshal(payload, header)
171 | if err == nil {
172 | event := header.Params
173 | t.dispatchNodeChange(&NodeChangeEvent{EventType: ChildNodeRemovedEvent, ParentNodeId: event.ParentNodeId, NodeId: event.NodeId})
174 | }
175 | })
176 | }
177 |
178 | func (t *Tab) dispatchNodeChange(evt *NodeChangeEvent) {
179 | select {
180 | case t.nodeChange <- evt:
181 | case <-t.exitCh:
182 | return
183 | }
184 | }
185 |
186 | /*
187 | func (t *Tab) subscribeInlineStyleInvalidated() {
188 | t.Subscribe("DOM.inlineStyleInvalidatedEvent", func(target *gcd.ChromeTarget, payload []byte) {
189 | event := &gcdapi.DOMInlineStyleInvalidatedEvent{}
190 | err := json.Unmarshal(payload, header)
191 | if err == nil {
192 | event = header.Params
193 | t.nodeChange <- &NodeChangeEvent{EventType: InlineStyleInvalidatedEvent, NodeIds: event.NodeIds}
194 | }
195 | })
196 | }
197 | */
198 | func (t *Tab) subscribeDocumentUpdated() {
199 | // node ids are no longer valid
200 | t.Subscribe("DOM.documentUpdated", func(target *gcd.ChromeTarget, payload []byte) {
201 | select {
202 | case t.nodeChange <- &NodeChangeEvent{EventType: DocumentUpdatedEvent}:
203 | case <-t.exitCh:
204 | }
205 | })
206 | }
207 |
--------------------------------------------------------------------------------
/testdata/attributes.html:
--------------------------------------------------------------------------------
1 |
2 |