Skip to content

Commit 7004c5d

Browse files
committed
feat(cli): Add support for delay_login_until_ready
Ref: #5749
1 parent 78ede50 commit 7004c5d

File tree

4 files changed

+339
-43
lines changed

4 files changed

+339
-43
lines changed

cli/cliui/agent.go

+106-20
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@ import (
1010
"time"
1111

1212
"github.com/briandowns/spinner"
13+
"github.com/muesli/reflow/indent"
14+
"github.com/muesli/reflow/wordwrap"
1315
"golang.org/x/xerrors"
1416

1517
"github.com/coder/coder/codersdk"
1618
)
1719

1820
type AgentOptions struct {
19-
WorkspaceName string
20-
Fetch func(context.Context) (codersdk.WorkspaceAgent, error)
21-
FetchInterval time.Duration
22-
WarnInterval time.Duration
21+
WorkspaceName string
22+
Fetch func(context.Context) (codersdk.WorkspaceAgent, error)
23+
FetchInterval time.Duration
24+
WarnInterval time.Duration
25+
SkipDelayLoginUntilReady bool
2326
}
2427

2528
// Agent displays a spinning indicator that waits for a workspace agent to connect.
@@ -36,14 +39,16 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
3639
return xerrors.Errorf("fetch: %w", err)
3740
}
3841

39-
if agent.Status == codersdk.WorkspaceAgentConnected {
42+
// Fast path if the agent is ready (avoid showing connecting prompt).
43+
if agent.Status == codersdk.WorkspaceAgentConnected &&
44+
(!agent.DelayLoginUntilReady || opts.SkipDelayLoginUntilReady || agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady) {
4045
return nil
4146
}
4247

4348
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
4449
spin.Writer = writer
4550
spin.ForceOutput = true
46-
spin.Suffix = " Waiting for connection from " + Styles.Field.Render(agent.Name) + "..."
51+
spin.Suffix = waitingMessage(agent).Spin
4752
spin.Start()
4853
defer spin.Stop()
4954

@@ -65,19 +70,19 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
6570
os.Exit(1)
6671
}()
6772

68-
var waitMessage string
73+
waitMessage := &message{}
6974
messageAfter := time.NewTimer(opts.WarnInterval)
7075
defer messageAfter.Stop()
7176
showMessage := func() {
7277
resourceMutex.Lock()
7378
defer resourceMutex.Unlock()
7479

7580
m := waitingMessage(agent)
76-
if m == waitMessage {
81+
if m.Prompt == waitMessage.Prompt {
7782
return
7883
}
7984
moveUp := ""
80-
if waitMessage != "" {
85+
if waitMessage.Prompt != "" {
8186
// If this is an update, move a line up
8287
// to keep it tidy and aligned.
8388
moveUp = "\033[1A"
@@ -86,20 +91,26 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
8691

8792
// Stop the spinner while we write our message.
8893
spin.Stop()
94+
spin.Suffix = waitMessage.Spin
8995
// Clear the line and (if necessary) move up a line to write our message.
90-
_, _ = fmt.Fprintf(writer, "\033[2K%s%s\n\n", moveUp, Styles.Paragraph.Render(Styles.Prompt.String()+waitMessage))
96+
_, _ = fmt.Fprintf(writer, "\033[2K%s\n%s\n", moveUp, waitMessage.Prompt)
9197
select {
9298
case <-ctx.Done():
9399
default:
94100
// Safe to resume operation.
95-
spin.Start()
101+
if spin.Suffix != "" {
102+
spin.Start()
103+
}
96104
}
97105
}
106+
messageAfterDone := make(chan struct{})
98107
go func() {
99108
select {
100109
case <-ctx.Done():
110+
close(messageAfterDone)
101111
case <-messageAfter.C:
102112
messageAfter.Stop()
113+
close(messageAfterDone)
103114
showMessage()
104115
}
105116
}()
@@ -121,26 +132,101 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
121132
resourceMutex.Unlock()
122133
switch agent.Status {
123134
case codersdk.WorkspaceAgentConnected:
135+
// NOTE(mafredri): Once we have access to the workspace agent's
136+
// startup script logs, we can show them here.
137+
// https://github.com/coder/coder/issues/2957
138+
if agent.DelayLoginUntilReady && !opts.SkipDelayLoginUntilReady {
139+
switch agent.LifecycleState {
140+
case codersdk.WorkspaceAgentLifecycleCreated, codersdk.WorkspaceAgentLifecycleStarting:
141+
select {
142+
case <-messageAfterDone:
143+
showMessage()
144+
default:
145+
// This state is normal, we don't want
146+
// to show a message prematurely.
147+
}
148+
case codersdk.WorkspaceAgentLifecycleReady:
149+
return nil
150+
default:
151+
showMessage()
152+
}
153+
continue
154+
}
124155
return nil
125156
case codersdk.WorkspaceAgentTimeout, codersdk.WorkspaceAgentDisconnected:
126157
showMessage()
127158
}
128159
}
129160
}
130161

131-
func waitingMessage(agent codersdk.WorkspaceAgent) string {
132-
var m string
162+
type message struct {
163+
Spin string
164+
Prompt string
165+
Troubleshoot bool
166+
Error bool
167+
}
168+
169+
func waitingMessage(agent codersdk.WorkspaceAgent) (m *message) {
170+
m = &message{
171+
Prompt: "Don't panic, your workspace is booting up!",
172+
Spin: fmt.Sprintf(" Waiting for connection from %s...", Styles.Field.Render(agent.Name)),
173+
}
174+
defer func() {
175+
// We don't want to wrap the troubleshooting URL, so we'll handle word
176+
// wrapping ourselves (vs using lipgloss).
177+
w := wordwrap.NewWriter(Styles.Paragraph.GetWidth() - Styles.Paragraph.GetMarginLeft()*2)
178+
w.Breakpoints = []rune{' ', '\n'}
179+
180+
_, _ = fmt.Fprint(w, m.Prompt)
181+
if m.Troubleshoot {
182+
if agent.TroubleshootingURL != "" {
183+
_, _ = fmt.Fprintf(w, " See troubleshooting instructions at:\n%s", agent.TroubleshootingURL)
184+
} else {
185+
_, _ = fmt.Fprint(w, " Wait for it to (re)connect or restart your workspace.")
186+
}
187+
}
188+
_, _ = fmt.Fprint(w, "\n")
189+
190+
if m.Error {
191+
_, _ = fmt.Fprint(w, "\nPress Ctrl+C to exit.\n")
192+
}
193+
194+
// We want to prefix the prompt with a caret, but we want text on the
195+
// following lines to align with the text on the first line (i.e. added
196+
// spacing).
197+
ind := " " + Styles.Prompt.String()
198+
iw := indent.NewWriter(1, func(w io.Writer) {
199+
_, _ = w.Write([]byte(ind))
200+
ind = " " // Set indentation to space after initial prompt.
201+
})
202+
_, _ = fmt.Fprint(iw, w.String())
203+
m.Prompt = iw.String()
204+
}()
205+
133206
switch agent.Status {
134207
case codersdk.WorkspaceAgentTimeout:
135-
m = "The workspace agent is having trouble connecting."
208+
m.Prompt = "The workspace agent is having trouble connecting."
136209
case codersdk.WorkspaceAgentDisconnected:
137-
m = "The workspace agent lost connection!"
210+
m.Prompt = "The workspace agent lost connection!"
211+
case codersdk.WorkspaceAgentConnected:
212+
m.Prompt = "Don't panic, your workspace is starting up!"
213+
m.Spin = fmt.Sprintf(" Waiting for %s to finish starting up...", Styles.Field.Render(agent.Name))
214+
215+
switch agent.LifecycleState {
216+
case codersdk.WorkspaceAgentLifecycleStartTimeout:
217+
m.Prompt = "The workspace agent is taking longer than expected to start."
218+
case codersdk.WorkspaceAgentLifecycleStartError:
219+
m.Spin = ""
220+
m.Prompt = "The workspace agent ran into a problem during startup."
221+
m.Error = true
222+
default:
223+
// Not a failure state, no troubleshooting necessary.
224+
return m
225+
}
138226
default:
139227
// Not a failure state, no troubleshooting necessary.
140-
return "Don't panic, your workspace is booting up!"
141-
}
142-
if agent.TroubleshootingURL != "" {
143-
return fmt.Sprintf("%s See troubleshooting instructions at: %s", m, agent.TroubleshootingURL)
228+
return m
144229
}
145-
return fmt.Sprintf("%s Wait for it to (re)connect or restart your workspace.", m)
230+
m.Troubleshoot = true
231+
return m
146232
}

0 commit comments

Comments
 (0)