github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/pkg/assets/dev.go (about)

     1  package assets
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/http/httputil"
    11  	"net/url"
    12  	"os"
    13  	"os/exec"
    14  	"strings"
    15  	"sync"
    16  	"syscall"
    17  
    18  	"github.com/pkg/errors"
    19  
    20  	"github.com/tilt-dev/tilt/pkg/logger"
    21  	"github.com/tilt-dev/tilt/pkg/model"
    22  	"github.com/tilt-dev/tilt/pkg/procutil"
    23  )
    24  
    25  const errorBodyStyle = `
    26    font-family: Inconsolata, monospace;
    27    background-color: #002b36;
    28    color: #ffffff;
    29    font-size: 20px;
    30    line-height: 1.5;
    31    margin: 0;
    32  `
    33  const errorDivStyle = `
    34    width: 100%;
    35    height: 100vh;
    36    display: flex;
    37    align-items: center;
    38    justify-content: center;
    39    flex-direction: column;
    40  `
    41  
    42  type devServer struct {
    43  	http.Handler
    44  	packageDir PackageDir
    45  	port       model.WebDevPort
    46  
    47  	mu       sync.Mutex
    48  	cmd      *exec.Cmd
    49  	disposed bool
    50  }
    51  
    52  func NewDevServer(packageDir PackageDir, devPort model.WebDevPort) (*devServer, error) {
    53  	loc, err := url.Parse(fmt.Sprintf("http://localhost:%d", devPort))
    54  	if err != nil {
    55  		return nil, errors.Wrap(err, "NewDevServer")
    56  	}
    57  	handler := httputil.NewSingleHostReverseProxy(loc)
    58  	handler.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, e error) {
    59  		extra := ""
    60  		extraHead := ""
    61  		if strings.Contains(e.Error(), "connection refused") {
    62  			extra = `"connection refused" expected on startup with --web-mode=local. Refreshing in a few seconds.`
    63  			extraHead = `<meta http-equiv="refresh" content="2">`
    64  		}
    65  		response := fmt.Sprintf(`
    66  <html>
    67    <head>%s</head>
    68    <body style="%s">
    69      <div style="%s">
    70        Error talking to asset server:<pre>%s</pre>
    71        <br>%s
    72      </div>
    73    </body>
    74  </html>`, extraHead, errorBodyStyle, errorDivStyle, e.Error(), extra)
    75  		_, _ = writer.Write([]byte(response))
    76  	}
    77  
    78  	return &devServer{
    79  		Handler:    handler,
    80  		packageDir: packageDir,
    81  		port:       devPort,
    82  	}, nil
    83  }
    84  
    85  func (s *devServer) TearDown(ctx context.Context) {
    86  	s.mu.Lock()
    87  	defer s.mu.Unlock()
    88  
    89  	cmd := s.cmd
    90  	procutil.KillProcessGroup(cmd)
    91  	s.disposed = true
    92  }
    93  
    94  func (s *devServer) start(ctx context.Context, stdout, stderr io.Writer) (*exec.Cmd, error) {
    95  	logger.Get(ctx).Infof("Installing Tilt NodeJS dependencies…")
    96  	cmd := exec.CommandContext(ctx, "yarn", "install")
    97  	cmd.Dir = s.packageDir.String()
    98  	stdoutString := &strings.Builder{}
    99  	stderrString := &strings.Builder{}
   100  	stdoutWriter := io.MultiWriter(stdoutString, logger.Get(ctx).Writer(logger.DebugLvl))
   101  	stderrWriter := io.MultiWriter(stderrString, logger.Get(ctx).Writer(logger.DebugLvl))
   102  	cmd.Stdout = stdoutWriter
   103  	cmd.Stderr = stderrWriter
   104  	err := cmd.Run()
   105  	if err != nil {
   106  		return nil, fmt.Errorf("Error installing Tilt webpack deps:\nstdout:\n%s\nstderr:\n%s\nerror: %s", stdoutString.String(), stderrString.String(), err)
   107  	}
   108  
   109  	logger.Get(ctx).Infof("Starting Tilt webpack server…")
   110  	cmd = exec.CommandContext(ctx, "yarn", "run", "start")
   111  	cmd.Dir = s.packageDir.String()
   112  	cmd.Env = append(os.Environ(), "BROWSER=none", fmt.Sprintf("PORT=%d", s.port))
   113  
   114  	attrs := &syscall.SysProcAttr{}
   115  
   116  	// yarn will spawn the dev server as a subprocess, so set
   117  	// a process group id so we can murder them all.
   118  	procutil.SetOptNewProcessGroup(attrs)
   119  
   120  	cmd.SysProcAttr = attrs
   121  
   122  	// The webpack devserver expects an stdin that never closes.
   123  	pipeReader, _ := io.Pipe()
   124  	cmd.Stdin = pipeReader
   125  
   126  	cmd.Stdout = stdout
   127  	cmd.Stderr = stderr
   128  
   129  	s.mu.Lock()
   130  	defer s.mu.Unlock()
   131  	if s.disposed {
   132  		return nil, nil
   133  	}
   134  
   135  	err = cmd.Start()
   136  	if err != nil {
   137  		return nil, errors.Wrap(err, "Starting dev web server")
   138  	}
   139  
   140  	s.cmd = cmd
   141  	return cmd, nil
   142  }
   143  
   144  func (s *devServer) Serve(ctx context.Context) error {
   145  	// webpack binds to 0.0.0.0
   146  	l, err := net.Listen("tcp4", fmt.Sprintf(":%d", int(s.port)))
   147  	if err != nil {
   148  		return errors.Wrapf(err, "Cannot start Tilt dev webpack server. "+
   149  			"Maybe another process is already running on port %d? "+
   150  			"Use --webdev-port to set a custom port", s.port)
   151  	}
   152  	_ = l.Close()
   153  
   154  	stdout := bytes.NewBuffer(nil)
   155  	stderr := bytes.NewBuffer(nil)
   156  	cmd, err := s.start(ctx, stdout, stderr)
   157  	if cmd == nil || err != nil {
   158  		return err
   159  	}
   160  
   161  	err = cmd.Wait()
   162  	if ctx.Err() != nil {
   163  		// Process was killed
   164  		return nil
   165  	}
   166  
   167  	if err != nil {
   168  		exitErr, isExit := err.(*exec.ExitError)
   169  		if isExit {
   170  			return errors.Wrapf(err, "Running dev web server. Stderr: %s", string(exitErr.Stderr))
   171  		}
   172  		return errors.Wrap(err, "Running dev web server")
   173  	}
   174  	return fmt.Errorf("Tilt dev server stopped unexpectedly\nStdout:\n%s\nStderr:\n%s\n",
   175  		stdout.String(), stderr.String())
   176  }