@@ -2,10 +2,15 @@ package cli
2
2
3
3
import (
4
4
"context"
5
+ "fmt"
5
6
"io"
6
7
"os"
8
+ "path/filepath"
7
9
"strings"
10
+ "time"
8
11
12
+ "github.com/gen2brain/beeep"
13
+ "github.com/gofrs/flock"
9
14
"github.com/google/uuid"
10
15
"github.com/mattn/go-isatty"
11
16
"github.com/spf13/cobra"
@@ -15,10 +20,15 @@ import (
15
20
16
21
"github.com/coder/coder/cli/cliflag"
17
22
"github.com/coder/coder/cli/cliui"
23
+ "github.com/coder/coder/coderd/autobuild/notify"
24
+ "github.com/coder/coder/coderd/autobuild/schedule"
18
25
"github.com/coder/coder/coderd/database"
19
26
"github.com/coder/coder/codersdk"
20
27
)
21
28
29
+ var autostopPollInterval = 30 * time .Second
30
+ var autostopNotifyCountdown = []time.Duration {5 * time .Minute }
31
+
22
32
func ssh () * cobra.Command {
23
33
var (
24
34
stdio bool
@@ -108,6 +118,9 @@ func ssh() *cobra.Command {
108
118
}
109
119
defer conn .Close ()
110
120
121
+ stopPolling := tryPollWorkspaceAutostop (cmd .Context (), client , workspace )
122
+ defer stopPolling ()
123
+
111
124
if stdio {
112
125
rawSSH , err := conn .SSH ()
113
126
if err != nil {
@@ -179,3 +192,57 @@ func ssh() *cobra.Command {
179
192
180
193
return cmd
181
194
}
195
+
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
+ lock := flock .New (filepath .Join (os .TempDir (), "coder-autostop-notify-" + workspace .ID .String ()))
201
+ condition := notifyCondition (ctx , client , workspace .ID , lock )
202
+ return notify .Notify (condition , autostopPollInterval , autostopNotifyCountdown ... )
203
+ }
204
+
205
+ // Notify the user if the workspace is due to shutdown.
206
+ func notifyCondition (ctx context.Context , client * codersdk.Client , workspaceID uuid.UUID , lock * flock.Flock ) notify.Condition {
207
+ return func (now time.Time ) (deadline time.Time , callback func ()) {
208
+ // Keep trying to regain the lock.
209
+ locked , err := lock .TryLockContext (ctx , autostopPollInterval )
210
+ if err != nil || ! locked {
211
+ return time.Time {}, nil
212
+ }
213
+
214
+ ws , err := client .Workspace (ctx , workspaceID )
215
+ if err != nil {
216
+ return time.Time {}, nil
217
+ }
218
+
219
+ if ws .AutostopSchedule == "" {
220
+ return time.Time {}, nil
221
+ }
222
+
223
+ sched , err := schedule .Weekly (ws .AutostopSchedule )
224
+ if err != nil {
225
+ return time.Time {}, nil
226
+ }
227
+
228
+ deadline = sched .Next (now )
229
+ callback = func () {
230
+ ttl := deadline .Sub (now )
231
+ var title , body string
232
+ if ttl > time .Minute {
233
+ title = fmt .Sprintf (`Workspace %s stopping in %.0f mins` , ws .Name , ttl .Minutes ())
234
+ body = fmt .Sprintf (
235
+ `Your Coder workspace %s is scheduled to stop at %s.` ,
236
+ ws .Name ,
237
+ deadline .Format (time .Kitchen ),
238
+ )
239
+ } else {
240
+ title = fmt .Sprintf ("Workspace %s stopping!" , ws .Name )
241
+ body = fmt .Sprintf ("Your Coder workspace %s is stopping any time now!" , ws .Name )
242
+ }
243
+ // notify user with a native system notification (best effort)
244
+ _ = beeep .Notify (title , body , "" )
245
+ }
246
+ return deadline .Truncate (time .Minute ), callback
247
+ }
248
+ }
0 commit comments