-
Notifications
You must be signed in to change notification settings - Fork 84
/
static_renderer.go
134 lines (114 loc) · 3.9 KB
/
static_renderer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package webloop
import (
"log"
"net/http"
"runtime"
"strings"
"sync"
"time"
"github.com/gotk3/gotk3/gtk"
)
// StaticRenderer generates and returns static HTML based on a snapshot of a Web
// page's computed HTML.
type StaticRenderer struct {
// TargetBaseURL is the baseURL of the dynamic content URLs.
TargetBaseURL string
// Context is the WebLoop context to create views in.
Context Context
// WaitTimeout is the maximum duration to wait for a loaded page to set
// window.$renderStaticReady.
WaitTimeout time.Duration
// ReturnUnfinishedPages is whether a page that has not set
// window.$renderStaticReady after WaitTimeout is sent to the browser in a
// (potentially) unfinished state. If false, an HTTP 502 Bad Gateway error
// will be returned.
//
// If you are unsure of whether all accessible pages set
// window.$renderStaticReady (perhaps you could forget to do so on a few
// pages), then setting ReturnUnfinishedPages would suppress errors for
// those pages, at the possible expense of sending out unfinished pages that
// take a long time to load.
ReturnUnfinishedPages bool
// RemoveJavaScript indicates whether <script> tags will be removed. When
// generating static HTML pages from a dynamic JavaScript app, this is often
// necessary because the JavaScript expects to run on a non-bootstrapped
// page. This option is not guaranteed to disable all <script> tags and
// should relied upon for security purposes.
RemoveScripts bool
// Log is the logger to use for log messages. If nil, there is no log
// output.
Log *log.Logger
viewLock sync.Mutex
view *View
}
var startGTKOnce sync.Once
// StartGTK ensures that the GTK+ main loop has started. If it has already been
// started by StartGTK, it will not start it again. If another goroutine is
// already running the GTK+ main loop, StartGTK's behavior is undefined.
func (h *StaticRenderer) StartGTK() {
startGTKOnce.Do(func() {
gtk.Init(nil)
go func() {
runtime.LockOSThread()
gtk.Main()
}()
})
}
// Release releases resources used by this handler, such as the view. If this
// handler is reused after calling Release, the view is automatically recreated.
func (h *StaticRenderer) Release() {
h.viewLock.Lock()
h.view.Close()
h.view = nil
defer h.viewLock.Unlock()
}
// ServeHTTP implements net/http.Handler.
func (h *StaticRenderer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.StartGTK()
h.viewLock.Lock()
defer h.viewLock.Unlock()
if h.view == nil {
h.view = h.Context.NewView()
}
targetURL := h.TargetBaseURL + r.URL.String()
h.logf("Rendering HTML for page at URL: %s", targetURL)
h.view.Open(targetURL)
h.view.Wait()
// Wait until window.$renderStaticReady is true.
start := time.Now()
for {
if time.Since(start) > h.WaitTimeout {
if h.ReturnUnfinishedPages {
h.logf("Page at URL %s did not set $renderStaticReady within timeout %s; returning unfinished page", targetURL, h.WaitTimeout)
break
}
h.logf("Page at URL %s did not set $renderStaticReady within timeout %s; returning HTTP error", targetURL, h.WaitTimeout)
http.Error(w, "No response from origin server within "+h.WaitTimeout.String(), http.StatusBadGateway)
return
}
ready, err := h.view.EvaluateJavaScript("window.$renderStaticReady")
if err != nil {
http.Error(w, "error checking $renderStaticReady: "+err.Error(), http.StatusInternalServerError)
return
}
if ready, _ := ready.(bool); ready {
break
}
}
result, err := h.view.EvaluateJavaScript("document.documentElement.outerHTML")
if err != nil {
h.logf("Failed to dump HTML for page at URL %s: %s", targetURL, err)
http.Error(w, "", http.StatusInternalServerError)
return
}
html := result.(string)
if h.RemoveScripts {
html = strings.Replace(html, "<script", `<script type="text/disabled"`, -1)
}
w.Write([]byte(html))
}
func (h *StaticRenderer) logf(msg string, v ...interface{}) {
if h.Log != nil {
h.Log.Printf(msg, v...)
}
}