blob: f59edfa0ace76e5d084f44da32e1902b62fd171b [file] [log] [blame]
// 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).
// In 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/tour" &&
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/tour@"+info.Main.Version).Output()
var tourRoot struct{ Dir string }
jsonError := json.Unmarshal(out, &tourRoot)
if execError == nil && jsonError == nil && isRoot(tourRoot.Dir) {
return tourRoot.Dir, 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