@@ -71,6 +71,7 @@ type Client interface {
71
71
WorkspaceAgentMetadata (ctx context.Context ) (codersdk.WorkspaceAgentMetadata , error )
72
72
ListenWorkspaceAgent (ctx context.Context ) (net.Conn , error )
73
73
AgentReportStats (ctx context.Context , log slog.Logger , stats func () * codersdk.AgentStats ) (io.Closer , error )
74
+ PostWorkspaceAgentState (ctx context.Context , state codersdk.PostWorkspaceAgentStateRequest ) error
74
75
PostWorkspaceAgentAppHealth (ctx context.Context , req codersdk.PostWorkspaceAppHealthsRequest ) error
75
76
PostWorkspaceAgentVersion (ctx context.Context , version string ) error
76
77
}
@@ -127,6 +128,9 @@ type agent struct {
127
128
sessionToken atomic.Pointer [string ]
128
129
sshServer * ssh.Server
129
130
131
+ stateMu sync.Mutex // Protects following.
132
+ state codersdk.WorkspaceAgentState
133
+
130
134
network * tailnet.Conn
131
135
}
132
136
@@ -156,6 +160,30 @@ func (a *agent) runLoop(ctx context.Context) {
156
160
}
157
161
}
158
162
163
+ func (a * agent ) setState (ctx context.Context , state codersdk.WorkspaceAgentState ) {
164
+ a .stateMu .Lock ()
165
+ defer a .stateMu .Unlock ()
166
+
167
+ a .state = state
168
+
169
+ var err error
170
+ for r := retry .New (time .Second , 30 * time .Second ); r .Wait (ctx ); {
171
+ err = a .client .PostWorkspaceAgentState (ctx , codersdk.PostWorkspaceAgentStateRequest {
172
+ State : state ,
173
+ })
174
+ if err == nil {
175
+ return
176
+ }
177
+ }
178
+ if xerrors .Is (err , context .Canceled ) || xerrors .Is (err , context .DeadlineExceeded ) || a .isClosed () {
179
+ return
180
+ }
181
+ if err != nil {
182
+ // If we fail to report the state we probably shouldn't exit, log only.
183
+ a .logger .Error (ctx , "post state" , slog .Error (err ))
184
+ }
185
+ }
186
+
159
187
func (a * agent ) run (ctx context.Context ) error {
160
188
// This allows the agent to refresh it's token if necessary.
161
189
// For instance identity this is required, since the instance
@@ -180,22 +208,55 @@ func (a *agent) run(ctx context.Context) error {
180
208
181
209
// The startup script should only execute on the first run!
182
210
if oldMetadata == nil {
211
+ scriptDone := make (chan error , 1 )
212
+ scriptStart := time .Now ()
213
+ go func () {
214
+ defer close (scriptDone )
215
+ scriptDone <- a .runStartupScript (ctx , metadata .StartupScript )
216
+ }()
183
217
go func () {
184
- err := a .runStartupScript (ctx , metadata .StartupScript )
218
+ var timeout <- chan time.Time
219
+ // If timeout is zero, an older version of the coder
220
+ // provider was used. Otherwise a timeout is always > 0.
221
+ if metadata .StartupScriptTimeout > 0 {
222
+ t := time .NewTimer (metadata .StartupScriptTimeout )
223
+ defer t .Stop ()
224
+ timeout = t .C
225
+ }
226
+
227
+ a .setState (ctx , codersdk .WorkspaceAgentStateStarting )
228
+
229
+ var err error
230
+ select {
231
+ case err = <- scriptDone :
232
+ case <- timeout :
233
+ a .logger .Warn (ctx , "startup script timed out" )
234
+ a .setState (ctx , codersdk .WorkspaceAgentStateStartTimeout )
235
+ err = <- scriptDone // The script can still complete after a timeout.
236
+ }
185
237
if errors .Is (err , context .Canceled ) {
186
238
return
187
239
}
240
+ execTime := time .Since (scriptStart )
188
241
if err != nil {
189
- a .logger .Warn (ctx , "agent script failed" , slog .Error (err ))
242
+ a .logger .Warn (ctx , "startup script failed" , slog .F ("execution_time" , execTime ), slog .Error (err ))
243
+ a .setState (ctx , codersdk .WorkspaceAgentStateStartError )
244
+ return
190
245
}
191
- }()
192
- }
246
+ a .logger .Info (ctx , "startup script completed" , slog .F ("execution_time" , execTime ))
193
247
194
- if metadata .GitAuthConfigs > 0 {
195
- err = gitauth .OverrideVSCodeConfigs (a .filesystem )
196
- if err != nil {
197
- return xerrors .Errorf ("override vscode configuration for git auth: %w" , err )
198
- }
248
+ // Perform overrides after startup script has completed to ensure
249
+ // there is no conflict with the user's scripts. We also want to
250
+ // ensure this is done before the workspace is marked as ready.
251
+ if metadata .GitAuthConfigs > 0 {
252
+ err = gitauth .OverrideVSCodeConfigs (a .filesystem )
253
+ if err != nil {
254
+ a .logger .Warn (ctx , "failed to override vscode git auth configs" , slog .Error (err ))
255
+ }
256
+ }
257
+
258
+ a .setState (ctx , codersdk .WorkspaceAgentStateReady )
259
+ }()
199
260
}
200
261
201
262
// This automatically closes when the context ends!
0 commit comments