@@ -10,16 +10,21 @@ import (
10
10
"time"
11
11
12
12
"github.com/briandowns/spinner"
13
+ "github.com/muesli/reflow/indent"
14
+ "github.com/muesli/reflow/wordwrap"
13
15
"golang.org/x/xerrors"
14
16
15
17
"github.com/coder/coder/codersdk"
16
18
)
17
19
20
+ var AgentStartError = xerrors .New ("agent startup exited with non-zero exit status" )
21
+
18
22
type AgentOptions struct {
19
23
WorkspaceName string
20
24
Fetch func (context.Context ) (codersdk.WorkspaceAgent , error )
21
25
FetchInterval time.Duration
22
26
WarnInterval time.Duration
27
+ NoWait bool // If true, don't wait for the agent to be ready.
23
28
}
24
29
25
30
// Agent displays a spinning indicator that waits for a workspace agent to connect.
@@ -36,48 +41,33 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
36
41
return xerrors .Errorf ("fetch: %w" , err )
37
42
}
38
43
39
- if agent .Status == codersdk .WorkspaceAgentConnected {
44
+ // Fast path if the agent is ready (avoid showing connecting prompt).
45
+ // We don't take the fast path for opts.NoWait yet because we want to
46
+ // show the message.
47
+ if agent .Status == codersdk .WorkspaceAgentConnected &&
48
+ (! agent .DelayLoginUntilReady || agent .LifecycleState == codersdk .WorkspaceAgentLifecycleReady ) {
40
49
return nil
41
50
}
42
51
52
+ ctx , cancel := signal .NotifyContext (ctx , os .Interrupt )
53
+ defer cancel ()
54
+
43
55
spin := spinner .New (spinner .CharSets [78 ], 100 * time .Millisecond , spinner .WithColor ("fgHiGreen" ))
44
56
spin .Writer = writer
45
57
spin .ForceOutput = true
46
- spin .Suffix = " Waiting for connection from " + Styles .Field .Render (agent .Name ) + "..."
47
- spin .Start ()
48
- defer spin .Stop ()
58
+ spin .Suffix = waitingMessage (agent , opts ).Spin
49
59
50
- ctx , cancelFunc := context .WithCancel (ctx )
51
- defer cancelFunc ()
52
- stopSpin := make (chan os.Signal , 1 )
53
- signal .Notify (stopSpin , os .Interrupt )
54
- defer signal .Stop (stopSpin )
55
- go func () {
56
- select {
57
- case <- ctx .Done ():
58
- return
59
- case <- stopSpin :
60
- }
61
- cancelFunc ()
62
- signal .Stop (stopSpin )
63
- spin .Stop ()
64
- // nolint:revive
65
- os .Exit (1 )
66
- }()
67
-
68
- var waitMessage string
69
- messageAfter := time .NewTimer (opts .WarnInterval )
70
- defer messageAfter .Stop ()
60
+ waitMessage := & message {}
71
61
showMessage := func () {
72
62
resourceMutex .Lock ()
73
63
defer resourceMutex .Unlock ()
74
64
75
- m := waitingMessage (agent )
76
- if m == waitMessage {
65
+ m := waitingMessage (agent , opts )
66
+ if m . Prompt == waitMessage . Prompt {
77
67
return
78
68
}
79
69
moveUp := ""
80
- if waitMessage != "" {
70
+ if waitMessage . Prompt != "" {
81
71
// If this is an update, move a line up
82
72
// to keep it tidy and aligned.
83
73
moveUp = "\033 [1A"
@@ -86,20 +76,43 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
86
76
87
77
// Stop the spinner while we write our message.
88
78
spin .Stop ()
79
+ spin .Suffix = waitMessage .Spin
89
80
// 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 ) )
81
+ _ , _ = fmt .Fprintf (writer , "\033 [2K%s\n %s\n " , moveUp , waitMessage . Prompt )
91
82
select {
92
83
case <- ctx .Done ():
93
84
default :
94
85
// Safe to resume operation.
95
- spin .Start ()
86
+ if spin .Suffix != "" {
87
+ spin .Start ()
88
+ }
96
89
}
97
90
}
91
+
92
+ // Fast path for showing the error message even when using no wait,
93
+ // we do this just before starting the spinner to avoid needless
94
+ // spinning.
95
+ if agent .Status == codersdk .WorkspaceAgentConnected &&
96
+ agent .DelayLoginUntilReady && opts .NoWait {
97
+ showMessage ()
98
+ return nil
99
+ }
100
+
101
+ // Start spinning after fast paths are handled.
102
+ if spin .Suffix != "" {
103
+ spin .Start ()
104
+ }
105
+ defer spin .Stop ()
106
+
107
+ warnAfter := time .NewTimer (opts .WarnInterval )
108
+ defer warnAfter .Stop ()
109
+ warningShown := make (chan struct {})
98
110
go func () {
99
111
select {
100
112
case <- ctx .Done ():
101
- case <- messageAfter .C :
102
- messageAfter .Stop ()
113
+ close (warningShown )
114
+ case <- warnAfter .C :
115
+ close (warningShown )
103
116
showMessage ()
104
117
}
105
118
}()
@@ -121,26 +134,108 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
121
134
resourceMutex .Unlock ()
122
135
switch agent .Status {
123
136
case codersdk .WorkspaceAgentConnected :
137
+ // NOTE(mafredri): Once we have access to the workspace agent's
138
+ // startup script logs, we can show them here.
139
+ // https://github.com/coder/coder/issues/2957
140
+ if agent .DelayLoginUntilReady && ! opts .NoWait {
141
+ switch agent .LifecycleState {
142
+ case codersdk .WorkspaceAgentLifecycleReady :
143
+ return nil
144
+ case codersdk .WorkspaceAgentLifecycleStartTimeout :
145
+ showMessage ()
146
+ case codersdk .WorkspaceAgentLifecycleStartError :
147
+ showMessage ()
148
+ return AgentStartError
149
+ default :
150
+ select {
151
+ case <- warningShown :
152
+ showMessage ()
153
+ default :
154
+ // This state is normal, we don't want
155
+ // to show a message prematurely.
156
+ }
157
+ }
158
+ continue
159
+ }
124
160
return nil
125
161
case codersdk .WorkspaceAgentTimeout , codersdk .WorkspaceAgentDisconnected :
126
162
showMessage ()
127
163
}
128
164
}
129
165
}
130
166
131
- func waitingMessage (agent codersdk.WorkspaceAgent ) string {
132
- var m string
167
+ type message struct {
168
+ Spin string
169
+ Prompt string
170
+ Troubleshoot bool
171
+ }
172
+
173
+ func waitingMessage (agent codersdk.WorkspaceAgent , opts AgentOptions ) (m * message ) {
174
+ m = & message {
175
+ Spin : fmt .Sprintf ("Waiting for connection from %s..." , Styles .Field .Render (agent .Name )),
176
+ Prompt : "Don't panic, your workspace is booting up!" ,
177
+ }
178
+ defer func () {
179
+ if opts .NoWait {
180
+ m .Spin = ""
181
+ }
182
+ if m .Spin != "" {
183
+ m .Spin = " " + m .Spin
184
+ }
185
+
186
+ // We don't want to wrap the troubleshooting URL, so we'll handle word
187
+ // wrapping ourselves (vs using lipgloss).
188
+ w := wordwrap .NewWriter (Styles .Paragraph .GetWidth () - Styles .Paragraph .GetMarginLeft ()* 2 )
189
+ w .Breakpoints = []rune {' ' , '\n' }
190
+
191
+ _ , _ = fmt .Fprint (w , m .Prompt )
192
+ if m .Troubleshoot {
193
+ if agent .TroubleshootingURL != "" {
194
+ _ , _ = fmt .Fprintf (w , " See troubleshooting instructions at:\n %s" , agent .TroubleshootingURL )
195
+ } else {
196
+ _ , _ = fmt .Fprint (w , " Wait for it to (re)connect or restart your workspace." )
197
+ }
198
+ }
199
+ _ , _ = fmt .Fprint (w , "\n " )
200
+
201
+ // We want to prefix the prompt with a caret, but we want text on the
202
+ // following lines to align with the text on the first line (i.e. added
203
+ // spacing).
204
+ ind := " " + Styles .Prompt .String ()
205
+ iw := indent .NewWriter (1 , func (w io.Writer ) {
206
+ _ , _ = w .Write ([]byte (ind ))
207
+ ind = " " // Set indentation to space after initial prompt.
208
+ })
209
+ _ , _ = fmt .Fprint (iw , w .String ())
210
+ m .Prompt = iw .String ()
211
+ }()
212
+
133
213
switch agent .Status {
134
214
case codersdk .WorkspaceAgentTimeout :
135
- m = "The workspace agent is having trouble connecting."
215
+ m . Prompt = "The workspace agent is having trouble connecting."
136
216
case codersdk .WorkspaceAgentDisconnected :
137
- m = "The workspace agent lost connection!"
217
+ m .Prompt = "The workspace agent lost connection!"
218
+ case codersdk .WorkspaceAgentConnected :
219
+ m .Spin = fmt .Sprintf ("Waiting for %s to become ready..." , Styles .Field .Render (agent .Name ))
220
+ m .Prompt = "Don't panic, your workspace agent has connected and the workspace is getting ready!"
221
+ if opts .NoWait {
222
+ m .Prompt = "Your workspace is still getting ready, it may be in an incomplete state."
223
+ }
224
+
225
+ switch agent .LifecycleState {
226
+ case codersdk .WorkspaceAgentLifecycleStartTimeout :
227
+ m .Prompt = "The workspace is taking longer than expected to get ready, the agent startup script is still executing."
228
+ case codersdk .WorkspaceAgentLifecycleStartError :
229
+ m .Spin = ""
230
+ m .Prompt = "The workspace ran into a problem while getting ready, the agent startup script exited with non-zero status."
231
+ default :
232
+ // Not a failure state, no troubleshooting necessary.
233
+ return m
234
+ }
138
235
default :
139
236
// 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 )
237
+ return m
144
238
}
145
- return fmt .Sprintf ("%s Wait for it to (re)connect or restart your workspace." , m )
239
+ m .Troubleshoot = true
240
+ return m
146
241
}
0 commit comments