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
}
}
|