| // Copyright 2011 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. |
| |
| package main |
| |
| import ( |
| "encoding/json" |
| "flag" |
| "go/build" |
| "html/template" |
| "io" |
| "log" |
| "net" |
| "net/http" |
| "net/url" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "runtime/debug" |
| "strings" |
| "time" |
| |
| "golang.org/x/tools/playground/socket" |
| "golang.org/x/website/internal/webtest" |
| ) |
| |
| const ( |
| socketPath = "/socket" |
| ) |
| |
| var ( |
| httpListen = flag.String("http", "127.0.0.1:3999", "host:port to listen on") |
| openBrowser = flag.Bool("openbrowser", true, "open browser automatically") |
| ) |
| |
| var ( |
| httpAddr string |
| ) |
| |
| // isRoot reports whether path is the root directory of the tour tree. |
| // To be the root, it must have content and template subdirectories. |
| func isRoot(path string) bool { |
| _, err := os.Stat(filepath.Join(path, "content", "welcome.article")) |
| if err == nil { |
| _, err = os.Stat(filepath.Join(path, "template", "index.tmpl")) |
| } |
| return err == nil |
| } |
| |
| // findRoot is a best-effort attempt to find a tour directory |
| // that contains the files it needs. It may not always work. |
| // |
| // TODO: Delete after Go 1.17 is out and we can just use embed; see CL 291849. |
| func findRoot() (string, bool) { |
| // Try finding the golang.org/x/website/tour package in the |
| // legacy GOPATH mode workspace or in build list. |
| p, err := build.Import("golang.org/x/website/tour", "", build.FindOnly) |
| if err == nil && isRoot(p.Dir) { |
| return p.Dir, true |
| } |
| // If that didn't work, perhaps we're not inside any module |
| // and the binary was built in module mode (e.g., 'go install |
| // golang.org/x/website/tour@latest' or 'go get golang.org/x/website/tour' |
| // outside a module). |
| // If that's the case, find out what version it is, |
| // and access its content from the module cache. |
| if info, ok := debug.ReadBuildInfo(); ok && |
| info.Main.Path == "golang.org/x/website" && |
| info.Main.Replace == nil && |
| info.Main.Version != "(devel)" { |
| // Make some assumptions for brevity: |
| // • the 'go' binary is in $PATH |
| // • the main module isn't replaced |
| // • the version isn't "(devel)" |
| // They should hold for the use cases we care about, until this |
| // entire mechanism is obsoleted by file embedding. |
| out, execError := exec.Command("go", "mod", "download", "-json", "--", "golang.org/x/website@"+info.Main.Version).Output() |
| var websiteRoot struct{ Dir string } |
| jsonError := json.Unmarshal(out, &websiteRoot) |
| tourDir := filepath.Join(websiteRoot.Dir, "tour") |
| if execError == nil && jsonError == nil && isRoot(tourDir) { |
| return tourDir, true |
| } |
| } |
| return "", false |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| if os.Getenv("GAE_ENV") == "standard" { |
| log.Println("running in App Engine Standard mode") |
| gaeMain() |
| return |
| } |
| |
| // find and serve the go tour files |
| root, ok := findRoot() |
| if !ok { |
| log.Fatalln("Couldn't find files for the Go tour. Try reinstalling it.") |
| } |
| |
| log.Println("Serving content from", root) |
| |
| host, port, err := net.SplitHostPort(*httpListen) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if host == "" { |
| host = "localhost" |
| } |
| if host != "127.0.0.1" && host != "localhost" { |
| log.Print(localhostWarning) |
| } |
| httpAddr = host + ":" + port |
| |
| if err := initTour(root, "SocketTransport"); err != nil { |
| log.Fatal(err) |
| } |
| |
| http.HandleFunc("/", rootHandler) |
| http.HandleFunc("/lesson/", lessonHandler) |
| |
| origin := &url.URL{Scheme: "http", Host: host + ":" + port} |
| http.Handle(socketPath, socket.NewHandler(origin)) |
| |
| registerStatic(root) |
| |
| h := webtest.HandlerWithCheck(http.DefaultServeMux, "/_readycheck", |
| filepath.Join(root, "testdata/*.txt")) |
| |
| go func() { |
| url := "http://" + httpAddr |
| if waitServer(url) && *openBrowser && startBrowser(url) { |
| log.Printf("A browser window should open. If not, please visit %s", url) |
| } else { |
| log.Printf("Please open your web browser and visit %s", url) |
| } |
| }() |
| log.Fatal(http.ListenAndServe(httpAddr, h)) |
| } |
| |
| // registerStatic registers handlers to serve static content |
| // from the directory root. |
| func registerStatic(root string) { |
| http.Handle("/favicon.ico", http.FileServer(http.Dir(filepath.Join(root, "static", "img")))) |
| static := http.FileServer(http.Dir(root)) |
| http.Handle("/content/img/", static) |
| http.Handle("/static/", static) |
| } |
| |
| // rootHandler returns a handler for all the requests except the ones for lessons. |
| func rootHandler(w http.ResponseWriter, r *http.Request) { |
| if err := renderUI(w); err != nil { |
| log.Println(err) |
| } |
| } |
| |
| // lessonHandler handler the HTTP requests for lessons. |
| func lessonHandler(w http.ResponseWriter, r *http.Request) { |
| lesson := strings.TrimPrefix(r.URL.Path, "/lesson/") |
| if err := writeLesson(lesson, w); err != nil { |
| if err == lessonNotFound { |
| http.NotFound(w, r) |
| } else { |
| log.Println(err) |
| } |
| } |
| } |
| |
| const localhostWarning = ` |
| WARNING! WARNING! WARNING! |
| |
| The tour server appears to be listening on an address that is |
| not localhost and is configured to run code snippets locally. |
| Anyone with access to this address and port will have access |
| to this machine as the user running gotour. |
| |
| If you don't understand this message, hit Control-C to terminate this process. |
| |
| WARNING! WARNING! WARNING! |
| ` |
| |
| // waitServer waits some time for the http Server to start |
| // serving url. The return value reports whether it starts. |
| func waitServer(url string) bool { |
| tries := 20 |
| for tries > 0 { |
| resp, err := http.Get(url) |
| if err == nil { |
| resp.Body.Close() |
| return true |
| } |
| time.Sleep(100 * time.Millisecond) |
| tries-- |
| } |
| return false |
| } |
| |
| // startBrowser tries to open the URL in a browser, and returns |
| // whether it succeed. |
| func startBrowser(url string) bool { |
| // try to start the browser |
| var args []string |
| switch runtime.GOOS { |
| case "darwin": |
| args = []string{"open"} |
| case "windows": |
| args = []string{"cmd", "/c", "start"} |
| default: |
| args = []string{"xdg-open"} |
| } |
| cmd := exec.Command(args[0], append(args[1:], url)...) |
| return cmd.Start() == nil |
| } |
| |
| // prepContent for the local tour simply returns the content as-is. |
| var prepContent = func(r io.Reader) io.Reader { return r } |
| |
| // socketAddr returns the WebSocket handler address. |
| var socketAddr = func() string { return "ws://" + httpAddr + socketPath } |
| |
| // analyticsHTML is optional analytics HTML to insert at the beginning of <head>. |
| var analyticsHTML template.HTML |