ui.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. package lorca
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io/ioutil"
  7. "os"
  8. "reflect"
  9. )
  10. // UI interface allows talking to the HTML5 UI from Go.
  11. type UI interface {
  12. Load(url string) error
  13. Bounds() (Bounds, error)
  14. SetBounds(Bounds) error
  15. Bind(name string, f interface{}) error
  16. Eval(js string) Value
  17. Done() <-chan struct{}
  18. Close() error
  19. }
  20. type ui struct {
  21. chrome *chrome
  22. done chan struct{}
  23. tmpDir string
  24. }
  25. var defaultChromeArgs = []string{
  26. "--disable-background-networking",
  27. "--disable-background-timer-throttling",
  28. "--disable-backgrounding-occluded-windows",
  29. "--disable-breakpad",
  30. "--disable-client-side-phishing-detection",
  31. "--disable-default-apps",
  32. "--disable-dev-shm-usage",
  33. "--disable-infobars",
  34. "--disable-extensions",
  35. "--disable-features=site-per-process",
  36. "--disable-hang-monitor",
  37. "--disable-ipc-flooding-protection",
  38. "--disable-popup-blocking",
  39. "--disable-prompt-on-repost",
  40. "--disable-renderer-backgrounding",
  41. "--disable-sync",
  42. "--disable-translate",
  43. "--disable-windows10-custom-titlebar",
  44. "--metrics-recording-only",
  45. "--no-first-run",
  46. "--no-default-browser-check",
  47. "--safebrowsing-disable-auto-update",
  48. "--enable-automation",
  49. "--password-store=basic",
  50. "--use-mock-keychain",
  51. }
  52. // New returns a new HTML5 UI for the given URL, user profile directory, window
  53. // size and other options passed to the browser engine. If URL is an empty
  54. // string - a blank page is displayed. If user profile directory is an empty
  55. // string - a temporary directory is created and it will be removed on
  56. // ui.Close(). You might want to use "--headless" custom CLI argument to test
  57. // your UI code.
  58. func New(url, dir string, width, height int, customArgs ...string) (UI, error) {
  59. if url == "" {
  60. url = "data:text/html,<html></html>"
  61. }
  62. tmpDir := ""
  63. if dir == "" {
  64. name, err := ioutil.TempDir("", "lorca")
  65. if err != nil {
  66. return nil, err
  67. }
  68. dir, tmpDir = name, name
  69. }
  70. args := append(defaultChromeArgs, fmt.Sprintf("--app=%s", url))
  71. args = append(args, fmt.Sprintf("--user-data-dir=%s", dir))
  72. args = append(args, fmt.Sprintf("--window-size=%d,%d", width, height))
  73. args = append(args, customArgs...)
  74. args = append(args, "--remote-debugging-port=0")
  75. chrome, err := newChromeWithArgs(ChromeExecutable(), args...)
  76. done := make(chan struct{})
  77. if err != nil {
  78. return nil, err
  79. }
  80. go func() {
  81. chrome.cmd.Wait()
  82. close(done)
  83. }()
  84. return &ui{chrome: chrome, done: done, tmpDir: tmpDir}, nil
  85. }
  86. func (u *ui) Done() <-chan struct{} {
  87. return u.done
  88. }
  89. func (u *ui) Close() error {
  90. // ignore err, as the chrome process might be already dead, when user close the window.
  91. u.chrome.kill()
  92. <-u.done
  93. if u.tmpDir != "" {
  94. if err := os.RemoveAll(u.tmpDir); err != nil {
  95. return err
  96. }
  97. }
  98. return nil
  99. }
  100. func (u *ui) Load(url string) error { return u.chrome.load(url) }
  101. func (u *ui) Bind(name string, f interface{}) error {
  102. v := reflect.ValueOf(f)
  103. // f must be a function
  104. if v.Kind() != reflect.Func {
  105. return errors.New("only functions can be bound")
  106. }
  107. // f must return either value and error or just error
  108. if n := v.Type().NumOut(); n > 2 {
  109. return errors.New("function may only return a value or a value+error")
  110. }
  111. return u.chrome.bind(name, func(raw []json.RawMessage) (interface{}, error) {
  112. if len(raw) != v.Type().NumIn() {
  113. return nil, errors.New("function arguments mismatch")
  114. }
  115. args := []reflect.Value{}
  116. for i := range raw {
  117. arg := reflect.New(v.Type().In(i))
  118. if err := json.Unmarshal(raw[i], arg.Interface()); err != nil {
  119. return nil, err
  120. }
  121. args = append(args, arg.Elem())
  122. }
  123. errorType := reflect.TypeOf((*error)(nil)).Elem()
  124. res := v.Call(args)
  125. switch len(res) {
  126. case 0:
  127. // No results from the function, just return nil
  128. return nil, nil
  129. case 1:
  130. // One result may be a value, or an error
  131. if res[0].Type().Implements(errorType) {
  132. if res[0].Interface() != nil {
  133. return nil, res[0].Interface().(error)
  134. }
  135. return nil, nil
  136. }
  137. return res[0].Interface(), nil
  138. case 2:
  139. // Two results: first one is value, second is error
  140. if !res[1].Type().Implements(errorType) {
  141. return nil, errors.New("second return value must be an error")
  142. }
  143. if res[1].Interface() == nil {
  144. return res[0].Interface(), nil
  145. }
  146. return res[0].Interface(), res[1].Interface().(error)
  147. default:
  148. return nil, errors.New("unexpected number of return values")
  149. }
  150. })
  151. }
  152. func (u *ui) Eval(js string) Value {
  153. v, err := u.chrome.eval(js)
  154. return value{err: err, raw: v}
  155. }
  156. func (u *ui) SetBounds(b Bounds) error {
  157. return u.chrome.setBounds(b)
  158. }
  159. func (u *ui) Bounds() (Bounds, error) {
  160. return u.chrome.bounds()
  161. }