├── .gitignore ├── README.org └── pfuture.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | # -*- fill-column: 100; eval: (auto-fill-mode t) -*- 2 | 3 | * Content :TOC:noexport: 4 | - [[#pfuture][Pfuture]] 5 | - [[#what-it-is][What it is]] 6 | - [[#practical-examples][Practical examples]] 7 | - [[#with-a-future-object][With a Future object]] 8 | - [[#with-a-callback][With a Callback]] 9 | - [[#about-asyncel][About async.el]] 10 | 11 | * Pfuture 12 | ** What it is 13 | 14 | pfuture.el offers a set of simple functions wrapping Emacs' existing process creation capabilities. 15 | It allows to conveniently deal with external processes in an asynchronous manner without having to 16 | worry about stdout buffers and filter- & sentinel-functions. 17 | 18 | The following examples practically demonstrate its capabilities. Detailed and formal documentation 19 | can be found in each function's eldoc. 20 | 21 | ** Practical examples 22 | 23 | Pfuture has 2 entry points. 24 | 25 | - ~pfuture-new~ creates a ~future~ object that can be stored, passed around to other functions and 26 | awaited to completion. 27 | - ~pfuture-callback~ allows starting an external process in a fire-and-forget fashion, alongside 28 | callbacks to execute when the process succeeds or fails, with full access to its output. 29 | 30 | *** With a Future object 31 | 32 | We can use pfuture to start an artficially long-running process (simulated with a sleep of 3 33 | seconds) twice and wait until both futures complete. Despite sleeping twice only 3 seconds will have 34 | passed since the processes run in parallel. 35 | 36 | #+BEGIN_SRC emacs-lisp 37 | (let ((start (float-time)) 38 | (future1 (pfuture-new "sleep" "3")) 39 | (future2 (pfuture-new "sleep" "3")) 40 | (future3 (pfuture-new "echo" "All futures have finished after %s seconds."))) 41 | (pfuture-await future1 :timeout 4 :just-this-one nil) 42 | (pfuture-await future2 :timeout 4 :just-this-one nil) 43 | (pfuture-await future3 :timeout 4 :just-this-one nil) 44 | (message (pfuture-result future3) (round (- (float-time) start)))) 45 | #+END_SRC 46 | 47 | Stdout and stderr in future objects are separate: 48 | 49 | #+BEGIN_SRC emacs-lisp 50 | (let ((future (pfuture-new "ls" "nonexsitent_file"))) 51 | (pfuture-await-to-finish future) 52 | (message "Future stdout: [%s]" (string-trim (pfuture-result future))) 53 | (message "Future stderr: [%s]" (string-trim (pfuture-stderr future)))) 54 | #+END_SRC 55 | 56 | Calls to ~pfuture-await~ (and especially ~pfuture-await-to-finish~) are blocking, so it is important 57 | to set an appropriate timeout (default is 1 second) or to be really sure that the process is going 58 | to terminate. 59 | 60 | *** With a Callback 61 | 62 | Here we start another process and instead of keeping the future around and eventually awaiting it we 63 | can simply define what steps to take once the process has completed, depending on whether it failed 64 | or not. 65 | 66 | (Note that ~pfuture-callback~ requires lexical scope) 67 | 68 | #+BEGIN_SRC emacs-lisp 69 | (defun showcase-error-callback (process status output) 70 | (message "Pfuture Error!") 71 | (message "Process: %s" process) 72 | (message "Status: %s" status) 73 | (message "Output: %s" output)) 74 | 75 | (let ((debug-callback (lambda (pfuture-process status _pfuture-buffer) 76 | (message "Pfuture Debug: Process [%s] changed sttaus to [%s]" pfuture-process status)))) 77 | (pfuture-callback ["ls" "-alh" "."] 78 | :directory "~/Documents/git/pfuture" 79 | :name "Pfuture Example" 80 | :on-success (message "Pfuture Finish:\n%s" (pfuture-callback-output)) 81 | :on-error #'showcase-error-callback 82 | :on-status-change debug-callback)) 83 | #+END_SRC 84 | 85 | ** About async.el 86 | 87 | You might be inclined to compare both packages since they both, at first glance, handle asynchronous 88 | processes, but in truth they have very little in common outside of their general asynchronous 89 | nature. 90 | 91 | Async.el allows you to start and handle an asynchronous Emacs instance, running Elisp code. Pfuture 92 | lets you start any external command like ~git~ or ~ls~ (as its mostly a wrapper around 93 | ~make-process~), and then read its output. So while the two packages may appear similar at first 94 | there is really nothing to compare. 95 | -------------------------------------------------------------------------------- /pfuture.el: -------------------------------------------------------------------------------- 1 | ;;; pfuture.el --- a simple wrapper around asynchronous processes -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2020 Alexander Miller 4 | 5 | ;; Author: Alexander Miller 6 | ;; Homepage: https://github.com/Alexander-Miller/pfuture 7 | ;; Package-Requires: ((emacs "25.2")) 8 | ;; Version: 1.10.3 9 | 10 | ;; This program is free software; you can redistribute it and/or modify 11 | ;; it under the terms of the GNU General Public License as published by 12 | ;; the Free Software Foundation, either version 3 of the License, or 13 | ;; (at your option) any later version. 14 | 15 | ;; This program is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with this program. If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;;; Code: 26 | 27 | (require 'cl-lib) 28 | (require 'inline) 29 | 30 | (defvar pfuture--dummy-buffer nil 31 | "Dummy buffer for stderr pipes.") 32 | 33 | (define-inline pfuture--delete-process (process) 34 | "Delete PROCESS with redisplay inhibited." 35 | (inline-letevals (process) 36 | (inline-quote 37 | (let ((inhibit-redisplay t)) 38 | (delete-process ,process))))) 39 | 40 | (defun pfuture--sentinel (process _) 41 | "Delete the stderr pipe process of PROCESS." 42 | (unless (process-live-p process) 43 | (let* ((stderr-process (process-get process 'stderr-process))) 44 | ;; Set stderr-process to nil so that await-to-finish does not delete 45 | ;; the buffer again. 46 | (process-put process 'stderr-process nil) 47 | ;; delete-process may trigger other sentinels. If there are many pfutures, 48 | ;; this will overflow the stack. 49 | (run-with-idle-timer 0 nil #'pfuture--delete-process stderr-process)) 50 | ;; Make sure the stdout buffer is deleted even if the future 51 | ;; is never awaited 52 | (unless (process-get process 'result) 53 | (let* ((buffer (process-get process 'buffer)) 54 | (result (with-current-buffer buffer (buffer-string)))) 55 | (kill-buffer buffer) 56 | (process-put process 'result result))))) 57 | 58 | ;;;###autoload 59 | (defun pfuture-new (&rest cmd) 60 | "Create a new future process for command CMD. 61 | Any arguments after the command are interpreted as arguments to the command. 62 | This will return a process object with additional \\='stderr and \\='stdout 63 | properties, which can be read via \(process-get process \\='stdout\) and 64 | \(process-get process \\='stderr\) or alternatively with 65 | \(pfuture-result process\) or \(pfuture-stderr process\). 66 | 67 | Note that CMD must be a *sequence* of strings, meaning 68 | this is wrong: (pfuture-new \"git status\") 69 | this is right: (pfuture-new \"git\" \"status\")" 70 | (let ((stderr (make-pipe-process 71 | :name " Process Future stderr" 72 | ;; Use a dummy buffer for the stderr process. make-pipe-process creates a 73 | ;; buffer unless one is specified, even when :filter is specified and the 74 | ;; buffer is not used at all. 75 | :buffer (or pfuture--dummy-buffer 76 | (setq pfuture--dummy-buffer (get-buffer-create " *pfuture stderr dummy*"))) 77 | :noquery t 78 | :filter #'pfuture--append-stderr))) 79 | ;; Make sure that the same buffer is not shared between processes. 80 | ;; This is not a race condition, since the pipe is not yet connected and 81 | ;; cannot receive input. 82 | (set-process-buffer stderr nil) 83 | (condition-case err 84 | (let* ((name (format " Pfuture-Buffer %s" cmd)) 85 | (pfuture-buffer 86 | (let (buffer-list-update-hook) 87 | (generate-new-buffer name))) 88 | (process 89 | (make-process 90 | :name "Process Future" 91 | :stderr stderr 92 | :sentinel #'pfuture--sentinel 93 | :filter #'pfuture--append-output-to-buffer 94 | :command cmd 95 | :noquery t)) 96 | ;; Make the processes share their plist so that 'stderr is easily accessible. 97 | (plist (list 'buffer pfuture-buffer 'stdout "" 'stderr "" 'stderr-process stderr))) 98 | (set-process-plist process plist) 99 | (set-process-plist stderr plist) 100 | process) 101 | (error 102 | (pfuture--delete-process stderr) 103 | (signal (car err) (cdr err)))))) 104 | 105 | (defmacro pfuture--decompose-fn-form (fn &rest args) 106 | "Expands into the correct call form for FN and ARGS. 107 | FN may either be a (sharp) quoted function, and unquoted function or an sexp." 108 | (declare (indent 1)) 109 | (pcase fn 110 | (`(function ,fn) 111 | `(,fn ,@args)) 112 | (`(quote ,fn) 113 | `(,fn ,@args)) 114 | ((or `(,_ . ,_) `(,_)) 115 | fn) 116 | ((pred null) 117 | (ignore fn)) 118 | (fn 119 | `(funcall ,fn ,@args)))) 120 | 121 | (cl-defmacro pfuture-callback 122 | (command &key 123 | on-success 124 | on-error 125 | on-status-change 126 | directory 127 | name 128 | connection-type 129 | buffer 130 | filter) 131 | "Pfuture variant that supports a callback-based workflow. 132 | Internally based on `make-process'. Requires lexical scope. 133 | 134 | The first - and only required - argument is COMMAND. It is an (unquoted) list 135 | of the command and the arguments for the process that should be started. A 136 | vector is likewise acceptable - the difference is purely cosmetic (this does not 137 | apply when command is passed as a variable, in this case it must be a list). 138 | 139 | The rest of the argument list is made up of the following keyword arguments: 140 | 141 | ON-SUCCESS is the code that will run once the process has finished with an exit 142 | code of 0. In its context, these variables are bound: `process': The process 143 | object, as passed to the sentinel callback function. `status': The string exit 144 | status, as passed to the sentinel callback function. `pfuture-buffer': The 145 | buffer where the output of the process is collected, including both stdin and 146 | stdout. You can use `pfuture-callback-output' to quickly grab the buffer's 147 | content. 148 | 149 | ON-SUCCESS may take one of 3 forms: an unquoted sexp, a quoted function or an 150 | unquoted function. In the former two cases the passed fuction will be called 151 | with `process', `status' and `buffer' as its arguments. 152 | 153 | ON-ERROR is the inverse to ON-SUCCESS; it will only run if the process has 154 | finished with a non-zero exit code. Otherwise the same conditions apply as for 155 | ON-SUCCESS. 156 | 157 | ON-STATUS-CHANGE will run on every status change, even if the process remains 158 | running. It is meant for debugging and has access to the same variables as 159 | ON-SUCCESS and ON-ERROR, including the (potentially incomplete) process output 160 | buffer. Otherwise the same conditions as for ON-SUCCESS and ON-ERROR apply. 161 | 162 | DIRECTORY is the value given to `default-directory' for the context of the 163 | process. If not given it will fall back the current value of 164 | `default-directory'. 165 | 166 | NAME will be passed to the :name property of `make-process'. If not given it 167 | will fall back to \"Pfuture Callback [$COMMAND]\". 168 | 169 | CONNECTION-TYPE will be passed to the :connection-process property of 170 | `make-process'. If not given it will fall back to \\='pipe. 171 | 172 | BUFFER is the buffer that will be used by the process to collect its output, 173 | quickly collectible with `pfuture-output-from-buffer'. 174 | Providing a buffer outside of specific use-cases is not necessary, as by default 175 | pfuture will assign every launched command its own unique buffer and kill it 176 | after ON-SUCCESS or ON-ERROR have finished running. However, no such cleanup 177 | will take place if a custom buffer is provided. 178 | 179 | FILTER is a process filter-function (quoted function reference) that can be used 180 | to overwrite pfuture's own filter. By default pfuture uses its filter function 181 | to collect the launched process' output in its buffer, thus when providing a 182 | custom filter output needs to be gathered another way. Note that the process' 183 | buffer is stored in its `buffer' property and is therefore accessible via 184 | \(process-get process \\='buffer\)." 185 | (declare (indent 1)) 186 | (let* ((command (if (vectorp command) 187 | `(quote ,(cl-map 'list #'identity command)) 188 | command)) 189 | (connection-type (or connection-type (quote 'pipe))) 190 | (directory (or directory default-directory))) 191 | (unless (or on-success on-error) 192 | (setq on-success '(function ignore))) 193 | `(let* ((default-directory ,directory) 194 | (name (or ,name (format " Pfuture-Callback %s" ,command))) 195 | ;; pfuture's buffers are internal implementation details 196 | ;; nobody should care if a new one is created 197 | (pfuture-buffer (or ,buffer (let (buffer-list-update-hook) (generate-new-buffer name)))) 198 | (process 199 | (make-process 200 | :name name 201 | :command ,command 202 | :connection-type ,connection-type 203 | :filter ,(or filter '(function pfuture--append-output-to-buffer)) 204 | :sentinel (lambda (process status) 205 | (ignore status) 206 | ,@(when on-status-change 207 | `((pfuture--decompose-fn-form ,on-status-change 208 | process status pfuture-buffer))) 209 | (unless (process-live-p process) 210 | (if (= 0 (process-exit-status process)) 211 | (pfuture--decompose-fn-form ,on-success 212 | process status pfuture-buffer) 213 | (pfuture--decompose-fn-form ,on-error 214 | process status pfuture-buffer)) 215 | ,(unless buffer 216 | `(kill-buffer (process-get process 'buffer)))))))) 217 | (process-put process 'buffer pfuture-buffer) 218 | process))) 219 | 220 | (defmacro pfuture-callback-output () 221 | "Retrieve the output from the pfuture-buffer variable in the current scope. 222 | Meant to be used with `pfuture-callback'." 223 | `(pfuture-output-from-buffer pfuture-buffer)) 224 | 225 | (cl-defun pfuture-await (process &key (timeout 1) (just-this-one t)) 226 | "Block until PROCESS has produced output and return it. 227 | 228 | Will accept the following optional keyword arguments: 229 | 230 | TIMEOUT: The timeout in seconds to wait for the process. May be a float to 231 | specify fractional number of seconds. In case of a timeout nil will be 232 | returned. 233 | 234 | JUST-THIS-ONE: When t only read from the process of FUTURE and no other. For 235 | details see documentation of `accept-process-output'." 236 | (let (inhibit-quit) 237 | (accept-process-output 238 | process timeout nil just-this-one)) 239 | (pfuture-result process)) 240 | 241 | (define-inline pfuture-result (process) 242 | "Return the output of a pfuture PROCESS. 243 | If the PROCESS is still alive only the output collected so far will be returned. 244 | To get the full output use either `pfuture-await' or `pfuture-await-to-finish'." 245 | (declare (side-effect-free t)) 246 | (inline-letevals (process) 247 | (inline-quote 248 | (let* ((result (process-get ,process 'result))) 249 | (cond 250 | (result result) 251 | ((process-live-p ,process) 252 | (let ((buffer (process-get ,process 'buffer))) 253 | (with-current-buffer buffer (buffer-string)))) 254 | (t 255 | (let* ((buffer (process-get ,process 'buffer)) 256 | (output (with-current-buffer buffer 257 | (buffer-string)))) 258 | (process-put ,process 'result output) 259 | (kill-buffer buffer) 260 | output))))))) 261 | 262 | (define-inline pfuture-stderr (process) 263 | "Return the error output of a pfuture PROCESS." 264 | (declare (side-effect-free t)) 265 | (inline-letevals (process) 266 | (inline-quote 267 | (process-get ,process 'stderr)))) 268 | 269 | (defun pfuture-await-to-finish (process) 270 | "Keep reading the output of PROCESS until it is done. 271 | Same as `pfuture-await', but will keep reading (and blocking) so long as the 272 | process is *alive*. 273 | 274 | If the process never quits this method will block forever. Use with caution!" 275 | ;; If the sentinel hasn't run, disable it. We are going to delete 276 | ;; the stderr process here. 277 | (set-process-sentinel process nil) 278 | (let (inhibit-quit) 279 | (while (accept-process-output process))) 280 | (let* ((plist (process-plist process)) 281 | (stderr-process (plist-get plist 'stderr-process))) 282 | (when stderr-process 283 | (pfuture--delete-process stderr-process)) 284 | (pfuture-result process))) 285 | 286 | (defun pfuture--append-output-to-buffer (process msg) 287 | "Append PROCESS' MSG to its output buffer." 288 | (with-current-buffer (process-get process 'buffer) 289 | (goto-char (point-max)) 290 | (insert msg))) 291 | 292 | (defun pfuture--append-stderr (process msg) 293 | "Append PROCESS' MSG to the already saved stderr output." 294 | (let* ((process-plist (process-plist process)) 295 | (previous-output (plist-get process-plist 'stderr))) 296 | (plist-put process-plist 'stderr 297 | (if (zerop (string-bytes previous-output)) 298 | msg 299 | (concat previous-output msg))))) 300 | 301 | (define-inline pfuture-output-from-buffer (buffer) 302 | "Return the process output collected in BUFFER." 303 | (declare (side-effect-free t)) 304 | (inline-letevals (buffer) 305 | (inline-quote 306 | (with-current-buffer ,buffer 307 | (buffer-string))))) 308 | 309 | (provide 'pfuture) 310 | 311 | ;;; pfuture.el ends here 312 | --------------------------------------------------------------------------------