github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/server/controller.go (about) 1 package server 2 3 import ( 4 "context" 5 "net/http" 6 "sync/atomic" 7 "time" 8 9 "github.com/pkg/browser" 10 "github.com/pkg/errors" 11 12 "github.com/windmilleng/tilt/internal/network" 13 "github.com/windmilleng/tilt/internal/store" 14 "github.com/windmilleng/tilt/pkg/assets" 15 "github.com/windmilleng/tilt/pkg/model" 16 ) 17 18 // The amount of time to wait for a reconnection before restarting the browser 19 // window. 20 const reconnectDur = 2 * time.Second 21 22 type HeadsUpServerController struct { 23 host model.WebHost 24 port model.WebPort 25 hudServer *HeadsUpServer 26 assetServer assets.Server 27 webURL model.WebURL 28 webLoadDone bool 29 initDone bool 30 noBrowser model.NoBrowser 31 } 32 33 func ProvideHeadsUpServerController(host model.WebHost, port model.WebPort, hudServer *HeadsUpServer, assetServer assets.Server, webURL model.WebURL, noBrowser model.NoBrowser) *HeadsUpServerController { 34 return &HeadsUpServerController{ 35 host: host, 36 port: port, 37 hudServer: hudServer, 38 assetServer: assetServer, 39 webURL: webURL, 40 noBrowser: noBrowser, 41 } 42 } 43 44 func (s *HeadsUpServerController) TearDown(ctx context.Context) { 45 s.assetServer.TearDown(ctx) 46 } 47 48 func (s *HeadsUpServerController) isWebsocketConnected() bool { 49 connCount := atomic.LoadInt32(&(s.hudServer.numWebsocketConns)) 50 return connCount > 0 51 } 52 53 func (s *HeadsUpServerController) maybeOpenBrowser(st store.RStore) { 54 if s.webURL.Empty() || s.webLoadDone || (bool)(s.noBrowser) { 55 return 56 } 57 58 if s.isWebsocketConnected() { 59 // Don't auto-open the web view. It's already opened. 60 s.webLoadDone = true 61 return 62 } 63 64 state := st.RLockState() 65 tiltfileCompleted := !state.TiltfileState.LastBuild().Empty() 66 startTime := state.TiltStartTime 67 st.RUnlockState() 68 69 // Only open the webview if the Tiltfile has completed. 70 if tiltfileCompleted { 71 s.webLoadDone = true 72 73 // Make sure we wait at least `reconnectDur` before opening the browser, to 74 // give any open pages time to reconnect. Do this on a goroutine so we don't 75 // hold the lock. 76 go func() { 77 runDur := time.Since(startTime) 78 if runDur < reconnectDur { 79 time.Sleep(reconnectDur - runDur) 80 } 81 82 if s.isWebsocketConnected() { 83 return 84 } 85 86 // We should probably dependency-inject a browser opener. 87 // 88 // It might also make sense to wait until the asset server is ready? 89 _ = browser.OpenURL(s.webURL.String()) 90 }() 91 } 92 } 93 94 func (s *HeadsUpServerController) OnChange(ctx context.Context, st store.RStore) { 95 s.maybeOpenBrowser(st) 96 97 defer func() { 98 s.initDone = true 99 }() 100 101 if s.initDone || s.port == 0 { 102 return 103 } 104 105 err := network.IsBindAddrFree(network.BindAddr(string(s.host), int(s.port))) 106 if err != nil { 107 st.Dispatch( 108 store.NewErrorAction( 109 errors.Wrapf(err, "Cannot start Tilt. Maybe another process is already running on port %d? Use --port to set a custom port", s.port))) 110 return 111 } 112 113 httpServer := &http.Server{ 114 Addr: network.BindAddr(string(s.host), int(s.port)), 115 Handler: http.DefaultServeMux, 116 } 117 http.Handle("/", s.hudServer.Router()) 118 119 go func() { 120 <-ctx.Done() 121 _ = httpServer.Shutdown(context.Background()) 122 }() 123 124 go func() { 125 err := s.assetServer.Serve(ctx) 126 if err != nil && ctx.Err() == nil { 127 st.Dispatch(store.NewErrorAction(err)) 128 } 129 }() 130 131 go func() { 132 err := httpServer.ListenAndServe() 133 if err != nil && err != http.ErrServerClosed && ctx.Err() == nil { 134 st.Dispatch(store.NewErrorAction(err)) 135 } 136 }() 137 } 138 139 var _ store.TearDowner = &HeadsUpServerController{}