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 |