Chromium Code Reviews| Index: go/src/infra/gae/epservice/epservice.go |
| diff --git a/go/src/infra/gae/epservice/epservice.go b/go/src/infra/gae/epservice/epservice.go |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..582fc44dd9103adc1dad33201b16425aacd0879a |
| --- /dev/null |
| +++ b/go/src/infra/gae/epservice/epservice.go |
| @@ -0,0 +1,250 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +// EPService will run a local appengine service with all of the endpoint |
| +// services under this directory. The tool assumes that: |
| +// * goapp exists in PATH (and understands all the import paths in the |
| +// defined services) |
| +package main |
| + |
| +import ( |
| + "bytes" |
| + "encoding/json" |
| + "flag" |
| + "fmt" |
| + "io/ioutil" |
| + "os" |
| + "os/exec" |
| + "os/signal" |
| + "path/filepath" |
| + "regexp" |
| + "strings" |
| + "sync" |
| + "text/template" |
| + "time" |
| + |
| + "infra/libs/jsutil" |
| +) |
| + |
| +var ( |
| + clearDatastore = flag.Bool("clear_datastore", false, "if set, clear the datastore.") |
| + leak = flag.Bool("leak", false, "if set, leak the temporary directory.") |
| + verbose = flag.Bool("verbose", false, "if set, print skipped packages.") |
| + servicePackagesBase = flag.String("base", "infra", |
| + "base Go package to walk to find service definitions.") |
| +) |
| + |
| +var serviceScript = `{{define "go"}} |
| +// DO NOT EDIT |
| +// Auto-generated by infra/gae/epservice |
| +// {{.Timestamp}} |
| + |
| +package main |
| + |
| +import ( |
| + "fmt" |
| + |
| +{{range $idx, $pkg := .Pkgs}} |
| + pkg{{$idx}} "{{$pkg.Imp}}"{{end}} |
| + |
| + "github.com/GoogleCloudPlatform/go-endpoints/endpoints" |
| +) |
| + |
| +func init() { |
| + var err error |
| + server := endpoints.NewServer("") |
| + |
| + {{range $idx, $pkg := .Pkgs }} |
| + err = pkg{{$idx}}.RegisterEndpointsService(server) |
| + if err != nil { |
| + panic(fmt.Errorf("Error while registering service {{$pkg}}: %s", err)) |
| + } |
| + {{end}} |
| + |
| + server.HandleHTTP(nil) |
| +} |
| +{{end}} |
| +` |
| + |
| +const appYaml = `{{define "yaml"}} |
| +# DO NOT EDIT |
| +# Auto-generated by infra/gae/epservice |
| +# {{.Timestamp}} |
| + |
| +application: epclient-tmp-app |
| +version: nope |
| +runtime: go |
| +api_version: go1 |
| + |
| +handlers: |
| +- url: /.* |
| + script: _go_app |
| +{{end}} |
| +` |
| + |
| +var templ = template.New("service") |
| + |
| +func init() { |
| + template.Must(templ.Parse(serviceScript)) |
| + template.Must(templ.Parse(appYaml)) |
| +} |
| + |
| +type templInput struct { |
| + Pkgs []Pkg |
| + Timestamp time.Time |
| +} |
| + |
| +// Pkg holds the import and real filesystem paths of an endpoint service |
| +// package. It's exported merely for reflection purposes, since it's used |
| +// by text/template. |
| +type Pkg struct { |
| + Imp string |
| + Pth string |
| +} |
| + |
| +func boom(err error) { |
| + if err != nil { |
| + panic(err) |
| + } |
| +} |
| + |
| +// NOTE: if you format your RegisterEndpointsService implementation like a bozo, |
| +// then this won't match. Don't do that. |
| +var reRegisterEndpointsService = regexp.MustCompile( |
| + `\nfunc RegisterEndpointsService\(\w* \*\w*\.Server\) error {\n`) |
| + |
| +func getPkgs() []Pkg { |
| + cmd := exec.Command("goapp", "list", "-json", filepath.Join(*servicePackagesBase+"...")) |
| + d, err := cmd.Output() |
| + boom(err) |
| + |
| + d = []byte("[" + strings.Replace(string(d), "}\n{", "},{", -1) + "]") |
| + |
| + js := interface{}(nil) |
| + boom(json.Unmarshal(d, &js)) |
| + |
| + ret := []Pkg{} |
| + |
| + type pkgOk struct { |
| + p Pkg |
| + ok bool |
| + } |
| + pkgs := make(chan pkgOk) |
| + wg := sync.WaitGroup{} |
| + |
| + for _, m := range js.([]interface{}) { |
| + pkg := Pkg{ |
| + jsutil.Get(m, "ImportPath").(string), |
| + jsutil.Get(m, "Dir").(string), |
| + } |
| + wg.Add(1) |
| + go func() { |
| + defer wg.Done() |
| + paths, err := ioutil.ReadDir(pkg.Pth) |
| + boom(err) |
| + |
| + for _, f := range paths { |
| + if !f.IsDir() && strings.HasSuffix(f.Name(), ".go") { |
| + data, err := ioutil.ReadFile(filepath.Join(pkg.Pth, f.Name())) |
| + boom(err) |
| + if reRegisterEndpointsService.Match(data) { |
| + pkgs <- pkgOk{pkg, true} |
| + return |
| + } |
| + } |
| + } |
| + pkgs <- pkgOk{pkg, false} |
| + }() |
| + } |
| + go func() { |
| + wg.Wait() |
| + close(pkgs) |
| + }() |
| + |
| + for p := range pkgs { |
| + if !p.ok { |
| + if *verbose { |
| + fmt.Println("skipping package", p.p.Imp, "(it doesn't impliment RegisterEndpointsService?)") |
| + } |
| + } else { |
| + fmt.Println("including package", p.p.Imp) |
| + ret = append(ret, p.p) |
| + } |
| + } |
| + |
| + return ret |
| +} |
| + |
| +func writeFiles(dir string, pkgs []Pkg) { |
| + input := &templInput{pkgs, time.Now()} |
| + for _, ext := range []string{"go", "yaml"} { |
| + buf := &bytes.Buffer{} |
| + boom(templ.ExecuteTemplate(buf, ext, input)) |
| + boom(ioutil.WriteFile(filepath.Join(dir, "app."+ext), buf.Bytes(), 0666)) |
| + } |
| +} |
| + |
| +func startServer(dir string) (func(), func()) { |
| + args := []string{"serve"} |
| + if *clearDatastore { |
| + args = append(args, "-clear_datastore") |
| + } |
| + args = append(args, dir) |
| + server := exec.Command("goapp", args...) |
| + server.Stdout = os.Stdout |
| + server.Stderr = os.Stderr |
| + boom(server.Start()) |
| + |
| + wait := func() { server.Wait() } |
| + stop := func() { |
| + server.Process.Signal(os.Interrupt) |
| + wait() |
| + } |
| + return stop, wait |
| +} |
| + |
| +func main() { |
| + _, err := exec.LookPath("goapp") |
| + if err != nil { |
| + panic("goapp must be on your path") |
| + } |
| + |
| + flag.Parse() |
| + pkgs := getPkgs() |
| + |
| + dir, err := ioutil.TempDir("", "epservice_gen") |
| + boom(err) |
| + prefix := "LEAKING" |
| + if !*leak { |
| + prefix = "generating" |
| + defer os.RemoveAll(dir) |
| + } |
| + fmt.Println(prefix, "files in:", dir) |
| + |
| + writeFiles(dir, pkgs) |
| + |
| + stop, wait := startServer(dir) |
| + defer wait() |
| + |
| + intC := make(chan os.Signal, 1) |
|
dnj
2015/06/09 02:56:58
Way less cute of a channel name :(
|
| + signal.Notify(intC, os.Interrupt, os.Kill) |
| + sigC := make(chan struct{}) |
| + go func() { |
| + defer close(sigC) |
| + <-intC |
| + stop() |
| + }() |
| + |
| + waitC := make(chan struct{}) |
| + go func() { |
| + defer close(waitC) |
| + wait() |
| + }() |
| + |
| + select { |
| + case <-sigC: |
| + case <-waitC: |
| + } |
| + time.Sleep(time.Second) |
|
dnj
2015/06/09 02:56:58
Why is this necessary?
iannucci
2015/06/09 07:41:11
added it while I was debugging something else. The
|
| +} |