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 }