Code

Add an initial version of sample code for my Go talk.
[go-talk.git] / grpc / ui / main.go
1 // Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions
6 // are met:
7 // 1. Redistributions of source code must retain the above copyright
8 //    notice, this list of conditions and the following disclaimer.
9 // 2. Redistributions in binary form must reproduce the above copyright
10 //    notice, this list of conditions and the following disclaimer in the
11 //    documentation and/or other materials provided with the distribution.
12 //
13 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
14 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
15 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
17 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
20 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
21 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
22 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
23 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 // ui is a web-based UI interacting with the backend server.
26 package main
28 import (
29         "bytes"
30         "errors"
31         "flag"
32         "fmt"
33         "html/template"
34         "io"
35         "log"
36         "net/http"
37         "strings"
38         "sync"
39         "time"
41         "golang.org/x/net/context"
42         "google.golang.org/grpc"
44         pb "tokkee.org/go-talk/grpc/proto/backend"
45 )
47 var (
48         listen  = flag.String("listen", ":8080", "listen address")
49         backend = flag.String("backend", "localhost:50051", "backend server address")
51         queryTmpl = template.Must(template.New("query").Parse(`<html>
52 <head><title>Backend Query</title></head>
53 <body>
54   <form action="/query" method="POST">
55     Query: <input type="text" name="q" placeholder="<cmd> <arg>..." value="{{.Query}}" />
56     <input type="submit" value="Submit">
57   </form>
58 {{range .Responses}}
59   <p><b>{{.Request}} =></b> {{.Type}}: {{.N}}</p>
60 {{end}}
61 </body>
62 </html>
63 `))
64 )
66 type server struct {
67         c pb.BackendClient
68 }
70 type queryResponse struct {
71         Request string
72         Type    string
73         N       int64
74 }
76 // runQueries executes multiple queries, separated by semicolon, in parallel.
77 func (s *server) runQueries(ctx context.Context, query string) ([]queryResponse, error) {
78         requests := strings.Split(query, ";")
79         for i, r := range requests {
80                 requests[i] = strings.TrimSpace(r)
81         }
83         responses := make([]queryResponse, len(requests))
84         errCh := make(chan error, len(requests))
86         for i, req := range requests {
87                 go func(i int, req string) {
88                         res, err := s.c.Query(ctx, &pb.QueryRequest{Query: req})
89                         defer func() { errCh <- err }()
90                         if err != nil {
91                                 return
92                         }
94                         responses[i] = queryResponse{
95                                 Request: req,
96                                 Type:    res.Type,
97                                 N:       res.N,
98                         }
99                 }(i, req)
100         }
102         timeout := time.After(50 * time.Millisecond)
104         for _ = range requests {
105                 select {
106                 case err := <-errCh:
107                         if err != nil {
108                                 return nil, err
109                         }
111                 case <-timeout:
112                         return nil, errors.New("request timed out")
113                 }
114         }
116         return responses, nil
119 func (s *server) query(ctx context.Context, w http.ResponseWriter, r *http.Request) {
120         data := &struct {
121                 Query     string
122                 Responses []queryResponse
123         }{}
125         data.Query = r.Form.Get("q")
126         if data.Query != "" {
127                 if r.Method != "POST" {
128                         // RFC 2616 requires us to set the "Allow" header.
129                         w.Header().Add("Allow", "POST")
130                         http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
131                         return
132                 }
133                 // TODO: use and validate XSRF tokens
135                 res, err := s.runQueries(ctx, data.Query)
136                 if err != nil {
137                         http.Error(w, fmt.Sprintf("query failed: %v", err), http.StatusBadRequest)
138                         return
139                 }
140                 data.Responses = res
141         }
143         var buf bytes.Buffer
144         if err := queryTmpl.Execute(&buf, data); err != nil {
145                 http.Error(w, fmt.Sprintf("template error: %v", err), http.StatusInternalServerError)
146                 return
147         }
148         io.Copy(w, &buf)
151 type handler func(context.Context, http.ResponseWriter, *http.Request)
153 // A mux multiplexes incoming requests based on the first part of the incoming
154 // request URI. It differs from http.ServerMux in that it only support routing
155 // based on the first part of the request path and it adds context handling
156 // and form parsing. mux implements http.Handler.
157 type mux struct {
158         mu       sync.RWMutex
159         handlers map[string]handler
162 // Handle registers a handler for the specified path. It panics if a handler
163 // has already been registered for the same path or if the path includes a
164 // slash.
165 func (m *mux) Handle(path string, h handler) {
166         if strings.Index(path, "/") != -1 {
167                 panic("invalid path: " + path)
168         }
169         if h == nil {
170                 panic("invalid nil handler")
171         }
173         m.mu.Lock()
174         defer m.mu.Unlock()
176         if m.handlers == nil {
177                 m.handlers = make(map[string]handler)
178         }
179         if m.handlers[path] != nil {
180                 panic(fmt.Sprintf("duplicate handlers registered for %q", path))
181         }
182         m.handlers[path] = h
185 // ServeHTTP handles incoming requests using the registered handlers and takes
186 // care of request-specific setup (context management and form parsing).
187 func (m *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
188         path := r.URL.Path
189         if len(path) > 0 && path[0] == '/' {
190                 path = path[1:]
191         }
192         if f := strings.Split(path, "/"); len(f) > 1 {
193                 path = f[0]
194         }
196         if err := r.ParseForm(); err != nil {
197                 log.Printf("Invalid URL query: %v", err)
198                 http.Error(w, fmt.Sprintf("invalid URL query: %v", err), http.StatusBadRequest)
199                 return
200         }
202         m.mu.RLock()
203         h := m.handlers[path]
204         m.mu.RUnlock()
206         if h == nil {
207                 log.Printf("Not found: %q (%v)", path, r.URL)
208                 http.NotFound(w, r)
209                 return
210         }
211         h(context.Background(), w, r)
214 func main() {
215         flag.Parse()
217         conn, err := grpc.Dial(*backend, grpc.WithInsecure())
218         if err != nil {
219                 log.Fatalf("Failed to connect to backend server at %s: %v", *backend, err)
220         }
221         defer conn.Close()
223         s := &server{c: pb.NewBackendClient(conn)}
224         m := &mux{}
225         m.Handle("query", s.query)
226         log.Printf("Listening on %s ...", *listen)
227         log.Fatal(http.ListenAndServe(*listen, m))