Home Download Docs Code Community
     1	/*
     2	Copyright 2013 The Perkeep Authors.
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    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	// StatusHandler publishes server status information.
    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					// TODO: something better. get notified on interesting events.
    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		// Sync
   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		// Storage
   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
Website layout inspired by memcached.
Content by the authors.