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  }