acpi.scm
| 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))))) |
| 306 |