chrome.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. package lorca
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "io/ioutil"
  9. "log"
  10. "os/exec"
  11. "regexp"
  12. "sync"
  13. "sync/atomic"
  14. "golang.org/x/net/websocket"
  15. )
  16. type h = map[string]interface{}
  17. // Result is a struct for the resulting value of the JS expression or an error.
  18. type result struct {
  19. Value json.RawMessage
  20. Err error
  21. }
  22. type bindingFunc func(args []json.RawMessage) (interface{}, error)
  23. // Msg is a struct for incoming messages (results and async events)
  24. type msg struct {
  25. ID int `json:"id"`
  26. Result json.RawMessage `json:"result"`
  27. Error json.RawMessage `json:"error"`
  28. Method string `json:"method"`
  29. Params json.RawMessage `json:"params"`
  30. }
  31. type chrome struct {
  32. sync.Mutex
  33. cmd *exec.Cmd
  34. ws *websocket.Conn
  35. id int32
  36. target string
  37. session string
  38. window int
  39. pending map[int]chan result
  40. bindings map[string]bindingFunc
  41. }
  42. func newChromeWithArgs(chromeBinary string, args ...string) (*chrome, error) {
  43. // The first two IDs are used internally during the initialization
  44. c := &chrome{
  45. id: 2,
  46. pending: map[int]chan result{},
  47. bindings: map[string]bindingFunc{},
  48. }
  49. // Start chrome process
  50. c.cmd = exec.Command(chromeBinary, args...)
  51. pipe, err := c.cmd.StderrPipe()
  52. if err != nil {
  53. return nil, err
  54. }
  55. if err := c.cmd.Start(); err != nil {
  56. return nil, err
  57. }
  58. // Wait for websocket address to be printed to stderr
  59. re := regexp.MustCompile(`^DevTools listening on (ws://.*?)\r?\n$`)
  60. m, err := readUntilMatch(pipe, re)
  61. if err != nil {
  62. c.kill()
  63. return nil, err
  64. }
  65. wsURL := m[1]
  66. // Open a websocket
  67. c.ws, err = websocket.Dial(wsURL, "", "http://127.0.0.1")
  68. if err != nil {
  69. c.kill()
  70. return nil, err
  71. }
  72. // Find target and initialize session
  73. c.target, err = c.findTarget()
  74. if err != nil {
  75. c.kill()
  76. return nil, err
  77. }
  78. c.session, err = c.startSession(c.target)
  79. if err != nil {
  80. c.kill()
  81. return nil, err
  82. }
  83. go c.readLoop()
  84. for method, args := range map[string]h{
  85. "Page.enable": nil,
  86. "Target.setAutoAttach": {"autoAttach": true, "waitForDebuggerOnStart": false},
  87. "Network.enable": nil,
  88. "Runtime.enable": nil,
  89. "Security.enable": nil,
  90. "Performance.enable": nil,
  91. "Log.enable": nil,
  92. } {
  93. if _, err := c.send(method, args); err != nil {
  94. c.kill()
  95. c.cmd.Wait()
  96. return nil, err
  97. }
  98. }
  99. if !contains(args, "--headless") {
  100. win, err := c.getWindowForTarget(c.target)
  101. if err != nil {
  102. c.kill()
  103. return nil, err
  104. }
  105. c.window = win.WindowID
  106. }
  107. return c, nil
  108. }
  109. func (c *chrome) findTarget() (string, error) {
  110. err := websocket.JSON.Send(c.ws, h{
  111. "id": 0, "method": "Target.setDiscoverTargets", "params": h{"discover": true},
  112. })
  113. if err != nil {
  114. return "", err
  115. }
  116. for {
  117. m := msg{}
  118. if err = websocket.JSON.Receive(c.ws, &m); err != nil {
  119. return "", err
  120. } else if m.Method == "Target.targetCreated" {
  121. target := struct {
  122. TargetInfo struct {
  123. Type string `json:"type"`
  124. ID string `json:"targetId"`
  125. } `json:"targetInfo"`
  126. }{}
  127. if err := json.Unmarshal(m.Params, &target); err != nil {
  128. return "", err
  129. } else if target.TargetInfo.Type == "page" {
  130. return target.TargetInfo.ID, nil
  131. }
  132. }
  133. }
  134. }
  135. func (c *chrome) startSession(target string) (string, error) {
  136. err := websocket.JSON.Send(c.ws, h{
  137. "id": 1, "method": "Target.attachToTarget", "params": h{"targetId": target},
  138. })
  139. if err != nil {
  140. return "", err
  141. }
  142. for {
  143. m := msg{}
  144. if err = websocket.JSON.Receive(c.ws, &m); err != nil {
  145. return "", err
  146. } else if m.ID == 1 {
  147. if m.Error != nil {
  148. return "", errors.New("Target error: " + string(m.Error))
  149. }
  150. session := struct {
  151. ID string `json:"sessionId"`
  152. }{}
  153. if err := json.Unmarshal(m.Result, &session); err != nil {
  154. return "", err
  155. }
  156. return session.ID, nil
  157. }
  158. }
  159. }
  160. // WindowState defines the state of the Chrome window, possible values are
  161. // "normal", "maximized", "minimized" and "fullscreen".
  162. type WindowState string
  163. const (
  164. // WindowStateNormal defines a normal state of the browser window
  165. WindowStateNormal WindowState = "normal"
  166. // WindowStateMaximized defines a maximized state of the browser window
  167. WindowStateMaximized WindowState = "maximized"
  168. // WindowStateMinimized defines a minimized state of the browser window
  169. WindowStateMinimized WindowState = "minimized"
  170. // WindowStateFullscreen defines a fullscreen state of the browser window
  171. WindowStateFullscreen WindowState = "fullscreen"
  172. )
  173. // Bounds defines settable window properties.
  174. type Bounds struct {
  175. Left int `json:"left"`
  176. Top int `json:"top"`
  177. Width int `json:"width"`
  178. Height int `json:"height"`
  179. WindowState WindowState `json:"windowState"`
  180. }
  181. type windowTargetMessage struct {
  182. WindowID int `json:"windowId"`
  183. Bounds Bounds `json:"bounds"`
  184. }
  185. func (c *chrome) getWindowForTarget(target string) (windowTargetMessage, error) {
  186. var m windowTargetMessage
  187. msg, err := c.send("Browser.getWindowForTarget", h{"targetId": target})
  188. if err != nil {
  189. return m, err
  190. }
  191. err = json.Unmarshal(msg, &m)
  192. return m, err
  193. }
  194. type targetMessageTemplate struct {
  195. ID int `json:"id"`
  196. Method string `json:"method"`
  197. Params struct {
  198. Name string `json:"name"`
  199. Payload string `json:"payload"`
  200. ID int `json:"executionContextId"`
  201. Args []struct {
  202. Type string `json:"type"`
  203. Value interface{} `json:"value"`
  204. } `json:"args"`
  205. } `json:"params"`
  206. Error struct {
  207. Message string `json:"message"`
  208. } `json:"error"`
  209. Result json.RawMessage `json:"result"`
  210. }
  211. type targetMessage struct {
  212. targetMessageTemplate
  213. Result struct {
  214. Result struct {
  215. Type string `json:"type"`
  216. Subtype string `json:"subtype"`
  217. Description string `json:"description"`
  218. Value json.RawMessage `json:"value"`
  219. ObjectID string `json:"objectId"`
  220. } `json:"result"`
  221. Exception struct {
  222. Exception struct {
  223. Value json.RawMessage `json:"value"`
  224. } `json:"exception"`
  225. } `json:"exceptionDetails"`
  226. } `json:"result"`
  227. }
  228. func (c *chrome) readLoop() {
  229. for {
  230. m := msg{}
  231. if err := websocket.JSON.Receive(c.ws, &m); err != nil {
  232. return
  233. }
  234. if m.Method == "Target.receivedMessageFromTarget" {
  235. params := struct {
  236. SessionID string `json:"sessionId"`
  237. Message string `json:"message"`
  238. }{}
  239. json.Unmarshal(m.Params, &params)
  240. if params.SessionID != c.session {
  241. continue
  242. }
  243. res := targetMessage{}
  244. json.Unmarshal([]byte(params.Message), &res)
  245. if res.ID == 0 && res.Method == "Runtime.consoleAPICalled" || res.Method == "Runtime.exceptionThrown" {
  246. log.Println(params.Message)
  247. } else if res.ID == 0 && res.Method == "Runtime.bindingCalled" {
  248. payload := struct {
  249. Name string `json:"name"`
  250. Seq int `json:"seq"`
  251. Args []json.RawMessage `json:"args"`
  252. }{}
  253. json.Unmarshal([]byte(res.Params.Payload), &payload)
  254. c.Lock()
  255. binding, ok := c.bindings[res.Params.Name]
  256. c.Unlock()
  257. if ok {
  258. jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }
  259. go func() {
  260. result, error := "", `""`
  261. if r, err := binding(payload.Args); err != nil {
  262. error = jsString(err.Error())
  263. } else if b, err := json.Marshal(r); err != nil {
  264. error = jsString(err.Error())
  265. } else {
  266. result = string(b)
  267. }
  268. expr := fmt.Sprintf(`
  269. if (%[4]s) {
  270. window['%[1]s']['errors'].get(%[2]d)(%[4]s);
  271. } else {
  272. window['%[1]s']['callbacks'].get(%[2]d)(%[3]s);
  273. }
  274. window['%[1]s']['callbacks'].delete(%[2]d);
  275. window['%[1]s']['errors'].delete(%[2]d);
  276. `, payload.Name, payload.Seq, result, error)
  277. c.send("Runtime.evaluate", h{"expression": expr, "contextId": res.Params.ID})
  278. }()
  279. }
  280. continue
  281. }
  282. c.Lock()
  283. resc, ok := c.pending[res.ID]
  284. delete(c.pending, res.ID)
  285. c.Unlock()
  286. if !ok {
  287. continue
  288. }
  289. if res.Error.Message != "" {
  290. resc <- result{Err: errors.New(res.Error.Message)}
  291. } else if res.Result.Exception.Exception.Value != nil {
  292. resc <- result{Err: errors.New(string(res.Result.Exception.Exception.Value))}
  293. } else if res.Result.Result.Type == "object" && res.Result.Result.Subtype == "error" {
  294. resc <- result{Err: errors.New(res.Result.Result.Description)}
  295. } else if res.Result.Result.Type != "" {
  296. resc <- result{Value: res.Result.Result.Value}
  297. } else {
  298. res := targetMessageTemplate{}
  299. json.Unmarshal([]byte(params.Message), &res)
  300. resc <- result{Value: res.Result}
  301. }
  302. } else if m.Method == "Target.targetDestroyed" {
  303. params := struct {
  304. TargetID string `json:"targetId"`
  305. }{}
  306. json.Unmarshal(m.Params, &params)
  307. if params.TargetID == c.target {
  308. c.kill()
  309. return
  310. }
  311. }
  312. }
  313. }
  314. func (c *chrome) send(method string, params h) (json.RawMessage, error) {
  315. id := atomic.AddInt32(&c.id, 1)
  316. b, err := json.Marshal(h{"id": int(id), "method": method, "params": params})
  317. if err != nil {
  318. return nil, err
  319. }
  320. resc := make(chan result)
  321. c.Lock()
  322. c.pending[int(id)] = resc
  323. c.Unlock()
  324. if err := websocket.JSON.Send(c.ws, h{
  325. "id": int(id),
  326. "method": "Target.sendMessageToTarget",
  327. "params": h{"message": string(b), "sessionId": c.session},
  328. }); err != nil {
  329. return nil, err
  330. }
  331. res := <-resc
  332. return res.Value, res.Err
  333. }
  334. func (c *chrome) load(url string) error {
  335. _, err := c.send("Page.navigate", h{"url": url})
  336. return err
  337. }
  338. func (c *chrome) eval(expr string) (json.RawMessage, error) {
  339. return c.send("Runtime.evaluate", h{"expression": expr, "awaitPromise": true, "returnByValue": true})
  340. }
  341. func (c *chrome) bind(name string, f bindingFunc) error {
  342. c.Lock()
  343. // check if binding already exists
  344. _, exists := c.bindings[name]
  345. c.bindings[name] = f
  346. c.Unlock()
  347. if exists {
  348. // Just replace callback and return, as the binding was already added to js
  349. // and adding it again would break it.
  350. return nil
  351. }
  352. if _, err := c.send("Runtime.addBinding", h{"name": name}); err != nil {
  353. return err
  354. }
  355. script := fmt.Sprintf(`(() => {
  356. const bindingName = '%s';
  357. const binding = window[bindingName];
  358. window[bindingName] = async (...args) => {
  359. const me = window[bindingName];
  360. let errors = me['errors'];
  361. let callbacks = me['callbacks'];
  362. if (!callbacks) {
  363. callbacks = new Map();
  364. me['callbacks'] = callbacks;
  365. }
  366. if (!errors) {
  367. errors = new Map();
  368. me['errors'] = errors;
  369. }
  370. const seq = (me['lastSeq'] || 0) + 1;
  371. me['lastSeq'] = seq;
  372. const promise = new Promise((resolve, reject) => {
  373. callbacks.set(seq, resolve);
  374. errors.set(seq, reject);
  375. });
  376. binding(JSON.stringify({name: bindingName, seq, args}));
  377. return promise;
  378. }})();
  379. `, name)
  380. _, err := c.send("Page.addScriptToEvaluateOnNewDocument", h{"source": script})
  381. if err != nil {
  382. return err
  383. }
  384. _, err = c.eval(script)
  385. return err
  386. }
  387. func (c *chrome) setBounds(b Bounds) error {
  388. if b.WindowState == "" {
  389. b.WindowState = WindowStateNormal
  390. }
  391. param := h{"windowId": c.window, "bounds": b}
  392. if b.WindowState != WindowStateNormal {
  393. param["bounds"] = h{"windowState": b.WindowState}
  394. }
  395. _, err := c.send("Browser.setWindowBounds", param)
  396. return err
  397. }
  398. func (c *chrome) bounds() (Bounds, error) {
  399. result, err := c.send("Browser.getWindowBounds", h{"windowId": c.window})
  400. if err != nil {
  401. return Bounds{}, err
  402. }
  403. bounds := struct {
  404. Bounds Bounds `json:"bounds"`
  405. }{}
  406. err = json.Unmarshal(result, &bounds)
  407. return bounds.Bounds, err
  408. }
  409. func (c *chrome) pdf(width, height int) ([]byte, error) {
  410. result, err := c.send("Page.printToPDF", h{
  411. "paperWidth": float32(width) / 96,
  412. "paperHeight": float32(height) / 96,
  413. })
  414. if err != nil {
  415. return nil, err
  416. }
  417. pdf := struct {
  418. Data []byte `json:"data"`
  419. }{}
  420. err = json.Unmarshal(result, &pdf)
  421. return pdf.Data, err
  422. }
  423. func (c *chrome) png(x, y, width, height int, bg uint32, scale float32) ([]byte, error) {
  424. if x == 0 && y == 0 && width == 0 && height == 0 {
  425. // By default either use SVG size if it's an SVG, or use A4 page size
  426. bounds, err := c.eval(`document.rootElement ? [document.rootElement.x.baseVal.value, document.rootElement.y.baseVal.value, document.rootElement.width.baseVal.value, document.rootElement.height.baseVal.value] : [0,0,816,1056]`)
  427. if err != nil {
  428. return nil, err
  429. }
  430. rect := make([]int, 4)
  431. if err := json.Unmarshal(bounds, &rect); err != nil {
  432. return nil, err
  433. }
  434. x, y, width, height = rect[0], rect[1], rect[2], rect[3]
  435. }
  436. _, err := c.send("Emulation.setDefaultBackgroundColorOverride", h{
  437. "color": h{
  438. "r": (bg >> 16) & 0xff,
  439. "g": (bg >> 8) & 0xff,
  440. "b": bg & 0xff,
  441. "a": (bg >> 24) & 0xff,
  442. },
  443. })
  444. if err != nil {
  445. return nil, err
  446. }
  447. result, err := c.send("Page.captureScreenshot", h{
  448. "clip": h{
  449. "x": x, "y": y, "width": width, "height": height, "scale": scale,
  450. },
  451. })
  452. if err != nil {
  453. return nil, err
  454. }
  455. pdf := struct {
  456. Data []byte `json:"data"`
  457. }{}
  458. err = json.Unmarshal(result, &pdf)
  459. return pdf.Data, err
  460. }
  461. func (c *chrome) kill() error {
  462. if c.ws != nil {
  463. if err := c.ws.Close(); err != nil {
  464. return err
  465. }
  466. }
  467. // TODO: cancel all pending requests
  468. if state := c.cmd.ProcessState; state == nil || !state.Exited() {
  469. return c.cmd.Process.Kill()
  470. }
  471. return nil
  472. }
  473. func readUntilMatch(r io.ReadCloser, re *regexp.Regexp) ([]string, error) {
  474. br := bufio.NewReader(r)
  475. for {
  476. if line, err := br.ReadString('\n'); err != nil {
  477. r.Close()
  478. return nil, err
  479. } else if m := re.FindStringSubmatch(line); m != nil {
  480. go io.Copy(ioutil.Discard, br)
  481. return m, nil
  482. }
  483. }
  484. }
  485. func contains(arr []string, x string) bool {
  486. for _, n := range arr {
  487. if x == n {
  488. return true
  489. }
  490. }
  491. return false
  492. }