 Chromium Code Reviews
 Chromium Code Reviews Issue 1153473008:
  A client/server helper wrapper for endpoints in Go.  (Closed) 
  Base URL: https://chromium.googlesource.com/infra/infra.git@master
    
  
    Issue 1153473008:
  A client/server helper wrapper for endpoints in Go.  (Closed) 
  Base URL: https://chromium.googlesource.com/infra/infra.git@master| 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
 | 
| +} |