golang.org/x/build@v0.0.0-20240506185731-218518f32b70/devapp/server.go (about) 1 // Copyright 2017 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "compress/gzip" 9 "context" 10 "fmt" 11 "html/template" 12 "log" 13 "math/rand" 14 "net/http" 15 "path" 16 "strconv" 17 "strings" 18 "sync" 19 "time" 20 21 "golang.org/x/build/devapp/owners" 22 "golang.org/x/build/maintner" 23 "golang.org/x/build/maintner/godata" 24 ) 25 26 // A server is an http.Handler that serves content within staticDir at root and 27 // the dynamically-generated dashboards at their respective endpoints. 28 type server struct { 29 mux *http.ServeMux 30 staticDir string 31 templateDir string 32 reloadTmpls bool 33 34 cMu sync.RWMutex // Used to protect the fields below. 35 corpus *maintner.Corpus 36 repo *maintner.GitHubRepo // The golang/go repo. 37 proj *maintner.GerritProject // The go.googlesource.com/go project. 38 helpWantedIssues []issueData 39 data pageData 40 41 // GopherCon-specific fields. Must still hold cMu when reading/writing these. 42 userMapping map[int]*maintner.GitHubUser // Gerrit Owner ID => GitHub user 43 activities []activity // All contribution activities 44 totalPoints int 45 } 46 47 type issueData struct { 48 id int32 49 titlePrefix string 50 } 51 52 type pageData struct { 53 release releaseData 54 reviews reviewsData 55 stats statsData 56 } 57 58 func newServer(mux *http.ServeMux, staticDir, templateDir string, reloadTmpls bool) *server { 59 s := &server{ 60 mux: mux, 61 staticDir: staticDir, 62 templateDir: templateDir, 63 reloadTmpls: reloadTmpls, 64 userMapping: map[int]*maintner.GitHubUser{}, 65 } 66 s.mux.Handle("/", http.FileServer(http.Dir(s.staticDir))) 67 s.mux.HandleFunc("/favicon.ico", s.handleFavicon) 68 s.mux.HandleFunc("/release", s.withTemplate("/release.tmpl", s.handleRelease)) 69 s.mux.HandleFunc("/reviews", s.withTemplate("/reviews.tmpl", s.handleReviews)) 70 s.mux.HandleFunc("/stats", s.withTemplate("/stats.tmpl", s.handleStats)) 71 s.mux.HandleFunc("/dir/", handleDirRedirect) 72 s.mux.HandleFunc("/owners", owners.Handler) 73 s.mux.Handle("/owners/", http.RedirectHandler("/owners", http.StatusPermanentRedirect)) // TODO: remove after clients updated to use URL without trailing slash 74 for _, p := range []string{"/imfeelinghelpful", "/imfeelinglucky"} { 75 s.mux.HandleFunc(p, s.handleRandomHelpWantedIssue) 76 } 77 s.mux.HandleFunc("/_/activities", s.handleActivities) 78 return s 79 } 80 81 func (s *server) withTemplate(tmpl string, fn func(*template.Template, http.ResponseWriter, *http.Request)) http.HandlerFunc { 82 t := template.Must(template.ParseFiles(path.Join(s.templateDir, tmpl))) 83 return func(w http.ResponseWriter, r *http.Request) { 84 if s.reloadTmpls { 85 t = template.Must(template.ParseFiles(path.Join(s.templateDir, tmpl))) 86 } 87 fn(t, w, r) 88 } 89 } 90 91 // initCorpus fetches a full maintner corpus, overwriting any existing data. 92 func (s *server) initCorpus(ctx context.Context) error { 93 s.cMu.Lock() 94 defer s.cMu.Unlock() 95 corpus, err := godata.Get(ctx) 96 if err != nil { 97 return fmt.Errorf("godata.Get: %v", err) 98 } 99 s.corpus = corpus 100 s.repo = s.corpus.GitHub().Repo("golang", "go") 101 if s.repo == nil { 102 return fmt.Errorf(`s.corpus.GitHub().Repo("golang", "go") = nil`) 103 } 104 s.proj = s.corpus.Gerrit().Project("go.googlesource.com", "go") 105 if s.proj == nil { 106 return fmt.Errorf(`s.corpus.Gerrit().Project("go.googlesource.com", "go") = nil`) 107 } 108 return nil 109 } 110 111 // corpusUpdateLoop continuously updates the server’s corpus until ctx’s Done 112 // channel is closed. 113 func (s *server) corpusUpdateLoop(ctx context.Context) { 114 log.Println("Starting corpus update loop ...") 115 for { 116 log.Println("Updating help wanted issues ...") 117 s.updateHelpWantedIssues() 118 log.Println("Updating activities ...") 119 s.updateActivities() 120 s.cMu.Lock() 121 s.data.release.dirty = true 122 s.data.reviews.dirty = true 123 s.data.stats.dirty = true 124 s.cMu.Unlock() 125 err := s.corpus.UpdateWithLocker(ctx, &s.cMu) 126 if err != nil { 127 if err == maintner.ErrSplit { 128 log.Println("Corpus out of sync. Re-fetching corpus.") 129 s.initCorpus(ctx) 130 } else { 131 log.Printf("corpus.Update: %v; sleeping 15s", err) 132 time.Sleep(15 * time.Second) 133 continue 134 } 135 } 136 137 select { 138 case <-ctx.Done(): 139 return 140 default: 141 continue 142 } 143 } 144 } 145 146 const ( 147 issuesURLBase = "https://golang.org/issue/" 148 149 labelHelpWantedID = 150880243 150 ) 151 152 func (s *server) updateHelpWantedIssues() { 153 s.cMu.Lock() 154 defer s.cMu.Unlock() 155 156 var issues []issueData 157 s.repo.ForeachIssue(func(i *maintner.GitHubIssue) error { 158 if i.Closed { 159 return nil 160 } 161 if i.HasLabelID(labelHelpWantedID) { 162 prefix := strings.SplitN(i.Title, ":", 2)[0] 163 issues = append(issues, issueData{id: i.Number, titlePrefix: prefix}) 164 } 165 return nil 166 }) 167 s.helpWantedIssues = issues 168 } 169 170 func (s *server) handleRandomHelpWantedIssue(w http.ResponseWriter, r *http.Request) { 171 s.cMu.RLock() 172 defer s.cMu.RUnlock() 173 if len(s.helpWantedIssues) == 0 { 174 http.Redirect(w, r, issuesURLBase, http.StatusSeeOther) 175 return 176 } 177 pkgs := r.URL.Query().Get("pkg") 178 var rid int32 179 if pkgs == "" { 180 rid = s.helpWantedIssues[rand.Intn(len(s.helpWantedIssues))].id 181 } else { 182 filtered := s.filteredHelpWantedIssues(strings.Split(pkgs, ",")...) 183 if len(filtered) == 0 { 184 http.Redirect(w, r, issuesURLBase, http.StatusSeeOther) 185 return 186 } 187 rid = filtered[rand.Intn(len(filtered))].id 188 } 189 http.Redirect(w, r, issuesURLBase+strconv.Itoa(int(rid)), http.StatusSeeOther) 190 } 191 192 func (s *server) filteredHelpWantedIssues(pkgs ...string) []issueData { 193 var issues []issueData 194 for _, i := range s.helpWantedIssues { 195 for _, p := range pkgs { 196 if strings.HasPrefix(i.titlePrefix, p) { 197 issues = append(issues, i) 198 break 199 } 200 } 201 } 202 return issues 203 } 204 205 func (s *server) handleFavicon(w http.ResponseWriter, r *http.Request) { 206 // Need to specify content type for consistent tests, without this it's 207 // determined from mime.types on the box the test is running on 208 w.Header().Set("Content-Type", "image/x-icon") 209 http.ServeFile(w, r, path.Join(s.staticDir, "/favicon.ico")) 210 } 211 212 // ServeHTTP satisfies the http.Handler interface. 213 func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 214 if r.TLS != nil { 215 w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") 216 } 217 if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 218 w.Header().Set("Content-Encoding", "gzip") 219 gz := gzip.NewWriter(w) 220 defer gz.Close() 221 gzw := &gzipResponseWriter{Writer: gz, ResponseWriter: w} 222 s.mux.ServeHTTP(gzw, r) 223 return 224 } 225 s.mux.ServeHTTP(w, r) 226 } 227 228 // handleDirRedirect accepts requests of the form: 229 // 230 // /dir/REPO/some/dir/ 231 // 232 // And redirects them to either: 233 // 234 // https://github.com/golang/REPO/tree/master/some/dir/ 235 // 236 // or: 237 // 238 // https://go.googlesource.com/REPO/+/master/some/dir/ 239 // 240 // ... depending on the Referer. This is so we can make links 241 // in Markdown docs that are clickable on both GitHub and 242 // in the go.googlesource.com viewer. If detection fails, we 243 // default to GitHub. 244 func handleDirRedirect(w http.ResponseWriter, r *http.Request) { 245 useGoog := strings.Contains(r.Referer(), "googlesource.com") 246 path := r.URL.Path 247 if !strings.HasPrefix(path, "/dir/") { 248 http.Error(w, "bad mux", http.StatusInternalServerError) 249 return 250 } 251 path = strings.TrimPrefix(path, "/dir/") 252 // path is now "REPO/some/dir/" 253 var repo string 254 slash := strings.IndexByte(path, '/') 255 if slash == -1 { 256 repo, path = path, "" 257 } else { 258 repo, path = path[:slash], path[slash+1:] 259 } 260 path = strings.TrimSuffix(path, "/") 261 var target string 262 if useGoog { 263 target = fmt.Sprintf("https://go.googlesource.com/%s/+/master/%s", repo, path) 264 } else { 265 target = fmt.Sprintf("https://github.com/golang/%s/tree/master/%s", repo, path) 266 } 267 http.Redirect(w, r, target, http.StatusFound) 268 }