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))))) |