mirror of https://tildegit.org/ben/dotfiles
393 lines
14 KiB
EmacsLisp
393 lines
14 KiB
EmacsLisp
|
;;; async.el --- Asynchronous processing in Emacs -*- lexical-binding: t -*-
|
|||
|
|
|||
|
;; Copyright (C) 2012-2016 Free Software Foundation, Inc.
|
|||
|
|
|||
|
;; Author: John Wiegley <jwiegley@gmail.com>
|
|||
|
;; Created: 18 Jun 2012
|
|||
|
;; Version: 1.9.3
|
|||
|
|
|||
|
;; Keywords: async
|
|||
|
;; X-URL: https://github.com/jwiegley/emacs-async
|
|||
|
|
|||
|
;; This program is free software; you can redistribute it and/or
|
|||
|
;; modify it under the terms of the GNU General Public License as
|
|||
|
;; published by the Free Software Foundation; either version 2, or (at
|
|||
|
;; your option) any later version.
|
|||
|
|
|||
|
;; This program is distributed in the hope that it will be useful, but
|
|||
|
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|||
|
;; General Public License for more details.
|
|||
|
|
|||
|
;; You should have received a copy of the GNU General Public License
|
|||
|
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
|||
|
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
|||
|
;; Boston, MA 02111-1307, USA.
|
|||
|
|
|||
|
;;; Commentary:
|
|||
|
|
|||
|
;; Adds the ability to call asynchronous functions and process with ease. See
|
|||
|
;; the documentation for `async-start' and `async-start-process'.
|
|||
|
|
|||
|
;;; Code:
|
|||
|
|
|||
|
(eval-when-compile (require 'cl-lib))
|
|||
|
|
|||
|
(defgroup async nil
|
|||
|
"Simple asynchronous processing in Emacs"
|
|||
|
:group 'emacs)
|
|||
|
|
|||
|
(defcustom async-variables-noprops-function #'async-variables-noprops
|
|||
|
"Default function to remove text properties in variables."
|
|||
|
:group 'async
|
|||
|
:type 'function)
|
|||
|
|
|||
|
(defvar async-debug nil)
|
|||
|
(defvar async-send-over-pipe t)
|
|||
|
(defvar async-in-child-emacs nil)
|
|||
|
(defvar async-callback nil)
|
|||
|
(defvar async-callback-for-process nil)
|
|||
|
(defvar async-callback-value nil)
|
|||
|
(defvar async-callback-value-set nil)
|
|||
|
(defvar async-current-process nil)
|
|||
|
(defvar async--procvar nil)
|
|||
|
|
|||
|
(defun async-variables-noprops (sequence)
|
|||
|
"Remove text properties in SEQUENCE.
|
|||
|
|
|||
|
Argument SEQUENCE may be a list or a string, if anything else it
|
|||
|
is returned unmodified.
|
|||
|
|
|||
|
Note that this is a naive function that doesn't remove text properties
|
|||
|
in SEQUENCE recursively, only at the first level which suffice in most
|
|||
|
cases."
|
|||
|
(cond ((stringp sequence)
|
|||
|
(substring-no-properties sequence))
|
|||
|
((listp sequence)
|
|||
|
(cl-loop for elm in sequence
|
|||
|
if (stringp elm)
|
|||
|
collect (substring-no-properties elm)
|
|||
|
else collect elm))
|
|||
|
(t sequence)))
|
|||
|
|
|||
|
(defun async-inject-variables
|
|||
|
(include-regexp &optional predicate exclude-regexp noprops)
|
|||
|
"Return a `setq' form that replicates part of the calling environment.
|
|||
|
|
|||
|
It sets the value for every variable matching INCLUDE-REGEXP and
|
|||
|
also PREDICATE. It will not perform injection for any variable
|
|||
|
matching EXCLUDE-REGEXP (if present) or representing a syntax-table
|
|||
|
i.e. ending by \"-syntax-table\".
|
|||
|
When NOPROPS is non nil it tries to strip out text properties of each
|
|||
|
variable's value with `async-variables-noprops-function'.
|
|||
|
|
|||
|
It is intended to be used as follows:
|
|||
|
|
|||
|
(async-start
|
|||
|
`(lambda ()
|
|||
|
(require 'smtpmail)
|
|||
|
(with-temp-buffer
|
|||
|
(insert ,(buffer-substring-no-properties (point-min) (point-max)))
|
|||
|
;; Pass in the variable environment for smtpmail
|
|||
|
,(async-inject-variables \"\\`\\(smtpmail\\|\\(user-\\)?mail\\)-\")
|
|||
|
(smtpmail-send-it)))
|
|||
|
'ignore)"
|
|||
|
`(setq
|
|||
|
,@(let (bindings)
|
|||
|
(mapatoms
|
|||
|
(lambda (sym)
|
|||
|
(let* ((sname (and (boundp sym) (symbol-name sym)))
|
|||
|
(value (and sname (symbol-value sym))))
|
|||
|
(when (and sname
|
|||
|
(or (null include-regexp)
|
|||
|
(string-match include-regexp sname))
|
|||
|
(or (null exclude-regexp)
|
|||
|
(not (string-match exclude-regexp sname)))
|
|||
|
(not (string-match "-syntax-table\\'" sname)))
|
|||
|
(unless (or (stringp value)
|
|||
|
(memq value '(nil t))
|
|||
|
(numberp value)
|
|||
|
(vectorp value))
|
|||
|
(setq value `(quote ,value)))
|
|||
|
(when noprops
|
|||
|
(setq value (funcall async-variables-noprops-function
|
|||
|
value)))
|
|||
|
(when (or (null predicate)
|
|||
|
(funcall predicate sym))
|
|||
|
(setq bindings (cons value bindings)
|
|||
|
bindings (cons sym bindings)))))))
|
|||
|
bindings)))
|
|||
|
|
|||
|
(defalias 'async-inject-environment 'async-inject-variables)
|
|||
|
|
|||
|
(defun async-handle-result (func result buf)
|
|||
|
(if (null func)
|
|||
|
(progn
|
|||
|
(set (make-local-variable 'async-callback-value) result)
|
|||
|
(set (make-local-variable 'async-callback-value-set) t))
|
|||
|
(unwind-protect
|
|||
|
(if (and (listp result)
|
|||
|
(eq 'async-signal (nth 0 result)))
|
|||
|
(signal (car (nth 1 result))
|
|||
|
(cdr (nth 1 result)))
|
|||
|
(funcall func result))
|
|||
|
(unless async-debug
|
|||
|
(kill-buffer buf)))))
|
|||
|
|
|||
|
(defun async-when-done (proc &optional _change)
|
|||
|
"Process sentinel used to retrieve the value from the child process."
|
|||
|
(when (eq 'exit (process-status proc))
|
|||
|
(with-current-buffer (process-buffer proc)
|
|||
|
(let ((async-current-process proc))
|
|||
|
(if (= 0 (process-exit-status proc))
|
|||
|
(if async-callback-for-process
|
|||
|
(if async-callback
|
|||
|
(prog1
|
|||
|
(funcall async-callback proc)
|
|||
|
(unless async-debug
|
|||
|
(kill-buffer (current-buffer))))
|
|||
|
(set (make-local-variable 'async-callback-value) proc)
|
|||
|
(set (make-local-variable 'async-callback-value-set) t))
|
|||
|
(goto-char (point-max))
|
|||
|
(backward-sexp)
|
|||
|
(async-handle-result async-callback (read (current-buffer))
|
|||
|
(current-buffer)))
|
|||
|
(set (make-local-variable 'async-callback-value)
|
|||
|
(list 'error
|
|||
|
(format "Async process '%s' failed with exit code %d"
|
|||
|
(process-name proc) (process-exit-status proc))))
|
|||
|
(set (make-local-variable 'async-callback-value-set) t))))))
|
|||
|
|
|||
|
(defun async--receive-sexp (&optional stream)
|
|||
|
(let ((sexp (decode-coding-string (base64-decode-string
|
|||
|
(read stream)) 'utf-8-auto))
|
|||
|
;; Parent expects UTF-8 encoded text.
|
|||
|
(coding-system-for-write 'utf-8-auto))
|
|||
|
(if async-debug
|
|||
|
(message "Received sexp {{{%s}}}" (pp-to-string sexp)))
|
|||
|
(setq sexp (read sexp))
|
|||
|
(if async-debug
|
|||
|
(message "Read sexp {{{%s}}}" (pp-to-string sexp)))
|
|||
|
(eval sexp)))
|
|||
|
|
|||
|
(defun async--insert-sexp (sexp)
|
|||
|
(let (print-level
|
|||
|
print-length
|
|||
|
(print-escape-nonascii t)
|
|||
|
(print-circle t))
|
|||
|
(prin1 sexp (current-buffer))
|
|||
|
;; Just in case the string we're sending might contain EOF
|
|||
|
(encode-coding-region (point-min) (point-max) 'utf-8-auto)
|
|||
|
(base64-encode-region (point-min) (point-max) t)
|
|||
|
(goto-char (point-min)) (insert ?\")
|
|||
|
(goto-char (point-max)) (insert ?\" ?\n)))
|
|||
|
|
|||
|
(defun async--transmit-sexp (process sexp)
|
|||
|
(with-temp-buffer
|
|||
|
(if async-debug
|
|||
|
(message "Transmitting sexp {{{%s}}}" (pp-to-string sexp)))
|
|||
|
(async--insert-sexp sexp)
|
|||
|
(process-send-region process (point-min) (point-max))))
|
|||
|
|
|||
|
(defun async-batch-invoke ()
|
|||
|
"Called from the child Emacs process' command-line."
|
|||
|
;; Make sure 'message' and 'prin1' encode stuff in UTF-8, as parent
|
|||
|
;; process expects.
|
|||
|
(let ((coding-system-for-write 'utf-8-auto))
|
|||
|
(setq async-in-child-emacs t
|
|||
|
debug-on-error async-debug)
|
|||
|
(if debug-on-error
|
|||
|
(prin1 (funcall
|
|||
|
(async--receive-sexp (unless async-send-over-pipe
|
|||
|
command-line-args-left))))
|
|||
|
(condition-case err
|
|||
|
(prin1 (funcall
|
|||
|
(async--receive-sexp (unless async-send-over-pipe
|
|||
|
command-line-args-left))))
|
|||
|
(error
|
|||
|
(prin1 (list 'async-signal err)))))))
|
|||
|
|
|||
|
(defun async-ready (future)
|
|||
|
"Query a FUTURE to see if it is ready.
|
|||
|
|
|||
|
I.e., if no blocking
|
|||
|
would result from a call to `async-get' on that FUTURE."
|
|||
|
(and (memq (process-status future) '(exit signal))
|
|||
|
(let ((buf (process-buffer future)))
|
|||
|
(if (buffer-live-p buf)
|
|||
|
(with-current-buffer buf
|
|||
|
async-callback-value-set)
|
|||
|
t))))
|
|||
|
|
|||
|
(defun async-wait (future)
|
|||
|
"Wait for FUTURE to become ready."
|
|||
|
(while (not (async-ready future))
|
|||
|
(sleep-for 0.05)))
|
|||
|
|
|||
|
(defun async-get (future)
|
|||
|
"Get the value from process FUTURE when it is ready.
|
|||
|
FUTURE is returned by `async-start' or `async-start-process' when
|
|||
|
its FINISH-FUNC is nil."
|
|||
|
(and future (async-wait future))
|
|||
|
(let ((buf (process-buffer future)))
|
|||
|
(when (buffer-live-p buf)
|
|||
|
(with-current-buffer buf
|
|||
|
(async-handle-result
|
|||
|
#'identity async-callback-value (current-buffer))))))
|
|||
|
|
|||
|
(defun async-message-p (value)
|
|||
|
"Return true of VALUE is an async.el message packet."
|
|||
|
(and (listp value)
|
|||
|
(plist-get value :async-message)))
|
|||
|
|
|||
|
(defun async-send (&rest args)
|
|||
|
"Send the given messages to the asychronous Emacs PROCESS."
|
|||
|
(let ((args (append args '(:async-message t))))
|
|||
|
(if async-in-child-emacs
|
|||
|
(if async-callback
|
|||
|
(funcall async-callback args))
|
|||
|
(async--transmit-sexp (car args) (list 'quote (cdr args))))))
|
|||
|
|
|||
|
(defun async-receive ()
|
|||
|
"Send the given messages to the asychronous Emacs PROCESS."
|
|||
|
(async--receive-sexp))
|
|||
|
|
|||
|
;;;###autoload
|
|||
|
(defun async-start-process (name program finish-func &rest program-args)
|
|||
|
"Start the executable PROGRAM asynchronously. See `async-start'.
|
|||
|
PROGRAM is passed PROGRAM-ARGS, calling FINISH-FUNC with the
|
|||
|
process object when done. If FINISH-FUNC is nil, the future
|
|||
|
object will return the process object when the program is
|
|||
|
finished. Set DEFAULT-DIRECTORY to change PROGRAM's current
|
|||
|
working directory."
|
|||
|
(let* ((buf (generate-new-buffer (concat "*" name "*")))
|
|||
|
(proc (let ((process-connection-type nil))
|
|||
|
(apply #'start-process name buf program program-args))))
|
|||
|
(with-current-buffer buf
|
|||
|
(set (make-local-variable 'async-callback) finish-func)
|
|||
|
(set-process-sentinel proc #'async-when-done)
|
|||
|
(unless (string= name "emacs")
|
|||
|
(set (make-local-variable 'async-callback-for-process) t))
|
|||
|
proc)))
|
|||
|
|
|||
|
(defvar async-quiet-switch "-Q"
|
|||
|
"The Emacs parameter to use to call emacs without config.
|
|||
|
Can be one of \"-Q\" or \"-q\".
|
|||
|
Default is \"-Q\" but it is sometimes useful to use \"-q\" to have a
|
|||
|
enhanced config or some more variables loaded.")
|
|||
|
|
|||
|
;;;###autoload
|
|||
|
(defun async-start (start-func &optional finish-func)
|
|||
|
"Execute START-FUNC (often a lambda) in a subordinate Emacs process.
|
|||
|
When done, the return value is passed to FINISH-FUNC. Example:
|
|||
|
|
|||
|
(async-start
|
|||
|
;; What to do in the child process
|
|||
|
(lambda ()
|
|||
|
(message \"This is a test\")
|
|||
|
(sleep-for 3)
|
|||
|
222)
|
|||
|
|
|||
|
;; What to do when it finishes
|
|||
|
(lambda (result)
|
|||
|
(message \"Async process done, result should be 222: %s\"
|
|||
|
result)))
|
|||
|
|
|||
|
If FINISH-FUNC is nil or missing, a future is returned that can
|
|||
|
be inspected using `async-get', blocking until the value is
|
|||
|
ready. Example:
|
|||
|
|
|||
|
(let ((proc (async-start
|
|||
|
;; What to do in the child process
|
|||
|
(lambda ()
|
|||
|
(message \"This is a test\")
|
|||
|
(sleep-for 3)
|
|||
|
222))))
|
|||
|
|
|||
|
(message \"I'm going to do some work here\") ;; ....
|
|||
|
|
|||
|
(message \"Waiting on async process, result should be 222: %s\"
|
|||
|
(async-get proc)))
|
|||
|
|
|||
|
If you don't want to use a callback, and you don't care about any
|
|||
|
return value from the child process, pass the `ignore' symbol as
|
|||
|
the second argument (if you don't, and never call `async-get', it
|
|||
|
will leave *emacs* process buffers hanging around):
|
|||
|
|
|||
|
(async-start
|
|||
|
(lambda ()
|
|||
|
(delete-file \"a remote file on a slow link\" nil))
|
|||
|
'ignore)
|
|||
|
|
|||
|
Note: Even when FINISH-FUNC is present, a future is still
|
|||
|
returned except that it yields no value (since the value is
|
|||
|
passed to FINISH-FUNC). Call `async-get' on such a future always
|
|||
|
returns nil. It can still be useful, however, as an argument to
|
|||
|
`async-ready' or `async-wait'."
|
|||
|
(let ((sexp start-func)
|
|||
|
;; Subordinate Emacs will send text encoded in UTF-8.
|
|||
|
(coding-system-for-read 'utf-8-auto))
|
|||
|
(setq async--procvar
|
|||
|
(async-start-process
|
|||
|
"emacs" (file-truename
|
|||
|
(expand-file-name invocation-name
|
|||
|
invocation-directory))
|
|||
|
finish-func
|
|||
|
async-quiet-switch "-l"
|
|||
|
;; Using `locate-library' ensure we use the right file
|
|||
|
;; when the .elc have been deleted.
|
|||
|
(locate-library "async")
|
|||
|
"-batch" "-f" "async-batch-invoke"
|
|||
|
(if async-send-over-pipe
|
|||
|
"<none>"
|
|||
|
(with-temp-buffer
|
|||
|
(async--insert-sexp (list 'quote sexp))
|
|||
|
(buffer-string)))))
|
|||
|
(if async-send-over-pipe
|
|||
|
(async--transmit-sexp async--procvar (list 'quote sexp)))
|
|||
|
async--procvar))
|
|||
|
|
|||
|
(defmacro async-sandbox(func)
|
|||
|
"Evaluate FUNC in a separate Emacs process, synchronously."
|
|||
|
`(async-get (async-start ,func)))
|
|||
|
|
|||
|
(defun async--fold-left (fn forms bindings)
|
|||
|
(let ((res forms))
|
|||
|
(dolist (binding bindings)
|
|||
|
(setq res (funcall fn res
|
|||
|
(if (listp binding)
|
|||
|
binding
|
|||
|
(list binding)))))
|
|||
|
res))
|
|||
|
|
|||
|
(defmacro async-let (bindings &rest forms)
|
|||
|
"Implements `let', but each binding is established asynchronously.
|
|||
|
For example:
|
|||
|
|
|||
|
(async-let ((x (foo))
|
|||
|
(y (bar)))
|
|||
|
(message \"%s %s\" x y))
|
|||
|
|
|||
|
expands to ==>
|
|||
|
|
|||
|
(async-start (foo)
|
|||
|
(lambda (x)
|
|||
|
(async-start (bar)
|
|||
|
(lambda (y)
|
|||
|
(message \"%s %s\" x y)))))"
|
|||
|
(declare (indent 1))
|
|||
|
(async--fold-left
|
|||
|
(lambda (acc binding)
|
|||
|
(let ((fun (pcase (cadr binding)
|
|||
|
((and (pred functionp) f) f)
|
|||
|
(f `(lambda () ,f)))))
|
|||
|
`(async-start ,fun
|
|||
|
(lambda (,(car binding))
|
|||
|
,acc))))
|
|||
|
`(progn ,@forms)
|
|||
|
(reverse bindings)))
|
|||
|
|
|||
|
(provide 'async)
|
|||
|
|
|||
|
;;; async.el ends here
|