File: states.go

package info (click to toggle)
gh 2.23.0%2Bdfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 18,040 kB
  • sloc: asm: 6,813; ansic: 258; sh: 100; makefile: 96
file content (137 lines) | stat: -rw-r--r-- 3,918 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package codespaces

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"time"

	"github.com/cli/cli/v2/internal/codespaces/api"
	"github.com/cli/cli/v2/internal/codespaces/rpc"
	"github.com/cli/cli/v2/internal/text"
	"github.com/cli/cli/v2/pkg/liveshare"
)

// PostCreateStateStatus is a string value representing the different statuses a state can have.
type PostCreateStateStatus string

func (p PostCreateStateStatus) String() string {
	return text.Title(string(p))
}

const (
	PostCreateStateRunning PostCreateStateStatus = "running"
	PostCreateStateSuccess PostCreateStateStatus = "succeeded"
	PostCreateStateFailed  PostCreateStateStatus = "failed"
)

// PostCreateState is a combination of a state and status value that is captured
// during codespace creation.
type PostCreateState struct {
	Name   string                `json:"name"`
	Status PostCreateStateStatus `json:"status"`
}

// PollPostCreateStates watches for state changes in a codespace,
// and calls the supplied poller for each batch of state changes.
// It runs until it encounters an error, including cancellation of the context.
func PollPostCreateStates(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
	noopLogger := log.New(io.Discard, "", 0)

	session, err := ConnectToLiveshare(ctx, progress, noopLogger, apiClient, codespace)
	if err != nil {
		return fmt.Errorf("connect to codespace: %w", err)
	}
	defer func() {
		if closeErr := session.Close(); err == nil {
			err = closeErr
		}
	}()

	// Ensure local port is listening before client (getPostCreateOutput) connects.
	listen, localPort, err := ListenTCP(0)
	if err != nil {
		return err
	}

	progress.StartProgressIndicatorWithLabel("Fetching SSH Details")
	invoker, err := rpc.CreateInvoker(ctx, session)
	if err != nil {
		return err
	}
	defer safeClose(invoker, &err)

	remoteSSHServerPort, sshUser, err := invoker.StartSSHServer(ctx)
	if err != nil {
		return fmt.Errorf("error getting ssh server details: %w", err)
	}
	progress.StopProgressIndicator()

	progress.StartProgressIndicatorWithLabel("Fetching status")
	tunnelClosed := make(chan error, 1) // buffered to avoid sender stuckness
	go func() {
		fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, false)
		tunnelClosed <- fwd.ForwardToListener(ctx, listen) // error is non-nil
	}()

	t := time.NewTicker(1 * time.Second)
	defer t.Stop()

	for ticks := 0; ; ticks++ {
		select {
		case <-ctx.Done():
			return ctx.Err()

		case err := <-tunnelClosed:
			return fmt.Errorf("connection failed: %w", err)

		case <-t.C:
			states, err := getPostCreateOutput(ctx, localPort, sshUser)
			// There is an active progress indicator before the first tick
			// to show that we are fetching statuses.
			// Once the first tick happens, we stop the indicator and let
			// the subsequent post create states manage their own progress.
			if ticks == 0 {
				progress.StopProgressIndicator()
			}
			if err != nil {
				return fmt.Errorf("get post create output: %w", err)
			}

			poller(states)
		}
	}
}

func getPostCreateOutput(ctx context.Context, tunnelPort int, user string) ([]PostCreateState, error) {
	cmd, err := NewRemoteCommand(
		ctx, tunnelPort, fmt.Sprintf("%s@localhost", user),
		"cat /workspaces/.codespaces/shared/postCreateOutput.json",
	)
	if err != nil {
		return nil, fmt.Errorf("remote command: %w", err)
	}

	stdout := new(bytes.Buffer)
	cmd.Stdout = stdout
	if err := cmd.Run(); err != nil {
		return nil, fmt.Errorf("run command: %w", err)
	}
	var output struct {
		Steps []PostCreateState `json:"steps"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
		return nil, fmt.Errorf("unmarshal output: %w", err)
	}

	return output.Steps, nil
}

func safeClose(closer io.Closer, err *error) {
	if closeErr := closer.Close(); *err == nil {
		*err = closeErr
	}
}