aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/uevent/uevent.go
blob: 61c0216efcbbf3d039c00635e7f964622f625124 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Package uevent provides userspace client for consuming events from a
// NETLINK_KOBJECT_UEVENT socket, as well as helpers for supplementing
// events received from the kernel.
package uevent

import (
	"context"
	"errors"
	"strconv"
	"sync/atomic"
	"syscall"

	"hakurei.app/internal/netlink"
)

type (
	// Recoverable is satisfied by errors that are safe to recover from.
	Recoverable interface{ recoverable() }
	// Nontrivial is satisfied by errors preferring a JSON encoding.
	Nontrivial interface{ nontrivial() }
)

// Conn represents a NETLINK_KOBJECT_UEVENT socket.
type Conn struct {
	conn *netlink.Conn

	// Whether currently between a call to enterExcl and exitExcl.
	excl atomic.Bool
}

// enterExcl must be called entering a critical section that interacts with conn.
func (c *Conn) enterExcl() error {
	if !c.excl.CompareAndSwap(false, true) {
		return syscall.EAGAIN
	}
	return nil
}

// exitExcl must be called exiting a critical section that interacts with conn.
func (c *Conn) exitExcl() { c.excl.Store(false) }

// Close closes the underlying socket.
func (c *Conn) Close() error { return c.conn.Close() }

// Dial returns the address of a newly connected [Conn].
func Dial() (*Conn, error) {
	// kernel group is hard coded in lib/kobject_uevent.c, undocumented
	c, err := netlink.Dial(syscall.NETLINK_KOBJECT_UEVENT, 1)
	if err != nil {
		return nil, err
	}
	return &Conn{conn: c}, err
}

var (
	// ErrBadSocket is returned by [Conn.Consume] for a reply from a
	// syscall.Sockaddr with unexpected concrete type.
	ErrBadSocket = errors.New("unexpected socket address")
)

// BadPortError is returned by [Conn.Consume] upon receiving a message that did
// not come from the kernel.
type BadPortError syscall.SockaddrNetlink

var _ Recoverable = new(BadPortError)

func (*BadPortError) recoverable() {}
func (e *BadPortError) Error() string {
	return "unexpected message from port id " + strconv.Itoa(int(e.Pid)) +
		" on NETLINK_KOBJECT_UEVENT"
}

// Consume continuously receives and parses events from the kernel. It returns
// the first error it encounters.
//
// Callers must not restart event processing after a non-nil error that does not
// satisfy [Recoverable] is returned.
func (c *Conn) Consume(ctx context.Context, events chan<- *Message) error {
	if err := c.enterExcl(); err != nil {
		return err
	}
	defer c.exitExcl()

	for {
		data, from, err := c.conn.Recvfrom(ctx, 0)
		if err != nil {
			return err
		}

		// lib/kobject_uevent.c:
		// set portid 0 to inform userspace message comes from kernel
		if v, ok := from.(*syscall.SockaddrNetlink); !ok {
			return ErrBadSocket
		} else if v.Pid != 0 {
			return (*BadPortError)(v)

		}

		var msg Message
		if err = msg.UnmarshalBinary(data); err != nil {
			return err
		}
		events <- &msg
	}
}