Initial commit

This commit is contained in:
Emery Hemingway 2023-05-01 22:26:19 +01:00
commit 8002390284
9 changed files with 388 additions and 0 deletions

2
.envrc Normal file
View File

@ -0,0 +1,2 @@
source_env ..
use flake work#battery_actor

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.direnv

6
README.md Normal file
View File

@ -0,0 +1,6 @@
# acpi_actor
A Syndicate actor for publishing Linux ACPI events as well as an example of interacting with Linux generic netlink sockets in pure Nim (Linux C headers aside).
Not very useful. I thought that I would get battery capacity information over netlink but apparently that information is polled from files in /sys despite any stated deprecations.
The lesson learned is that Netlink is of the sort of quality to be expected from the Linux kernel.

2
Tuprules.tup Normal file
View File

@ -0,0 +1,2 @@
include ../syndicate-nim/depends.tup
NIM_FLAGS += --path:$(TUP_CWD)/../syndicate-nim/src

3
acpi.prs Normal file
View File

@ -0,0 +1,3 @@
version 1.
AcpiEvent = <acpi_event @deviceClass string @busId string @type int @data int>.

13
acpi_actor.nimble Normal file
View File

@ -0,0 +1,13 @@
# Package
version = "20230502"
author = "Emery Hemingway"
description = "Syndicate actor for publishing Linux ACPI events"
license = "Unlicense"
srcDir = "src"
bin = @["acpi_actor"]
# Dependencies
requires "nim >= 1.6.12", "syndicate"

2
src/Tupfile Normal file
View File

@ -0,0 +1,2 @@
include_rules
: acpi_actor.nim | $(SYNDICATE_PROTOCOL) |> !nim_bin |>

50
src/acpi_actor.nim Normal file
View File

@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/asyncdispatch
import preserves, syndicate
import ./netlink
type AcpiGenlEvent {.packed, preservesRecord: "acpi_event".} = object
## Found in the Linux source, see linux/drivers/acpi/event.c
device_class: array[20, char]
bus_id: array[15, char]
`type`, data: uint32
proc toPreserveHook(chars: openarray[char]; E = void): Preserve[E] =
## Hack to convert fixed-width character strings to Preserves strings.
result = Preserve[E](kind: pkString, string: newStringOfCap(len(chars)))
for c in chars:
if c == '\0': break
add(result.string, c)
proc recvAcpiEvent(nls: NetlinkSocket; family: uint16): AcpiGenlEvent =
var msg = recvMsg(nls)
if msg.hdr.n.nlmsg_type == family:
var parser: NlattrParser
while parse(parser, msg):
# If there is a label on these events then I haven't found it.
if copyObj(result, parser): return
next(parser)
proc relayEvents(ds: Ref; facet: Facet) =
var info: MulticastInfo
block:
let nls = openSocket()
info = resolveMulticastInfo(nls, "acpi_event\0")
close(nls)
let mcast = openSocket(int info.mcastGrpId)
proc relayEvent =
let event = recvAcpiEvent(mcast, info.familyId)
run(facet) do (turn: var Turn):
message(turn, ds, event)
callSoon: relayEvent()
callSoon: relayEvent()
# TODO seccomp
bootDataspace("main") do (ds: Ref; turn: var Turn):
connectStdio(ds, turn)
relayEvents(ds, turn.facet)
runForever()

309
src/netlink.nim Normal file
View File

@ -0,0 +1,309 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
# https://kernel.org/doc/html/next/userspace-api/netlink/intro.html
import std/[nativesockets, net, os, posix]
var
AF_NETLINK* {.importc, header: "sys/socket.h", nodecl.}: uint16
SOL_NETLINK {.importc, header: "<sys/socket.h>", nodecl.}: cint
{.pragma: netlinkNodecl, importc, header: "linux/netlink.h", nodecl.}
type Family* = distinct cint
var
NETLINK_ROUTE* {.netlinkNodecl.}: Family
NETLINK_W1* {.netlinkNodecl.}: Family
NETLINK_USERSOCK* {.netlinkNodecl.}: Family
NETLINK_FIREWALL* {.netlinkNodecl.}: Family
NETLINK_SOCK_DIAG* {.netlinkNodecl.}: Family
NETLINK_INET_DIAG* {.netlinkNodecl.}: Family
NETLINK_NFLOG* {.netlinkNodecl.}: Family
NETLINK_XFRM* {.netlinkNodecl.}: Family
NETLINK_SELINUX* {.netlinkNodecl.}: Family
NETLINK_ISCSI* {.netlinkNodecl.}: Family
NETLINK_AUDIT* {.netlinkNodecl.}: Family
NETLINK_FIB_LOOKUP* {.netlinkNodecl.}: Family
NETLINK_CONNECTOR* {.netlinkNodecl.}: Family
NETLINK_NETFILTER* {.netlinkNodecl.}: Family
NETLINK_SCSITRANSPORT* {.netlinkNodecl.}: Family
NETLINK_RDMA* {.netlinkNodecl.}: Family
NETLINK_IP6_FW* {.netlinkNodecl.}: Family
NETLINK_DNRTMSG* {.netlinkNodecl.}: Family
NETLINK_KOBJECT_UEVENT* {.netlinkNodecl.}: Family
NETLINK_GENERIC* {.netlinkNodecl.}: Family
NETLINK_CRYPTO* {.netlinkNodecl.}: Family
NETLINK_ADD_MEMBERSHIP* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_DROP_MEMBERSHIP* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_PKTINFO* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_BROADCAST_ERROR* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_NO_ENOBUFS* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_RX_RING* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_TX_RING* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_LISTEN_ALL_NSID* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_LIST_MEMBERSHIPS* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_CAP_ACK* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_EXT_ACK* {.importc, header: "linux/netlink.h", nodecl.}: int
NETLINK_GET_STRICT_CHK* {.importc, header: "linux/netlink.h", nodecl.}: int
type MessageType* = distinct uint16
var
NLMSG_NOOP* {.netlinkNodecl.}: MessageType
NLMSG_ERROR* {.netlinkNodecl.}: MessageType
NLMSG_DONE* {.netlinkNodecl.}: MessageType
func `==`(a, b: MessageType): bool {.borrow.}
type Flags* = distinct uint16
var
NLM_F_REQUEST* {.netlinkNodecl.}: Flags
NLM_F_MULTI* {.netlinkNodecl.}: Flags
NLM_F_ACK* {.netlinkNodecl.}: Flags
NLM_F_ECHO* {.netlinkNodecl.}: Flags
NLM_F_ROOT* {.netlinkNodecl.}: Flags
NLM_F_MATCH* {.netlinkNodecl.}: Flags
NLM_F_ATOMIC* {.netlinkNodecl.}: Flags
NLM_F_DUMP* {.netlinkNodecl.}: Flags
NLM_F_REPLACE* {.netlinkNodecl.}: Flags
NLM_F_EXCL* {.netlinkNodecl.}: Flags
NLM_F_CREATE* {.netlinkNodecl.}: Flags
NLM_F_APPEND* {.netlinkNodecl.}: Flags
proc `or`*(a, b: Flags): Flags {.borrow.}
type
Sockaddr_nl {.importc: "struct sockaddr_nl", header: "linux/netlink.h".} = object
nl_family: uint16
nl_pad: uint16
nl_pid: uint32
nl_groups: uint32
proc initSockaddr: Sockaddr_nl =
Sockaddr_nl(nl_family: AF_NETLINK)
proc saddr(sa_nl: var Sockaddr_nl): ptr Sockaddr =
cast[ptr Sockaddr](addr sa_nl)
type
Nlmsghdr {.importc: "struct nlmsghdr", header: "linux/netlink.h", completeStruct.} = object
nlmsg_len*: uint32
nlmsg_type*: uint16
nlmsg_flags*: Flags
nlmsg_seq*: uint32
nlmsg_pid*: uint32
Nlmsgerr* {.importc: "struct nlmsgerr", header: "linux/netlink.h".} = object
error*: cint
msg*: Nlmsghdr
Nlattr* {.importc: "struct nlattr", header: "linux/netlink.h", completeStruct.} = object
nla_len*, nla_type*: uint16
Genlmsghdr* {.importc: "struct genlmsghdr", header: "linux/genetlink.h", completeStruct.} = object
cmd*, version*: uint8
reserved: uint16
{.pragma: genetlink, importc, header: "linux/genetlink.h", nodecl.}
var
GENL_ID_CTRL* {.genetlink.}: MessageType
CTRL_CMD_UNSPEC* {.genetlink.}: uint8
CTRL_CMD_NEWFAMILY* {.genetlink.}: uint8
CTRL_CMD_DELFAMILY* {.genetlink.}: uint8
CTRL_CMD_GETFAMILY* {.genetlink.}: uint8
CTRL_CMD_NEWOPS* {.genetlink.}: uint8
CTRL_CMD_DELOPS* {.genetlink.}: uint8
CTRL_CMD_GETOPS* {.genetlink.}: uint8
CTRL_CMD_NEWMCAST_GRP* {.genetlink.}: uint8
CTRL_CMD_DELMCAST_GRP* {.genetlink.}: uint8
CTRL_CMD_GETMCAST_GRP* {.genetlink.}: uint8 ## unused
CTRL_CMD_GETPOLICY* {.genetlink.}: uint8
CTRL_ATTR_UNSPEC* {.genetlink.}: uint16
CTRL_ATTR_FAMILY_ID* {.genetlink.}: uint16
CTRL_ATTR_FAMILY_NAME* {.genetlink.}: uint16
CTRL_ATTR_VERSION* {.genetlink.}: uint16
CTRL_ATTR_HDRSIZE* {.genetlink.}: uint16
CTRL_ATTR_MAXATTR* {.genetlink.}: uint16
CTRL_ATTR_OPS* {.genetlink.}: uint16
CTRL_ATTR_MCAST_GROUPS* {.genetlink.}: uint16
CTRL_ATTR_POLICY* {.genetlink.}: uint16
CTRL_ATTR_OP_POLICY* {.genetlink.}: uint16
CTRL_ATTR_OP* {.genetlink.}: uint16
CTRL_ATTR_MCAST_GRP_UNSPEC* {.genetlink.}: uint16
CTRL_ATTR_MCAST_GRP_NAME* {.genetlink.}: uint16
CTRL_ATTR_MCAST_GRP_ID* {.genetlink.}: uint16
type NetlinkSocket* = ref object
saLocal, saPeer: Sockaddr_nl
sock: Socket
seqNum: uint32
proc openSocket*(multicastGroups: varargs[int]): NetlinkSocket =
result = NetlinkSocket(
saLocal: initSockaddr(),
saPeer: initSockaddr(),
sock: newSocket(cint AF_NETLINK, posix.SOCK_RAW, cint NETLINK_GENERIC)
)
let fd = getFd(result.sock)
if bindAddr(fd, saddr result.saLocal, SockLen sizeof(result.saLocal)) != 0:
close(result.sock)
raiseOSError(osLastError(), "failed to bind Netlink socket")
var
check: Sockaddr_nl
checkSize = SockLen sizeof(check)
if getsockname(fd, saddr check, addr checkSize) != 0:
# check what kind of socket the kernel has bound
close(result.sock)
raiseOSError(osLastError(), "failed to examine Netlink socket")
if checkSize.int != sizeof(check):
close(result.sock)
raise newException(IOError, "bad socket len")
if result.saLocal.nl_family != AF_NETLINK:
close(result.sock)
raise newException(IOError, "Netlink not supported")
result.saLocal = check
for group in multicastGroups:
setSockOptInt(fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, group)
proc close*(nls: NetlinkSocket) =
close(nls.sock)
type
GenericHeader {.packed.} = object
n*: Nlmsghdr
g*: Genlmsghdr
GenericMessage* {.union.} = object
hdr*: GenericHeader
buf: array[4096, byte]
proc initGenericMessage: GenericMessage =
result.hdr.n.nlmsg_len = uint32 sizeOf(GenericHeader)
func len(msg: GenericMessage): int {.inline.} = int msg.hdr.n.nlmsg_len
proc incLen(msg: var GenericMessage; n: Natural) =
msg.hdr.n.nlmsg_len = (msg.hdr.n.nlmsg_len + n.uint32 + 3) and (not 3'u32)
doAssert(len(msg) < sizeOf(GenericMessage))
proc addAttr(msg: var GenericMessage; ctrl: uint16; data: openarray[char]) =
let
off = len(msg)
len = sizeOf(Nlattr) + data.len
if off + len > sizeOf(msg):
raise newException(ResourceExhaustedError, "netlink message overflow")
let attr = cast[ptr Nlattr](addr msg.buf[off])
attr.nla_len = uint16 len
attr.nla_type = ctrl
copyMem(addr msg.buf[off+sizeOf(Nlattr)], unsafeAddr data[0], data.len)
incLen(msg, attr.nla_len)
type NlattrParser* = object
off: int
attr*: Nlattr
data: pointer
proc parse*(parser: var NlattrParser; msg: GenericMessage): bool =
if parser.off == 0: parser.off = sizeOf(GenericHeader)
if parser.off+sizeOf(Nlattr) <= msg.len:
copyMem(addr parser.attr, unsafeAddr msg.buf[parser.off], sizeOf(parser.attr))
if parser.off+parser.attr.nla_len.int <= msg.len:
parser.data = msg.buf[parser.off+sizeof(Nlattr)].unsafeAddr.pointer
result = true
proc next*(parser: var NlattrParser) =
inc(parser.off, (parser.attr.nla_len.int + 3) and (not 3))
func len*(attr: Nlattr|ptr Nlattr): int {.inline.} = attr.nla_len.int - sizeOf(Nlattr)
proc copyString*(s: var string; parser: NlattrParser): bool =
let n = parser.attr.len
result = n > 0
if result:
setLen(s, parser.attr.len)
copyMem(addr s[0], parser.data, s.len)
proc copyNum*(i: var SomeInteger; parser: NlattrParser): bool =
let n = parser.attr.len
result = n <= sizeOf(i)
if result:
reset(i)
copyMem(addr i, parser.data, n)
proc copyObj*[T](x: var T; parser: NlattrParser): bool =
let n = parser.attr.len
result = sizeOf(T) <= n
if result: copyMem(addr x, parser.data, sizeOf(T))
proc send(nls: NetlinkSocket; buf: pointer; len: int): int =
result = sendTo(
nls.sock.getFd,
buf, len, 0,
saddr nls.saPeer, SockLen sizeOf(nls.saPeer))
if result < 0:
raiseOSError(osLastError(), "sendTo Netlink socket failed")
proc sendMsg*(nls: NetlinkSocket; msg: var GenericMessage) =
inc(nls.seqNum)
msg.hdr.n.nlmsg_seq = nls.seqNum
if send(nls, msg.addr, msg.len) != msg.len:
raise newException(IOError, "Netlink short send")
proc recvMsg*(nls: NetlinkSocket): GenericMessage =
var
sa = initSockaddr()
sl = SockLen sizeOf(sa)
let n = recvFrom(
nls.sock.getFd,
addr(result), sizeof(result), cint 0,
saddr sa, addr sl)
if n < 0 or result.hdr.n.nlmsg_len.int != n:
raiseOSError(osLastError(), "recvFrom Netlink socket failed")
type MulticastInfo* = tuple
mcastGrpName: string
mcastGrpId: uint64
familyId: uint16
proc resolveMulticastInfo*(nls: NetlinkSocket; family: string): MulticastInfo =
block:
var msg = initGenericMessage()
msg.hdr.n.nlmsg_type = uint16 GENL_ID_CTRL
msg.hdr.n.nlmsg_flags = NLM_F_REQUEST or NLM_F_ACK
msg.hdr.g.cmd = uint8 CTRL_CMD_GETFAMILY
addAttr(msg, CTRL_ATTR_FAMILY_NAME, family)
sendMsg(nls, msg)
var reply = recvMsg(nls)
if reply.hdr.n.nlmsg_type != GENL_ID_CTRL.uint16:
raise newException(IOError, "Expected GENL_ID_CTRL")
if reply.hdr.g.cmd != CTRL_CMD_NEWFAMILY:
raise newException(IOError, "Expected CTRL_CMD_NEWFAMILY")
var parser: NlattrParser
while parse(parser, reply):
if parser.attr.nla_type == CTRL_ATTR_FAMILY_NAME:
var name: string
if copyString(name, parser) and name != family:
raise newException(IOError, "Expected " & family & " but got " & name)
elif parser.attr.nla_type == CTRL_ATTR_FAMILY_ID:
if not copyNum(result.familyId, parser):
raise newException(IOError, "Failed to collect family id")
elif parser.attr.nla_type == CTRL_ATTR_MCAST_GROUPS.uint16:
var parser = parser
inc(parser.off, sizeof(Nlattr)*2) # why two nlattrs headers?
while parse(parser, reply):
if parser.attr.nla_type == CTRL_ATTR_MCAST_GRP_NAME:
if not copyString(result.mcastGrpName, parser):
raise newException(IOError, "Failed to collect multicast group name")
elif parser.attr.nla_type == CTRL_ATTR_MCAST_GRP_ID:
if not copyNum(result.mcastGrpId, parser):
raise newException(IOError, "Failed to collect multicast group id")
next(parser)
next(parser)