1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17 package server
18
19 import (
20 "encoding/json"
21 "errors"
22 "fmt"
23 "html"
24 "io"
25 "log"
26 "net/http"
27 "os"
28 "reflect"
29 "regexp"
30 "runtime"
31 "strconv"
32 "strings"
33 "time"
34
35 "perkeep.org/internal/httputil"
36 "perkeep.org/internal/osutil"
37 "perkeep.org/pkg/blobserver"
38 "perkeep.org/pkg/buildinfo"
39 "perkeep.org/pkg/env"
40 "perkeep.org/pkg/index"
41 "perkeep.org/pkg/search"
42 "perkeep.org/pkg/server/app"
43 "perkeep.org/pkg/types/camtypes"
44
45 "cloud.google.com/go/compute/metadata"
46 "go4.org/jsonconfig"
47 )
48
49
50 type StatusHandler struct {
51 prefix string
52 handlerFinder blobserver.FindHandlerByTyper
53 }
54
55 func init() {
56 blobserver.RegisterHandlerConstructor("status", newStatusFromConfig)
57 }
58
59 var _ blobserver.HandlerIniter = (*StatusHandler)(nil)
60
61 func newStatusFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) {
62 if err := conf.Validate(); err != nil {
63 return nil, err
64 }
65 return &StatusHandler{
66 prefix: ld.MyPrefix(),
67 handlerFinder: ld,
68 }, nil
69 }
70
71 func (sh *StatusHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
72 _, h, err := hl.FindHandlerByType("search")
73 if err == blobserver.ErrHandlerTypeNotFound {
74 return nil
75 }
76 if err != nil {
77 return err
78 }
79 go func() {
80 var lastSend *status
81 for {
82 cur := sh.currentStatus()
83 if reflect.DeepEqual(cur, lastSend) {
84
85 time.Sleep(10 * time.Second)
86 continue
87 }
88 lastSend = cur
89 js, _ := json.MarshalIndent(cur, "", " ")
90 h.(*search.Handler).SendStatusUpdate(js)
91 }
92 }()
93 return nil
94 }
95
96 func (sh *StatusHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
97 suffix := httputil.PathSuffix(req)
98 if suffix == "restart" {
99 sh.serveRestart(rw, req)
100 return
101 }
102 if !httputil.IsGet(req) {
103 http.Error(rw, "Illegal status method.", http.StatusMethodNotAllowed)
104 return
105 }
106 switch suffix {
107 case "status.json":
108 sh.serveStatusJSON(rw, req)
109 case "":
110 sh.serveStatusHTML(rw, req)
111 default:
112 http.Error(rw, "Illegal status path.", http.StatusNotFound)
113 }
114 }
115
116 type status struct {
117 Version string `json:"version"`
118 GoInfo string `json:"goInfo"`
119 Errors []camtypes.StatusError `json:"errors,omitempty"`
120 Sync map[string]syncStatus `json:"sync"`
121 Storage map[string]storageStatus `json:"storage"`
122 importerRoot string
123 rootPrefix string
124
125 ImporterAccounts interface{} `json:"importerAccounts"`
126 }
127
128 func (st *status) addError(msg, url string) {
129 st.Errors = append(st.Errors, camtypes.StatusError{
130 Error: msg,
131 URL: url,
132 })
133 }
134
135 func (st *status) isHandler(pfx string) bool {
136 if pfx == st.importerRoot {
137 return true
138 }
139 if _, ok := st.Sync[pfx]; ok {
140 return true
141 }
142 if _, ok := st.Storage[pfx]; ok {
143 return true
144 }
145 return false
146 }
147
148 type storageStatus struct {
149 Primary bool `json:"primary,omitempty"`
150 IsIndex bool `json:"isIndex,omitempty"`
151 Type string `json:"type"`
152 ApproxBlobs int `json:"approxBlobs,omitempty"`
153 ApproxBytes int `json:"approxBytes,omitempty"`
154 ImplStatus interface{} `json:"implStatus,omitempty"`
155 }
156
157 func (sh *StatusHandler) currentStatus() *status {
158 res := &status{
159 Version: buildinfo.Summary(),
160 GoInfo: fmt.Sprintf("%s %s/%s cgo=%v", runtime.Version(), runtime.GOOS, runtime.GOARCH, cgoEnabled),
161 Storage: make(map[string]storageStatus),
162 Sync: make(map[string]syncStatus),
163 }
164 if v := os.Getenv("CAMLI_FAKE_STATUS_ERROR"); v != "" {
165 res.addError(v, "/status/#fakeerror")
166 }
167 _, hi, err := sh.handlerFinder.FindHandlerByType("root")
168 if err != nil {
169 res.addError(fmt.Sprintf("Error finding root handler: %v", err), "")
170 return res
171 }
172 rh := hi.(*RootHandler)
173 res.rootPrefix = rh.Prefix
174
175 if pfx, h, err := sh.handlerFinder.FindHandlerByType("importer"); err == nil {
176 res.importerRoot = pfx
177 as := h.(interface {
178 AccountsStatus() (interface{}, []camtypes.StatusError)
179 })
180 var errs []camtypes.StatusError
181 res.ImporterAccounts, errs = as.AccountsStatus()
182 res.Errors = append(res.Errors, errs...)
183 }
184
185 types, handlers := sh.handlerFinder.AllHandlers()
186
187
188 for pfx, h := range handlers {
189 sh, ok := h.(*SyncHandler)
190 if !ok {
191 continue
192 }
193 res.Sync[pfx] = sh.currentStatus()
194 }
195
196
197 for pfx, typ := range types {
198 if !strings.HasPrefix(typ, "storage-") {
199 continue
200 }
201 h := handlers[pfx]
202 _, isIndex := h.(*index.Index)
203 res.Storage[pfx] = storageStatus{
204 Type: strings.TrimPrefix(typ, "storage-"),
205 Primary: pfx == rh.BlobRoot,
206 IsIndex: isIndex,
207 }
208 }
209
210 return res
211 }
212
213 func (sh *StatusHandler) serveStatusJSON(rw http.ResponseWriter, req *http.Request) {
214 httputil.ReturnJSON(rw, sh.currentStatus())
215 }
216
217 func (sh *StatusHandler) googleCloudConsole() (string, error) {
218 if !env.OnGCE() {
219 return "", errors.New("not on GCE")
220 }
221 projID, err := metadata.ProjectID()
222 if err != nil {
223 return "", fmt.Errorf("Error getting project ID: %v", err)
224 }
225 return "https://console.cloud.google.com/compute/instances?project=" + projID, nil
226 }
227
228 var quotedPrefix = regexp.MustCompile(`[;"]/(\S+?/)[&"]`)
229
230 func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Request) {
231 st := sh.currentStatus()
232 f := func(p string, a ...interface{}) {
233 if len(a) == 0 {
234 io.WriteString(rw, p)
235 } else {
236 fmt.Fprintf(rw, p, a...)
237 }
238 }
239 f("<html><head><title>perkeepd status</title></head>")
240 f("<body>")
241
242 f("<h1>perkeepd status</h1>")
243
244 f("<h2>Versions</h2><ul>")
245 var envStr string
246 if env.OnGCE() {
247 envStr = " (on GCE)"
248 }
249 f("<li><b>Perkeep</b>: %s%s</li>", html.EscapeString(buildinfo.Summary()), envStr)
250 f("<li><b>Go</b>: %s/%s %s, cgo=%v</li>", runtime.GOOS, runtime.GOARCH, runtime.Version(), cgoEnabled)
251 f("<li><b>djpeg</b>: %s", html.EscapeString(buildinfo.DjpegStatus()))
252 f("</ul>")
253
254 f("<h2>Logs</h2><ul>")
255 f(" <li><a href='/debug/config'>/debug/config</a> - server config</li>\n")
256 if env.OnGCE() {
257 f(" <li><a href='/debug/logs/perkeepd'>perkeepd logs on Google Cloud Logging</a></li>\n")
258 f(" <li><a href='/debug/logs/system'>system logs from Google Compute Engine</a></li>\n")
259 }
260 f("</ul>")
261
262 f("<h2>Admin</h2>")
263 f("<ul>")
264 f(" <li><form method='post' action='restart' onsubmit='return confirm(\"Really restart now?\")'><button>restart server</button> ")
265 f("<input type='checkbox' name='reindex' id='reindex'><label for='reindex'> reindex </label>")
266 f("<select name='recovery'><option selected='true' disabled='disabled'>select recovery mode</option>")
267 f("<option value='0'>no recovery</option>")
268 f("<option value='1'>fast recovery</option>")
269 f("<option value='2'>full recovery</option>")
270 f("</select>")
271 f("</form></li>")
272 if env.OnGCE() {
273 console, err := sh.googleCloudConsole()
274 if err != nil {
275 log.Printf("error getting Google Cloud Console URL: %v", err)
276 } else {
277 f(" <li><b>Updating:</b> When a new image for Perkeep on GCE is available, you can update by hitting \"Reset\" (or \"Stop\", then \"Start\") for your instance on your <a href='%s'>Google Cloud Console</a>.<br>Alternatively, you can ssh to your instance and restart the Perkeep service with: <b>sudo systemctl restart perkeepd</b>.</li>", console)
278 }
279 }
280 f("</ul>")
281
282 f("<h2>Handlers</h2>")
283 f("<p>As JSON: <a href='status.json'>status.json</a>; and the <a href='%s?camli.mode=config'>discovery JSON</a>.</p>", st.rootPrefix)
284 f("<p>Not yet pretty HTML UI:</p>")
285 js, err := json.MarshalIndent(st, "", " ")
286 if err != nil {
287 log.Printf("JSON marshal error: %v", err)
288 }
289 jsh := html.EscapeString(string(js))
290 jsh = quotedPrefix.ReplaceAllStringFunc(jsh, func(in string) string {
291 pfx := in[1 : len(in)-1]
292 if st.isHandler(pfx) {
293 return fmt.Sprintf("%s<a href='%s'>%s</a>%s", in[:1], pfx, pfx, in[len(in)-1:])
294 }
295 return in
296 })
297 f("<pre>%s</pre>", jsh)
298 }
299
300 func (sh *StatusHandler) serveRestart(rw http.ResponseWriter, req *http.Request) {
301 if req.Method != "POST" {
302 http.Error(rw, "POST to restart", http.StatusMethodNotAllowed)
303 return
304 }
305
306 _, handlers := sh.handlerFinder.AllHandlers()
307 for _, h := range handlers {
308 ah, ok := h.(*app.Handler)
309 if !ok {
310 continue
311 }
312 log.Printf("Sending SIGINT to %s", ah.ProgramName())
313 err := ah.Quit()
314 if err != nil {
315 msg := fmt.Sprintf("Not restarting: couldn't interrupt app %s: %v", ah.ProgramName(), err)
316 log.Printf(msg)
317 http.Error(rw, msg, http.StatusInternalServerError)
318 return
319 }
320 }
321
322 reindex := (req.FormValue("reindex") == "on")
323 recovery, _ := strconv.Atoi(req.FormValue("recovery"))
324
325 log.Println("Restarting perkeepd")
326 rw.Header().Set("Connection", "close")
327 http.Redirect(rw, req, sh.prefix, http.StatusFound)
328 if f, ok := rw.(http.Flusher); ok {
329 f.Flush()
330 }
331 osutil.RestartProcess(fmt.Sprintf("-reindex=%t", reindex), fmt.Sprintf("-recovery=%d", recovery))
332 }
333
334 var cgoEnabled bool