Introduce ACPI netlink multicast sub-protocol. This high-level API allows applications to listen for ACPI event notifications coming from a Linux kernel. * Makefile.am: add netlink/acpi.scm to list of source files. * netlink/acpi.scm: new file.
Makefile.am
| 1 | 1 | include guile.am | |
| 2 | 2 | ||
| 3 | 3 | SOURCES= \ | |
| 4 | + | netlink/acpi.scm \ | |
| 4 | 5 | netlink/connection.scm \ | |
| 5 | 6 | netlink/constant.scm \ | |
| 6 | 7 | netlink/data.scm \ |
netlink/acpi.scm unknown status 1
| 1 | + | ;;;; This file is part of Guile Netlink | |
| 2 | + | ;;;; | |
| 3 | + | ;;;; Copyright (C) 2025 Dale Mellor | |
| 4 | + | ;;;; Copyright (C) 2025 Julien Lepiller <julien@lepiller.eu> | |
| 5 | + | ;;;; | |
| 6 | + | ;;;; This library is free software: you can redistribute it and/or modify | |
| 7 | + | ;;;; it under the terms of the GNU General Public License as published by | |
| 8 | + | ;;;; the Free Software Foundation, either version 3 of the License, or | |
| 9 | + | ;;;; (at your option) any later version. | |
| 10 | + | ;;;; | |
| 11 | + | ;;;; This library is distributed in the hope that it will be useful, | |
| 12 | + | ;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 | + | ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 | + | ;;;; GNU General Public License for more details. | |
| 15 | + | ;;;; | |
| 16 | + | ;;;; You should have received a copy of the GNU General Public License | |
| 17 | + | ;;;; along with this library. If not, see <https://www.gnu.org/licenses/>. | |
| 18 | + | ||
| 19 | + | ||
| 20 | + | (define-module (netlink acpi) | |
| 21 | + | #:use-module (ice-9 match) | |
| 22 | + | #:use-module ((netlink connection) #:prefix NL:) | |
| 23 | + | #:use-module ((netlink constant) #:prefix NL:) | |
| 24 | + | #:use-module ((netlink data) #:prefix NL:) | |
| 25 | + | #:use-module ((netlink deserialize) #:prefix NL:) | |
| 26 | + | #:use-module ((netlink message) #:prefix NL:) | |
| 27 | + | #:use-module ((netlink route) #:prefix NL:) | |
| 28 | + | #:use-module ((netlink route attrs) #:prefix NL:) | |
| 29 | + | #:use-module (rnrs bytevectors) | |
| 30 | + | #:use-module (rnrs bytevectors gnu) ;; For bytevector-slice. | |
| 31 | + | #:export ( | |
| 32 | + | ;; Return accessor procedures to a port listening for power state change | |
| 33 | + | ;; notifications from the kernel. The application SHOULD (close-port _) | |
| 34 | + | ;; on it when it becomes of no further use (the common exception being | |
| 35 | + | ;; when the port is required for the entire lifetime of the application). | |
| 36 | + | ;; | |
| 37 | + | ;; This procedure returns three procedures (use (ice-9 receive) to unpack | |
| 38 | + | ;; them): (read-select), (process-data ACTION), and (close-connection). | |
| 39 | + | ;; The ACTION argument to the second procedure should be a procedure like | |
| 40 | + | ;; (action EVENT), and the four procedures below can be used to extract | |
| 41 | + | ;; data from the EVENT object. | |
| 42 | + | connect-events ;; > [ read-selector process-data close-connection ] | |
| 43 | + | ||
| 44 | + | ;; The above takes a procedure ACTION, of acpi-event; the following | |
| 45 | + | ;; procedures allow for the extraction of data items from such an object. | |
| 46 | + | event-device-class ;; < event > string | |
| 47 | + | event-bus-id ;; < event > string | |
| 48 | + | event-kind ;; < event > u32 | |
| 49 | + | event-data)) ;; < event > u32 | |
| 50 | + | ||
| 51 | + | ||
| 52 | + | ||
| 53 | + | ||
| 54 | + | ;; GLOSSARY | |
| 55 | + | ;; | |
| 56 | + | ;; ACPI : advanced configuration and power interface (firmware standard) | |
| 57 | + | ;; ATTR : attribute | |
| 58 | + | ;; BV : bytevector | |
| 59 | + | ;; GENL : generic netlink | |
| 60 | + | ;; MSG : message | |
| 61 | + | ;; NL : netlink | |
| 62 | + | ;; POS : position (byte offset) | |
| 63 | + | ||
| 64 | + | ||
| 65 | + | ||
| 66 | + | ;; These are privately defined for the kernel in drivers/acpi/event.c. | |
| 67 | + | ;; | |
| 68 | + | ;; The terminal nulls are needed because serializing string types does not | |
| 69 | + | ;; work properly. | |
| 70 | + | (define linux/ACPI_GENL_FAMILY_NAME "acpi_event\0") | |
| 71 | + | (define linux/ACPI_GENL_MCAST_GROUP_NAME "acpi_mc_group\0") | |
| 72 | + | (define linux/ACPI_GENL_ATTR_EVENT 1) | |
| 73 | + | (define linux/ACPI_GENL_CMD_EVENT 1) | |
| 74 | + | ||
| 75 | + | ||
| 76 | + | (define (bytevector-string-length bv max) | |
| 77 | + | "Return the position of the first null byte in the bytevector @var{bv}, up to | |
| 78 | + | a possible @var{max} value." | |
| 79 | + | (let loop ((i 0)) | |
| 80 | + | (if (or (eq? max i) | |
| 81 | + | (eq? 0 (array-ref bv i))) | |
| 82 | + | i | |
| 83 | + | (loop (1+ i))))) | |
| 84 | + | ||
| 85 | + | (define (bytevector->string bv maxlen) | |
| 86 | + | "Return the string corresponding to the null-terminated bytes in bytevector | |
| 87 | + | @var{bv}, which can hold no more than @var{maxlen} bytes." | |
| 88 | + | (utf8->string (bytevector-slice bv 0 (bytevector-string-length bv maxlen)))) | |
| 89 | + | ||
| 90 | + | ||
| 91 | + | ;; This type, genlmsghdr, is defined at | |
| 92 | + | ;; [kernel]/include/uapi/linux/genetlink.h. | |
| 93 | + | (NL:define-data-type | |
| 94 | + | genlmsghdr | |
| 95 | + | length-genlmsghdr | |
| 96 | + | serialize-genlmsghdr | |
| 97 | + | (cmd genlmsghdr-cmd genlmsghdr-type-cmd) ;; Single byte linux/CTRL_CMD_*. | |
| 98 | + | (attrs genlmsghdr-attrs genlmsghdr-type-attrs)) ;; Attributes. | |
| 99 | + | ||
| 100 | + | (define (length-genlmsghdr msg) | |
| 101 | + | "The length of the serialization of this header and its attributes on the wire." | |
| 102 | + | (+ 4 (NL:route-attr-list-size (genlmsghdr-type-attrs msg)))) | |
| 103 | + | ||
| 104 | + | (define (serialize-genlmsghdr msg pos bv) | |
| 105 | + | "Serialize @var{msg} to @var{pos} in the bytevector @var{bv}." | |
| 106 | + | (match msg | |
| 107 | + | (($ genlmsghdr-type cmd attrs) | |
| 108 | + | (bytevector-u8-set! bv pos cmd) | |
| 109 | + | (bytevector-u8-set! bv (1+ pos) 1) ;; Version. | |
| 110 | + | (bytevector-u16-set! bv (+ 2 pos) 0 (native-endianness)) ;; Reserved. | |
| 111 | + | (NL:serialize-route-attr-list attrs (+ 4 pos) bv)))) ;; Payload | |
| 112 | + | ||
| 113 | + | (define (deserialize-genlmsghdr decoder bv pos) | |
| 114 | + | (make-genlmsghdr (bytevector-u8-ref bv pos) | |
| 115 | + | (NL:deserialize-attr-list 'genl-id-attr | |
| 116 | + | decoder | |
| 117 | + | bv | |
| 118 | + | (+ pos 4)))) | |
| 119 | + | ||
| 120 | + | (NL:define-data-type | |
| 121 | + | mcast-groups | |
| 122 | + | length-mcast-groups | |
| 123 | + | serialize-mcast-groups | |
| 124 | + | (attrs mcast-groups-attrs mcast-groups-type-attrs)) | |
| 125 | + | ||
| 126 | + | (define (length-mcast-groups msg) | |
| 127 | + | (+ 4 (NL:route-attr-list-size (mcast-groups-type-attrs msg)))) | |
| 128 | + | ||
| 129 | + | (define (serialize-mcast-groups msg pos bv) | |
| 130 | + | (match msg | |
| 131 | + | (($ mcast-groups-type attrs) | |
| 132 | + | (bytevector-u16-set! bv | |
| 133 | + | pos | |
| 134 | + | (length-mcast-groups msg) | |
| 135 | + | (native-endianness)) | |
| 136 | + | (bytevector-u16-set! bv (+ 2 pos) 1 (native-endianness)) | |
| 137 | + | (NL:serialize-route-attr-list attrs (+ 4 pos) bv)))) | |
| 138 | + | ||
| 139 | + | (define (deserialize-mcast-groups decoder bv pos) | |
| 140 | + | (let* ((length (bytevector-u16-ref bv pos (native-endianness))) | |
| 141 | + | (buffer (make-bytevector (- length 4)))) | |
| 142 | + | (bytevector-copy! bv (+ pos 4) buffer 0 (- length 4)) | |
| 143 | + | (let ((ret | |
| 144 | + | (make-mcast-groups | |
| 145 | + | (NL:deserialize-attr-list 'mcast-groups-attr | |
| 146 | + | decoder | |
| 147 | + | buffer | |
| 148 | + | 0)))) | |
| 149 | + | ret))) | |
| 150 | + | ||
| 151 | + | (NL:define-data-type | |
| 152 | + | acpi-event | |
| 153 | + | (lambda (msg) 44) | |
| 154 | + | serialize-acpi-event | |
| 155 | + | ;; This serializes like a char[20]. | |
| 156 | + | (device-class event-device-class event-type-device-class) | |
| 157 | + | ;; This serializes like a char[15], followed by a slack byte(!). | |
| 158 | + | (bus-id event-bus-id event-type-bus-id) | |
| 159 | + | (kind event-kind event-type-kind) | |
| 160 | + | (data event-data event-type-data)) | |
| 161 | + | ||
| 162 | + | ;; We don't need to do this -- but it does help us understand what is | |
| 163 | + | ;; happening. | |
| 164 | + | (define (serialize-acpi-event msg pos bv) | |
| 165 | + | (match msg | |
| 166 | + | (($ acpi-event-type device-class bus-id kind data) | |
| 167 | + | (bytevector-copy! bv pos (make-bytevector 36 0) 0 36) | |
| 168 | + | (let ((source (string->utf8 (event-type-device-class msg)))) | |
| 169 | + | (bytevector-copy! source 0 bv pos (bytevector-length source))) | |
| 170 | + | (let ((source (string->utf8 (event-type-bus-id msg)))) | |
| 171 | + | (bytevector-copy! source 0 bv (+ pos 20) (bytevector-length source))) | |
| 172 | + | (bytevector-u32-set! bv (+ pos 36) kind (native-endianness)) | |
| 173 | + | (bytevector-u32-set! bv (+ pos 40) data (native-endianness))))) | |
| 174 | + | ||
| 175 | + | ;; This fluid will be filled with the application-supplied procedure to act on | |
| 176 | + | ;; ACPI event messages received from the kernel, before the following | |
| 177 | + | ;; deserializer gets invoked. | |
| 178 | + | (define %action (make-parameter '())) | |
| 179 | + | ||
| 180 | + | (define (deserialize-acpi-event decoder bv pos) | |
| 181 | + | (let ((ret (make-acpi-event | |
| 182 | + | (bytevector->string (bytevector-slice bv pos 20) 20) | |
| 183 | + | (bytevector->string (bytevector-slice bv (+ pos 20) 15) 15) | |
| 184 | + | (bytevector-u32-ref bv (+ pos 36) (native-endianness)) | |
| 185 | + | (bytevector-u32-ref bv (+ pos 40) (native-endianness))))) | |
| 186 | + | ((%action) ret) | |
| 187 | + | ret)) | |
| 188 | + | ||
| 189 | + | ||
| 190 | + | ;; These are filled in as a side effect of parsing the return from the | |
| 191 | + | ;; NETLINK_GENERIC/GENL_ID_CTRL/CTRL_CMD_GETFAMILY message from the kernel, in | |
| 192 | + | ;; the two specialized deserializers immediately below. | |
| 193 | + | (define %genl-acpi-family-id (make-parameter 0)) | |
| 194 | + | (define %acpi-mcast-group-id (make-parameter 0)) | |
| 195 | + | ||
| 196 | + | (define (deserialize-mcast-grp-id decoder bv pos) | |
| 197 | + | (let ((ret (NL:deserialize-route-attr-data-u32 decoder bv pos))) | |
| 198 | + | (%acpi-mcast-group-id (NL:nl-data-data ret)) | |
| 199 | + | ret)) | |
| 200 | + | ||
| 201 | + | (define (deserialize-genl-family-id decoder bv pos) | |
| 202 | + | (let ((ret (NL:deserialize-route-attr-data-u16 decoder bv pos))) | |
| 203 | + | (%genl-acpi-family-id (NL:nl-data-data ret)) | |
| 204 | + | ret)) | |
| 205 | + | ||
| 206 | + | ||
| 207 | + | (define genl-decoder | |
| 208 | + | `(;; This is the decoding lookup table used by | |
| 209 | + | ;; NL:receive-and-decode-message. | |
| 210 | + | (message ,NL:deserialize-message | |
| 211 | + | ;; This will catch the final acknowledgement, which we force onto | |
| 212 | + | ;; all return messages. | |
| 213 | + | ,@NL:%default-message-decoder | |
| 214 | + | (,NL:GENL_ID_CTRL . ,deserialize-genlmsghdr)) | |
| 215 | + | ;; This table is used by deserialize-genlmsghdr above. | |
| 216 | + | (genl-id-attr | |
| 217 | + | ,(NL:deserialize-route-attr 'genl-id-attr) | |
| 218 | + | (,NL:CTRL_ATTR_FAMILY_ID . ,deserialize-genl-family-id) | |
| 219 | + | (,NL:CTRL_ATTR_MCAST_GROUPS . ,deserialize-mcast-groups) | |
| 220 | + | (default . ,NL:deserialize-route-attr-data-bv)) | |
| 221 | + | ;; This table is used by deserialize-mcast-groups above. | |
| 222 | + | (mcast-groups-attr | |
| 223 | + | ,(NL:deserialize-route-attr 'mcast-groups-attr) | |
| 224 | + | (,NL:CTRL_ATTR_MCAST_GRP_NAME . ,NL:deserialize-route-attr-data-string) | |
| 225 | + | (,NL:CTRL_ATTR_MCAST_GRP_ID . ,deserialize-mcast-grp-id)))) | |
| 226 | + | ||
| 227 | + | (define (connect-events) | |
| 228 | + | "Return three procedures to manage a connected listener for kernel Netlink ACPI messages. | |
| 229 | + | ||
| 230 | + | The procedures are, nominally (but you can call them what you want), | |
| 231 | + | - (read-selector) which returns an object which can be used in the read | |
| 232 | + | list of a select procedure call. | |
| 233 | + | - (process-data ACTION) which applies ACTION to any ACPI events which | |
| 234 | + | the kernel ??casts to us. The ACTION procedure should be of the | |
| 235 | + | form (action event), where EVENT will be an acpi-event object. | |
| 236 | + | - (close-socket) which closes the connection to the kernel. | |
| 237 | + | ||
| 238 | + | The application SHOULD (close-socket) when it becomes of no further use (the | |
| 239 | + | common exception being when the connection is required for the entire lifetime | |
| 240 | + | of the application)." | |
| 241 | + | (define socket (NL:connect NL:NETLINK_GENERIC | |
| 242 | + | (NL:get-addr NL:AF_NETLINK 0 0) | |
| 243 | + | #:flags SOCK_NONBLOCK)) | |
| 244 | + | ||
| 245 | + | (define msg (NL:make-message | |
| 246 | + | NL:GENL_ID_CTRL ;; Sub-system | |
| 247 | + | NL:NLM_F_REQUEST ;; Flags | |
| 248 | + | 1 ;; sequence number | |
| 249 | + | 0 ;; kernel PID | |
| 250 | + | (make-genlmsghdr | |
| 251 | + | NL:CTRL_CMD_GETFAMILY | |
| 252 | + | (list | |
| 253 | + | (NL:make-route-attr | |
| 254 | + | NL:CTRL_ATTR_FAMILY_NAME | |
| 255 | + | (NL:make-string-route-attr | |
| 256 | + | linux/ACPI_GENL_FAMILY_NAME)))))) | |
| 257 | + | ||
| 258 | + | (NL:send-msg msg socket) | |
| 259 | + | ||
| 260 | + | ;; These get filled in just by calling NL:receive-and-decode-message! | |
| 261 | + | (parameterize ((%genl-acpi-family-id 0) | |
| 262 | + | (%acpi-mcast-group-id 0)) | |
| 263 | + | ||
| 264 | + | ;; We don??t actually care about the decoded message; we are only | |
| 265 | + | ;; interested in the side-effect of the decoding of furnishing the fluids | |
| 266 | + | ;; %genl-acpi-family-id and %acpi-mcast-group-id. | |
| 267 | + | (NL:receive-and-decode-msg socket genl-decoder) | |
| 268 | + | ||
| 269 | + | ;; Subscribe the socket to ACPI broadcasts. | |
| 270 | + | (NL:add-socket-membership socket (%acpi-mcast-group-id)) | |
| 271 | + | ||
| 272 | + | (values | |
| 273 | + | ;; Get the read-select object. | |
| 274 | + | (lambda () socket) | |
| 275 | + | ||
| 276 | + | ;; Process data at the port, passing it to (action acpi-event). | |
| 277 | + | (let ((genl-acpi-family-id (%genl-acpi-family-id))) | |
| 278 | + | (lambda (action) | |
| 279 | + | (process-event-port socket genl-acpi-family-id action))) | |
| 280 | + | ||
| 281 | + | ;; Close the connection. | |
| 282 | + | (lambda () (close-port socket))))) | |
| 283 | + | ||
| 284 | + | ||
| 285 | + | ||
| 286 | + | (define (process-event-port socket acpi-family-id action) | |
| 287 | + | "Read Netlink messages from socket and apply ACTION to any acpi-event objects found." | |
| 288 | + | (parameterize ((%action action)) | |
| 289 | + | ||
| 290 | + | ;; Once again, we are not interested in the actual decoded message, we | |
| 291 | + | ;; just use the side-effect of having ACTION run every time we decode an | |
| 292 | + | ;; acpi-event object (it happens during the deserialize-acpi-event). | |
| 293 | + | (NL:receive-and-decode-msg | |
| 294 | + | socket | |
| 295 | + | `(;; The table used directly by NL:receive-and-decode-msg. | |
| 296 | + | (message ,NL:deserialize-message | |
| 297 | + | ,@NL:%default-message-decoder | |
| 298 | + | (,acpi-family-id . ,deserialize-genlmsghdr) | |
| 299 | + | (default . ,NL:deserialize-message-header)) | |
| 300 | + | ;; The table used by deserialize-genlmsghdr above. | |
| 301 | + | (genl-id-attr ,(NL:deserialize-route-attr 'genl-id-attr) | |
| 302 | + | (,linux/ACPI_GENL_ATTR_EVENT . ,deserialize-acpi-event) | |
| 303 | + | (default . ,NL:deserialize-route-attr-data-bv)) | |
| 304 | + | ;; Needed to decode headers repeated in error messages. | |
| 305 | + | (message-hdr ,NL:deserialize-message-header))))) |