Files
hakurei/internal/kobject/kobject.go
Ophestra d15f965d0c
All checks were successful
Test / Create distribution (push) Successful in 2m40s
Test / Sandbox (push) Successful in 2m46s
Test / ShareFS (push) Successful in 3m43s
Test / Hakurei (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m20s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m20s
internal/kobject: range over objects
For matching devices based on some criteria.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-25 23:17:14 +09:00

471 lines
12 KiB
Go

// Package kobject interprets uevent messages from a NETLINK_KOBJECT_UEVENT socket.
package kobject
import (
"context"
"fmt"
"maps"
"slices"
"strconv"
"sync"
"hakurei.app/internal/uevent"
)
const (
// StateColdboot denotes an [Object] populated by a coldboot event. It is
// eligible for all event actions.
StateColdboot = iota
// StateNew denotes an [Object] previously populated by a [uevent.KOBJ_ADD]
// event, but has not yet been targeted by a [uevent.KOBJ_BIND] event, or
// has been targeted by a [uevent.KOBJ_UNBIND] event.
StateNew
// StateBound denotes an [Object] that has been targeted by a
// [uevent.KOBJ_BIND] and has not been targeted by a [uevent.KOBJ_UNBIND]
// after that.
StateBound
)
// Object represents a kernel object.
type Object struct {
// Origin of the object.
State int `json:"-"`
// Set by [uevent.KOBJ_OFFLINE] and [uevent.KOBJ_ONLINE].
Offline bool
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// registered per-driver (optional)
ModAlias string `json:"modalias,omitempty"`
// dev_driver_uevent: drv->name (optional)
Driver string `json:"driver,omitempty"`
// SUBSYSTEM value set by the kernel.
Subsystem string `json:"subsystem"`
// Uninterpreted environment variable pairs. An entry missing a separator
// gains the value "\x00".
Env map[string]string `json:"env"`
}
// Clone returns a copy of o.
func (o *Object) Clone() Object {
v := *o
v.Env = maps.Clone(o.Env)
return v
}
// GoString returns compound literal for the underlying value.
func (o *Object) GoString() string {
return fmt.Sprintf("&%#v", *o)
}
// merge merges uninterpreted environment variable pairs from an [Event].
func (o *Object) merge(env map[string]string) {
for k, v := range env {
if v == "\x00" {
continue
}
switch k {
case "MODALIAS":
o.ModAlias = v
continue
case "DRIVER":
o.Driver = v
continue
default:
if o.Env == nil {
o.Env = make(map[string]string)
}
o.Env[k] = v
}
}
}
// update updates o with pairs from env, optionally stripping visited pairs.
func (o *Object) update(env map[string]string, strip bool) {
for k := range o.Env {
if v, ok := env[k]; ok {
if strip {
delete(env, k)
}
o.Env[k] = v
}
}
}
// A pendingIterator is a callback currently iterating through objects targeted
// by ongoing events.
type pendingIterator struct {
f func(o *Object) bool
done chan<- struct{}
}
// State processes a stream of [Event] populated from [uevent.Message] received
// from a NETLINK_KOBJECT_UEVENT socket and presents an efficient representation
// of kernel state.
type State struct {
// Next expected SEQNUM.
seq uint64
// DevPath to environment variables.
uevent map[string]*Object
// Synchronises access to uevent and its objects.
ueventMu sync.RWMutex
// Alive iterators.
iter []*pendingIterator
// Synchronises access to iter.
iterMu sync.Mutex
// UUID for synthetic [uevent.Coldboot] events.
coldboot uevent.UUID
// Called on [uevent.KOBJ_CHANGE] with stripped environment variables.
handleChange func(o *Object, env map[string]string)
// Reports errors populating [Event] from [uevent.Message]. A user-supplied
// nil value is replaced with a noop.
reportErr func(error)
}
// New returns the address of a new [State].
func New(
coldboot uevent.UUID,
handleChange func(o *Object, env map[string]string),
reportErr func(error),
) *State {
return &State{
uevent: make(map[string]*Object),
coldboot: coldboot,
handleChange: handleChange,
reportErr: reportErr,
}
}
// deleteIter removes an iterator from s. Must be called after acquiring iterMu.
func (s *State) deleteIter(p *pendingIterator) {
s.iter = slices.DeleteFunc(s.iter, func(v *pendingIterator) bool {
return p == v
})
}
// dispatchIter broadcasts an [Object] to all alive iterators.
func (s *State) dispatchIter(o *Object) {
s.iterMu.Lock()
defer s.iterMu.Unlock()
for _, p := range s.iter {
if !p.f(o) {
s.deleteIter(p)
close(p.done)
}
}
}
// Range calls f on all current and upcoming [Object] values tracked by s until
// f returns false or the context is cancelled. f must not retain o or modify
// the value it points to.
func (s *State) Range(ctx context.Context, f func(o *Object) bool) {
done := make(chan struct{})
p := pendingIterator{f, done}
s.iterMu.Lock()
s.ueventMu.RLock()
for _, o := range s.uevent {
if !f(o) {
s.ueventMu.RUnlock()
s.iterMu.Unlock()
return
}
}
s.ueventMu.RUnlock()
s.iter = append(s.iter, &p)
s.iterMu.Unlock()
select {
case <-ctx.Done():
s.iterMu.Lock()
s.deleteIter(&p)
s.iterMu.Unlock()
return
case <-done:
// deregistered by dispatchIter
return
}
}
// UnexpectedColdbootError is reported by [State.Consume] for a coldboot event
// with action other than the expected [uevent.KOBJ_ADD].
type UnexpectedColdbootError Event
func (e UnexpectedColdbootError) Error() string {
return "unexpected " + e.Action.String() + " coldboot event"
}
// DuplicateAddError is reported by [State.Consume] for a [uevent.KOBJ_ADD]
// event on a still-existing entry that was not the result of a coldboot.
type DuplicateAddError Event
func (e DuplicateAddError) Error() string {
return "duplicate add event on devpath " + strconv.Quote(e.DevPath)
}
// TargetError is reported by [State.Consume] for an event on a nonexistent
// entry. This is generally only possible before coldboot completes.
type TargetError Event
func (e TargetError) Error() string {
return "unexpected " + e.Action.String() +
" event on devpath " + strconv.Quote(e.DevPath)
}
// RemoveStateError is reported by [State.Consume] for a [uevent.KOBJ_REMOVE]
// event targeting an entry in a state other than [StateColdboot] and [StateNew].
type RemoveStateError Object
func (e RemoveStateError) Error() string {
return "remove event targeting devpath " + strconv.Quote(e.DevPath) +
" in state " + strconv.Itoa(e.State)
}
// BindStateError is reported by [State.Consume] for a [uevent.KOBJ_BIND] event
// targeting an entry in a state other than [StateColdboot] and [StateNew].
type BindStateError Object
func (e BindStateError) Error() string {
return "bind event targeting devpath " + strconv.Quote(e.DevPath) +
" in state " + strconv.Itoa(e.State)
}
// UnbindStateError is reported by [State.Consume] for a [uevent.KOBJ_UNBIND]
// event targeting an entry in a state other than [StateBound].
type UnbindStateError Object
func (e UnbindStateError) Error() string {
return "unbind event targeting devpath " + strconv.Quote(e.DevPath) +
" in state " + strconv.Itoa(e.State)
}
// MalformedMoveError is reported by [State.Consume] for a [uevent.KOBJ_MOVE]
// event missing the DEVPATH_OLD environment variable.
type MalformedMoveError Event
func (e MalformedMoveError) Error() string {
return "move event targeting devpath " + strconv.Quote(e.DevPath) +
" missing DEVPATH_OLD"
}
// UnexpectedOfflineError is reported by [State.Consume] for a
// [uevent.KOBJ_OFFLINE] or [uevent.KOBJ_ONLINE] event targeting an already
// offline or online object.
type UnexpectedOfflineError Object
func (e UnexpectedOfflineError) Error() string {
if e.Offline {
return "offline event targeting devpath " + strconv.Quote(e.DevPath)
}
return "online event targeting devpath " + strconv.Quote(e.DevPath)
}
// processEvent merges an event into s.
func (s *State) processEvent(e *Event) {
s.ueventMu.Lock()
defer s.ueventMu.Unlock()
coldboot := e.Synth != nil
if e.Action != uevent.KOBJ_ADD && coldboot {
s.reportErr(UnexpectedColdbootError(e.Clone()))
return
}
switch e.Action {
case uevent.KOBJ_ADD:
if e.Synth == nil {
if o, ok := s.uevent[e.DevPath]; ok {
s.reportErr(DuplicateAddError(e.Clone()))
o.merge(e.Env)
s.dispatchIter(o)
return
}
}
o := e.makeColdboot()
if !coldboot {
o.State = StateNew
}
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o)
return
case uevent.KOBJ_REMOVE:
if o, ok := s.uevent[e.DevPath]; !ok {
s.reportErr(TargetError(e.Clone()))
return
} else if o.State != StateColdboot && o.State != StateNew {
s.reportErr(RemoveStateError(o.Clone()))
}
delete(s.uevent, e.DevPath)
return
case uevent.KOBJ_CHANGE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(TargetError(e.Clone()))
// this suffers from the coldboot race window similar to KOBJ_MOVE,
// however this action combines driver-specific and change-specific
// environment variables and combines them with environment
// variables meant to convey state of the kobject, and it is not
// possible to reliably separate them, so this fallback avoids the
// race at the cost of including some garbage in tracked state
o = e.makeColdboot()
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o)
return
}
o.update(e.Env, true)
if s.handleChange != nil {
s.handleChange(o, e.Env)
}
s.dispatchIter(o)
return
case uevent.KOBJ_MOVE:
var o *Object
if old, ok := e.Env["DEVPATH_OLD"]; !ok {
s.reportErr(MalformedMoveError(e.Clone()))
// not reached
o = e.makeColdboot()
} else if o, ok = s.uevent[old]; !ok {
s.reportErr(TargetError(e.Clone()))
// this generally happens during coldboot, dropping the event here
// may cause inconsistent state if the coldboot event for this
// object was generated before the bind event
delete(e.Env, "DEVPATH_OLD")
o = e.makeColdboot()
} else {
delete(s.uevent, old)
delete(e.Env, "DEVPATH_OLD")
}
o.merge(e.Env)
s.uevent[e.DevPath] = o
o.DevPath = e.DevPath
s.dispatchIter(o)
return
case uevent.KOBJ_ONLINE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(TargetError(e.Clone()))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
o.merge(e.Env)
}
if !o.Offline {
s.reportErr(UnexpectedOfflineError(o.Clone()))
}
o.Offline = false
s.dispatchIter(o)
return
case uevent.KOBJ_OFFLINE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(TargetError(e.Clone()))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
o.merge(e.Env)
}
if o.Offline {
s.reportErr(UnexpectedOfflineError(o.Clone()))
}
o.Offline = true
s.dispatchIter(o)
return
case uevent.KOBJ_BIND:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(TargetError(e.Clone()))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
}
if o.State != StateColdboot && o.State != StateNew {
s.reportErr(BindStateError(o.Clone()))
}
o.State = StateBound
o.merge(e.Env)
s.dispatchIter(o)
return
case uevent.KOBJ_UNBIND:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(TargetError(e.Clone()))
// coldboot race window similar to an unexpected KOBJ_MOVE, but does
// not result in inconsistent state if dropped
return
}
if o.State != StateBound {
s.reportErr(UnbindStateError(o.Clone()))
}
o.State = StateNew
o.Driver = ""
s.dispatchIter(o)
return
default: // not reached
s.reportErr(fmt.Errorf("invalid action %d", e.Action))
return
}
}
// BadSequenceError is reported by [State.Consume] for an unexpected SEQNUM.
type BadSequenceError struct{ Got, Want uint64 }
func (e BadSequenceError) Error() string {
return "SEQNUM=" + strconv.FormatUint(e.Got, 10) +
", want " + strconv.FormatUint(e.Want, 10)
}
// Consume receives uevent messages and updates s to reflect state of kernel.
func (s *State) Consume(ctx context.Context, events <-chan *uevent.Message) {
if s.uevent == nil {
s.uevent = make(map[string]*Object)
}
if s.reportErr == nil {
s.reportErr = func(error) {}
}
var e Event
for {
select {
case <-ctx.Done():
return
case m, ok := <-events:
if !ok {
return
}
e.Populate(s.reportErr, m)
// skip external synthetic event
if e.Synth != nil && *e.Synth != s.coldboot {
continue
}
if s.seq == 0 {
s.seq = e.Sequence
}
if s.seq != e.Sequence {
s.reportErr(BadSequenceError{e.Sequence, s.seq})
}
s.seq++
s.processEvent(&e)
}
}
}