dbus-service.scm 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2021, 2022 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  6. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or (at
  9. ;;; your option) any later version.
  10. ;;;
  11. ;;; GNU Guix is distributed in the hope that it will be useful, but
  12. ;;; 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 GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  18. ;;; Commentary:
  19. ;;;
  20. ;;; This module contains procedures to interact with D-Bus via the 'dbus-send'
  21. ;;; command line utility. Before using any public procedure
  22. ;;;
  23. ;;; Code:
  24. (define-module (gnu build dbus-service)
  25. #:use-module (ice-9 match)
  26. #:use-module (srfi srfi-1)
  27. #:use-module (srfi srfi-19)
  28. #:use-module (srfi srfi-26)
  29. #:autoload (d-bus protocol connections) (d-bus-conn?
  30. d-bus-conn-flush
  31. d-bus-connect
  32. d-bus-disconnect
  33. d-bus-session-bus-address
  34. d-bus-system-bus-address)
  35. #:autoload (d-bus protocol messages) (MESSAGE_TYPE_METHOD_CALL
  36. d-bus-headers-ref
  37. d-bus-message-body
  38. d-bus-message-headers
  39. d-bus-read-message
  40. d-bus-write-message
  41. header-PATH
  42. header-DESTINATION
  43. header-INTERFACE
  44. header-MEMBER
  45. header-SIGNATURE
  46. make-d-bus-message)
  47. #:export (%dbus-query-timeout
  48. initialize-dbus-connection!
  49. %current-dbus-connection
  50. send-dbus
  51. call-dbus-method
  52. dbus-available-services
  53. dbus-service-available?
  54. with-retries))
  55. (define %dbus-query-timeout 2) ;in seconds
  56. ;;; Use Fibers' sleep to enable cooperative scheduling in Shepherd >= 0.9.0,
  57. ;;; which is required at least for the Jami service.
  58. (define sleep*
  59. (lambda () ;delay execution
  60. (if (resolve-module '(fibers) #f #:ensure #f)
  61. (module-ref (resolve-interface '(fibers)) 'sleep)
  62. (begin
  63. (format #t "Fibers not available -- blocking 'sleep' in use~%")
  64. sleep))))
  65. ;;;
  66. ;;; Utilities.
  67. ;;;
  68. (define-syntax-rule (with-retries n delay body ...)
  69. "Retry the code in BODY up to N times until it doesn't raise an exception nor
  70. return #f, else raise an error. A delay of DELAY seconds is inserted before
  71. each retry."
  72. (let loop ((attempts 0))
  73. (catch #t
  74. (lambda ()
  75. (let ((result (begin body ...)))
  76. (if (not result)
  77. (error "failed attempt" attempts)
  78. result)))
  79. (lambda args
  80. (if (< attempts n)
  81. (begin
  82. ((sleep*) delay) ;else wait and retry
  83. (loop (+ 1 attempts)))
  84. (error "maximum number of retry attempts reached"
  85. body ... args))))))
  86. ;;;
  87. ;;; Low level wrappers above AC/D-Bus.
  88. ;;;
  89. ;; The active D-Bus connection (a parameter) used by the other procedures.
  90. (define %current-dbus-connection (make-parameter #f))
  91. (define* (initialize-dbus-connection!
  92. #:key (address (or (d-bus-session-bus-address)
  93. (d-bus-system-bus-address))))
  94. "Initialize the D-Bus connection. ADDRESS should be the address of the D-Bus
  95. session, e.g. \"unix:path=/var/run/dbus/system_bus_socket\", the default value
  96. if ADDRESS is not provided and DBUS_SESSION_BUS_ADDRESS is not set. Return
  97. the initialized D-Bus connection."
  98. ;; Clear current correction if already active.
  99. (when (d-bus-conn? (%current-dbus-connection))
  100. (d-bus-disconnect (%current-dbus-connection)))
  101. (let ((connection (d-bus-connect address)))
  102. (%current-dbus-connection connection) ;update connection parameter
  103. (call-dbus-method "Hello")) ;initial handshake
  104. (%current-dbus-connection))
  105. (define* (send-dbus message #:key
  106. (connection (%current-dbus-connection))
  107. timeout)
  108. "Send a D-Bus MESSAGE to CONNECTION and return the body of its reply. Up to
  109. READ-RETRIES replies are read until a matching reply is found, else an error
  110. is raised. MESSAGE is to be constructed with `make-d-bus-message'. When the
  111. body contains a single element, it is returned directly, else the body
  112. elements are returned as a list. TIMEOUT is a timeout value in seconds."
  113. (let ((serial (d-bus-write-message connection message))
  114. (start-time (current-time time-monotonic))
  115. (timeout* (or timeout %dbus-query-timeout)))
  116. (d-bus-conn-flush connection)
  117. (let retry ()
  118. (when (> (time-second (time-difference (current-time time-monotonic)
  119. start-time))
  120. timeout*)
  121. (error 'dbus "fail to get reply in timeout" timeout*))
  122. (let* ((reply (d-bus-read-message connection))
  123. (reply-headers (d-bus-message-headers reply))
  124. (reply-serial (d-bus-headers-ref reply-headers 'REPLY_SERIAL))
  125. (error-name (d-bus-headers-ref reply-headers 'ERROR_NAME))
  126. (body (d-bus-message-body reply)))
  127. ;; Validate the reply matches the message.
  128. (when error-name
  129. (error 'dbus "method failed with error" error-name body))
  130. ;; Some replies do not include a serial header, such as the for the
  131. ;; org.freedesktop.DBus NameAcquired one.
  132. (if (and reply-serial (= serial reply-serial))
  133. (match body
  134. ((x x* ..1) ;contains 2 ore more elements
  135. body)
  136. ((x)
  137. x) ;single element; return it directly
  138. (#f #f))
  139. (retry))))))
  140. (define (argument->signature-type argument)
  141. "Infer the D-Bus signature type from ARGUMENT."
  142. ;; XXX: avoid ..1 when using vectors due to a bug (?) in (ice-9 match).
  143. (match argument
  144. ((? boolean?) "b")
  145. ((? string?) "s")
  146. (#((? string?) (? string?) ...) "as")
  147. (#(((? string?) . (? string?))
  148. ((? string?) . (? string?)) ...) "a{ss}")
  149. (_ (error 'dbus "no rule to infer type from argument" argument))))
  150. (define* (call-dbus-method method
  151. #:key
  152. (path "/org/freedesktop/DBus")
  153. (destination "org.freedesktop.DBus")
  154. (interface "org.freedesktop.DBus")
  155. (connection (%current-dbus-connection))
  156. arguments
  157. timeout)
  158. "Call the D-Bus method specified by METHOD, PATH, DESTINATION and INTERFACE.
  159. The currently active D-Bus CONNECTION is used unless explicitly provided.
  160. Method arguments may be provided via ARGUMENTS sent as the message body.
  161. TIMEOUT limit the maximum time to allow for the reply. Return the body of the
  162. reply."
  163. (let ((message (make-d-bus-message
  164. MESSAGE_TYPE_METHOD_CALL 0 #f '()
  165. `#(,(header-PATH path)
  166. ,(header-DESTINATION destination)
  167. ,(header-INTERFACE interface)
  168. ,(header-MEMBER method)
  169. ,@(if arguments
  170. (list (header-SIGNATURE
  171. (string-join
  172. (map argument->signature-type arguments)
  173. "")))
  174. '()))
  175. arguments)))
  176. (send-dbus message #:connection connection #:timeout timeout)))
  177. ;;;
  178. ;;; Higher-level, D-Bus procedures.
  179. ;;;
  180. (define (dbus-available-services)
  181. "Return the list of available (acquired) D-Bus services."
  182. (let ((names (vector->list (call-dbus-method "ListNames"))))
  183. ;; Remove entries such as ":1.7".
  184. (remove (cut string-prefix? ":" <>) names)))
  185. (define (dbus-service-available? service)
  186. "Predicate to check for the D-Bus SERVICE availability."
  187. (member service (dbus-available-services)))
  188. ;; Local Variables:
  189. ;; eval: (put 'with-retries 'scheme-indent-function 2)
  190. ;; End: