Search code examples
asynchronousemacselisplexical-scope

Override function value with asynchronous call


How can I use cl-letf or similar to override a symbol's function value during an async call? I want to stop a buffer being displayed after calls to start-process or start-process-shell-command, and instead get back a string instead.

Here is a simplified example where binding display-buffer works for the synchronous version but not the async version. Also, I have set lexical-binding to true.

(defun tst-fun-sync (url)
  (call-process "wget" nil "*wget*" nil url "-O" "-")
  (with-current-buffer "*wget*"
    (display-buffer (current-buffer))))

(defun tst-fun-async (url)
  (set-process-sentinel
   (start-process "wget" "*wget*" "wget" url "-O" "-")
   #'(lambda (p _m)
       (when (zerop (process-exit-status p))
         (with-current-buffer (process-buffer p)
           (display-buffer (current-buffer)))))))

(defun tst-fun-no-display (fun &rest args)
  (cl-letf (((symbol-function 'display-buffer)
             #'(lambda (&rest _ignored)
                 (message "%s" (buffer-string)))))
    (apply fun args)))

;; The first has desired result, but not the second
;; (tst-fun-no-display 'tst-fun-sync "http://www.stackoverflow.com")
;; (tst-fun-no-display 'tst-fun-async "http://www.stackoverflow.com")

Solution

  • Let's define a macro which temporarily rebinds set-process-sentinel so that the sentinel function can be decorated with a wrapper function.

    (defmacro with-sentinel-wrapper (wrapper-fn &rest body)
      (let ((sps (gensym))
            (proc (gensym))
            (fn (gensym)))
        `(let ((,sps (symbol-function 'set-process-sentinel)))
           (cl-letf (((symbol-function 'set-process-sentinel)
                      (lambda (,proc ,fn)
                        (funcall ,sps ,proc (funcall ,wrapper-fn ,fn)))))
                    ,@body))))
    

    The wrapper can change the dynamic context in which the sentinel is called, by establishing any useful dynamic bindings. Here, I reuse your cl-letf to change what display does:

    (with-sentinel-wrapper (lambda (fn)
                            (lexical-let ((fun fn)) 
                              (lambda (p m)
                                (cl-letf (((symbol-function 'display-buffer)
                                           #'(lambda (&rest _ignored)
                                               (message "%s" (buffer-string)))))
                                  (funcall fun p m)))))
      (tst-fun-async "http://www.stackoverflow.com"))
    

    Now, if you aren't sure that the asynchronous process actually calls set-process-sentinel, you may have to hack other functions.