mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: send native system notification on scheduled workspace shutdown (#1414)
* feat: send native system notification on scheduled workspace shutdown This commit adds a fairly generic notification package and uses it to notify users connected over SSH of pending workspace shutdowns. Only one notification will be sent at most 5 minutes prior to the scheduled shutdown, and only one CLI instance will send notifications if multiple instances are running.
This commit is contained in:
67
cli/ssh.go
67
cli/ssh.go
@ -2,10 +2,15 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/gofrs/flock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/spf13/cobra"
|
||||
@ -15,10 +20,15 @@ import (
|
||||
|
||||
"github.com/coder/coder/cli/cliflag"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/coderd/autobuild/notify"
|
||||
"github.com/coder/coder/coderd/autobuild/schedule"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
var autostopPollInterval = 30 * time.Second
|
||||
var autostopNotifyCountdown = []time.Duration{5 * time.Minute}
|
||||
|
||||
func ssh() *cobra.Command {
|
||||
var (
|
||||
stdio bool
|
||||
@ -108,6 +118,9 @@ func ssh() *cobra.Command {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
stopPolling := tryPollWorkspaceAutostop(cmd.Context(), client, workspace)
|
||||
defer stopPolling()
|
||||
|
||||
if stdio {
|
||||
rawSSH, err := conn.SSH()
|
||||
if err != nil {
|
||||
@ -179,3 +192,57 @@ func ssh() *cobra.Command {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Attempt to poll workspace autostop. We write a per-workspace lockfile to
|
||||
// avoid spamming the user with notifications in case of multiple instances
|
||||
// of the CLI running simultaneously.
|
||||
func tryPollWorkspaceAutostop(ctx context.Context, client *codersdk.Client, workspace codersdk.Workspace) (stop func()) {
|
||||
lock := flock.New(filepath.Join(os.TempDir(), "coder-autostop-notify-"+workspace.ID.String()))
|
||||
condition := notifyCondition(ctx, client, workspace.ID, lock)
|
||||
return notify.Notify(condition, autostopPollInterval, autostopNotifyCountdown...)
|
||||
}
|
||||
|
||||
// Notify the user if the workspace is due to shutdown.
|
||||
func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID, lock *flock.Flock) notify.Condition {
|
||||
return func(now time.Time) (deadline time.Time, callback func()) {
|
||||
// Keep trying to regain the lock.
|
||||
locked, err := lock.TryLockContext(ctx, autostopPollInterval)
|
||||
if err != nil || !locked {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
ws, err := client.Workspace(ctx, workspaceID)
|
||||
if err != nil {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
if ws.AutostopSchedule == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
sched, err := schedule.Weekly(ws.AutostopSchedule)
|
||||
if err != nil {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
deadline = sched.Next(now)
|
||||
callback = func() {
|
||||
ttl := deadline.Sub(now)
|
||||
var title, body string
|
||||
if ttl > time.Minute {
|
||||
title = fmt.Sprintf(`Workspace %s stopping in %.0f mins`, ws.Name, ttl.Minutes())
|
||||
body = fmt.Sprintf(
|
||||
`Your Coder workspace %s is scheduled to stop at %s.`,
|
||||
ws.Name,
|
||||
deadline.Format(time.Kitchen),
|
||||
)
|
||||
} else {
|
||||
title = fmt.Sprintf("Workspace %s stopping!", ws.Name)
|
||||
body = fmt.Sprintf("Your Coder workspace %s is stopping any time now!", ws.Name)
|
||||
}
|
||||
// notify user with a native system notification (best effort)
|
||||
_ = beeep.Notify(title, body, "")
|
||||
}
|
||||
return deadline.Truncate(time.Minute), callback
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user