github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/hud.go (about)

     1  package hud
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"runtime"
     8  	"runtime/pprof"
     9  	"strconv"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/windmilleng/tilt/pkg/logger"
    14  
    15  	"github.com/gdamore/tcell"
    16  	"github.com/pkg/browser"
    17  	"github.com/pkg/errors"
    18  
    19  	"github.com/windmilleng/tilt/internal/analytics"
    20  	"github.com/windmilleng/tilt/internal/hud/view"
    21  	"github.com/windmilleng/tilt/internal/store"
    22  	"github.com/windmilleng/tilt/pkg/model"
    23  )
    24  
    25  // The main loop ensures the HUD updates at least this often
    26  const DefaultRefreshInterval = 100 * time.Millisecond
    27  
    28  // number of arrows a pgup/dn is equivalent to
    29  // (we don't currently worry about trying to know how big a page is, and instead just support pgup/dn as "faster arrows"
    30  const pgUpDownCount = 20
    31  
    32  type HeadsUpDisplay interface {
    33  	store.Subscriber
    34  
    35  	Run(ctx context.Context, dispatch func(action store.Action), refreshRate time.Duration) error
    36  	Update(v view.View, vs view.ViewState) error
    37  	Close()
    38  	SetNarrationMessage(ctx context.Context, msg string) error
    39  }
    40  
    41  type Hud struct {
    42  	r      *Renderer
    43  	webURL model.WebURL
    44  
    45  	currentView      view.View
    46  	currentViewState view.ViewState
    47  	mu               sync.RWMutex
    48  	isRunning        bool
    49  	a                *analytics.TiltAnalytics
    50  }
    51  
    52  var _ HeadsUpDisplay = (*Hud)(nil)
    53  
    54  func NewDefaultHeadsUpDisplay(renderer *Renderer, webURL model.WebURL, analytics *analytics.TiltAnalytics) (HeadsUpDisplay, error) {
    55  	return &Hud{
    56  		r:      renderer,
    57  		webURL: webURL,
    58  		a:      analytics,
    59  	}, nil
    60  }
    61  
    62  func (h *Hud) SetNarrationMessage(ctx context.Context, msg string) error {
    63  	h.mu.Lock()
    64  	defer h.mu.Unlock()
    65  	currentViewState := h.currentViewState
    66  	currentViewState.ShowNarration = true
    67  	currentViewState.NarrationMessage = msg
    68  	return h.setViewState(ctx, currentViewState)
    69  }
    70  
    71  func (h *Hud) Run(ctx context.Context, dispatch func(action store.Action), refreshRate time.Duration) error {
    72  	h.mu.Lock()
    73  	h.isRunning = true
    74  	h.mu.Unlock()
    75  
    76  	defer func() {
    77  		h.mu.Lock()
    78  		h.isRunning = false
    79  		h.mu.Unlock()
    80  	}()
    81  
    82  	screenEvents, err := h.r.SetUp()
    83  	if err != nil {
    84  		return errors.Wrap(err, "setting up screen")
    85  	}
    86  
    87  	defer h.Close()
    88  
    89  	if refreshRate == 0 {
    90  		refreshRate = DefaultRefreshInterval
    91  	}
    92  	ticker := time.NewTicker(refreshRate)
    93  
    94  	for {
    95  		select {
    96  		case <-ctx.Done():
    97  			err := ctx.Err()
    98  			if err != context.Canceled {
    99  				return err
   100  			} else {
   101  				return nil
   102  			}
   103  		case e := <-screenEvents:
   104  			done := h.handleScreenEvent(ctx, dispatch, e)
   105  			if done {
   106  				return nil
   107  			}
   108  		case <-ticker.C:
   109  			err := h.Refresh(ctx)
   110  			if err != nil {
   111  				return err
   112  			}
   113  		}
   114  	}
   115  }
   116  
   117  func (h *Hud) Close() {
   118  	h.r.Reset()
   119  }
   120  
   121  func (h *Hud) recordInteraction(name string) {
   122  	h.a.Incr(fmt.Sprintf("ui.interactions.%s", name), map[string]string{})
   123  }
   124  
   125  func (h *Hud) handleScreenEvent(ctx context.Context, dispatch func(action store.Action), ev tcell.Event) (done bool) {
   126  	h.mu.Lock()
   127  	defer h.mu.Unlock()
   128  
   129  	escape := func() {
   130  		am := h.activeModal()
   131  		if am != nil {
   132  			am.Close(&h.currentViewState)
   133  		}
   134  	}
   135  
   136  	switch ev := ev.(type) {
   137  	case *tcell.EventKey:
   138  		switch ev.Key() {
   139  		case tcell.KeyEscape:
   140  			escape()
   141  		case tcell.KeyRune:
   142  			switch r := ev.Rune(); {
   143  			case r == 'b': // [B]rowser
   144  				// If we have an endpoint(s), open the first one
   145  				// TODO(nick): We might need some hints on what load balancer to
   146  				// open if we have multiple, or what path to default to on the opened manifest.
   147  				_, selected := h.selectedResource()
   148  				if len(selected.Endpoints) > 0 {
   149  					h.recordInteraction("open_preview")
   150  					err := browser.OpenURL(selected.Endpoints[0])
   151  					if err != nil {
   152  						h.currentViewState.AlertMessage = fmt.Sprintf("error opening url '%s' for resource '%s': %v",
   153  							selected.Endpoints[0], selected.Name, err)
   154  					}
   155  				} else {
   156  					h.currentViewState.AlertMessage = fmt.Sprintf("no urls for resource '%s' ¯\\_(ツ)_/¯", selected.Name)
   157  				}
   158  			case r == 'l': // Tilt [L]og
   159  				if h.webURL.Empty() {
   160  					break
   161  				}
   162  				url := h.webURL
   163  				url.Path = "/"
   164  				_ = browser.OpenURL(url.String())
   165  			case r == 'k':
   166  				h.activeScroller().Up()
   167  				h.refreshSelectedIndex()
   168  			case r == 'j':
   169  				h.activeScroller().Down()
   170  				h.refreshSelectedIndex()
   171  			case r == 'q': // [Q]uit
   172  				escape()
   173  			case r == 'R': // hidden key for recovering from printf junk during demos
   174  				h.r.screen.Sync()
   175  			case r == 'x':
   176  				h.recordInteraction("cycle_view_log_state")
   177  				h.currentViewState.CycleViewLogState()
   178  			case r == '1':
   179  				h.recordInteraction("tab_all_log")
   180  				h.currentViewState.TabState = view.TabAllLog
   181  			case r == '2':
   182  				h.recordInteraction("tab_build_log")
   183  				h.currentViewState.TabState = view.TabBuildLog
   184  			case r == '3':
   185  				h.recordInteraction("tab_pod_log")
   186  				h.currentViewState.TabState = view.TabPodLog
   187  			}
   188  		case tcell.KeyUp:
   189  			h.activeScroller().Up()
   190  			h.refreshSelectedIndex()
   191  		case tcell.KeyDown:
   192  			h.activeScroller().Down()
   193  			h.refreshSelectedIndex()
   194  		case tcell.KeyPgUp:
   195  			for i := 0; i < pgUpDownCount; i++ {
   196  				h.activeScroller().Up()
   197  			}
   198  			h.refreshSelectedIndex()
   199  		case tcell.KeyPgDn:
   200  			for i := 0; i < pgUpDownCount; i++ {
   201  				h.activeScroller().Down()
   202  			}
   203  			h.refreshSelectedIndex()
   204  		case tcell.KeyEnter:
   205  			if len(h.currentView.Resources) == 0 {
   206  				break
   207  			}
   208  			_, r := h.selectedResource()
   209  
   210  			if h.webURL.Empty() {
   211  				break
   212  			}
   213  			url := h.webURL
   214  			url.Path = fmt.Sprintf("/r/%s/", r.Name)
   215  			h.a.Incr("ui.interactions.open_log", map[string]string{"is_tiltfile": strconv.FormatBool(r.Name == store.TiltfileManifestName)})
   216  			_ = browser.OpenURL(url.String())
   217  		case tcell.KeyRight:
   218  			i, _ := h.selectedResource()
   219  			h.currentViewState.Resources[i].CollapseState = view.CollapseNo
   220  		case tcell.KeyLeft:
   221  			i, _ := h.selectedResource()
   222  			h.currentViewState.Resources[i].CollapseState = view.CollapseYes
   223  		case tcell.KeyHome:
   224  			h.activeScroller().Top()
   225  		case tcell.KeyEnd:
   226  			h.activeScroller().Bottom()
   227  		case tcell.KeyCtrlC:
   228  			h.Close()
   229  			dispatch(NewExitAction(nil))
   230  			return true
   231  		case tcell.KeyCtrlD:
   232  			dispatch(DumpEngineStateAction{})
   233  		case tcell.KeyCtrlP:
   234  			if h.currentView.IsProfiling {
   235  				dispatch(StopProfilingAction{})
   236  			} else {
   237  				dispatch(StartProfilingAction{})
   238  			}
   239  		case tcell.KeyCtrlO:
   240  			go writeHeapProfile(ctx)
   241  		case tcell.KeyCtrlT:
   242  			dispatch(SetLogTimestampsAction{!h.currentView.LogTimestamps})
   243  		}
   244  
   245  	case *tcell.EventResize:
   246  		// since we already refresh after the switch, don't need to do anything here
   247  		// just marking this as where sigwinch gets handled
   248  	}
   249  
   250  	err := h.refresh(ctx)
   251  	if err != nil {
   252  		dispatch(NewExitAction(err))
   253  	}
   254  
   255  	return false
   256  }
   257  
   258  func (h *Hud) OnChange(ctx context.Context, st store.RStore) {
   259  	state := st.RLockState()
   260  	view := store.StateToView(state)
   261  	st.RUnlockState()
   262  
   263  	h.mu.Lock()
   264  	defer h.mu.Unlock()
   265  	err := h.setView(ctx, view)
   266  	if err != nil {
   267  		st.Dispatch(NewExitAction(err))
   268  	}
   269  }
   270  
   271  func (h *Hud) Refresh(ctx context.Context) error {
   272  	h.mu.Lock()
   273  	defer h.mu.Unlock()
   274  	return h.refresh(ctx)
   275  }
   276  
   277  // Must hold the lock
   278  func (h *Hud) setView(ctx context.Context, view view.View) error {
   279  	// if we're going from 1 resource (i.e., the Tiltfile) to more than 1, reset
   280  	// the resource selection, so that we're not scrolled to the bottom with the Tiltfile selected
   281  	if len(h.currentView.Resources) == 1 && len(view.Resources) > 1 {
   282  		h.resetResourceSelection()
   283  	}
   284  	h.currentView = view
   285  	h.refreshSelectedIndex()
   286  
   287  	// if the hud isn't running, make sure new logs are visible on stdout
   288  	logLen := view.Log.Len()
   289  	if !h.isRunning && h.currentViewState.ProcessedLogByteCount < logLen {
   290  		fmt.Print(view.Log.String()[h.currentViewState.ProcessedLogByteCount:])
   291  	}
   292  
   293  	h.currentViewState.ProcessedLogByteCount = logLen
   294  
   295  	return h.refresh(ctx)
   296  }
   297  
   298  // Must hold the lock
   299  func (h *Hud) setViewState(ctx context.Context, currentViewState view.ViewState) error {
   300  	h.currentViewState = currentViewState
   301  	return h.refresh(ctx)
   302  }
   303  
   304  // Must hold the lock
   305  func (h *Hud) refresh(ctx context.Context) error {
   306  	// TODO: We don't handle the order of resources changing
   307  	for len(h.currentViewState.Resources) < len(h.currentView.Resources) {
   308  		h.currentViewState.Resources = append(h.currentViewState.Resources, view.ResourceViewState{})
   309  	}
   310  
   311  	vs := h.currentViewState
   312  	vs.Resources = append(vs.Resources, h.currentViewState.Resources...)
   313  
   314  	return h.Update(h.currentView, h.currentViewState)
   315  }
   316  
   317  func (h *Hud) Update(v view.View, vs view.ViewState) error {
   318  	err := h.r.Render(v, vs)
   319  	return errors.Wrap(err, "error rendering hud")
   320  }
   321  
   322  func (h *Hud) resetResourceSelection() {
   323  	rty := h.r.rty
   324  	if rty == nil {
   325  		return
   326  	}
   327  	// wipe out any scroll/selection state for resources
   328  	// it will get re-set in the next call to render
   329  	rty.RegisterElementScroll("resources", []string{})
   330  }
   331  
   332  func (h *Hud) refreshSelectedIndex() {
   333  	rty := h.r.rty
   334  	if rty == nil {
   335  		return
   336  	}
   337  	scroller := rty.ElementScroller("resources")
   338  	if scroller == nil {
   339  		return
   340  	}
   341  	i := scroller.GetSelectedIndex()
   342  	h.currentViewState.SelectedIndex = i
   343  }
   344  
   345  func (h *Hud) selectedResource() (i int, resource view.Resource) {
   346  	return selectedResource(h.currentView, h.currentViewState)
   347  }
   348  
   349  func selectedResource(view view.View, state view.ViewState) (i int, resource view.Resource) {
   350  	i = state.SelectedIndex
   351  	if i >= 0 && i < len(view.Resources) {
   352  		resource = view.Resources[i]
   353  	}
   354  	return i, resource
   355  
   356  }
   357  
   358  var _ store.Subscriber = &Hud{}
   359  
   360  func writeHeapProfile(ctx context.Context) {
   361  	f, err := os.Create("tilt.heap_profile")
   362  	if err != nil {
   363  		logger.Get(ctx).Infof("error creating file for heap profile: %v", err)
   364  		return
   365  	}
   366  	runtime.GC()
   367  	logger.Get(ctx).Infof("writing heap profile to %s", f.Name())
   368  	err = pprof.WriteHeapProfile(f)
   369  	if err != nil {
   370  		logger.Get(ctx).Infof("error writing heap profile: %v", err)
   371  		return
   372  	}
   373  	err = f.Close()
   374  	if err != nil {
   375  		logger.Get(ctx).Infof("error closing file for heap profile: %v", err)
   376  		return
   377  	}
   378  	logger.Get(ctx).Infof("wrote heap profile to %s", f.Name())
   379  }