github.com/bazelbuild/rules_webtesting@v0.2.0/go/wtl/service/server.go (about) 1 // Copyright 2016 Google Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package service 16 17 import ( 18 "context" 19 "fmt" 20 "net" 21 "net/http" 22 "strconv" 23 "strings" 24 "time" 25 26 "github.com/bazelbuild/rules_webtesting/go/errors" 27 "github.com/bazelbuild/rules_webtesting/go/healthreporter" 28 "github.com/bazelbuild/rules_webtesting/go/httphelper" 29 "github.com/bazelbuild/rules_webtesting/go/portpicker" 30 "github.com/bazelbuild/rules_webtesting/go/wtl/diagnostics" 31 ) 32 33 // Server is a service that starts an external server. 34 type Server struct { 35 *Cmd 36 address string 37 port int 38 healthPattern string 39 timeout time.Duration 40 } 41 42 // NewServer creates a new service for starting an external server on the host machine. 43 // Args should contain {port}, which will be replaced with the selected port number. 44 func NewServer(name string, d diagnostics.Diagnostics, exe, healthPattern string, xvfb bool, timeout time.Duration, env map[string]string, args ...string) (*Server, error) { 45 port, err := portpicker.PickUnusedPort() 46 if err != nil { 47 return nil, errors.New(name, err) 48 } 49 updatedArgs := []string{} 50 for _, arg := range args { 51 updatedArgs = append(updatedArgs, strings.Replace(arg, "{port}", strconv.Itoa(port), -1)) 52 } 53 54 cmd, err := NewCmd(name, d, exe, xvfb, env, updatedArgs...) 55 if err != nil { 56 return nil, err 57 } 58 return &Server{ 59 Cmd: cmd, 60 address: net.JoinHostPort("localhost", strconv.Itoa(port)), 61 port: port, 62 healthPattern: healthPattern, 63 timeout: timeout, 64 }, nil 65 } 66 67 // Start starts the server, waits for it to become healhy, and monitors it to ensure that it 68 // stays healthy. 69 func (s *Server) Start(ctx context.Context) error { 70 if err := s.Cmd.Start(ctx); err != nil { 71 return err 72 } 73 74 healthyCtx, cancel := context.WithTimeout(ctx, s.timeout) 75 defer cancel() 76 if err := healthreporter.WaitForHealthy(healthyCtx, s); err != nil { 77 return errors.New(s.Name(), err) 78 } 79 return nil 80 } 81 82 // Stop stops the server. 83 func (s *Server) Stop(ctx context.Context) error { 84 if err := s.Cmd.Stop(ctx); err != nil { 85 return err 86 } 87 return nil 88 } 89 90 // Healthy returns nil if the server responds OK to requests to health page. 91 func (s *Server) Healthy(ctx context.Context) error { 92 if err := s.Cmd.Healthy(ctx); err != nil { 93 return err 94 } 95 if s.healthPattern == "" { 96 dialer := &net.Dialer{} 97 c, err := dialer.DialContext(ctx, "tcp", s.address) 98 if err != nil { 99 return err 100 } 101 102 switch t := c.(type) { 103 case *net.TCPConn: 104 t.Close() 105 } 106 107 return nil 108 } 109 url := fmt.Sprintf(s.healthPattern, s.address) 110 resp, err := httphelper.Get(ctx, url) 111 if err != nil { 112 return errors.New(s.Name(), fmt.Errorf("error getting %s: %v", url, err)) 113 } 114 resp.Body.Close() 115 if resp.StatusCode != http.StatusOK { 116 return errors.New(s.Name(), fmt.Errorf("request to %s returned status %v", url, resp.StatusCode)) 117 } 118 return nil 119 } 120 121 // Port returns the port this server is running on as a string. 122 func (s *Server) Port() string { 123 return fmt.Sprintf("%d", s.port) 124 } 125 126 // Address returns the address of the server (localhost:%port%). 127 func (s *Server) Address() string { 128 return s.address 129 }