@@ -200,12 +200,12 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
200
200
201
201
switch agent .LifecycleState {
202
202
case codersdk .WorkspaceAgentLifecycleReady :
203
- sw .Complete (stage , agent .ReadyAt . Sub ( * agent .StartedAt ))
203
+ sw .Complete (stage , safeDuration ( sw , agent .ReadyAt , agent .StartedAt ))
204
204
case codersdk .WorkspaceAgentLifecycleStartTimeout :
205
205
sw .Fail (stage , 0 )
206
206
sw .Log (time.Time {}, codersdk .LogLevelWarn , "Warning: A startup script timed out and your workspace may be incomplete." )
207
207
case codersdk .WorkspaceAgentLifecycleStartError :
208
- sw .Fail (stage , agent .ReadyAt . Sub ( * agent .StartedAt ))
208
+ sw .Fail (stage , safeDuration ( sw , agent .ReadyAt , agent .StartedAt ))
209
209
// Use zero time (omitted) to separate these from the startup logs.
210
210
sw .Log (time.Time {}, codersdk .LogLevelWarn , "Warning: A startup script exited with an error and your workspace may be incomplete." )
211
211
sw .Log (time.Time {}, codersdk .LogLevelWarn , troubleshootingMessage (agent , "https://coder.com/docs/v2/latest/templates#startup-script-exited-with-an-error" ))
@@ -221,7 +221,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
221
221
case agent .LifecycleState .ShuttingDown ():
222
222
// We no longer know if the startup script failed or not,
223
223
// but we need to tell the user something.
224
- sw .Complete (stage , agent .ReadyAt . Sub ( * agent .StartedAt ))
224
+ sw .Complete (stage , safeDuration ( sw , agent .ReadyAt , agent .StartedAt ))
225
225
return errAgentShuttingDown
226
226
}
227
227
}
@@ -238,13 +238,13 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
238
238
sw .Log (time .Now (), codersdk .LogLevelWarn , "Wait for it to reconnect or restart your workspace." )
239
239
sw .Log (time .Now (), codersdk .LogLevelWarn , troubleshootingMessage (agent , "https://coder.com/docs/v2/latest/templates#agent-connection-issues" ))
240
240
241
- disconnectedAt := * agent .DisconnectedAt
241
+ disconnectedAt := agent .DisconnectedAt
242
242
for agent .Status == codersdk .WorkspaceAgentDisconnected {
243
243
if agent , err = fetch (); err != nil {
244
244
return xerrors .Errorf ("fetch: %w" , err )
245
245
}
246
246
}
247
- sw .Complete (stage , agent .LastConnectedAt . Sub ( disconnectedAt ))
247
+ sw .Complete (stage , safeDuration ( sw , agent .LastConnectedAt , disconnectedAt ))
248
248
}
249
249
}
250
250
}
@@ -257,6 +257,25 @@ func troubleshootingMessage(agent codersdk.WorkspaceAgent, url string) string {
257
257
return m
258
258
}
259
259
260
+ // safeDuration returns a-b. If a or b is nil, it returns 0.
261
+ // This is because we often dereference a time pointer, which can
262
+ // cause a panic. These dereferences are used to calculate durations,
263
+ // which are not critical, and therefor should not break things
264
+ // when it fails.
265
+ // A panic has been observed in a test.
266
+ func safeDuration (sw * stageWriter , a , b * time.Time ) time.Duration {
267
+ if a == nil || b == nil {
268
+ if sw != nil {
269
+ // Ideally the message includes which fields are <nil>, but you can
270
+ // use the surrounding log lines to figure that out. And passing more
271
+ // params makes this unwieldy.
272
+ sw .Log (time .Now (), codersdk .LogLevelWarn , "Warning: Failed to calculate duration from a time being <nil>." )
273
+ }
274
+ return 0
275
+ }
276
+ return a .Sub (* b )
277
+ }
278
+
260
279
type closeFunc func () error
261
280
262
281
func (c closeFunc ) Close () error {
0 commit comments