Skip to content

Commit fc2abdb

Browse files
committed
add notify-send cmd; tests
1 parent 1f0d3eb commit fc2abdb

6 files changed

Lines changed: 174 additions & 19 deletions

File tree

Makefile

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,51 @@
11
.DEFAULT_GOAL := help
22

33
PROJECT := notifymem
4+
BINARY := bin/$(PROJECT)-linux-amd64
45
SOURCES := $(wildcard *.go cmd/*/*.go)
5-
VERSION := $(shell git describe --tags 2>/dev/null || echo "Unknown")
6+
VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "Unknown")
67

78
build: $(SOURCES) ## Build the project
89
@echo "Building $(PROJECT) ($(VERSION))"
9-
CGO_ENABLED=0 go build -ldflags "-X 'main.version=$(VERSION)'" -o bin/$(PROJECT) cmd/notifymem/*.go
10+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'main.version=$(VERSION)'" -o $(BINARY) cmd/notifymem/*.go
1011

1112
.PHONY: help
1213
help: ## Display help
1314
@COL_W=20
1415
@grep -h '##' $(MAKEFILE_LIST) | \
1516
grep -v grep | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-'$$COL_W's\033[0m %s\n", $$1, $$2}'
1617

18+
.PHONY: logs
19+
logs:
20+
journalctl -u $(PROJECT) -f
21+
22+
.PHONY: reload
23+
reload:
24+
sudo systemctl daemon-reload
25+
26+
.PHONY: restart
27+
restart:
28+
sudo systemctl restart $(PROJECT)
29+
1730
.PHONY: run
1831
run: build ## Run the project
19-
./bin/$(PROJECT)
32+
./$(BINARY)
33+
34+
.PHONY: start
35+
start:
36+
sudo systemctl start $(PROJECT)
37+
38+
.PHONY: status
39+
status:
40+
sudo systemctl status $(PROJECT)
41+
42+
.PHONY: stop
43+
stop:
44+
sudo systemctl stop $(PROJECT)
45+
46+
.PHONY: test
47+
test: ## Run tests
48+
go test -v ./...
2049

2150
.PHONY: tidy
2251
tidy: ## Run go mod tidy

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ Features:
1010

1111
## Usage
1212

13-
Example usage that monitors memory usage by checking every 2 seconds, sends a notification when memory usage is above 80% and will not send another notification until after 60 seconds.
13+
Example usage that monitors memory usage by checking every 2 seconds, sends a notification when memory usage is above 80% and will not send another notification until after 10 seconds.
1414

1515
```bash
16-
notifymem -threshold 80 -delay 60 -interval 2
16+
notifymem -threshold 80 -delay 10 -interval 2
17+
```
18+
19+
Run a test which will check memory usage and send a test notification:
20+
21+
```bash
22+
notifymem -test
1723
```
1824

1925
## Installation
@@ -25,14 +31,15 @@ Next, create a systemd service file in `/etc/systemd/system/notifymem.service` w
2531
```ini
2632
[Unit]
2733
Description=notifymem service: notify when memory usage reaches a threshold
28-
After=network.target
34+
After=network.target,systemd-user-sessions.service,systemd-journald.service
2935

3036
[Service]
3137
Type=simple
3238
Restart=always
3339
RestartSec=3
3440
User=your-username
35-
ExecStart=/opt/notifymem/bin/notifymem -threshold 80 -delay 60 -interval 2
41+
ExecStart=/opt/notifymem/bin/notifymem -threshold 80 -delay 30 -interval 2
42+
Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
3643

3744
[Install]
3845
WantedBy=multi-user.target
@@ -42,6 +49,7 @@ Make sure to:
4249

4350
- replace `your-username` with your actual username
4451
- set `ExecStart` to the path where you copied the binary and use the desired options
52+
- set `Environment` to the correct `DBUS_SESSION_BUS_ADDRESS` value for your system, which can be found by running `echo $DBUS_SESSION_BUS_ADDRESS`
4553

4654
Finally, enable and start the service:
4755

cmd/notifymem/main.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ func main() {
2424
debug bool
2525
interval int
2626
resendDelay int
27+
test bool
2728
threshold int
2829
}
2930

3031
f := flags{}
3132
flag.BoolVar(&f.debug, "debug", false, "enable debug mode")
3233
flag.IntVar(&f.interval, "interval", 2, "interval between memory checks in seconds")
33-
flag.IntVar(&f.resendDelay, "delay", 30, "delay between notifications being sent in seconds")
34+
flag.IntVar(&f.resendDelay, "delay", 10, "delay between notifications being sent in seconds")
35+
flag.BoolVar(&f.test, "test", false, "run a test to check memory usage and send notification")
3436
flag.IntVar(&f.threshold, "threshold", 80, "memory threshold as a percentage")
3537
flag.Parse()
3638

@@ -50,6 +52,14 @@ func main() {
5052
errorExit(err)
5153
}
5254

55+
if f.test {
56+
err := m.Test()
57+
if err != nil {
58+
errorExit(err)
59+
}
60+
os.Exit(0)
61+
}
62+
5363
ctx := context.Background()
5464
if err := m.Run(ctx); err != nil && err != context.Canceled {
5565
errorExit(err)

monitor.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ type MonitorOptions struct {
2323
}
2424

2525
type Monitor struct {
26-
debugFunc func(string)
27-
interval time.Duration
28-
lastSentAt time.Time
29-
notifier Notifier
30-
resendDelay time.Duration
31-
threshold int
26+
debugFunc func(string)
27+
interval time.Duration
28+
lastSentAt time.Time
29+
notificationsSent int
30+
notifier Notifier
31+
resendDelay time.Duration
32+
threshold int
3233
}
3334

3435
func NewMonitor(notifier Notifier, opts MonitorOptions) (*Monitor, error) {
@@ -63,12 +64,14 @@ func NewMonitor(notifier Notifier, opts MonitorOptions) (*Monitor, error) {
6364
return m, nil
6465
}
6566

67+
// debug prints a debug message when debug mode is enabled
6668
func (m *Monitor) debug(msg string) {
6769
if m.debugFunc != nil {
6870
m.debugFunc(msg)
6971
}
7072
}
7173

74+
// isThresholdReached checks if the memory usage threshold has been reached
7275
func (m *Monitor) isThresholdReached() (bool, int, error) {
7376
mem, err := readMemory()
7477
if err != nil {
@@ -87,20 +90,32 @@ func (m *Monitor) isThresholdReached() (bool, int, error) {
8790
return false, usage, nil
8891
}
8992

93+
// notify sends a notification if the resend delay has been reached
9094
func (m *Monitor) notify(usage int) error {
9195
if time.Since(m.lastSentAt) < m.resendDelay {
9296
m.debug("resend delay not reached")
9397
return nil
9498
}
9599

96100
m.debug("sending notification")
101+
fmt.Printf("threshold reached: %d%%\n", usage)
102+
nSentStr := ""
103+
if m.notificationsSent > 0 {
104+
nSentStr = fmt.Sprintf(" (%d)", m.notificationsSent)
105+
}
97106
m.lastSentAt = time.Now()
98-
return m.notifier.Notify(
99-
"Memory Usage Threshold Reached [notifymem]",
107+
err := m.notifier.Notify(
108+
"Memory Usage Threshold Reached [notifymem]"+nSentStr,
100109
fmt.Sprintf("Memory usage at %d%%", usage),
101110
)
111+
m.notificationsSent++
112+
if err != nil {
113+
return err
114+
}
115+
return nil
102116
}
103117

118+
// Run starts the monitor
104119
func (m *Monitor) Run(ctx context.Context) error {
105120
m.debug("staring monitor")
106121
for {
@@ -117,7 +132,6 @@ func (m *Monitor) Run(ctx context.Context) error {
117132
m.debug(fmt.Sprintf("memory usage: %d%%", usage))
118133

119134
if reached {
120-
m.debug(fmt.Sprintf("threshold reached: %d%%", usage))
121135
if err := m.notify(usage); err != nil {
122136
return err
123137
}
@@ -127,6 +141,18 @@ func (m *Monitor) Run(ctx context.Context) error {
127141
}
128142
}
129143

144+
// Test executes a test to check memory usage and send a notification
145+
func (m *Monitor) Test() error {
146+
m.debug("running test")
147+
_, usage, err := m.isThresholdReached()
148+
if err != nil {
149+
return err
150+
}
151+
m.notify(usage)
152+
return nil
153+
}
154+
155+
// memoryUsage calculates memory usage as a percentage
130156
func memoryUsage(m memory) (int, error) {
131157
if m.total == 0 {
132158
return 0, errors.New("total memory is 0")
@@ -135,6 +161,7 @@ func memoryUsage(m memory) (int, error) {
135161
return int((float64(m.total-m.avail) / float64(m.total)) * 100), nil
136162
}
137163

164+
// readMemory reads memory information from /proc/meminfo
138165
func readMemory() (memory, error) {
139166
m := memory{}
140167
f, err := os.Open("/proc/meminfo")

monitor_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package notifymem
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestNewMonitor(t *testing.T) {
9+
// test NewMonitor
10+
n := NewNotifier()
11+
mOpts := MonitorOptions{
12+
Interval: 2 * time.Second,
13+
ResendDelay: 30 * time.Second,
14+
Threshold: 80,
15+
}
16+
_, err := NewMonitor(n, mOpts)
17+
if err != nil {
18+
t.Fatalf("expected nil, got: %s", err)
19+
}
20+
21+
mOpts.Interval = 0 * time.Millisecond
22+
_, err = NewMonitor(n, mOpts)
23+
if err == nil {
24+
t.Fatalf("expected bad interval error, got nil")
25+
}
26+
27+
mOpts.Interval = 2 * time.Second
28+
mOpts.ResendDelay = 0 * time.Second
29+
_, err = NewMonitor(n, mOpts)
30+
if err == nil {
31+
t.Fatalf("expected bad resend delay error, got nil")
32+
}
33+
34+
mOpts.ResendDelay = 30 * time.Second
35+
mOpts.Threshold = -1
36+
_, err = NewMonitor(n, mOpts)
37+
if err == nil {
38+
t.Fatalf("expected bad threshold error, got nil")
39+
}
40+
}
41+
42+
type mockNotifier struct {
43+
notifyCalled bool
44+
}
45+
46+
func (m *mockNotifier) Notify(title, message string) error {
47+
m.notifyCalled = true
48+
return nil
49+
}
50+
51+
func TestMonitorNotify(t *testing.T) {
52+
// test Monitor.Notify
53+
m := &Monitor{}
54+
m.notifier = &mockNotifier{}
55+
m.notify(80)
56+
if !m.notifier.(*mockNotifier).notifyCalled {
57+
t.Fatalf("expected notify to be called")
58+
}
59+
}
60+
func TestMemoryUsage(t *testing.T) {
61+
mem := memory{
62+
total: 100,
63+
avail: 50,
64+
}
65+
66+
usage, err := memoryUsage(mem)
67+
if err != nil {
68+
t.Fatalf("expected nil, got: %s", err)
69+
}
70+
71+
if usage != 50 {
72+
t.Fatalf("expected 50, got: %d", usage)
73+
}
74+
}

notifier.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package notifymem
22

3-
import "fmt"
3+
import (
4+
"os/exec"
5+
)
46

57
type Notifier interface {
68
Notify(title, message string) error
@@ -13,7 +15,12 @@ func NewNotifier() *notifier {
1315
return &notifier{}
1416
}
1517

18+
// Notify sends a notification using notify-send
1619
func (n *notifier) Notify(title, message string) error {
17-
fmt.Println("notify:", title, message)
20+
cmd := exec.Command("notify-send", title, message)
21+
err := cmd.Run()
22+
if err != nil {
23+
return err
24+
}
1825
return nil
1926
}

0 commit comments

Comments
 (0)