From 5807c4fa28aba30e78d2ef679bb99cd9fe45c9b9 Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Fri, 12 Oct 2018 11:18:43 -0400 Subject: tour: make the tour deployable on App Engine again Move the main Go files into the same directory as the app.yaml file. This changes the command location to be at golang.org/x/tour instead of golang.org/x/tour/gotour. Add a placeholder command in gotour so that those who use the previous installation command will know to use the new one and it won't just seem like it has vanished. Also update the documentation to take into account that the tour is no longer distributed with releases as of golang.org/cl/131156 Fixes golang/go#28163 Change-Id: I60737f0cfaa93d12902a75fbc0924d96672a8c9b Reviewed-on: https://go-review.googlesource.com/c/141857 Run-TryBot: Andrew Bonventre TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov --- README.md | 11 +- app.yaml | 5 +- appengine.go | 97 +++++++++++++++++ content/welcome.article | 45 ++++---- fmt.go | 61 +++++++++++ gotour/appengine.go | 97 ----------------- gotour/fmt.go | 61 ----------- gotour/local.go | 216 ------------------------------------- gotour/main.go | 15 +++ gotour/tour.go | 282 ------------------------------------------------ local.go | 216 +++++++++++++++++++++++++++++++++++++ tour.go | 282 ++++++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 695 insertions(+), 693 deletions(-) create mode 100644 appengine.go create mode 100644 fmt.go delete mode 100644 gotour/appengine.go delete mode 100644 gotour/fmt.go delete mode 100644 gotour/local.go create mode 100644 gotour/main.go delete mode 100644 gotour/tour.go create mode 100644 local.go create mode 100644 tour.go diff --git a/README.md b/README.md index 3db4f9a..0f4cce8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,11 @@ A Tour of Go is an introduction to the Go programming language. -The easiest way to install the tour locally is to install -[a binary release of Go](https://golang.org/dl/) and then run: - - $ go tool tour - -To install the tour from source, first +To install the tour from source, first [set up a workspace](https://golang.org/doc/code.html) and then run: - $ go get golang.org/x/tour/gotour + $ go get golang.org/x/tour -This will place a `gotour` binary in your workspace's `bin` directory. +This will place a `tour` binary in your workspace's `bin` directory. Unless otherwise noted, the go-tour source files are distributed under the BSD-style license found in the LICENSE file. diff --git a/app.yaml b/app.yaml index dfff2d5..e6552eb 100644 --- a/app.yaml +++ b/app.yaml @@ -1,5 +1,4 @@ -application: go-tour -version: 1 +service: tour runtime: go api_version: go1 @@ -7,7 +6,7 @@ default_expiration: "7d" handlers: -# Keep these static file handlers in sync with gotour/local.go. +# Keep these static file handlers in sync with local.go. - url: /favicon.ico static_files: static/img/favicon.ico upload: static/img/favicon.ico diff --git a/appengine.go b/appengine.go new file mode 100644 index 0000000..4767a2c --- /dev/null +++ b/appengine.go @@ -0,0 +1,97 @@ +// 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. + +// +build appengine + +package main + +import ( + "bufio" + "bytes" + "io" + "net/http" + "strings" + + "appengine" + + _ "golang.org/x/tools/playground" +) + +const runUrl = "https://golang.org/compile" + +func init() { + http.Handle("/lesson/", hstsHandler(lessonHandler)) + http.Handle("/", hstsHandler(rootHandler)) + + if err := initTour(".", "HTTPTransport"); err != nil { + panic(err) + } +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + if err := renderUI(w); err != nil { + c.Criticalf("UI render: %v", err) + } +} + +func lessonHandler(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + lesson := strings.TrimPrefix(r.URL.Path, "/lesson/") + if err := writeLesson(lesson, w); err != nil { + if err == lessonNotFound { + http.NotFound(w, r) + } else { + c.Criticalf("tour render: %v", err) + } + } +} + +// prepContent returns a Reader that produces the content from the given +// Reader, but strips the prefix "#appengine: " from each line. It also drops +// any non-blank like that follows a series of 1 or more lines with the prefix. +func prepContent(in io.Reader) io.Reader { + var prefix = []byte("#appengine: ") + out, w := io.Pipe() + go func() { + r := bufio.NewReader(in) + drop := false + for { + b, err := r.ReadBytes('\n') + if err != nil && err != io.EOF { + w.CloseWithError(err) + return + } + if bytes.HasPrefix(b, prefix) { + b = b[len(prefix):] + drop = true + } else if drop { + if len(b) > 1 { + b = nil + } + drop = false + } + if len(b) > 0 { + w.Write(b) + } + if err == io.EOF { + w.Close() + return + } + } + }() + return out +} + +// socketAddr returns the WebSocket handler address. +// The App Engine version does not provide a WebSocket handler. +func socketAddr() string { return "" } + +// hstsHandler wraps an http.HandlerFunc such that it sets the HSTS header. +func hstsHandler(fn http.HandlerFunc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") + fn(w, r) + }) +} diff --git a/content/welcome.article b/content/welcome.article index 791b73c..6df361f 100644 --- a/content/welcome.article +++ b/content/welcome.article @@ -70,48 +70,41 @@ The tour is available in other languages: Click the [[javascript:highlightAndClick(".next-page")]["next"]] button or type `PageDown` to continue. #appengine: * Go offline -#appengine: +#appengine: #appengine: This tour is also available as a stand-alone program that you can use #appengine: without access to the internet. -#appengine: +#appengine: #appengine: The stand-alone tour is faster, as it builds and runs the code samples #appengine: on your own machine. -#appengine: -#appengine: To run the tour locally first -#appengine: [[https://golang.org/dl/][download and install Go]] -#appengine: then start the tour from the command line: -#appengine: -#appengine: go tool tour -#appengine: -#appengine: Or, you can install and run this tour manually if you have any trouble -#appengine: running the above command: -#appengine: -#appengine: go get golang.org/x/tour/gotour -#appengine: gotour -#appengine: +#appengine: +#appengine: To run the tour locally install and run the tour binary: +#appengine: +#appengine: go get golang.org/x/tour +#appengine: tour +#appengine: #appengine: The tour program will open a web browser displaying #appengine: your local version of the tour. -#appengine: +#appengine: #appengine: Or, of course, you can continue to take the tour through this web site. #appengine: * The Go Playground -#appengine: +#appengine: #appengine: This tour is built atop the [[https://play.golang.org/][Go Playground]], a #appengine: web service that runs on [[https://golang.org/][golang.org]]'s servers. -#appengine: +#appengine: #appengine: The service receives a Go program, compiles, links, and runs the program inside #appengine: a sandbox, then returns the output. -#appengine: -#appengine: There are limitations to the programs that can be run in the playground: -#appengine: +#appengine: +#appengine: There are limitations to the programs that can be run in the playground: +#appengine: #appengine: - In the playground the time begins at 2009-11-10 23:00:00 UTC (determining the significance of this date is an exercise for the reader). This makes it easier to cache programs by giving them deterministic output. -#appengine: -#appengine: - There are also limits on execution time and on CPU and memory usage, and the program cannot access external network hosts. -#appengine: +#appengine: +#appengine: - There are also limits on execution time and on CPU and memory usage, and the program cannot access external network hosts. +#appengine: #appengine: The playground uses the latest stable release of Go. -#appengine: +#appengine: #appengine: Read "[[https://blog.golang.org/playground][Inside the Go Playground]]" to learn more. -#appengine: +#appengine: #appengine: .play welcome/sandbox.go * Congratulations diff --git a/fmt.go b/fmt.go new file mode 100644 index 0000000..0a0be16 --- /dev/null +++ b/fmt.go @@ -0,0 +1,61 @@ +// Copyright 2012 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 ( + "bytes" + "encoding/json" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "net/http" + + "golang.org/x/tools/imports" +) + +func init() { + http.HandleFunc("/fmt", fmtHandler) +} + +type fmtResponse struct { + Body string + Error string +} + +func fmtHandler(w http.ResponseWriter, r *http.Request) { + resp := new(fmtResponse) + var body string + var err error + if r.FormValue("imports") == "true" { + var b []byte + b, err = imports.Process("prog.go", []byte(r.FormValue("body")), nil) + body = string(b) + } else { + body, err = gofmt(r.FormValue("body")) + } + if err != nil { + resp.Error = err.Error() + } else { + resp.Body = body + } + json.NewEncoder(w).Encode(resp) +} + +func gofmt(body string) (string, error) { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "prog.go", body, parser.ParseComments) + if err != nil { + return "", err + } + ast.SortImports(fset, f) + var buf bytes.Buffer + config := &printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8} + err = config.Fprint(&buf, fset, f) + if err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/gotour/appengine.go b/gotour/appengine.go deleted file mode 100644 index 4767a2c..0000000 --- a/gotour/appengine.go +++ /dev/null @@ -1,97 +0,0 @@ -// 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. - -// +build appengine - -package main - -import ( - "bufio" - "bytes" - "io" - "net/http" - "strings" - - "appengine" - - _ "golang.org/x/tools/playground" -) - -const runUrl = "https://golang.org/compile" - -func init() { - http.Handle("/lesson/", hstsHandler(lessonHandler)) - http.Handle("/", hstsHandler(rootHandler)) - - if err := initTour(".", "HTTPTransport"); err != nil { - panic(err) - } -} - -func rootHandler(w http.ResponseWriter, r *http.Request) { - c := appengine.NewContext(r) - if err := renderUI(w); err != nil { - c.Criticalf("UI render: %v", err) - } -} - -func lessonHandler(w http.ResponseWriter, r *http.Request) { - c := appengine.NewContext(r) - lesson := strings.TrimPrefix(r.URL.Path, "/lesson/") - if err := writeLesson(lesson, w); err != nil { - if err == lessonNotFound { - http.NotFound(w, r) - } else { - c.Criticalf("tour render: %v", err) - } - } -} - -// prepContent returns a Reader that produces the content from the given -// Reader, but strips the prefix "#appengine: " from each line. It also drops -// any non-blank like that follows a series of 1 or more lines with the prefix. -func prepContent(in io.Reader) io.Reader { - var prefix = []byte("#appengine: ") - out, w := io.Pipe() - go func() { - r := bufio.NewReader(in) - drop := false - for { - b, err := r.ReadBytes('\n') - if err != nil && err != io.EOF { - w.CloseWithError(err) - return - } - if bytes.HasPrefix(b, prefix) { - b = b[len(prefix):] - drop = true - } else if drop { - if len(b) > 1 { - b = nil - } - drop = false - } - if len(b) > 0 { - w.Write(b) - } - if err == io.EOF { - w.Close() - return - } - } - }() - return out -} - -// socketAddr returns the WebSocket handler address. -// The App Engine version does not provide a WebSocket handler. -func socketAddr() string { return "" } - -// hstsHandler wraps an http.HandlerFunc such that it sets the HSTS header. -func hstsHandler(fn http.HandlerFunc) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") - fn(w, r) - }) -} diff --git a/gotour/fmt.go b/gotour/fmt.go deleted file mode 100644 index 0a0be16..0000000 --- a/gotour/fmt.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2012 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 ( - "bytes" - "encoding/json" - "go/ast" - "go/parser" - "go/printer" - "go/token" - "net/http" - - "golang.org/x/tools/imports" -) - -func init() { - http.HandleFunc("/fmt", fmtHandler) -} - -type fmtResponse struct { - Body string - Error string -} - -func fmtHandler(w http.ResponseWriter, r *http.Request) { - resp := new(fmtResponse) - var body string - var err error - if r.FormValue("imports") == "true" { - var b []byte - b, err = imports.Process("prog.go", []byte(r.FormValue("body")), nil) - body = string(b) - } else { - body, err = gofmt(r.FormValue("body")) - } - if err != nil { - resp.Error = err.Error() - } else { - resp.Body = body - } - json.NewEncoder(w).Encode(resp) -} - -func gofmt(body string) (string, error) { - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "prog.go", body, parser.ParseComments) - if err != nil { - return "", err - } - ast.SortImports(fset, f) - var buf bytes.Buffer - config := &printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8} - err = config.Fprint(&buf, fset, f) - if err != nil { - return "", err - } - return buf.String(), nil -} diff --git a/gotour/local.go b/gotour/local.go deleted file mode 100644 index c51010f..0000000 --- a/gotour/local.go +++ /dev/null @@ -1,216 +0,0 @@ -// 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. - -// +build !appengine - -package main - -import ( - "flag" - "fmt" - "go/build" - "io" - "log" - "net" - "net/http" - "net/url" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "time" - - "golang.org/x/tools/playground/socket" - - // Imports so that go build/install automatically installs them. - _ "golang.org/x/tour/pic" - _ "golang.org/x/tour/tree" - _ "golang.org/x/tour/wc" -) - -const ( - basePkg = "golang.org/x/tour/" - 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 ( - // GOPATH containing the tour packages - gopath = os.Getenv("GOPATH") - - 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 -} - -func findRoot() (string, error) { - ctx := build.Default - p, err := ctx.Import(basePkg, "", build.FindOnly) - if err == nil && isRoot(p.Dir) { - return p.Dir, nil - } - tourRoot := filepath.Join(runtime.GOROOT(), "misc", "tour") - ctx.GOPATH = tourRoot - p, err = ctx.Import(basePkg, "", build.FindOnly) - if err == nil && isRoot(tourRoot) { - gopath = tourRoot - return tourRoot, nil - } - return "", fmt.Errorf("could not find go-tour content; check $GOROOT and $GOPATH") -} - -func main() { - flag.Parse() - - // find and serve the go tour files - root, err := findRoot() - if err != nil { - log.Fatalf("Couldn't find tour files: %v", err) - } - - 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)) - - // Keep these static file handlers in sync with ../app.yaml. - static := http.FileServer(http.Dir(root)) - http.Handle("/content/img/", static) - http.Handle("/static/", static) - imgDir := filepath.Join(root, "static", "img") - http.Handle("/favicon.ico", http.FileServer(http.Dir(imgDir))) - - 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, nil)) -} - -// 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! - -I appear to be listening on an address that is not localhost. -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! -` - -type response struct { - Output string `json:"output"` - Errors string `json:"compile_errors"` -} - -func init() { - socket.Environ = environ -} - -// environ returns the original execution environment with GOPATH -// replaced (or added) with the value of the global var gopath. -func environ() (env []string) { - for _, v := range os.Environ() { - if !strings.HasPrefix(v, "GOPATH=") { - env = append(env, v) - } - } - env = append(env, "GOPATH="+gopath) - return -} - -// 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. -func prepContent(r io.Reader) io.Reader { return r } - -// socketAddr returns the WebSocket handler address. -func socketAddr() string { return "ws://" + httpAddr + socketPath } diff --git a/gotour/main.go b/gotour/main.go new file mode 100644 index 0000000..b0b2d50 --- /dev/null +++ b/gotour/main.go @@ -0,0 +1,15 @@ +// Copyright 2018 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 ( + "io" + "os" +) + +func main() { + io.WriteString(os.Stderr, "golang.org/x/tour/gotour has moved to golang.org/x/tour\n") + os.Exit(1) +} diff --git a/gotour/tour.go b/gotour/tour.go deleted file mode 100644 index 0d6ea3b..0000000 --- a/gotour/tour.go +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright 2013 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 "golang.org/x/tour/gotour" - -import ( - "bytes" - "crypto/sha1" - "encoding/base64" - "encoding/json" - "fmt" - "html/template" - "io" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "golang.org/x/tools/godoc/static" - "golang.org/x/tools/present" -) - -var ( - uiContent []byte - lessons = make(map[string][]byte) - lessonNotFound = fmt.Errorf("lesson not found") -) - -// initTour loads tour.article and the relevant HTML templates from the given -// tour root, and renders the template to the tourContent global variable. -func initTour(root, transport string) error { - // Make sure playground is enabled before rendering. - present.PlayEnabled = true - - // Set up templates. - action := filepath.Join(root, "template", "action.tmpl") - tmpl, err := present.Template().ParseFiles(action) - if err != nil { - return fmt.Errorf("parse templates: %v", err) - } - - // Init lessons. - contentPath := filepath.Join(root, "content") - if err := initLessons(tmpl, contentPath); err != nil { - return fmt.Errorf("init lessons: %v", err) - } - - // Init UI - index := filepath.Join(root, "template", "index.tmpl") - ui, err := template.ParseFiles(index) - if err != nil { - return fmt.Errorf("parse index.tmpl: %v", err) - } - buf := new(bytes.Buffer) - - data := struct { - SocketAddr string - Transport template.JS - }{socketAddr(), template.JS(transport)} - - if err := ui.Execute(buf, data); err != nil { - return fmt.Errorf("render UI: %v", err) - } - uiContent = buf.Bytes() - - return initScript(root) -} - -// initLessonss finds all the lessons in the passed directory, renders them, -// using the given template and saves the content in the lessons map. -func initLessons(tmpl *template.Template, content string) error { - dir, err := os.Open(content) - if err != nil { - return err - } - files, err := dir.Readdirnames(0) - if err != nil { - return err - } - for _, f := range files { - if filepath.Ext(f) != ".article" { - continue - } - content, err := parseLesson(tmpl, filepath.Join(content, f)) - if err != nil { - return fmt.Errorf("parsing %v: %v", f, err) - } - name := strings.TrimSuffix(f, ".article") - lessons[name] = content - } - return nil -} - -// File defines the JSON form of a code file in a page. -type File struct { - Name string - Content string - Hash string -} - -// Page defines the JSON form of a tour lesson page. -type Page struct { - Title string - Content string - Files []File -} - -// Lesson defines the JSON form of a tour lesson. -type Lesson struct { - Title string - Description string - Pages []Page -} - -// parseLesson parses and returns a lesson content given its name and -// the template to render it. -func parseLesson(tmpl *template.Template, path string) ([]byte, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - doc, err := present.Parse(prepContent(f), path, 0) - if err != nil { - return nil, err - } - - lesson := Lesson{ - doc.Title, - doc.Subtitle, - make([]Page, len(doc.Sections)), - } - - for i, sec := range doc.Sections { - p := &lesson.Pages[i] - w := new(bytes.Buffer) - if err := sec.Render(w, tmpl); err != nil { - return nil, fmt.Errorf("render section: %v", err) - } - p.Title = sec.Title - p.Content = w.String() - codes := findPlayCode(sec) - p.Files = make([]File, len(codes)) - for i, c := range codes { - f := &p.Files[i] - f.Name = c.FileName - f.Content = string(c.Raw) - hash := sha1.Sum(c.Raw) - f.Hash = base64.StdEncoding.EncodeToString(hash[:]) - } - } - - w := new(bytes.Buffer) - if err := json.NewEncoder(w).Encode(lesson); err != nil { - return nil, fmt.Errorf("encode lesson: %v", err) - } - return w.Bytes(), nil -} - -// findPlayCode returns a slide with all the Code elements in the given -// Elem with Play set to true. -func findPlayCode(e present.Elem) []*present.Code { - var r []*present.Code - switch v := e.(type) { - case present.Code: - if v.Play { - r = append(r, &v) - } - case present.Section: - for _, s := range v.Elem { - r = append(r, findPlayCode(s)...) - } - } - return r -} - -// writeLesson writes the tour content to the provided Writer. -func writeLesson(name string, w io.Writer) error { - if uiContent == nil { - panic("writeLesson called before successful initTour") - } - if len(name) == 0 { - return writeAllLessons(w) - } - l, ok := lessons[name] - if !ok { - return lessonNotFound - } - _, err := w.Write(l) - return err -} - -func writeAllLessons(w io.Writer) error { - if _, err := fmt.Fprint(w, "{"); err != nil { - return err - } - nLessons := len(lessons) - for k, v := range lessons { - if _, err := fmt.Fprintf(w, "%q:%s", k, v); err != nil { - return err - } - nLessons-- - if nLessons != 0 { - if _, err := fmt.Fprint(w, ","); err != nil { - return err - } - } - } - _, err := fmt.Fprint(w, "}") - return err -} - -// renderUI writes the tour UI to the provided Writer. -func renderUI(w io.Writer) error { - if uiContent == nil { - panic("renderUI called before successful initTour") - } - _, err := w.Write(uiContent) - return err -} - -// nocode returns true if the provided Section contains -// no Code elements with Play enabled. -func nocode(s present.Section) bool { - for _, e := range s.Elem { - if c, ok := e.(present.Code); ok && c.Play { - return false - } - } - return true -} - -// initScript concatenates all the javascript files needed to render -// the tour UI and serves the result on /script.js. -func initScript(root string) error { - modTime := time.Now() - b := new(bytes.Buffer) - - content, ok := static.Files["playground.js"] - if !ok { - return fmt.Errorf("playground.js not found in static files") - } - b.WriteString(content) - - // Keep this list in dependency order - files := []string{ - "static/lib/jquery.min.js", - "static/lib/jquery-ui.min.js", - "static/lib/angular.min.js", - "static/lib/codemirror/lib/codemirror.js", - "static/lib/codemirror/mode/go/go.js", - "static/lib/angular-ui.min.js", - "static/js/app.js", - "static/js/controllers.js", - "static/js/directives.js", - "static/js/services.js", - "static/js/values.js", - } - - for _, file := range files { - f, err := ioutil.ReadFile(filepath.Join(root, file)) - if err != nil { - return fmt.Errorf("couldn't open %v: %v", file, err) - } - _, err = b.Write(f) - if err != nil { - return fmt.Errorf("error concatenating %v: %v", file, err) - } - } - - http.HandleFunc("/script.js", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-type", "application/javascript") - // Set expiration time in one week. - w.Header().Set("Cache-control", "max-age=604800") - http.ServeContent(w, r, "", modTime, bytes.NewReader(b.Bytes())) - }) - - return nil -} diff --git a/local.go b/local.go new file mode 100644 index 0000000..170dcf0 --- /dev/null +++ b/local.go @@ -0,0 +1,216 @@ +// 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. + +// +build !appengine + +package main + +import ( + "flag" + "fmt" + "go/build" + "io" + "log" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "golang.org/x/tools/playground/socket" + + // Imports so that go build/install automatically installs them. + _ "golang.org/x/tour/pic" + _ "golang.org/x/tour/tree" + _ "golang.org/x/tour/wc" +) + +const ( + basePkg = "golang.org/x/tour/" + 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 ( + // GOPATH containing the tour packages + gopath = os.Getenv("GOPATH") + + 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 +} + +func findRoot() (string, error) { + ctx := build.Default + p, err := ctx.Import(basePkg, "", build.FindOnly) + if err == nil && isRoot(p.Dir) { + return p.Dir, nil + } + tourRoot := filepath.Join(runtime.GOROOT(), "misc", "tour") + ctx.GOPATH = tourRoot + p, err = ctx.Import(basePkg, "", build.FindOnly) + if err == nil && isRoot(tourRoot) { + gopath = tourRoot + return tourRoot, nil + } + return "", fmt.Errorf("could not find go-tour content; check $GOROOT and $GOPATH") +} + +func main() { + flag.Parse() + + // find and serve the go tour files + root, err := findRoot() + if err != nil { + log.Fatalf("Couldn't find tour files: %v", err) + } + + 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)) + + // Keep these static file handlers in sync with app.yaml. + static := http.FileServer(http.Dir(root)) + http.Handle("/content/img/", static) + http.Handle("/static/", static) + imgDir := filepath.Join(root, "static", "img") + http.Handle("/favicon.ico", http.FileServer(http.Dir(imgDir))) + + 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, nil)) +} + +// 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! + +I appear to be listening on an address that is not localhost. +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! +` + +type response struct { + Output string `json:"output"` + Errors string `json:"compile_errors"` +} + +func init() { + socket.Environ = environ +} + +// environ returns the original execution environment with GOPATH +// replaced (or added) with the value of the global var gopath. +func environ() (env []string) { + for _, v := range os.Environ() { + if !strings.HasPrefix(v, "GOPATH=") { + env = append(env, v) + } + } + env = append(env, "GOPATH="+gopath) + return +} + +// 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. +func prepContent(r io.Reader) io.Reader { return r } + +// socketAddr returns the WebSocket handler address. +func socketAddr() string { return "ws://" + httpAddr + socketPath } diff --git a/tour.go b/tour.go new file mode 100644 index 0000000..1a74285 --- /dev/null +++ b/tour.go @@ -0,0 +1,282 @@ +// Copyright 2013 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 "golang.org/x/tour" + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "fmt" + "html/template" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "golang.org/x/tools/godoc/static" + "golang.org/x/tools/present" +) + +var ( + uiContent []byte + lessons = make(map[string][]byte) + lessonNotFound = fmt.Errorf("lesson not found") +) + +// initTour loads tour.article and the relevant HTML templates from the given +// tour root, and renders the template to the tourContent global variable. +func initTour(root, transport string) error { + // Make sure playground is enabled before rendering. + present.PlayEnabled = true + + // Set up templates. + action := filepath.Join(root, "template", "action.tmpl") + tmpl, err := present.Template().ParseFiles(action) + if err != nil { + return fmt.Errorf("parse templates: %v", err) + } + + // Init lessons. + contentPath := filepath.Join(root, "content") + if err := initLessons(tmpl, contentPath); err != nil { + return fmt.Errorf("init lessons: %v", err) + } + + // Init UI + index := filepath.Join(root, "template", "index.tmpl") + ui, err := template.ParseFiles(index) + if err != nil { + return fmt.Errorf("parse index.tmpl: %v", err) + } + buf := new(bytes.Buffer) + + data := struct { + SocketAddr string + Transport template.JS + }{socketAddr(), template.JS(transport)} + + if err := ui.Execute(buf, data); err != nil { + return fmt.Errorf("render UI: %v", err) + } + uiContent = buf.Bytes() + + return initScript(root) +} + +// initLessonss finds all the lessons in the passed directory, renders them, +// using the given template and saves the content in the lessons map. +func initLessons(tmpl *template.Template, content string) error { + dir, err := os.Open(content) + if err != nil { + return err + } + files, err := dir.Readdirnames(0) + if err != nil { + return err + } + for _, f := range files { + if filepath.Ext(f) != ".article" { + continue + } + content, err := parseLesson(tmpl, filepath.Join(content, f)) + if err != nil { + return fmt.Errorf("parsing %v: %v", f, err) + } + name := strings.TrimSuffix(f, ".article") + lessons[name] = content + } + return nil +} + +// File defines the JSON form of a code file in a page. +type File struct { + Name string + Content string + Hash string +} + +// Page defines the JSON form of a tour lesson page. +type Page struct { + Title string + Content string + Files []File +} + +// Lesson defines the JSON form of a tour lesson. +type Lesson struct { + Title string + Description string + Pages []Page +} + +// parseLesson parses and returns a lesson content given its name and +// the template to render it. +func parseLesson(tmpl *template.Template, path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + doc, err := present.Parse(prepContent(f), path, 0) + if err != nil { + return nil, err + } + + lesson := Lesson{ + doc.Title, + doc.Subtitle, + make([]Page, len(doc.Sections)), + } + + for i, sec := range doc.Sections { + p := &lesson.Pages[i] + w := new(bytes.Buffer) + if err := sec.Render(w, tmpl); err != nil { + return nil, fmt.Errorf("render section: %v", err) + } + p.Title = sec.Title + p.Content = w.String() + codes := findPlayCode(sec) + p.Files = make([]File, len(codes)) + for i, c := range codes { + f := &p.Files[i] + f.Name = c.FileName + f.Content = string(c.Raw) + hash := sha1.Sum(c.Raw) + f.Hash = base64.StdEncoding.EncodeToString(hash[:]) + } + } + + w := new(bytes.Buffer) + if err := json.NewEncoder(w).Encode(lesson); err != nil { + return nil, fmt.Errorf("encode lesson: %v", err) + } + return w.Bytes(), nil +} + +// findPlayCode returns a slide with all the Code elements in the given +// Elem with Play set to true. +func findPlayCode(e present.Elem) []*present.Code { + var r []*present.Code + switch v := e.(type) { + case present.Code: + if v.Play { + r = append(r, &v) + } + case present.Section: + for _, s := range v.Elem { + r = append(r, findPlayCode(s)...) + } + } + return r +} + +// writeLesson writes the tour content to the provided Writer. +func writeLesson(name string, w io.Writer) error { + if uiContent == nil { + panic("writeLesson called before successful initTour") + } + if len(name) == 0 { + return writeAllLessons(w) + } + l, ok := lessons[name] + if !ok { + return lessonNotFound + } + _, err := w.Write(l) + return err +} + +func writeAllLessons(w io.Writer) error { + if _, err := fmt.Fprint(w, "{"); err != nil { + return err + } + nLessons := len(lessons) + for k, v := range lessons { + if _, err := fmt.Fprintf(w, "%q:%s", k, v); err != nil { + return err + } + nLessons-- + if nLessons != 0 { + if _, err := fmt.Fprint(w, ","); err != nil { + return err + } + } + } + _, err := fmt.Fprint(w, "}") + return err +} + +// renderUI writes the tour UI to the provided Writer. +func renderUI(w io.Writer) error { + if uiContent == nil { + panic("renderUI called before successful initTour") + } + _, err := w.Write(uiContent) + return err +} + +// nocode returns true if the provided Section contains +// no Code elements with Play enabled. +func nocode(s present.Section) bool { + for _, e := range s.Elem { + if c, ok := e.(present.Code); ok && c.Play { + return false + } + } + return true +} + +// initScript concatenates all the javascript files needed to render +// the tour UI and serves the result on /script.js. +func initScript(root string) error { + modTime := time.Now() + b := new(bytes.Buffer) + + content, ok := static.Files["playground.js"] + if !ok { + return fmt.Errorf("playground.js not found in static files") + } + b.WriteString(content) + + // Keep this list in dependency order + files := []string{ + "static/lib/jquery.min.js", + "static/lib/jquery-ui.min.js", + "static/lib/angular.min.js", + "static/lib/codemirror/lib/codemirror.js", + "static/lib/codemirror/mode/go/go.js", + "static/lib/angular-ui.min.js", + "static/js/app.js", + "static/js/controllers.js", + "static/js/directives.js", + "static/js/services.js", + "static/js/values.js", + } + + for _, file := range files { + f, err := ioutil.ReadFile(filepath.Join(root, file)) + if err != nil { + return fmt.Errorf("couldn't open %v: %v", file, err) + } + _, err = b.Write(f) + if err != nil { + return fmt.Errorf("error concatenating %v: %v", file, err) + } + } + + http.HandleFunc("/script.js", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-type", "application/javascript") + // Set expiration time in one week. + w.Header().Set("Cache-control", "max-age=604800") + http.ServeContent(w, r, "", modTime, bytes.NewReader(b.Bytes())) + }) + + return nil +} -- cgit v1.3