Skip to content

Commit

Permalink
feat(linux/mem): ✨ send oom events to Home Assistant
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuar committed Nov 19, 2024
1 parent c88e5a8 commit c491e81
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"linux/media",
"agent/sensor",
"linux/cpu",
"linux/system"
"linux/system",
"linux/mem"
],
"go.testFlags": ["-v"],
"[markdown]": {
Expand Down
1 change: 1 addition & 0 deletions internal/agent/workers_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ var sensorLaptopWorkersInitFuncs = []func(ctx context.Context) (*linux.EventSens

// eventWorkersInitFuncs are event workers that produce events rather than sensors.
var eventWorkersInitFuncs = []func(ctx context.Context) (*linux.EventWorker, error){
mem.NewOOMEventsWorker,
system.NewUserSessionEventsWorker,
}

Expand Down
136 changes: 136 additions & 0 deletions internal/linux/mem/oomEvents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2024 Joshua Rich <[email protected]>.
// SPDX-License-Identifier: MIT

package mem

import (
"context"
"fmt"
"log/slog"
"net/url"
"strings"

"github.com/davecgh/go-spew/spew"

"github.com/joshuar/go-hass-agent/internal/hass/event"
"github.com/joshuar/go-hass-agent/internal/linux"
"github.com/joshuar/go-hass-agent/internal/logging"
"github.com/joshuar/go-hass-agent/pkg/linux/dbusx"
)

const (
oomEventsWorkerID = "oom_events_worker"
oomDBusPath = "/org/freedesktop/systemd1/unit"
unitPathPrefix = "/org/freedesktop/systemd1/unit"
oomEventName = "oom_event"
)

type oomEventData struct {
Process string `json:"process"`
PID int `json:"pid"`
}

type OOMEventsWorker struct {
triggerCh chan dbusx.Trigger
linux.EventWorker
}

func (w *OOMEventsWorker) Events(ctx context.Context) (<-chan event.Event, error) {

Check failure on line 38 in internal/linux/mem/oomEvents.go

View workflow job for this annotation

GitHub Actions / golangci

cognitive complexity 31 of func `(*OOMEventsWorker).Events` is high (> 20) (gocognit)
eventCh := make(chan event.Event)

go func() {
defer close(eventCh)

for {
select {
case <-ctx.Done():
return
case trigger := <-w.triggerCh:
props, err := dbusx.ParsePropertiesChanged(trigger.Content)
if err != nil {
logging.FromContext(ctx).Debug("Could not parse changed properties for unit.", slog.Any("error", err))
continue
}
// if !strings.HasPrefix(trigger.Path, unitPathPrefix+"/app-") {
// continue
// }
// Ignore events that don't indicate a result change.
if _, found := props.Changed["Result"]; !found {
continue
}

result, err := dbusx.VariantToValue[string](props.Changed["Result"])
if err != nil {
logging.FromContext(ctx).Debug("Could not parse result.", slog.Any("error", err))
continue
}

slog.Info("received event",
slog.String("path", trigger.Path),
slog.String("interface", props.Interface),
slog.Any("result", result),
)

if result == "oom-kill" {
spew.Dump(trigger)
// Naming is defined in
// https://systemd.io/DESKTOP_ENVIRONMENTS/. The strings seem
// to be percent-encoded with % replaced by _.
processStr, err := url.PathUnescape(strings.ReplaceAll(trigger.Path, "_", "%"))
if err != nil {
logging.FromContext(ctx).Debug("Could not unescape process path string.", slog.Any("error", err))
}

// Trim the D-Bus unit path prefix.
processStr = strings.TrimPrefix(processStr, unitPathPrefix+"/")
// Trim any "app-" prefix.
processStr = strings.TrimPrefix(processStr, "app-")
// Trim the ".service" suffix.
processStr = strings.TrimSuffix(processStr, ".service")
// Ignore the <RANDOM> string that might be appended.
processStr, _, _ = strings.Cut(processStr, "@")
// Get the PID.
pid, _ := dbusx.VariantToValue[int](props.Changed["MainPID"])

Check failure on line 93 in internal/linux/mem/oomEvents.go

View workflow job for this annotation

GitHub Actions / golangci

Error return value is not checked (errcheck)
if pid == 0 {
continue
}
// Send an event.
eventCh <- event.Event{
EventType: oomEventName,
EventData: oomEventData{
Process: processStr,
PID: pid,
},
}
}
}
}
}()

return eventCh, nil
}

func NewOOMEventsWorker(ctx context.Context) (*linux.EventWorker, error) {
worker := linux.NewEventWorker(oomEventsWorkerID)

bus, ok := linux.CtxGetSessionBus(ctx)
if !ok {
return worker, linux.ErrNoSessionBus
}

eventWorker := &OOMEventsWorker{}

triggerCh, err := dbusx.NewWatch(
dbusx.MatchPathNamespace(oomDBusPath),
dbusx.MatchPropChanged(),
).Start(ctx, bus)
if err != nil {
return nil, fmt.Errorf("unable to set-up D-Bus watch for OOM events: %w", err)
}

eventWorker.triggerCh = triggerCh

worker.EventType = eventWorker

return worker, nil
}

0 comments on commit c491e81

Please sign in to comment.