mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
* 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.
101 lines
2.6 KiB
Go
101 lines
2.6 KiB
Go
package notify
|
|
|
|
import (
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Notifier calls a Condition at most once for each count in countdown.
|
|
type Notifier struct {
|
|
lock sync.Mutex
|
|
condition Condition
|
|
notifiedAt map[time.Duration]bool
|
|
countdown []time.Duration
|
|
}
|
|
|
|
// Condition is a function that gets executed with a certain time.
|
|
// - It should return the deadline for the notification, as well as a
|
|
// callback function to execute once the time to the deadline is
|
|
// less than one of the notify attempts. If deadline is the zero
|
|
// time, callback will not be executed.
|
|
// - Callback is executed once for every time the difference between deadline
|
|
// and the current time is less than an element of countdown.
|
|
// - To enforce a minimum interval between consecutive callbacks, truncate
|
|
// the returned deadline to the minimum interval.
|
|
type Condition func(now time.Time) (deadline time.Time, callback func())
|
|
|
|
// Notify is a convenience function that initializes a new Notifier
|
|
// with the given condition, interval, and countdown.
|
|
// It is the responsibility of the caller to call close to stop polling.
|
|
func Notify(cond Condition, interval time.Duration, countdown ...time.Duration) (close func()) {
|
|
notifier := New(cond, countdown...)
|
|
ticker := time.NewTicker(interval)
|
|
go notifier.Poll(ticker.C)
|
|
return ticker.Stop
|
|
}
|
|
|
|
// New returns a Notifier that calls cond once every time it polls.
|
|
// - Duplicate values are removed from countdown, and it is sorted in
|
|
// descending order.
|
|
func New(cond Condition, countdown ...time.Duration) *Notifier {
|
|
// Ensure countdown is sorted in descending order and contains no duplicates.
|
|
ct := unique(countdown)
|
|
sort.Slice(ct, func(i, j int) bool {
|
|
return ct[i] < ct[j]
|
|
})
|
|
|
|
n := &Notifier{
|
|
countdown: ct,
|
|
condition: cond,
|
|
notifiedAt: make(map[time.Duration]bool),
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
// Poll polls once immediately, and then once for every value from ticker.
|
|
// Poll exits when ticker is closed.
|
|
func (n *Notifier) Poll(ticker <-chan time.Time) {
|
|
// poll once immediately
|
|
n.pollOnce(time.Now())
|
|
for t := range ticker {
|
|
n.pollOnce(t)
|
|
}
|
|
}
|
|
|
|
func (n *Notifier) pollOnce(tick time.Time) {
|
|
n.lock.Lock()
|
|
defer n.lock.Unlock()
|
|
|
|
deadline, callback := n.condition(tick)
|
|
if deadline.IsZero() {
|
|
return
|
|
}
|
|
|
|
timeRemaining := deadline.Sub(tick)
|
|
for _, tock := range n.countdown {
|
|
if n.notifiedAt[tock] {
|
|
continue
|
|
}
|
|
if timeRemaining > tock {
|
|
continue
|
|
}
|
|
callback()
|
|
n.notifiedAt[tock] = true
|
|
return
|
|
}
|
|
}
|
|
|
|
func unique(ds []time.Duration) []time.Duration {
|
|
m := make(map[time.Duration]bool)
|
|
for _, d := range ds {
|
|
m[d] = true
|
|
}
|
|
var ks []time.Duration
|
|
for k := range m {
|
|
ks = append(ks, k)
|
|
}
|
|
return ks
|
|
}
|