github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/snapshot.go (about)

     1  package cli
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/grpc-ecosystem/grpc-gateway/runtime"
    14  	"github.com/mattn/go-tty"
    15  	"github.com/pkg/browser"
    16  	"github.com/spf13/cobra"
    17  	"golang.org/x/sync/errgroup"
    18  	"google.golang.org/protobuf/types/known/timestamppb"
    19  
    20  	"github.com/tilt-dev/tilt/internal/analytics"
    21  	engineanalytics "github.com/tilt-dev/tilt/internal/engine/analytics"
    22  	"github.com/tilt-dev/tilt/internal/snapshots"
    23  	proto_webview "github.com/tilt-dev/tilt/pkg/webview"
    24  )
    25  
    26  func newSnapshotCmd() *cobra.Command {
    27  	result := &cobra.Command{
    28  		Use: "snapshot",
    29  	}
    30  
    31  	result.AddCommand(newViewCommand())
    32  	result.AddCommand(newCreateSnapshotCommand())
    33  
    34  	return result
    35  }
    36  
    37  type serveCmd struct {
    38  	noOpen bool
    39  }
    40  
    41  func newViewCommand() *cobra.Command {
    42  	c := &serveCmd{}
    43  	result := &cobra.Command{
    44  		Use:   "view <path/to/snapshot.json>",
    45  		Short: "Serves the specified snapshot file and optionally opens it in the browser",
    46  		Long:  "Serves the specified snapshot file and optionally opens it in the browser",
    47  		Example: `
    48  # Run tilt ci and save a snapshot
    49  tilt ci --output-snapshot-on-exit=snapshot.json
    50  # View that snapshot
    51  tilt snapshot view snapshot.json
    52  
    53  # Or pipe the snapshot to stdin and specify the snapshot as '-'
    54  curl http://myci.com/path/to/snapshot | tilt snapshot view -
    55  `,
    56  		Args: cobra.ExactArgs(1),
    57  		Run:  c.run,
    58  	}
    59  
    60  	result.Flags().BoolVar(&c.noOpen, "no-open", false, "Do not automatically open the snapshot in the browser")
    61  	addStartSnapshotViewServerFlags(result)
    62  
    63  	return result
    64  }
    65  
    66  // blocks until any key is pressed or ctx is canceled
    67  func waitForKey(ctx context.Context) error {
    68  	t, err := tty.Open()
    69  	if err != nil {
    70  		return err
    71  	}
    72  	defer func() { _ = t.Close() }()
    73  
    74  	done := make(chan struct{})
    75  	errCh := make(chan error)
    76  
    77  	go func() {
    78  		_, err = t.ReadRune()
    79  		if err != nil {
    80  			errCh <- err
    81  			return
    82  		}
    83  		close(done)
    84  	}()
    85  
    86  	select {
    87  	case <-ctx.Done():
    88  		return nil
    89  	case <-done:
    90  		return nil
    91  	case err := <-errCh:
    92  		return err
    93  	}
    94  }
    95  
    96  func (c *serveCmd) run(_ *cobra.Command, args []string) {
    97  	err := c.serveSnapshot(args[0])
    98  	if err != nil {
    99  		_, _ = fmt.Fprintf(os.Stderr, "error: %v", err)
   100  		os.Exit(1)
   101  	}
   102  }
   103  
   104  func readSnapshot(snapshotArg string) ([]byte, error) {
   105  	var r io.Reader
   106  	if snapshotArg == "-" {
   107  		r = os.Stdin
   108  	} else {
   109  		f, err := os.Open(snapshotArg)
   110  		if err != nil {
   111  			return nil, err
   112  		}
   113  		r = f
   114  		defer func() { _ = f.Close() }()
   115  	}
   116  
   117  	return io.ReadAll(r)
   118  }
   119  
   120  func (c *serveCmd) serveSnapshot(snapshotPath string) error {
   121  	ctx := preCommand(context.Background(), "snapshot view")
   122  	a := analytics.Get(ctx)
   123  	cmdTags := engineanalytics.CmdTags(map[string]string{})
   124  	a.Incr("cmd.snapshot.view", cmdTags.AsMap())
   125  	defer a.Flush(time.Second)
   126  
   127  	host := provideWebHost()
   128  	l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, snapshotViewPortFlag))
   129  	if err != nil {
   130  		return fmt.Errorf("could not get a free port: %w", err)
   131  	}
   132  	defer l.Close()
   133  
   134  	port := l.Addr().(*net.TCPAddr).Port
   135  	url := fmt.Sprintf("http://%s:%d/snapshot/local",
   136  		strings.Replace(string(host), "0.0.0.0", "127.0.0.1", 1),
   137  		port)
   138  
   139  	fmt.Printf("Serving snapshot at %s\n", url)
   140  
   141  	wg, ctx := errgroup.WithContext(ctx)
   142  	wg.Go(func() error {
   143  		snapshot, err := readSnapshot(snapshotPath)
   144  		if err != nil {
   145  			return err
   146  		}
   147  		return snapshots.Serve(ctx, l, snapshot)
   148  	})
   149  
   150  	// give the server a little bit of time to spin up
   151  	time.Sleep(200 * time.Millisecond)
   152  
   153  	if !c.noOpen {
   154  		err := browser.OpenURL(url)
   155  		if err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	keyPressed := errors.New("pressed key to exit")
   161  	wg.Go(func() error {
   162  		fmt.Println("Press any key to exit")
   163  		err := waitForKey(ctx)
   164  		if err != nil {
   165  			return err
   166  		}
   167  		return keyPressed
   168  	})
   169  
   170  	err = wg.Wait()
   171  	if err != nil && err != keyPressed {
   172  		return err
   173  	}
   174  
   175  	return nil
   176  }
   177  
   178  func newCreateSnapshotCommand() *cobra.Command {
   179  	result := &cobra.Command{
   180  		Use:   "create [file to save]",
   181  		Short: "Creates a snapshot file from a currently running Tilt instance",
   182  		Long:  "Creates a snapshot file that can be viewed with `tilt snapshot view`",
   183  		Example: `
   184  tilt snapshot create snapshot.json
   185  # or if no file is specified, it goes to stdout
   186  tilt snapshot create > snapshot.json
   187  
   188  # to view the snapshot
   189  tilt snapshot view snapshot.json
   190  `,
   191  		Args: cobra.MaximumNArgs(1),
   192  		Run:  createSnapshot,
   193  	}
   194  
   195  	addConnectServerFlags(result)
   196  
   197  	return result
   198  }
   199  
   200  func createSnapshot(cmd *cobra.Command, args []string) {
   201  	body := apiGet("view")
   202  
   203  	snapshot := proto_webview.Snapshot{
   204  		View:      &proto_webview.View{},
   205  		CreatedAt: timestamppb.Now(),
   206  	}
   207  
   208  	jsEncoder := &runtime.JSONPb{}
   209  	err := jsEncoder.NewDecoder(body).Decode(&snapshot.View)
   210  	if err != nil {
   211  		cmdFail(fmt.Errorf("error reading snapshot from tilt: %v", err))
   212  	}
   213  
   214  	out := os.Stdout
   215  	if len(args) > 0 {
   216  		out, err = os.Create(args[0])
   217  		if err != nil {
   218  			cmdFail(fmt.Errorf("error creating %s: %v", args[0], err))
   219  		}
   220  	}
   221  
   222  	err = jsEncoder.NewEncoder(out).Encode(&snapshot)
   223  	if err != nil {
   224  		cmdFail(fmt.Errorf("error serializing snapshot: %v", err))
   225  	}
   226  }