From 833553dd9aec0072961a7f1a7797f9481855a07f Mon Sep 17 00:00:00 2001 From: Michael Albinus Date: Thu, 28 May 2026 10:03:05 +0200 Subject: [PATCH] dbus-call-method-asynchronously supports also an ERROR-HANDLER * doc/misc/dbus.texi (Asynchronous Methods): HANDLER can also be (HANDLER . ERROR-HANDLER). * etc/NEWS: Mention ERROR-HANDLER of dbus-call-method-asynchronously. * lisp/net/dbus.el (dbus-call-method-asynchronously): Adapt docstring. (dbus-check-event, dbus-handle-event): HANDLER can also be (HANDLER . ERROR-HANDLER). * src/dbusbind.c (Fdbus_message_internal): HANDLER can also be (HANDLER . ERROR-HANDLER). (Bug#80952) * test/lisp/net/dbus-tests.el (dbus--test-method-another-handler) (dbus--test-method-error-handler): New defvars. (dbus--test-method-another-handler) (dbus--test-method-error-handler): New functions. (dbus-test04-call-method-error-handler): New test. (dbus-test10-keep-fd): Extend test. --- doc/misc/dbus.texi | 54 ++++++++++++++++++++------ etc/NEWS | 8 ++++ lisp/net/dbus.el | 75 ++++++++++++++++++++++++++++-------- src/dbusbind.c | 24 +++++++++--- test/lisp/net/dbus-tests.el | 77 ++++++++++++++++++++++++++++++++++++- 5 files changed, 203 insertions(+), 35 deletions(-) diff --git a/doc/misc/dbus.texi b/doc/misc/dbus.texi index 8764fcade90..d63e26755d9 100644 --- a/doc/misc/dbus.texi +++ b/doc/misc/dbus.texi @@ -1340,9 +1340,20 @@ keyword @code{:session}. D-Bus object path, @var{service} is registered at. @var{interface} is an interface offered by @var{service}. It must provide @var{method}. -@var{handler} is a Lisp function, which is called when the -corresponding return message arrives. If @var{handler} is @code{nil}, -no return message will be expected. +@var{handler} is a Lisp function, which is called when the corresponding +return message has arrived. It uses the returned values from the +@var{method} call as arguments. These are the same arguments which are +returned when @code{dbus-call-method} is invoked instead, +@pxref{Synchronous Methods}. If @var{handler} is @code{nil}, no return +message will be expected. + +@var{handler} can also be the cons cell @code{(@var{handler} +. @var{error-handler})}. In this case, @var{error-handler} will be +called in case an error is returned from D-Bus. It uses the returned +D-Bus error as argument. + +Neither the return value of @var{handler} nor the return value of +@var{error-handler} is used. If the parameter @code{:timeout} is given, the following integer @var{timeout} specifies the maximum number of milliseconds before a @@ -1366,19 +1377,40 @@ arguments. They are converted into D-Bus types as described in If @var{handler} is a Lisp function, the function returns a key into the hash table @code{dbus-registered-objects-table}. The corresponding entry in the hash table is removed, when the return -message arrives, and @var{handler} is called. Example: +message arrives, and @var{handler} is called. Examples: + +The return value of @samp{org.freedesktop.portal.Settings.ReadOne} is a variant. @lisp (dbus-call-method-asynchronously - :system "org.freedesktop.Hal" - "/org/freedesktop/Hal/devices/computer" - "org.freedesktop.Hal.Device" "GetPropertyString" - (lambda (msg) (message "%s" msg)) - "system.kernel.machine") + :session "org.freedesktop.portal.Desktop" + "/org/freedesktop/portal/desktop" + "org.freedesktop.portal.Settings" "ReadOne" + '((lambda (msg) (message "Method handler %s" msg)) . + (lambda (err) (message "Error handler %s" err))) + "org.freedesktop.appearance" "color-scheme") -@print{} i686 +@print{} Method handler (0) -@result{} (:serial :system 2) +@result{} (:serial :session 4) +@end lisp + +There does not exist a method @samp{org.freedesktop.portal.Settings.ReadTwo}. + +@lisp +(dbus-call-method-asynchronously + :session "org.freedesktop.portal.Desktop" + "/org/freedesktop/portal/desktop" + "org.freedesktop.portal.Settings" "ReadTwo" + '((lambda (msg) (message "Method handler %s" msg)) . + (lambda (err) (message "Error handler %s" err))) + "org.freedesktop.appearance" "color-scheme") + +@print{} Error handler + (dbus-error "org.freedesktop.DBus.Error.UnknownMethod + No such method "ReadTwo") + +@result{} (:serial :session 5) @end lisp @end defun diff --git a/etc/NEWS b/etc/NEWS index e6fd8a7f747..a5806a99e31 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -69,6 +69,14 @@ Emacs 30. It allows Lisp programs that present completion candidates ("completion frontends") to provide additional information which can be used to adjust or optimize completion candidates computation. +** D-Bus + ++++ +*** Support error handler in asynchronous method calls. +The HANDLER argument of 'dbus-call-method-asynchronously' can be a cons +cell '(HANDLER . ERROR-HANDLER)'. ERROR-HANDLER is invoked if the +method call returns with a D-Bus error; the error is passed as argument. + * Changes in Emacs 32.1 on Non-Free Operating Systems diff --git a/lisp/net/dbus.el b/lisp/net/dbus.el index 0c748e76fcf..3a5cf48b92f 100644 --- a/lisp/net/dbus.el +++ b/lisp/net/dbus.el @@ -445,8 +445,17 @@ object path SERVICE is registered at. INTERFACE is an interface offered by SERVICE. It must provide METHOD. HANDLER is a Lisp function, which is called when the corresponding -return message has arrived. If HANDLER is nil, no return message -will be expected. +return message has arrived. It uses the returned values from the METHOD +call as arguments. These are the same arguments which are returned when +`dbus-call-method' is invoked instead. If HANDLER is nil, no return +message will be expected. + +HANDLER can also be the cons cell `(HANDLER . ERROR-HANDLER)'. In this +case, ERROR-HANDLER will be called in case an error is returned from +D-Bus. It uses the returned D-Bus error as argument. + +Neither the return value of HANDLER nor the return value of +ERROR-HANDLER is used. If the parameter `:timeout' is given, the following integer TIMEOUT specifies the maximum number of milliseconds before the @@ -477,18 +486,37 @@ about type keywords, see Info node `(dbus)Type Conversion'. If HANDLER is a Lisp function, the function returns a key into the hash table `dbus-registered-objects-table'. The corresponding entry in the hash table is removed, when the return message arrives, -and HANDLER is called. +and HANDLER is called. Examples: -Example: +The return value of \"org.freedesktop.portal.Settings.ReadOne\" is a variant. \(dbus-call-method-asynchronously - :system \"org.freedesktop.Hal\" \"/org/freedesktop/Hal/devices/computer\" - \"org.freedesktop.Hal.Device\" \"GetPropertyString\" #\\='message - \"system.kernel.machine\") + :session \"org.freedesktop.portal.Desktop\" + \"/org/freedesktop/portal/desktop\" + \"org.freedesktop.portal.Settings\" \"ReadOne\" + \\='((lambda (msg) (message \"Method handler %s\" msg)) . + (lambda (err) (message \"Error handler %s\" err))) + \"org.freedesktop.appearance\" \"color-scheme\") - -| i686 + -| Method handler (0) - => (:serial :system 2)" + => (:serial :session 4) + +There does not exist a method \"org.freedesktop.portal.Settings.ReadTwo\". + +\(dbus-call-method-asynchronously + :session \"org.freedesktop.portal.Desktop\" + \"/org/freedesktop/portal/desktop\" + \"org.freedesktop.portal.Settings\" \"ReadTwo\" + \\='((lambda (msg) (message \"Method handler %s\" msg)) . + (lambda (err) (message \"Error handler %s\" err))) + \"org.freedesktop.appearance\" \"color-scheme\") + + -| Error handler + (dbus-error org.freedesktop.DBus.Error.UnknownMethod + No such method \"ReadTwo\") + + => (:serial :session 5)" (or (featurep 'dbusbind) (signal 'dbus-error (list "Emacs not compiled with dbus support"))) @@ -504,6 +532,7 @@ Example: (or (stringp method) (signal 'wrong-type-argument (list 'stringp method))) (or (null handler) (functionp handler) + (and (listp handler) (functionp (car handler)) (functionp (cdr handler))) (signal 'wrong-type-argument (list 'functionp handler))) (apply #'dbus-message-internal dbus-message-type-method-call @@ -1111,9 +1140,11 @@ INTERFACE and MEMBER denote the message which has been sent. When TYPE is `dbus-message-type-error', MEMBER is the error name. HANDLER is the function which has been registered for this -message. ARGS are the typed arguments as returned from the -message. They are passed to HANDLER without type information, -when it is called during event handling in `dbus-handle-event'. +message. It can also be a cons cell (HANDLER . ERROR-HANDLER). + +ARGS are the typed arguments as returned from the message. They are +passed to HANDLER without type information, when it is called during +event handling in `dbus-handle-event'. This function signals a `dbus-error' if the event is not well formed." @@ -1150,7 +1181,10 @@ formed." (or (= dbus-message-type-method-return (nth 2 event)) (stringp (nth 8 event))) ;; Handler. - (functionp (nth 9 event)) + (or (functionp (nth 9 event)) + (and (consp (nth 9 event)) + (functionp (car (nth 9 event))) + (functionp (cdr (nth 9 event))))) ;; Arguments. (listp (nthcdr 10 event))) (signal 'dbus-error (list "Not a valid D-Bus event" event)))) @@ -1207,10 +1241,17 @@ If the HANDLER returns a `dbus-error', it is propagated as return message." (setq result (dbus-ignore-errors (apply (nth 9 event) args))) ;; Error messages must be propagated. The error name is in ;; the member slot. - (when (= dbus-message-type-error (nth 2 event)) - (signal 'dbus-error (cons (nth 8 event) args))) - ;; Apply the handler. - (setq result (apply (nth 9 event) args)) + (let* ((handler (nth 9 event)) + (error-handler (if (functionp handler) #'signal + (prog1 (cdr handler) + (setq handler (car handler)))))) + (setq result + (if (= dbus-message-type-error (nth 2 event)) + (funcall + error-handler + (cons 'dbus-error (cons (nth 8 event) args))) + ;; Apply the handler. + (apply handler args)))) ;; Return an (error) message when it is a message call. (when (= dbus-message-type-method-call (nth 2 event)) (dbus-ignore-errors diff --git a/src/dbusbind.c b/src/dbusbind.c index 95fedeb166b..7039eac3dbe 100644 --- a/src/dbusbind.c +++ b/src/dbusbind.c @@ -1411,7 +1411,11 @@ usage: (dbus-message-internal &rest REST) */) XD_DBUS_VALIDATE_PATH (path); XD_DBUS_VALIDATE_INTERFACE (interface); XD_DBUS_VALIDATE_MEMBER (member); - if (!NILP (handler) && !FUNCTIONP (handler)) + if (!NILP (handler) + && !(FUNCTIONP (handler) + || (CONSP (handler) + && FUNCTIONP (CAR_SAFE (handler)) + && FUNCTIONP (CDR_SAFE (handler))))) wrong_type_argument (Qinvalid_function, handler); } @@ -1562,6 +1566,12 @@ usage: (dbus-message-internal &rest REST) */) if (mtype != DBUS_MESSAGE_TYPE_METHOD_CALL) XD_SIGNAL1 (build_string (":keep-fd is only supported on method calls")); + /* This is because the error handler and the keepfd path use + the same slot in Vdbus_registered_objects_table. */ + if (CONSP (handler)) + XD_SIGNAL1 + (build_string + (":keep-fd cannot be used when there is an error handler")); /* Ignore this keyword if unsupported. */ #ifdef DBUS_TYPE_UNIX_FD @@ -1842,9 +1852,6 @@ xd_read_message_1 (DBusConnection *connection, Lisp_Object bus) /* Remove the entry. */ Fremhash (key, Vdbus_registered_objects_table); - /* Store the event. */ - xd_store_event (CONSP (value) ? CAR_SAFE (value) : value, args, event_args); - #ifdef DBUS_TYPE_UNIX_FD /* Check, whether there is a file descriptor to be kept. value is (handler . path) @@ -1857,8 +1864,12 @@ xd_read_message_1 (DBusConnection *connection, Lisp_Object bus) Fcons (Fcons (CAR_SAFE (CDR_SAFE (CAR_SAFE (args))), CDR_SAFE (value)), xd_registered_fds); + value = CAR_SAFE (value); } #endif + + /* Store the event. */ + xd_store_event (value, args, event_args); } else /* DBUS_MESSAGE_TYPE_METHOD_CALL, DBUS_MESSAGE_TYPE_SIGNAL. */ @@ -2141,8 +2152,9 @@ means a wildcard then. OBJECT is either the handler to be called when a D-Bus message, which matches the key criteria, arrives (TYPE `:method', `:signal' and -`:monitor'), or a list (ACCESS EMITS-SIGNAL VALUE) for TYPE -`:property'. +`:monitor'), or a list (ACCESS EMITS-SIGNAL VALUE) for TYPE `:property'. +For type `:message', the handler slot can also be a cons cell (HANDLER +. ERROR-HANDLER) or (HANDLER . KEEP-FD-PATH). For entries of type `:signal' or `:monitor', there is also a fifth element RULE, which keeps the match string the signal or monitor is diff --git a/test/lisp/net/dbus-tests.el b/test/lisp/net/dbus-tests.el index 3d0ab522d3f..c8ff2941f3c 100644 --- a/test/lisp/net/dbus-tests.el +++ b/test/lisp/net/dbus-tests.el @@ -842,6 +842,73 @@ Returns the respective error." dbus--test-interface "Foo" :authorizable t "foo") :type 'dbus-error))) +(defvar dbus--test-method-another-handler nil) +(defun dbus--test-method-another-handler (&rest args) + "Method handler for `dbus-test04-call-method-error-handler'." + (should args) + (setq dbus--test-method-another-handler t)) + +(defvar dbus--test-method-error-handler nil) +(defun dbus--test-method-error-handler (&rest args) + "Error handler for `dbus-test04-call-method-error-handler'." + (should (eq 'dbus-error (caar args))) + (setq dbus--test-method-error-handler t)) + +(ert-deftest dbus-test04-call-method-error-handler () + "Verify `dbus-call-method-asynchronously' error handler." + :tags '(:expensive-test) + (skip-unless dbus--test-enabled-session-bus) + (dbus-ignore-errors (dbus-unregister-service :session dbus--test-service)) + (dbus-register-service :session dbus--test-service) + + (unwind-protect + (let ((method "Method") + (method-handler #'dbus--test-method-handler) + (handler #'dbus--test-method-another-handler) + (error-handler #'dbus--test-method-error-handler) + ;dbus-debug ; There would be errors otherwise. + registered) + + ;; Register. + (should + (equal + (setq + registered + (dbus-register-method + :session dbus--test-service dbus--test-path + dbus--test-interface method method-handler)) + `((:method :session ,dbus--test-interface ,method) + (,dbus--test-service ,dbus--test-path ,method-handler)))) + + ;; Call HANDLER. + (setq dbus--test-method-another-handler nil) + (dbus-call-method-asynchronously + :session dbus--test-service dbus--test-path + dbus--test-interface method `(,handler . ,error-handler) "foo") + (with-timeout (1 (dbus--test-timeout-handler)) + (while (not dbus--test-method-another-handler) + (read-event nil nil 0.1))) + (should dbus--test-method-another-handler) + + ;; Call ERROR-HANDLER. + (setq dbus--test-method-error-handler nil) + (dbus-call-method-asynchronously + :session dbus--test-service dbus--test-path + dbus--test-interface method `(,handler . ,error-handler) + "foo" "foo" "foo") + (with-timeout (1 (dbus--test-timeout-handler)) + (while (not dbus--test-method-error-handler) + (read-event nil nil 0.1))) + (should dbus--test-method-error-handler) + + ;; Unregister method. + (should (dbus-unregister-object registered)) + (should-not (dbus-unregister-object registered))) + + ;; Cleanup. + (ignore-errors (kill-buffer "*Warnings*")) + (dbus-unregister-service :session dbus--test-service))) + (defvar dbus--test-event-expected nil "The expected event in `dbus--test-signal-handler'.") @@ -2416,7 +2483,15 @@ The argument EXPECTED-ARGS is a list of expected arguments for the method." ;; Closing them again is a noop. (should-not (dbus--fd-close lock1)) - (should-not (dbus--fd-close lock2)))) + (should-not (dbus--fd-close lock2)) + + ;; `:keep-fd' cannot be used together with an error handler. + (should-error + (dbus-call-method-asynchronously + :system dbus--test-systemd-service dbus--test-systemd-path + dbus--test-systemd-manager-interface "Inhibit" + '(ignore . ignore) :keep-fd what who why mode) + :type 'dbus-error))) (ert-deftest dbus-test10-open-close-fd () "Check D-Bus open/close a file descriptor."