pax_global_header00006660000000000000000000000064136510407150014514gustar00rootroot0000000000000052 comment=d7926de3ba0105a36cfd00811fd6278aea903eef pfuture-1.9/000077500000000000000000000000001365104071500130575ustar00rootroot00000000000000pfuture-1.9/.gitignore000066400000000000000000000000061365104071500150430ustar00rootroot00000000000000*.elc pfuture-1.9/README.org000066400000000000000000000076451365104071500145410ustar00rootroot00000000000000# -*- fill-column: 100; eval: (auto-fill-mode t) -*- * Content :TOC:noexport: - [[#pfuture][Pfuture]] - [[#what-it-is][What it is]] - [[#practical-examples][Practical examples]] - [[#with-a-future-object][With a Future object]] - [[#with-a-callback][With a Callback]] - [[#about-asyncel][About async.el]] * Pfuture ** What it is pfuture.el offers a set of simple functions wrapping Emacs' existing process creation capabilities. It allows to conveniently deal with external processes in an asynchronous manner without having to worry about stdout buffers and filter- & sentinel-functions. The following examples practically demonstrate its capabilities. Detailed and formal documentation can be found in each function's eldoc. ** Practical examples Pfuture has 2 entry points. - ~pfuture-new~ creates a ~future~ object that can be stored, passed around to other functions and awaited to completion. - ~pfuture-callback~ allows starting an external process in a fire-and-forget fashion, alongside callbacks to execute when the process succeeds or fails, with full access to its output. *** With a Future object We can use pfuture to start an artficially long-running process (simulated with a sleep of 3 seconds) twice and wait until both futures complete. Despite sleeping twice only 3 seconds will have passed since the processes run in parallel. #+BEGIN_SRC emacs-lisp (let ((start (float-time)) (future1 (pfuture-new "sleep" "3")) (future2 (pfuture-new "sleep" "3")) (future3 (pfuture-new "echo" "All futures have finished after %s seconds."))) (pfuture-await future1 :timeout 4 :just-this-one nil) (pfuture-await future2 :timeout 4 :just-this-one nil) (pfuture-await future3 :timeout 4 :just-this-one nil) (message (pfuture-result future3) (round (- (float-time) start)))) #+END_SRC Stdout and stderr in future objects are separate: #+BEGIN_SRC emacs-lisp (let ((future (pfuture-new "ls" "nonexsitent_file"))) (pfuture-await-to-finish future) (message "Future stdout: [%s]" (string-trim (pfuture-result future))) (message "Future stderr: [%s]" (string-trim (pfuture-stderr future)))) #+END_SRC Calls to ~pfuture-await~ (and especially ~pfuture-await-to-finish~) are blocking, so it is important to set an appropriate timeout (default is 1 second) or to be really sure that the process is going to terminate. *** With a Callback Here we start another process and instead of keeping the future around and eventually awaiting it we can simply define what steps to take once the process has completed, depending on whether it failed or not. (Note that ~pfuture-callback~ requires lexical scope) #+BEGIN_SRC emacs-lisp (defun showcase-error-callback (process status output) (message "Pfuture Error!") (message "Process: %s" process) (message "Status: %s" status) (message "Output: %s" output)) (let ((debug-callback (lambda (pfuture-process status _pfuture-buffer) (message "Pfuture Debug: Process [%s] changed sttaus to [%s]" pfuture-process status)))) (pfuture-callback ["ls" "-alh" "."] :directory "~/Documents/git/pfuture" :name "Pfuture Example" :on-success (message "Pfuture Finish:\n%s" (pfuture-callback-output)) :on-error #'showcase-error-callback :on-status-change debug-callback)) #+END_SRC ** About async.el You might be inclined to compare both packages since they both, at first glance, handle asynchronous processes, but in truth they have very little in common outside of their general asynchronous nature. Async.el allows you to start and handle an asynchronous Emacs instance, running Elisp code. Pfuture lets you start any external command like ~git~ or ~ls~ (as its mostly a wrapper around ~make-process~), and then read its output. So while the two packages may appear similar at first there is really nothing to compare. pfuture-1.9/pfuture.el000066400000000000000000000301451365104071500150760ustar00rootroot00000000000000;;; pfuture.el --- a simple wrapper around asynchronous processes -*- lexical-binding: t -*- ;; Copyright (C) 2020 Alexander Miller ;; Author: Alexander Miller ;; Homepage: https://github.com/Alexander-Miller/pfuture ;; Package-Requires: ((emacs "25.2")) ;; Version: 1.9 ;; 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 3 of the License, 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 this program. If not, see . ;;; Commentary: ;;; Code: (require 'cl-lib) (require 'inline) (defvar pfuture--dummy-buffer nil "Dummy buffer for stderr pipes.") (define-inline pfuture--delete-process (process) "Delete PROCESS with redisplay inhibited." (inline-letevals (process) (inline-quote (let ((inhibit-redisplay t)) (delete-process ,process))))) (defun pfuture--sentinel (process _) "Delete the stderr pipe process of PROCESS." (unless (process-live-p process) (let* ((stderr-process (process-get process 'stderr-process))) ;; Set stderr-process to nil so that await-to-finish does not delete ;; the buffer again. (process-put process 'stderr-process nil) ;; delete-process may trigger other sentinels. If there are many pfutures, ;; this will overflow the stack. (run-with-idle-timer 0 nil #'pfuture--delete-process stderr-process)))) ;;;###autoload (defun pfuture-new (&rest cmd) "Create a new future process for command CMD. Any arguments after the command are interpreted as arguments to the command. This will return a process object with additional 'stderr and 'stdout properties, which can be read via \(process-get process 'stdout\) and \(process-get process 'stderr\) or alternatively with \(pfuture-result process\) or \(pfuture-stderr process\). Note that CMD must be a *sequence* of strings, meaning this is wrong: (pfuture-new \"git status\") this is right: (pfuture-new \"git\" \"status\")" (let ((stderr (make-pipe-process :name "Process Future stderr" ;; Use a dummy buffer for the stderr process. make-pipe-process creates a ;; buffer unless one is specified, even when :filter is specified and the ;; buffer is not used at all. :buffer (or pfuture--dummy-buffer (setq pfuture--dummy-buffer (get-buffer-create " *pfuture stderr dummy*"))) :noquery t :filter #'pfuture--append-stderr))) ;; Make sure that the same buffer is not shared between processes. ;; This is not a race condition, since the pipe is not yet connected and ;; cannot receive input. (set-process-buffer stderr nil) (condition-case err (let ((process (make-process :name "Process Future" :stderr stderr :sentinel #'pfuture--sentinel :filter #'pfuture--append-stdout :command cmd :noquery t)) ;; Make the processes share their plist so that 'stderr is easily accessible. (plist (list 'stdout "" 'stderr "" 'stderr-process stderr))) (set-process-plist process plist) (set-process-plist stderr plist) process) (error (pfuture--delete-process stderr) (signal (car err) (cdr err)))))) (defmacro pfuture--decompose-fn-form (fn &rest args) "Expands into the correct call form for FN and ARGS. FN may either be a (sharp) quoted function, and unquoted function or an sexp." (declare (indent 1)) (pcase fn (`(function ,fn) `(,fn ,@args)) (`(quote ,fn) `(,fn ,@args)) ((or `(,_ . ,_) `(,_)) fn) ((pred null) (ignore fn)) (fn `(funcall ,fn ,@args)))) (cl-defmacro pfuture-callback (command &key directory on-success on-error on-status-change name connection-type buffer filter) "Pfuture variant that supports a callback-based workflow. Internally based on `make-process'. Requires lexical scope. The first - and only required - argument is COMMAND. It is an (unquoted) list of the command and the arguments for the process that should be started. A vector is likewise acceptable - the difference is purely cosmetic (this does not apply when command is passed as a variable, in this case it must be a list). The rest of the argument list is made up of the following keyword arguments: ON-SUCCESS is the code that will run once the process has finished with an exit code of 0. In its context, these variables are bound: `process': The process object, as passed to the sentinel callback function. `status': The string exit status, as passed to the sentinel callback function. `pfuture-buffer': The buffer where the output of the process is collected, including both stdin and stdout. You can use `pfuture-callback-output' to quickly grab the buffer's content. ON-SUCCESS may take one of 3 forms: an unquoted sexp, a quoted function or an unquoted function. In the former two cases the passed fuction will be called with `process', `status' and `buffer' as its arguments. ON-FAILURE is the inverse to ON-SUCCESS; it will only run if the process has finished with a non-zero exit code. Otherwise the same conditions apply as for ON-SUCCESS. ON-STATUS-CHANGE will run on every status change, even if the process remains running. It is meant for debugging and has access to the same variables as ON-SUCCESS and ON-ERROR, including the (potentially incomplete) process output buffer. Otherwise the same conditions as for ON-SUCCESS and ON-ERROR apply. DIRECTORY is the value given to `default-directory' for the context of the process. If not given it will fall back the current value of `default-directory'. NAME will be passed to the :name property of `make-process'. If not given it will fall back to \"Pfuture Callback [$COMMAND]\". CONNECTION-TYPE will be passed to the :connection-process property of `make-process'. If not given it will fall back to 'pipe. BUFFER is the buffer that will be used by the process to collect its output, quickly collectible with `pfuture-output-from-buffer'. Providing a buffer outside of specific use-cases is not necessary, as by default pfuture will assign every launched command its own unique buffer and kill it after ON-SUCCESS or ON-ERROR have finished running. However, no such cleanup will take place if a custom buffer is provided. FILTER is a process filter-function (quoted function reference) that can be used to overwrite pfuture's own filter. By default pfuture uses its filter function to collect the launched process' output in its buffer, thus when providing a custom filter output needs to be gathered another way. Note that the process' buffer is stored in its `buffer' property and is therefore accessible via \(process-get process 'buffer\)." (declare (indent 1)) (let* ((command (if (vectorp command) `(quote ,(cl-map 'list #'identity command)) command)) (connection-type (or connection-type (quote 'pipe))) (directory (or directory default-directory))) (unless (or on-success on-error) (setq on-success '(function ignore))) `(let* ((default-directory ,directory) (name (or ,name (format "Pfuture-Callback %s" ,command))) ;; pfuture's buffers are internal implementation details ;; nobody should care if a new one is created (pfuture-buffer (or ,buffer (let (buffer-list-update-hook) (generate-new-buffer name)))) (process (make-process :name name :command ,command :connection-type ,connection-type :filter ,(or filter '(function pfuture--append-output-to-buffer)) :sentinel (lambda (process status) (ignore status) ,@(when on-status-change `((pfuture--decompose-fn-form ,on-status-change process status pfuture-buffer))) (unless (process-live-p process) (if (= 0 (process-exit-status process)) (pfuture--decompose-fn-form ,on-success process status pfuture-buffer) (pfuture--decompose-fn-form ,on-error process status pfuture-buffer)) ,(unless buffer `(kill-buffer (process-get process 'buffer)))))))) (process-put process 'buffer pfuture-buffer) process))) (defmacro pfuture-callback-output () "Retrieve the output from the pfuture-buffer variable in the current scope. Meant to be used with `pfuture-callback'." `(pfuture-output-from-buffer pfuture-buffer)) (cl-defun pfuture-await (process &key (timeout 1) (just-this-one t)) "Block until PROCESS has produced output and return it. Will accept the following optional keyword arguments: TIMEOUT: The timeout in seconds to wait for the process. May be a float to specify fractional number of seconds. In case of a timeout nil will be returned. JUST-THIS-ONE: When t only read from the process of FUTURE and no other. For details see documentation of `accept-process-output'." (let (inhibit-quit) (accept-process-output process timeout nil just-this-one)) (process-get process 'result)) (cl-macrolet ((define-getter (name doc variable ) `(define-inline ,name (process) ,doc (declare (side-effect-free t)) (inline-letevals (process) (inline-quote (process-get ,',process ',variable)))))) (define-getter pfuture-result "Return the output of a pfuture PROCESS." stdout) (define-getter pfuture-stderr "Return the error output of a pfuture PROCESS." stderr)) (defun pfuture-await-to-finish (process) "Keep reading the output of PROCESS until it is done. Same as `pfuture-await', but will keep reading (and blocking) so long as the process is *alive*. If the process never quits this method will block forever. Use with caution!" ;; If the sentinel hasn't run, disable it. We are going to delete ;; the stderr process here. (set-process-sentinel process nil) (let (inhibit-quit) (while (accept-process-output process))) (let* ((plist (process-plist process)) (stderr-process (plist-get plist 'stderr-process))) (when stderr-process (pfuture--delete-process stderr-process)) (plist-get plist 'stdout))) (defun pfuture--append-output-to-buffer (process msg) "Append PROCESS' MSG to its output buffer." (with-current-buffer (process-get process 'buffer) (goto-char (point-max)) (insert msg))) (defun pfuture--append-stdout (process msg) "Append PROCESS' MSG to the already saved stdout output." (let* ((process-plist (process-plist process)) (previous-output (plist-get process-plist 'stdout))) (plist-put process-plist 'stdout (if (zerop (string-bytes previous-output)) msg (concat previous-output msg))))) (defun pfuture--append-stderr (process msg) "Append PROCESS' MSG to the already saved stderr output." (let* ((process-plist (process-plist process)) (previous-output (plist-get process-plist 'stderr))) (plist-put process-plist 'stderr (if (zerop (string-bytes previous-output)) msg (concat previous-output msg))))) (define-inline pfuture-output-from-buffer (buffer) "Return the process output collected in BUFFER." (declare (side-effect-free t)) (inline-letevals (buffer) (inline-quote (with-current-buffer ,buffer (buffer-string))))) (provide 'pfuture) ;;; pfuture.el ends here