renderer.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package renderer
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "runtime"
  10. "time"
  11. "strconv"
  12. "strings"
  13. "github.com/grafana/grafana/pkg/log"
  14. "github.com/grafana/grafana/pkg/middleware"
  15. "github.com/grafana/grafana/pkg/models"
  16. "github.com/grafana/grafana/pkg/setting"
  17. "github.com/grafana/grafana/pkg/util"
  18. )
  19. type RenderOpts struct {
  20. Path string
  21. Width string
  22. Height string
  23. Timeout string
  24. OrgId int64
  25. UserId int64
  26. OrgRole models.RoleType
  27. Timezone string
  28. IsAlertContext bool
  29. Encoding string
  30. }
  31. var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
  32. var rendererLog log.Logger = log.New("png-renderer")
  33. func isoTimeOffsetToPosixTz(isoOffset string) string {
  34. // invert offset
  35. if strings.HasPrefix(isoOffset, "UTC+") {
  36. return strings.Replace(isoOffset, "UTC+", "UTC-", 1)
  37. }
  38. if strings.HasPrefix(isoOffset, "UTC-") {
  39. return strings.Replace(isoOffset, "UTC-", "UTC+", 1)
  40. }
  41. return isoOffset
  42. }
  43. func appendEnviron(baseEnviron []string, name string, value string) []string {
  44. results := make([]string, 0)
  45. prefix := fmt.Sprintf("%s=", name)
  46. for _, v := range baseEnviron {
  47. if !strings.HasPrefix(v, prefix) {
  48. results = append(results, v)
  49. }
  50. }
  51. return append(results, fmt.Sprintf("%s=%s", name, value))
  52. }
  53. func RenderToPng(params *RenderOpts) (string, error) {
  54. rendererLog.Info("Rendering", "path", params.Path)
  55. var executable = "phantomjs"
  56. if runtime.GOOS == "windows" {
  57. executable = executable + ".exe"
  58. }
  59. localDomain := "localhost"
  60. if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
  61. localDomain = setting.HttpAddr
  62. }
  63. // &render=1 signals to the legacy redirect layer to
  64. // avoid redirect these requests.
  65. url := fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, localDomain, setting.HttpPort, params.Path)
  66. binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
  67. scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
  68. pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
  69. pngPath = pngPath + ".png"
  70. orgRole := params.OrgRole
  71. if params.IsAlertContext {
  72. orgRole = models.ROLE_ADMIN
  73. }
  74. renderKey := middleware.AddRenderAuthKey(params.OrgId, params.UserId, orgRole)
  75. defer middleware.RemoveRenderAuthKey(renderKey)
  76. timeout, err := strconv.Atoi(params.Timeout)
  77. if err != nil {
  78. timeout = 15
  79. }
  80. phantomDebugArg := "--debug=false"
  81. if log.GetLogLevelFor("png-renderer") >= log.LvlDebug {
  82. phantomDebugArg = "--debug=true"
  83. }
  84. cmdArgs := []string{
  85. "--ignore-ssl-errors=true",
  86. "--web-security=false",
  87. phantomDebugArg,
  88. scriptPath,
  89. "url=" + url,
  90. "width=" + params.Width,
  91. "height=" + params.Height,
  92. "png=" + pngPath,
  93. "domain=" + localDomain,
  94. "timeout=" + strconv.Itoa(timeout),
  95. "renderKey=" + renderKey,
  96. }
  97. if params.Encoding != "" {
  98. cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", params.Encoding)}, cmdArgs...)
  99. }
  100. cmd := exec.Command(binPath, cmdArgs...)
  101. output, err := cmd.StdoutPipe()
  102. if err != nil {
  103. rendererLog.Error("Could not acquire stdout pipe", err)
  104. return "", err
  105. }
  106. cmd.Stderr = cmd.Stdout
  107. if params.Timezone != "" {
  108. baseEnviron := os.Environ()
  109. cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(params.Timezone))
  110. }
  111. err = cmd.Start()
  112. if err != nil {
  113. rendererLog.Error("Could not start command", err)
  114. return "", err
  115. }
  116. logWriter := log.NewLogWriter(rendererLog, log.LvlDebug, "[phantom] ")
  117. go io.Copy(logWriter, output)
  118. done := make(chan error)
  119. go func() {
  120. if err := cmd.Wait(); err != nil {
  121. rendererLog.Error("failed to render an image", "error", err)
  122. }
  123. close(done)
  124. }()
  125. select {
  126. case <-time.After(time.Duration(timeout) * time.Second):
  127. if err := cmd.Process.Kill(); err != nil {
  128. rendererLog.Error("failed to kill", "error", err)
  129. }
  130. return "", ErrTimeout
  131. case <-done:
  132. }
  133. rendererLog.Debug("Image rendered", "path", pngPath)
  134. return pngPath, nil
  135. }