| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Outyet is a web server that announces whether or not a particular Go version |
| // has been tagged. |
| package main |
| |
| import ( |
| "expvar" |
| "flag" |
| "fmt" |
| "html/template" |
| "log" |
| "net/http" |
| "sync" |
| "time" |
| ) |
| |
| // Command-line flags. |
| var ( |
| httpAddr = flag.String("http", "localhost:8080", "Listen address") |
| pollPeriod = flag.Duration("poll", 5*time.Second, "Poll period") |
| version = flag.String("version", "1.4", "Go version") |
| ) |
| |
| const baseChangeURL = "https://go.googlesource.com/go/+/" |
| |
| func main() { |
| flag.Parse() |
| changeURL := fmt.Sprintf("%sgo%s", baseChangeURL, *version) |
| http.Handle("/", NewServer(*version, changeURL, *pollPeriod)) |
| log.Printf("serving http://%s", *httpAddr) |
| log.Fatal(http.ListenAndServe(*httpAddr, nil)) |
| } |
| |
| // Exported variables for monitoring the server. |
| // These are exported via HTTP as a JSON object at /debug/vars. |
| var ( |
| hitCount = expvar.NewInt("hitCount") |
| pollCount = expvar.NewInt("pollCount") |
| pollError = expvar.NewString("pollError") |
| pollErrorCount = expvar.NewInt("pollErrorCount") |
| ) |
| |
| // Server implements the outyet server. |
| // It serves the user interface (it's an http.Handler) |
| // and polls the remote repository for changes. |
| type Server struct { |
| version string |
| url string |
| period time.Duration |
| |
| mu sync.RWMutex // protects the yes variable |
| yes bool |
| } |
| |
| // NewServer returns an initialized outyet server. |
| func NewServer(version, url string, period time.Duration) *Server { |
| s := &Server{version: version, url: url, period: period} |
| go s.poll() |
| return s |
| } |
| |
| // poll polls the change URL for the specified period until the tag exists. |
| // Then it sets the Server's yes field true and exits. |
| func (s *Server) poll() { |
| for !isTagged(s.url) { |
| pollSleep(s.period) |
| } |
| s.mu.Lock() |
| s.yes = true |
| s.mu.Unlock() |
| pollDone() |
| } |
| |
| // Hooks that may be overridden for integration tests. |
| var ( |
| pollSleep = time.Sleep |
| pollDone = func() {} |
| ) |
| |
| // isTagged makes an HTTP HEAD request to the given URL and reports whether it |
| // returned a 200 OK response. |
| func isTagged(url string) bool { |
| pollCount.Add(1) |
| r, err := http.Head(url) |
| if err != nil { |
| log.Print(err) |
| pollError.Set(err.Error()) |
| pollErrorCount.Add(1) |
| return false |
| } |
| return r.StatusCode == http.StatusOK |
| } |
| |
| // ServeHTTP implements the HTTP user interface. |
| func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| hitCount.Add(1) |
| s.mu.RLock() |
| data := struct { |
| URL string |
| Version string |
| Yes bool |
| }{ |
| s.url, |
| s.version, |
| s.yes, |
| } |
| s.mu.RUnlock() |
| err := tmpl.Execute(w, data) |
| if err != nil { |
| log.Print(err) |
| } |
| } |
| |
| // tmpl is the HTML template that drives the user interface. |
| var tmpl = template.Must(template.New("tmpl").Parse(` |
| <!DOCTYPE html><html><body><center> |
| <h2>Is Go {{.Version}} out yet?</h2> |
| <h1> |
| {{if .Yes}} |
| <a href="{{.URL}}">YES!</a> |
| {{else}} |
| No. :-( |
| {{end}} |
| </h1> |
| </center></body></html> |
| `)) |