@@ -10,16 +10,19 @@ 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
18
20
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
23
26
}
24
27
25
28
// 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 {
36
39
return xerrors .Errorf ("fetch: %w" , err )
37
40
}
38
41
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 ) {
40
45
return nil
41
46
}
42
47
43
48
spin := spinner .New (spinner .CharSets [78 ], 100 * time .Millisecond , spinner .WithColor ("fgHiGreen" ))
44
49
spin .Writer = writer
45
50
spin .ForceOutput = true
46
- spin .Suffix = " Waiting for connection from " + Styles . Field . Render (agent . Name ) + "..."
51
+ spin .Suffix = waitingMessage (agent ). Spin
47
52
spin .Start ()
48
53
defer spin .Stop ()
49
54
@@ -65,19 +70,19 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
65
70
os .Exit (1 )
66
71
}()
67
72
68
- var waitMessage string
73
+ waitMessage := & message {}
69
74
messageAfter := time .NewTimer (opts .WarnInterval )
70
75
defer messageAfter .Stop ()
71
76
showMessage := func () {
72
77
resourceMutex .Lock ()
73
78
defer resourceMutex .Unlock ()
74
79
75
80
m := waitingMessage (agent )
76
- if m == waitMessage {
81
+ if m . Prompt == waitMessage . Prompt {
77
82
return
78
83
}
79
84
moveUp := ""
80
- if waitMessage != "" {
85
+ if waitMessage . Prompt != "" {
81
86
// If this is an update, move a line up
82
87
// to keep it tidy and aligned.
83
88
moveUp = "\033 [1A"
@@ -86,20 +91,26 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
86
91
87
92
// Stop the spinner while we write our message.
88
93
spin .Stop ()
94
+ spin .Suffix = waitMessage .Spin
89
95
// 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 )
91
97
select {
92
98
case <- ctx .Done ():
93
99
default :
94
100
// Safe to resume operation.
95
- spin .Start ()
101
+ if spin .Suffix != "" {
102
+ spin .Start ()
103
+ }
96
104
}
97
105
}
106
+ messageAfterDone := make (chan struct {})
98
107
go func () {
99
108
select {
100
109
case <- ctx .Done ():
110
+ close (messageAfterDone )
101
111
case <- messageAfter .C :
102
112
messageAfter .Stop ()
113
+ close (messageAfterDone )
103
114
showMessage ()
104
115
}
105
116
}()
@@ -121,26 +132,101 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
121
132
resourceMutex .Unlock ()
122
133
switch agent .Status {
123
134
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
+ }
124
155
return nil
125
156
case codersdk .WorkspaceAgentTimeout , codersdk .WorkspaceAgentDisconnected :
126
157
showMessage ()
127
158
}
128
159
}
129
160
}
130
161
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 , "\n Press 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
+
133
206
switch agent .Status {
134
207
case codersdk .WorkspaceAgentTimeout :
135
- m = "The workspace agent is having trouble connecting."
208
+ m . Prompt = "The workspace agent is having trouble connecting."
136
209
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
+ }
138
226
default :
139
227
// 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
144
229
}
145
- return fmt .Sprintf ("%s Wait for it to (re)connect or restart your workspace." , m )
230
+ m .Troubleshoot = true
231
+ return m
146
232
}
0 commit comments