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 }