@@ -2,9 +2,11 @@ package cli
2
2
3
3
import (
4
4
"context"
5
+ "errors"
5
6
"fmt"
6
7
"io"
7
8
"os"
9
+ "path/filepath"
8
10
"strings"
9
11
"time"
10
12
@@ -24,6 +26,9 @@ import (
24
26
"github.com/coder/coder/codersdk"
25
27
)
26
28
29
+ var autostopPollInterval = 30 * time .Second
30
+ var autostopNotifyCountdown = []time.Duration {5 * time .Minute , time .Minute }
31
+
27
32
func ssh () * cobra.Command {
28
33
var (
29
34
stdio bool
@@ -113,19 +118,8 @@ func ssh() *cobra.Command {
113
118
}
114
119
defer conn .Close ()
115
120
116
- // Notify the user if the workspace is due to shutdown. This uses
117
- // the library gen2brain/beeep under the hood.
118
- countdown := []time.Duration {
119
- 30 * time .Minute ,
120
- 10 * time .Minute ,
121
- 5 * time .Minute ,
122
- time .Minute ,
123
- }
124
- condition := notifyWorkspaceAutostop (cmd .Context (), client , workspace .ID )
125
- notifier := notify .New (condition , countdown ... )
126
- ticker := time .NewTicker (30 * time .Second )
127
- defer ticker .Stop ()
128
- go notifier .Poll (ticker .C )
121
+ stopPolling := tryPollWorkspaceAutostop (cmd .Context (), client , workspace )
122
+ defer stopPolling ()
129
123
130
124
if stdio {
131
125
rawSSH , err := conn .SSH ()
@@ -199,7 +193,36 @@ func ssh() *cobra.Command {
199
193
return cmd
200
194
}
201
195
202
- func notifyWorkspaceAutostop (ctx context.Context , client * codersdk.Client , workspaceID uuid.UUID ) notify.Condition {
196
+ // Attempt to poll workspace autostop. We write a per-workspace lockfile to
197
+ // avoid spamming the user with notifications in case of multiple instances
198
+ // of the CLI running simultaneously.
199
+ func tryPollWorkspaceAutostop (ctx context.Context , client * codersdk.Client , workspace codersdk.Workspace ) (stop func ()) {
200
+ lockPath := filepath .Join (os .TempDir (), "coder-autostop-notify-" + workspace .ID .String ())
201
+ lockStat , err := os .Stat (lockPath )
202
+ if err == nil {
203
+ // Lock file already exists for this workspace. How old is it?
204
+ lockAge := lockStat .ModTime ().Sub (time .Now ())
205
+ if lockAge < 3 * autostopPollInterval {
206
+ // Lock file exists and is still "fresh". Do nothing.
207
+ return func () {}
208
+ }
209
+ }
210
+ if ! errors .Is (err , os .ErrNotExist ) {
211
+ // No permission to write to temp? Not much we can do.
212
+ return func () {}
213
+ }
214
+ lockFile , err := os .Create (lockPath )
215
+ if err != nil {
216
+ // Someone got there already?
217
+ return func () {}
218
+ }
219
+
220
+ condition := notifyCondition (ctx , client , workspace .ID , lockFile )
221
+ return notify .Notify (condition , autostopPollInterval , autostopNotifyCountdown ... )
222
+ }
223
+
224
+ // Notify the user if the workspace is due to shutdown.
225
+ func notifyCondition (ctx context.Context , client * codersdk.Client , workspaceID uuid.UUID , lockFile * os.File ) notify.Condition {
203
226
return func (now time.Time ) (deadline time.Time , callback func ()) {
204
227
ws , err := client .Workspace (ctx , workspaceID )
205
228
if err != nil {
@@ -230,7 +253,10 @@ func notifyWorkspaceAutostop(ctx context.Context, client *codersdk.Client, works
230
253
title = fmt .Sprintf ("Workspace %s stopping!" , ws .Name )
231
254
body = fmt .Sprintf ("Your Coder workspace %s is stopping any time now!" , ws .Name )
232
255
}
233
- beeep .Notify (title , body , "" )
256
+ // notify user with a native system notification (best effort)
257
+ _ = beeep .Notify (title , body , "" )
258
+ // update lockFile (best effort)
259
+ _ = os .Chtimes (lockFile .Name (), now , now )
234
260
}
235
261
return deadline .Truncate (time .Minute ), callback
236
262
}
0 commit comments