Hi Jack, Jack Kamm <jackk...@gmail.com> writes:
> Bruno Barbier <brubar...@gmail.com> writes: > >> I'm not using it with official org backends (yet). I'm using it with >> several custom backends that I'm working on. One of the backend >> delegate the block executions to emacs subprocesses: so I have a kind of >> asynchronous executions for free for any language, including elisp >> itself. > > For sessions, wouldn't running in a subprocess prevent the user from > directly interacting with the REPL outside of Org? Good point. The REPL should be created in the same subprocess; the REPL display and interaction must happen in the user main emacs. If the REPL is based on comint, it should be relatively easy to implement. > If so, that's a problem. Org-babel sessions need to play nicely with > inferior Python, inferior ESS, and other interactive comint modes. With this solution, the user and the REPL/execution will be in separate processes; so there will be disavantages. For basic interactions, mostly based on text input/output, it should work well. >> So, here we go. You'll find attach a set of patchs. It works for me with >> Emacsc 30.50 and 9.7-pre (from today). > > I suggest to keep these patches on a public branch somewhere, see: > https://orgmode.org/worg/org-contribute.html#patches > > "When discussing important changes, it is sometimes not so useful to > send long and/or numerous patches. > > In this case, you can maintain your changes on a public branch of a > public clone of Org and send a link to the diff between your changes > and the latest Org commit that sits in your clone." Good point. I'll switch to such a solution as soon as possible. > I tried running your example on emacs29 using > > emacs -q -L /path/to/org-mode/lisp my-async-tests.org > > but it fails with the error below. Also "make" gives a bunch of > compilation warnings (which I've put at the bottom). > ... My bad: I should have compiled the demo code in a standalone emacs. I forgot to require some libraries: cl-lib and org-id. I've now tested with your command line (thanks). It should now work. Sorry about that. > Finally here are the warnings when running "make": I should have fixed everything; no more (new) warnings. Thanks! Please find attached the new set of patchs. I'll switch to using a clone and a branch soon, in case if you prefer to wait. Thanks again! Bruno
>From f67829454ac0d3cd142da1bd0006efa37acce588 Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Fri, 16 Feb 2024 14:31:36 +0100 Subject: [PATCH 1/8] ob-core async: Add faces [1/5] lisp/org-faces.el (org-async-scheduled, org-async-pending, org-async-failure): new faces --- lisp/org-faces.el | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lisp/org-faces.el b/lisp/org-faces.el index 0e20de51a..5a8a8fd51 100644 --- a/lisp/org-faces.el +++ b/lisp/org-faces.el @@ -736,6 +736,24 @@ (defface org-mode-line-clock-overrun "Face used for clock display for overrun tasks in mode line." :group 'org-faces) +(defface org-async-scheduled '((t :inherit org-tag :background "gray")) + "Face for babel results for code blocks that are scheduled for execution." + :group 'org-faces + :version "27.2" + :package-version '(Org . "9.5")) + +(defface org-async-pending '((t :inherit org-checkbox :background "dark orange")) + "Face for babel results for code blocks that are running." + :group 'org-faces + :version "27.2" + :package-version '(Org . "9.5")) + +(defface org-async-failure '((t :inherit org-warning)) + "Face for babel results for code blocks that have failed." + :group 'org-faces + :version "27.2" + :package-version '(Org . "9.5")) + (provide 'org-faces) ;;; org-faces.el ends here -- 2.43.0
>From 9f135bd5e8e153323bed5a3274851fa78f246b83 Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Fri, 16 Feb 2024 14:32:00 +0100 Subject: [PATCH 2/8] ob-core async: Add org-babel--async tools [2/5] --- lisp/ob-core.el | 213 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/lisp/ob-core.el b/lisp/ob-core.el index bfeac257b..d98626fe8 100644 --- a/lisp/ob-core.el +++ b/lisp/ob-core.el @@ -792,6 +792,219 @@ (defun org-babel-session-buffer (&optional info) (when (org-babel-comint-buffer-livep buffer-name) buffer-name))) +(defun org-babel--async-status-face (status) + (pcase status + (:scheduled 'org-async-scheduled) + (:pending 'org-async-pending) + (:failure 'org-async-failure) + (:success nil) + (_ (error "Not a status")) + )) + +(defun org-babel--async-make-overlay (beg end) + "Create an overlay between positions BEG and END and return it." + (let ((overlay (make-overlay beg end)) + (read-only + (list + (lambda (&rest _) + (user-error + "Cannot modify an area being updated")))) + ) + (cl-flet ((make-read-only + (ovl) + (overlay-put ovl 'modification-hooks read-only) + (overlay-put ovl 'insert-in-front-hooks read-only) + (overlay-put ovl 'insert-behind-hooks read-only)) + ) + (overlay-put overlay 'org-babel--async-type 'org-babel--async-note) + (overlay-put overlay 'face 'secondary-selection) + (overlay-put overlay 'help-echo "Pending src block result...") + (make-read-only overlay) + overlay))) + +(defun org-babel--async-result-region (inline-elem &optional info) + "Return the region of the results, for the source block at point." + (unless info (setq info (org-babel-get-src-block-info))) + (save-excursion + (when-let ((res-begin (org-babel-where-is-src-block-result nil info))) + (cons res-begin + (save-excursion + (goto-char res-begin) + (if inline-elem + ;; Logic copy/pasted from org-babel-where-is-src-block-result. + (let ((result (org-element-context))) + (and (org-element-type-p result 'macro) + (string= (org-element-property :key result) + "results") + (progn + (goto-char (org-element-end result)) + (skip-chars-backward " \t") + (point)))) + ;; Logic copy/pasted from hide-result + (beginning-of-line) + (let ((case-fold-search t)) + (unless (re-search-forward org-babel-result-regexp nil t) + (error "Not looking at a result line"))) + (org-babel-result-end) + )))))) + +(defun org-babel--async-feedbacks (info handle-result + result-params exec-start-time) + "Flag the result as \='scheduled\=' and return how to handle feedbacks. + +Use overlays to report progress and status to the user. Do not delete +the existing result unless a new one is available. When the result is +available, remove the async overlays and insert the result as usual, +like for a synchronous result. In case of failure, use an overlay to +report the error. + +The returned function handles 3 types of feedbacks: + - (:success R): Evaluation is successful; result is R. + - (:failure ERR): Evaluation failed; error is ERR. + - (:pending P): Outcome still pending; current progress is P." + ;; FIXME: INFO CMD ... Nothing is used but handle-result here !! + (let (;; copy/pasted from org-babel-insert-result + (inline-elem (let ((context (org-element-context))) + (and (memq (org-element-type context) + '(inline-babel-call inline-src-block)) + context)))) + (cl-labels + ((eot-point (start) + "Move to End Of Title after START" + (if inline-elem + (org-element-end inline-elem) + (save-excursion (goto-char start) + (forward-line 1) (point)))) + (after-indent (pt) + "Move after indentation, starting at PT." + (save-excursion (goto-char pt) (re-search-forward "[[:blank:]]*"))) + (mk-result-overlays () + ;; Make 2 overlays to handle the pending result: one title + ;; (first line) and one for the body. + (pcase-let ((`(,start . ,end) (org-babel--async-result-region + inline-elem info))) + (let ((anchor-end (eot-point start))) + (cons (org-babel--async-make-overlay + (after-indent start) + (1- anchor-end)) + (org-babel--async-make-overlay + anchor-end end))))) + (add-style (status txt) + ;; Add the style matching STATUS over the text TXT. + (propertize txt 'face (org-babel--async-status-face status))) + + (short-version-of (msg) + ;; Compute the short version of MSG, to display in the header. + ;; Must return a string. + (if msg + (car (split-string (format "%s" msg) "\n" :omit-nulls)) + "")) + (update (ovl-title status msg) + ;; Update the title overlay to match STATUS and MSG. + (overlay-put ovl-title + 'face + (org-babel--async-status-face status)) + (overlay-put ovl-title + 'before-string (pcase status + (:scheduled "⏱") + (:pending "⏳") + (:failure "❌") + (:success "✔️"))) + (overlay-put ovl-title + 'after-string + (propertize (format " |%s|" + (if (eq :failure status) + (if (consp msg) (car msg) + (format "%s" msg)) + (short-version-of msg))) + 'face (org-babel--async-status-face status)))) + (remove-previous-overlays () + ;; Remove previous title and body overlays. + (mapc (lambda (ovl) + (when (eq 'org-babel--async-note + (overlay-get ovl 'org-babel--async-type)) + (delete-overlay ovl))) + (when-let ((region (org-babel--async-result-region + inline-elem info))) + ;; Not sure why, but we do need to start before + ;; point min, else, in some cases, some overlays + ;; are not found. + (overlays-in (max (1- (car region)) (point-min)) + (cdr region)))))) + + (remove-previous-overlays) + + ;; Ensure there is a non-empty region for the result. + (save-excursion + (unless (org-babel-where-is-src-block-result (not inline-elem) nil nil) + (org-babel-insert-result + ;; Use " " for the empty result. That cannot be nil, else it's interpreted + ;; as a list. We need at least one char, to separate markers if any. + " \n" + result-params + info nil + (nth 0 info) ; lang + exec-start-time + ))) + + ;; Create the overlays that span the result title and its body. + (pcase-let ((`(,title-ovl . ,body-ovl) (mk-result-overlays))) + ;; Flag the result as ":scheduled". + (update title-ovl :scheduled nil) + + ;; The callback, that runs in the org buffer at point. + (let ((buf (current-buffer)) + (pt (point-marker))) + (lambda (feedback) + (message "ob-core: Handling outcome at %s@%s: %s" pt buf feedback) + (with-current-buffer buf + (save-excursion + (goto-char pt) + (pcase feedback + (`(:success ,r) + ;; Visual beep that the result is available. + (update title-ovl :success r) + (sit-for 0.2) + ;; We remove all overlays and let org insert the result + ;; as it would in the synchronous case. + (delete-overlay title-ovl) + (delete-overlay body-ovl) + (funcall handle-result r)) + + (`(:pending ,r) + ;; Still waiting for the outcome. Update our + ;; overlays with the progress info R. + (message "Updating block at %s@%s" pt buf) + (update title-ovl :pending r)) + + (`(:failure ,err) + ;; We didn't get a result. We update our overlays + ;; to report that failure. And unlock the old + ;; result. + (overlay-put title-ovl 'face nil) + (update title-ovl :failure err) + (delete-overlay body-ovl)) + + (_ (error "Invalid outcome")) + ) + )) + nil)))))) + + +(cl-defun org-babel--async-p (params &key default) + "Return a non-nil value when the execution is asynchronous. +Get the value of the :nasync argument and convert it." + (if-let ((binding (assq :nasync params))) + (pcase (cdr binding) + ((pred (not stringp)) + (error "Invalid value for :nasync argument")) + ((or "no" "n") nil) + ((or "yes" "y") t) + (_ (error "Invalid value for :nasync argument"))) + default)) + + + ;;;###autoload (defun org-babel-execute-src-block (&optional arg info params executor-type) "Execute the current source code block and return the result. -- 2.43.0
>From b0cdd3f5a9bd6e4e72adeac91f968741da95f98f Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Fri, 16 Feb 2024 14:32:22 +0100 Subject: [PATCH 3/8] ob-core async: Refactor handle-result [3/5] lisp/ob-core.el (org-babel-execute-src-block): Refactor the code to prepare for the next change: move the part handling the result in its own function `handle-result'. --- lisp/ob-core.el | 105 ++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/lisp/ob-core.el b/lisp/ob-core.el index d98626fe8..d1adba61c 100644 --- a/lisp/ob-core.el +++ b/lisp/ob-core.el @@ -1086,7 +1086,58 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) (make-directory d 'parents) d)))) (cmd (intern (concat "org-babel-execute:" lang))) - result exec-start-time) + (exec-start-time (current-time)) + (handle-result + (lambda (result) + (setq result + (if (and (eq (cdr (assq :result-type params)) 'value) + (or (member "vector" result-params) + (member "table" result-params)) + (not (listp result))) + (list (list result)) + result)) + (let ((file (and (member "file" result-params) + (cdr (assq :file params))))) + ;; If non-empty result and :file then write to :file. + (when file + ;; If `:results' are special types like `link' or + ;; `graphics', don't write result to `:file'. Only + ;; insert a link to `:file'. + (when (and result + (not (or (member "link" result-params) + (member "graphics" result-params)))) + (with-temp-file file + (insert (org-babel-format-result + result + (cdr (assq :sep params))))) + ;; Set file permissions if header argument + ;; `:file-mode' is provided. + (when (assq :file-mode params) + (set-file-modes file (cdr (assq :file-mode params))))) + (setq result file)) + ;; Possibly perform post process provided its + ;; appropriate. Dynamically bind "*this*" to the + ;; actual results of the block. + (let ((post (cdr (assq :post params)))) + (when post + (let ((*this* (if (not file) result + (org-babel-result-to-file + file + (org-babel--file-desc params result) + 'attachment)))) + (setq result (org-babel-ref-resolve post)) + (when file + (setq result-params (remove "file" result-params)))))) + (unless (member "none" result-params) + (org-babel-insert-result + result result-params info + ;; append/prepend cannot handle hash as we accumulate + ;; multiple outputs together. + (when (member "replace" result-params) new-hash) + lang + (time-subtract (current-time) exec-start-time))) + (run-hooks 'org-babel-after-execute-hook) + result)))) (unless (fboundp cmd) (error "No org-babel-execute function for %s!" lang)) (message "Executing %s %s %s..." @@ -1101,57 +1152,7 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) (if name (format "(%s)" name) (format "at position %S" (nth 5 info))))) - (setq exec-start-time (current-time) - result - (let ((r (save-current-buffer (funcall cmd body params)))) - (if (and (eq (cdr (assq :result-type params)) 'value) - (or (member "vector" result-params) - (member "table" result-params)) - (not (listp r))) - (list (list r)) - r))) - (let ((file (and (member "file" result-params) - (cdr (assq :file params))))) - ;; If non-empty result and :file then write to :file. - (when file - ;; If `:results' are special types like `link' or - ;; `graphics', don't write result to `:file'. Only - ;; insert a link to `:file'. - (when (and result - (not (or (member "link" result-params) - (member "graphics" result-params)))) - (with-temp-file file - (insert (org-babel-format-result - result - (cdr (assq :sep params))))) - ;; Set file permissions if header argument - ;; `:file-mode' is provided. - (when (assq :file-mode params) - (set-file-modes file (cdr (assq :file-mode params))))) - (setq result file)) - ;; Possibly perform post process provided its - ;; appropriate. Dynamically bind "*this*" to the - ;; actual results of the block. - (let ((post (cdr (assq :post params)))) - (when post - (let ((*this* (if (not file) result - (org-babel-result-to-file - file - (org-babel--file-desc params result) - 'attachment)))) - (setq result (org-babel-ref-resolve post)) - (when file - (setq result-params (remove "file" result-params)))))) - (unless (member "none" result-params) - (org-babel-insert-result - result result-params info - ;; append/prepend cannot handle hash as we accumulate - ;; multiple outputs together. - (when (member "replace" result-params) new-hash) - lang - (time-subtract (current-time) exec-start-time)))) - (run-hooks 'org-babel-after-execute-hook) - result))))))) + (funcall handle-result (save-current-buffer (funcall cmd body params)))))))))) (defun org-babel-expand-body:generic (body params &optional var-lines) "Expand BODY with PARAMS. -- 2.43.0
>From 019a0d2d8ba042606632e13976f8dfaeb37a8e74 Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Fri, 16 Feb 2024 14:32:45 +0100 Subject: [PATCH 4/8] ob-core async: Handle :nasync param [4/5] --- lisp/ob-core.el | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lisp/ob-core.el b/lisp/ob-core.el index d1adba61c..262218923 100644 --- a/lisp/ob-core.el +++ b/lisp/ob-core.el @@ -1085,7 +1085,10 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) (let ((d (file-name-as-directory (expand-file-name dir)))) (make-directory d 'parents) d)))) - (cmd (intern (concat "org-babel-execute:" lang))) + (async (org-babel--async-p params)) + (cmd (intern (concat "org-babel-" + (if async "schedule" "execute") + ":" lang))) (exec-start-time (current-time)) (handle-result (lambda (result) @@ -1139,8 +1142,8 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) (run-hooks 'org-babel-after-execute-hook) result)))) (unless (fboundp cmd) - (error "No org-babel-execute function for %s!" lang)) - (message "Executing %s %s %s..." + (error "No org-babel-execute function for %s: %s!" lang (symbol-name cmd))) + (message "Executing %s %s %s %s..." (capitalize lang) (pcase executor-type ('src-block "code block") @@ -1148,11 +1151,17 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) ('babel-call "call") ('inline-babel-call "inline call") (e (symbol-name e))) + (if async "async" "") (let ((name (nth 4 info))) (if name (format "(%s)" name) (format "at position %S" (nth 5 info))))) - (funcall handle-result (save-current-buffer (funcall cmd body params)))))))))) + (if (not async) + (funcall handle-result (save-current-buffer (funcall cmd body params))) + (let ((handle-feedback + (org-babel--async-feedbacks info handle-result result-params exec-start-time))) + (funcall cmd body params handle-feedback)))))))))) + (defun org-babel-expand-body:generic (body params &optional var-lines) "Expand BODY with PARAMS. -- 2.43.0
>From c17aaea885e3aa6087563d55045e74ce71557dbf Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Fri, 16 Feb 2024 14:33:09 +0100 Subject: [PATCH 5/8] ob-core async: Add :execute-with [5/5] --- lisp/ob-core.el | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lisp/ob-core.el b/lisp/ob-core.el index 262218923..64f434f71 100644 --- a/lisp/ob-core.el +++ b/lisp/ob-core.el @@ -1086,9 +1086,18 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) (make-directory d 'parents) d)))) (async (org-babel--async-p params)) - (cmd (intern (concat "org-babel-" - (if async "schedule" "execute") - ":" lang))) + (execute-with (let ((be (cdr (assq :execute-with params)))) + (when (equal be "none") (setq be nil)) + be)) + (cmd (intern (or (and execute-with + (concat execute-with "-" (if async "schedule" "execute"))) + (concat "org-babel-" + (if async "schedule" "execute") + ":" lang)))) + (cmd-args (let ((ps (list body params))) + (when execute-with + (setq ps (cons lang ps))) + ps)) (exec-start-time (current-time)) (handle-result (lambda (result) @@ -1157,10 +1166,10 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) (format "(%s)" name) (format "at position %S" (nth 5 info))))) (if (not async) - (funcall handle-result (save-current-buffer (funcall cmd body params))) + (funcall handle-result (save-current-buffer (apply cmd cmd-args))) (let ((handle-feedback (org-babel--async-feedbacks info handle-result result-params exec-start-time))) - (funcall cmd body params handle-feedback)))))))))) + (apply cmd (nconc cmd-args (list handle-feedback)))))))))))) (defun org-babel-expand-body:generic (body params &optional var-lines) -- 2.43.0
>From d5766eada2c31d0886f514f5ac6b38ad342e158f Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Fri, 16 Feb 2024 14:33:23 +0100 Subject: [PATCH 6/8] lisp/org-elib-async.el: New package about async helpers --- lisp/org-elib-async.el | 327 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 lisp/org-elib-async.el diff --git a/lisp/org-elib-async.el b/lisp/org-elib-async.el new file mode 100644 index 000000000..f0a1e4432 --- /dev/null +++ b/lisp/org-elib-async.el @@ -0,0 +1,327 @@ +;;; org-elib-async.el --- Helper to write asynchronous functions -*- lexical-binding: t -*- + +;; Copyright (C) 2024 Bruno BARBIER + +;; Author: Bruno BARBIER +;; Version: 0.0.0 +;; Maintainer: Bruno BARBIER +;; Keywords: +;; Status: WORK IN PROGRESS. DO NOT USE. +;; URL: +;; Compatibility: GNU Emacs 30.0.50 +;; +;; This file is NOT (yet) part of GNU Emacs. + +;; 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 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, write to the Free +;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +;; MA 02111-1307 USA + + +;;; Commentary: +;; Names with "--" are for functions and variables that are meant to be for +;; internal use only. + +;;;; Description +;; Some functions to help dealing with asynchronous tasks. + +;; The prefix 'org-elib' means that this package should evenutally be +;; moved into core Emacs. The functions defined here do NOT depend +;; nor rely on org itself. + +;;; TODOs +;; +;; - Keywords +;; + + +;;; Code: +;; +(require 'cl-lib) +(require 'org-id) + +;;;; Process +;; +(cl-defun org-elib-async-process (command &key input callback) + "Execute COMMAND. + +A quick naive featureless boggus wrapper around `make-process' to +receive the result when the process is done. + +When INPUT is non-nil, use it as the COMMAND standard input. Let DATA +be the COMMAND output, if COMMAND succeeds, call CALLBACK with +(:success DATA), else, call CALLBACK with (:failure DATA)." + (let* ((stdout-buffer (generate-new-buffer "*org-elib-async-process*")) + (get-outcome + (lambda (process) + (with-current-buffer stdout-buffer + (let* ((exit-code (process-exit-status process)) + (real-end ;; Getting rid of the user message. + (progn (goto-char (point-max)) + (forward-line -1) + (point))) + (txt (string-trim (buffer-substring-no-properties + (point-min) real-end)))) + (list (if (eq 0 exit-code) :success :failure) + (if (not (string-empty-p txt)) txt + (and (not (eq 0 exit-code)) exit-code))))))) + (process (make-process + :name "*org-elib-async-process*" + :buffer stdout-buffer + :command command + :connection-type 'pipe)) + (sentinel + (lambda (&rest _whatever) + (pcase (process-status process) + ('run ) + ('stop) + ((or 'exit 'signal) + (funcall callback (funcall get-outcome process))) + (_ (error "Not a real process")))))) + (add-function :after (process-sentinel process) sentinel) + (when input + (process-send-string process input) + (process-send-eof process)) + process)) +;; (org-elib-async-process (list "date") :callback (lambda (o) (message "outcome: %S" o))) +;; (org-elib-async-process (list "false") :callback (lambda (o) (message "outcome: %S" o))) +;; (org-elib-async-process (list "true") :callback (lambda (o) (message "outcome: %S" o))) +;; (org-elib-async-process (list "bash" "-c" "bash") :input "date" :callback (lambda (o) (message "outcome: %S" o))) +;; (org-elib-async-process (list "bash") :input "date" :callback (lambda (o) (message "outcome: %S" o))) +;; (org-elib-async-process (list "bash") :input "false" :callback (lambda (o) (message "outcome: %S" o))) +;; (org-elib-async-process (list "bash") :input "true" :callback (lambda (o) (message "outcome: %S" o))) +;; (org-elib-async-process (list "bash") :input "sleep 2; date" :callback (lambda (o) (message "outcome: %S" o))) + + +;;;; Wait for a process until some condition becomes true. + +(define-error 'org-elib-async-timeout-error + "Timeout waiting for a process.") + +(cl-defun org-elib-async-wait-condition ( cond-p + &key + (tick .3) (message "Waiting") + (nb_secs_between_messages 5) + timeout) + "Wait until the condition COND-P returns non-nil. +Repeatedly call COND-P with no arguments, about every TICK seconds, +until it returns a non-nil value. Return that non-nil value. When +TIMEOUT (seconds) is non-nil, raise an `org-elib-async-timeout-error' if +the COND-P is still nil after TIMEOUT seconds. Assume COND-P calls cost +0s. Do NOT block display updates. Do not block process outputs. Do +not block idle timers. Do block the user, letting him/her know why, but +do not display more messages than one every NB_SECS_BETWEEN_MESSAGES. +Default MESSAGE is \"Waiting\". Use 0.3s as the default for TICK." + ;; FIXME: Still not sure if it's possible to write such a function. + (let ((keep-waiting t) + (result nil) + (start (float-time)) + elapsed + last-elapsed) + (while keep-waiting + (setq result (funcall cond-p)) + (if result + (setq keep-waiting nil) + (sleep-for 0.01) + (redisplay :force) + (setq elapsed (- (float-time) start)) + (when (and timeout (> elapsed timeout)) + (signal 'org-timeout-error (list message elapsed))) + ;; Let the user know, without flooding the message area. + (if (and last-elapsed (> (- elapsed last-elapsed) nb_secs_between_messages)) + (message (format "%s ...(%.1fs)" message elapsed))) + (unless (sit-for tick :redisplay) + ;; Emacs has something to do; let it process new + ;; sub-processes outputs in case there are some. + (accept-process-output nil 0.01)) + (setq last-elapsed elapsed))) + result)) + + + +;;;; Comint: a FIFO queue of tasks with callbacks +;; org-elib-async-comint-queue executes tasks in a FIFO order. For each +;; task, it identifies the text output for that +;; task. org-elib-async-comint-queue does NOT remove prompts, or other +;; useless texts; this is the responsibility of the user. Currently, +;; org-elib-async-comint-queue assume it has the full control of the +;; session: no user interaction, no other direct modifications. + +(defvar-local org-elib-async-comint-queue--todo :NOT-SET + "A FIFO queue of pending executions.") + + +(defvar-local org-elib-async-comint-queue--unused-output "" + "Process output that has not been used yet.") + +(defvar-local org-elib-async-comint-queue--incoming-text "" + "Newly incoming text, added by the process filter, not yet handled.") + +(defvar-local org-elib-async-comint-queue--current-task nil + "The task that is currently running.") + +(defvar-local org-elib-async-comint-queue--process-filter-running nil + "non-nil when filter is running.") + +(defvar-local org-elib-async-comint-queue--incoming-timer nil + "A timer, when handling incoming text is scheduled or running.") + + +(defvar-local org-elib-async-comint-queue--handle-incoming-running + nil + "True when the incoming text handler is running.") + +(defun org-elib-async-comint-queue--handle-incoming () + (when org-elib-async-comint-queue--handle-incoming-running + (error "Bad call to handle-incoming: kill buffer %s!" (current-buffer))) + (setq org-elib-async-comint-queue--handle-incoming-running t) + + ;; Take the incoming text. + (setq org-elib-async-comint-queue--unused-output + (concat org-elib-async-comint-queue--unused-output + org-elib-async-comint-queue--incoming-text)) + (setq org-elib-async-comint-queue--incoming-text "") + + ;; Process the unused text with the queued tasks + (unless org-elib-async-comint-queue--current-task + (when org-elib-async-comint-queue--todo + (setq org-elib-async-comint-queue--current-task (pop org-elib-async-comint-queue--todo)))) + (when-let ((task org-elib-async-comint-queue--current-task)) + (let ((unused org-elib-async-comint-queue--unused-output) + (session-buffer (current-buffer)) + task-start) + (setq org-elib-async-comint-queue--unused-output + (with-temp-buffer + (insert unused) + (goto-char (point-min)) + (while (and task + (setq task-start (point)) + (search-forward (car task) nil t)) + (when (cdr task) + (let ((txt (buffer-substring-no-properties task-start + (- (point) (length (car task)))))) + (save-excursion (funcall (cdr task) txt)))) + (setq task (and (buffer-live-p session-buffer) + (with-current-buffer session-buffer (pop org-elib-async-comint-queue--todo))))) + (buffer-substring (point) (point-max)))) + (setq org-elib-async-comint-queue--current-task task))) + + ;; Signal that we are done. If we already have some new incoming text, + ;; reschedule to run. + (setq org-elib-async-comint-queue--incoming-timer + (if (string-empty-p org-elib-async-comint-queue--incoming-text) + nil + (org-elib-async-comint-queue--wake-up-handle-incoming))) + + ;; We reset it only on success. If it failed for some reason, the + ;; comint buffer is in an unknown state: you'll need to kill that + ;; buffer. + (setq org-elib-async-comint-queue--handle-incoming-running nil)) + + +(defun org-elib-async-comint-queue--wake-up-handle-incoming () + "Wake up the handling of incoming chunks of text. +Assume we are called from the comint buffer." + (setq org-elib-async-comint-queue--incoming-timer + (run-with-timer + 0.01 nil + (let ((comint-buffer (current-buffer))) + (lambda () + (with-local-quit + (with-current-buffer comint-buffer + (org-elib-async-comint-queue--handle-incoming)))))))) + + +(defun org-elib-async-comint-queue--process-filter (chunk) + "Accept the arbitrary CHUNK of text." + (setq org-elib-async-comint-queue--incoming-text + (concat org-elib-async-comint-queue--incoming-text + chunk)) + :; We delegate the real work outside the process filter, as it is + ; not reliable to do anything here. + (unless org-elib-async-comint-queue--incoming-timer + (org-elib-async-comint-queue--wake-up-handle-incoming))) + + + +(define-error 'org-elib-async-comint-queue-task-error + "Task failure.") + +(cl-defun org-elib-async-comint-queue--push (exec &key handle-feedback) + "Push the execution of EXEC into the FIFO queue. +When the task completed, call HANDLE-FEEDBACK with its outcome. Return +a function that waits for and return the result on succes, raise on +failure." + (let* ((tid (org-id-uuid)) + (start-tag (format "ORG-ELIB-ASYNC_START_%s" tid)) + (end-tag (format "ORG-ELIB-ASYNC_END___%s" tid)) + (result-sb (make-symbol "result")) + (on-start + (lambda (_) + ;; TODO: Use (point) in session to link back to it. + (when handle-feedback + (funcall handle-feedback '(:pending "running"))))) + (on-result + (lambda (result) + ;; Get the result, and report success using HANDLE-FEEDBACK. + ;; If something fails, report failure using HANDLE-FEEDBACK. + (unwind-protect + (let ((outcome + (condition-case-unless-debug exc + (list :success (funcall exec :post-process result)) + (error (list :failure exc))))) + (when handle-feedback (save-excursion (funcall handle-feedback outcome))) + (set result-sb outcome)) + (funcall exec :finally))))) + + ;; TODO: Add detect-properties => alist of properties that can be used: PS1 and PS2 + (let ((comint-buffer (funcall exec :get-comint-buffer))) + (with-current-buffer comint-buffer + (setq org-elib-async-comint-queue--todo + (nconc org-elib-async-comint-queue--todo + (list (cons start-tag on-start) + (cons end-tag on-result)))) + (funcall exec :send-instrs-to-session + (funcall exec :instrs-to-enter)) + (funcall exec :send-instrs-to-session + (funcall exec :instr-to-emit-tag start-tag)) + (funcall exec :send-instrs-to-session + (funcall exec :get-code)) + (funcall exec :send-instrs-to-session + (funcall exec :instr-to-emit-tag end-tag)) + (funcall exec :send-instrs-to-session + (funcall exec :instrs-to-exit)) + + (lambda () + (org-elib-async-wait-condition (lambda () + (boundp result-sb))) + (pcase (symbol-value result-sb) + (`(:success ,r) r) + (`(:failure ,err) (signal (car err) (cdr err))))) + )))) + + + +(defun org-elib-async-comint-queue-init-if-needed (buffer) + "Initialize the FIFO queue in BUFFER if needed." + (with-current-buffer buffer + (unless (local-variable-p 'org-elib-async-comint-queue--todo) + (setq-local org-elib-async-comint-queue--todo nil) + (add-hook 'comint-output-filter-functions + #'org-elib-async-comint-queue--process-filter nil :local)))) + + + +;;;; Provide +(provide 'org-elib-async) +;;; org-elib-async.el ends here -- 2.43.0
>From 1129770cc62b0580c71ba2a3f6a94f6fd18574b3 Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Fri, 16 Feb 2024 14:33:40 +0100 Subject: [PATCH 7/8] lisp/ob-core.el: Notify when execution fails lisp/ob-core.el (org-babel-popup-failure-details): New function. (org-babel-execute-src-block): For synchronous execution, use `org-babel-popup-failure-details' on failure. (org-babel--async-feedbacks): Add call to `org-babel-popup-failure-details' on user request. --- lisp/ob-core.el | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/lisp/ob-core.el b/lisp/ob-core.el index 64f434f71..e8f9e9ad9 100644 --- a/lisp/ob-core.el +++ b/lisp/ob-core.el @@ -917,7 +917,17 @@ (defun org-babel--async-feedbacks (info handle-result (if (consp msg) (car msg) (format "%s" msg)) (short-version-of msg))) - 'face (org-babel--async-status-face status)))) + 'face (org-babel--async-status-face status))) + (when (eq :failure status) + (overlay-put ovl-title + 'keymap + (let ((km (make-sparse-keymap))) + (define-key km (kbd "<mouse-1>") + (lambda () + "Display failure details." + (interactive) + (org-babel-popup-failure-details msg))) + km)))) (remove-previous-overlays () ;; Remove previous title and body overlays. (mapc (lambda (ovl) @@ -1166,11 +1176,26 @@ (defun org-babel-execute-src-block (&optional arg info params executor-type) (format "(%s)" name) (format "at position %S" (nth 5 info))))) (if (not async) - (funcall handle-result (save-current-buffer (apply cmd cmd-args))) + (let ((res-sb (make-symbol "result"))) + (condition-case exc + (set res-sb (save-current-buffer (apply cmd cmd-args))) + (error (org-babel-popup-failure-details exc))) + (when (boundp res-sb) + (funcall handle-result (symbol-value res-sb)))) (let ((handle-feedback (org-babel--async-feedbacks info handle-result result-params exec-start-time))) (apply cmd (nconc cmd-args (list handle-feedback)))))))))))) +(defun org-babel-popup-failure-details (exc) + "Notify/display" + (when-let ((buf (get-buffer org-babel-error-buffer-name))) + (with-current-buffer buf (erase-buffer))) + (org-babel-eval-error-notify + 127 ; Don't have exit-code + (if (consp exc) + (format "%s\n%s\n" (car exc) (cdr exc)) + (format "%s\n" exc)))) + (defun org-babel-expand-body:generic (body params &optional var-lines) "Expand BODY with PARAMS. -- 2.43.0
>From 2078be0ebd741127c383a386f672dbc3d741206d Mon Sep 17 00:00:00 2001 From: Bruno BARBIER <brubar...@gmail.com> Date: Wed, 21 Feb 2024 15:54:05 +0100 Subject: [PATCH 8/8] scratch/bba-ob-core-async: Some temporary test files --- scratch/bba-ob-core-async/my-async-tests.el | 339 ++++++++ scratch/bba-ob-core-async/my-async-tests.org | 820 +++++++++++++++++++ 2 files changed, 1159 insertions(+) create mode 100644 scratch/bba-ob-core-async/my-async-tests.el create mode 100644 scratch/bba-ob-core-async/my-async-tests.org diff --git a/scratch/bba-ob-core-async/my-async-tests.el b/scratch/bba-ob-core-async/my-async-tests.el new file mode 100644 index 000000000..918a4b142 --- /dev/null +++ b/scratch/bba-ob-core-async/my-async-tests.el @@ -0,0 +1,339 @@ +;;; my-async-tests.el --- Scratch/temporary file: some tests about async -*- lexical-binding: t -*- + +;; Copyright (C) 2024 Bruno BARBIER + +;; Author: Bruno BARBIER +;; Status: Temporary tests. +;; Compatibility: GNU Emacs 30.0.50 +;; +;; This file is NOT part of GNU Emacs. + +;; 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 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, write to the Free +;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +;; MA 02111-1307 USA + +(require 'cl-lib) +(require 'org) +(require 'org-elib-async) + +(require 'ob-shell) +(require 'ob-python) + +;;; Shells +;; + +;;;; One shell script + +;; Standalone direct asynchronous execution. + +(defun my-shell-babel-schedule (lang body _params handle-feedback) + "Execute the bash script BODY. +Execute the shell script BODY using bash. Use HANDLE-FEEDBACK to report +the outcome (success or failure)." + (unless (equal "bash" lang) + (error "Only for bash")) + (funcall handle-feedback (list :pending "started")) + (org-elib-async-process (list "bash") :input body :callback handle-feedback)) + + +;;;; Asynchronous using ob-shell + +(defun my-org-babel-shell-how-to-execute (body params) + "Return how to execute BODY using a POSIX shell. +Return how to execute, as expected by +`org-elib-async-comint-queue--execution'." + ;; Code mostly extracted from ob-shell, following + ;; `org-babel-execute:shell' and `org-babel-sh-evaluate'. + ;; Results are expected to differ from ob-shell as we follow the + ;; same process for all execution paths: asynchronous or not, with + ;; session or without. + (let* ((session (org-babel-sh-initiate-session + (cdr (assq :session params)))) + (stdin (let ((stdin (cdr (assq :stdin params)))) + (when stdin (org-babel-sh-var-to-string + (org-babel-ref-resolve stdin))))) + (result-params (cdr (assq :result-params params))) + (value-is-exit-status + (or (and + (equal '("replace") result-params) + (not org-babel-shell-results-defaults-to-output)) + (member "value" result-params))) + (cmdline (cdr (assq :cmdline params))) + (shebang (cdr (assq :shebang params))) + (full-body (concat + (org-babel-expand-body:generic + body params (org-babel-variable-assignments:shell params)) + (when value-is-exit-status "\necho $?"))) + (post-process + (lambda (r) + (setq r (org-trim r)) + (org-babel-reassemble-table + (org-babel-result-cond result-params + r + (let ((tmp-file (org-babel-temp-file "sh-"))) + (with-temp-file tmp-file (insert r)) + (org-babel-import-elisp-from-file tmp-file))) + (org-babel-pick-name + (cdr (assq :colname-names params)) (cdr (assq :colnames params))) + (org-babel-pick-name + (cdr (assq :rowname-names params)) (cdr (assq :rownames params)))))) + comint-buffer + finally + to-run) + + (setq comint-buffer + (if session session + ;; No session. We create a temporary one and use 'finally' to + ;; destroy it once we are done. + ;; + ;; FIXME: This session code should be refactored and moved into + ;; ob-core. + (let ((s-buf (org-babel-sh-initiate-session + (generate-new-buffer-name (format "*ob-shell-no-session*"))))) + (setq finally (lambda () + ;; We cannot delete it immediately as we are called from it. + (run-with-idle-timer + 0.1 nil + (lambda () + (when (buffer-live-p s-buf) + (let ((kill-buffer-query-functions nil) + (kill-buffer-hook nil)) + (kill-buffer s-buf))))))) + s-buf))) + + (org-elib-async-comint-queue-init-if-needed comint-buffer) + + (setq to-run + (cond + ((or stdin cmdline) ; external shell script w/STDIN + (let ((script-file (org-babel-temp-file "sh-script-")) + (stdin-file (org-babel-temp-file "sh-stdin-")) + (padline (not (string= "no" (cdr (assq :padline params)))))) + (with-temp-file script-file + (when shebang (insert shebang "\n")) + (when padline (insert "\n")) + (insert full-body)) + (set-file-modes script-file #o755) + (with-temp-file stdin-file (insert (or stdin ""))) + (with-temp-buffer + (with-connection-local-variables + (concat + (mapconcat #'shell-quote-argument + (cons (if shebang (file-local-name script-file) + shell-file-name) + (if shebang (when cmdline (list cmdline)) + (list shell-command-switch + (concat (file-local-name script-file) " " cmdline)))) + " ") + "<" (shell-quote-argument stdin-file)))))) + (session ; session evaluation + full-body) + ;; External shell script, with or without a predefined + ;; shebang. + ((org-string-nw-p shebang) + (let ((script-file (org-babel-temp-file "sh-script-")) + (padline (not (equal "no" (cdr (assq :padline params)))))) + (with-temp-file script-file + (insert shebang "\n") + (when padline (insert "\n")) + (insert full-body)) + (set-file-modes script-file #o755) + (if (file-remote-p script-file) + ;; Run remote script using its local path as COMMAND. + ;; The remote execution is ensured by setting + ;; correct `default-directory'. + (let ((default-directory (file-name-directory script-file))) + (file-local-name script-file) + script-file "")))) + (t + (let ((script-file (org-babel-temp-file "sh-script-"))) + (with-temp-file script-file + (insert full-body)) + (set-file-modes script-file #o755) + (mapconcat #'shell-quote-argument + (list shell-file-name + shell-command-switch + (if (file-remote-p script-file) + (file-local-name script-file) + script-file)) + " "))))) + ;; TODO: How to handle `value-is-exit-status'? + (lambda (&rest q) + (pcase q + (`(:instrs-to-enter) + ;; FIXME: This is wrong. + "export PS1=''; export PS2='';") + (`(:instrs-to-exit)) + (`(:finally) (when finally (funcall finally))) + (`(:instr-to-emit-tag ,tag) (format "printf '%s\\n'" tag)) + (`(:post-process ,r) (when post-process (funcall post-process r))) + (`(:send-instrs-to-session ,code) + (with-current-buffer comint-buffer + (when code + (goto-char (point-max)) + (insert code) (insert "\n") + (comint-send-input nil t)))) + (`(:get-code) to-run) + (`(:get-comint-buffer) comint-buffer) + (_ (error "Unknown query")))))) + + + + + +;;; Python +;; + +;;;; Asynchronous using ob-python + +(defun my-org-babel-python-how-to-execute (body params) + "Return how to execute BODY using python. +Return how to execute, as expected by +`org-elib-async-comint-queue--execution'." + ;; Code mostly extracted from ob-python, following + ;; `org-babel-python-evaluate-session'. + ;; Results are expected to differ from ob-python as we follow the + ;; same process for all execution paths: asynchronous or not, with + ;; session or without. + (let* ((org-babel-python-command + (or (cdr (assq :python params)) + org-babel-python-command)) + (session-key (org-babel-python-initiate-session + (cdr (assq :session params)))) + (graphics-file (and (member "graphics" (assq :result-params params)) + (org-babel-graphical-output-file params))) + (result-params (cdr (assq :result-params params))) + (result-type (cdr (assq :result-type params))) + (results-file (when (eq 'value result-type) + (or graphics-file + (org-babel-temp-file "python-")))) + (return-val (when (eq result-type 'value) + (cdr (assq :return params)))) + (full-body + (concat + (org-babel-expand-body:generic + body params + (org-babel-variable-assignments:python params)) + (when return-val + (format "\n%s" return-val)))) + (post-process + (lambda (r) + (setq r (string-trim r)) + (when (string-prefix-p "Traceback (most recent call last):" r) + (signal 'user-error (list r))) + (when (eq 'value result-type) + (setq r (org-babel-eval-read-file results-file))) + (org-babel-reassemble-table + (org-babel-result-cond result-params + r + (org-babel-python-table-or-string r)) + (org-babel-pick-name (cdr (assq :colname-names params)) + (cdr (assq :colnames params))) + (org-babel-pick-name (cdr (assq :rowname-names params)) + (cdr (assq :rownames params)))))) + (tmp-src-file (org-babel-temp-file "python-")) + (session-body + ;; The real code we evaluate in the session. + (pcase result-type + (`output + (format (string-join + (list "with open('%s') as f:\n" + " exec(compile(f.read(), f.name, 'exec'))\n")) + (org-babel-process-file-name + tmp-src-file 'noquote))) + (`value + ;; FIXME: In this case, any output is an error. + (org-babel-python-format-session-value + tmp-src-file results-file result-params)))) + comint-buffer + finally) + + + (unless session-key + ;; No session. We create a temporary one and use 'finally' to + ;; destroy it once we are done. + ;; + ;; FIXME: This session code should be refactored and moved into + ;; ob-core. + (setq session-key (org-babel-python-initiate-session + ;; We can't use a simple `generate-new-buffer' + ;; due to the earmuffs game. + (org-babel-python-without-earmuffs + (format "*ob-python-no-session-%s*" (org-id-uuid))))) + (setq finally (lambda () + (when-let ((s-buf + (get-buffer (org-babel-python-with-earmuffs session-key)))) + ;; We cannot delete it immediately as we are called from it. + (run-with-idle-timer + 0.1 nil + (lambda () + (when (buffer-live-p s-buf) + (let ((kill-buffer-query-functions nil) + (kill-buffer-hook nil)) + (kill-buffer s-buf))))))))) + + (setq comint-buffer + (get-buffer (org-babel-python-with-earmuffs session-key))) + (org-elib-async-comint-queue-init-if-needed comint-buffer) + (with-temp-file tmp-src-file + (insert (if (and graphics-file (eq result-type 'output)) + (format org-babel-python--output-graphics-wrapper + full-body graphics-file) + full-body))) + + (lambda (&rest q) + (pcase q + (`(:instrs-to-enter) + ;; FIXME: This is wrong. + "import sys; sys.ps1=''; sys.ps2=''") + (`(:instrs-to-exit)) + (`(:finally) (when finally (funcall finally))) + (`(:instr-to-emit-tag ,tag) (format "print ('%s')" tag)) + (`(:post-process ,r) (when post-process (funcall post-process r))) + (`(:send-instrs-to-session ,code) + ;; See org-babel-python-send-string + (with-current-buffer comint-buffer + (let ((python-shell-buffer-name + (org-babel-python-without-earmuffs session-key))) + (python-shell-send-string (concat code "\n"))))) + (`(:get-code) session-body) + (`(:get-comint-buffer) comint-buffer) + (_ (error "Unknown query")))))) + + +;;; Org babel 'execute-with'. + + +;;;; Asynchronous +;; +(defun my-org-babel-schedule (lang body params handle-feedback) + "Schedule the execution of BODY according to PARAMS. +This function is called by `org-babel-execute-src-block'. Return a +function that waits and returns the result on success, raise on failure." + (let ((exec (pcase lang + ("python" (my-org-babel-python-how-to-execute body params)) + ("bash" (my-org-babel-shell-how-to-execute body params)) + (_ (error "Not handled (yet): %s" lang))))) + (org-elib-async-comint-queue--push exec :handle-feedback handle-feedback))) + + +;;;; Synchronous +;; +(defun my-org-babel-execute (lang body params) + "Execute Python BODY according to PARAMS. +This function is called by `org-babel-execute-src-block'." + ;; We just start the asynchronous execution, wait for it, and return + ;; the result (or raise the exception). No custom code, and, + ;; synchronous and asynchronous should just mix nicely together. + (funcall (my-org-babel-schedule lang body params nil))) diff --git a/scratch/bba-ob-core-async/my-async-tests.org b/scratch/bba-ob-core-async/my-async-tests.org new file mode 100644 index 000000000..08284c470 --- /dev/null +++ b/scratch/bba-ob-core-async/my-async-tests.org @@ -0,0 +1,820 @@ +#+PROPERTY: HEADER-ARGS+ :eval no-export :exports both +* Intro + +An org document with code blocks to help test the proposed patchs. + +See [[*On top of ob-shell][On top of ob-shell]] for asynchronous shell scripts using ob-shell. + +See [[*On top of ob-python][On top of ob-python]] for asynchronous python scripts using ob-python. + +You need to load: + #+begin_src elisp :results silent + (setq-local org-confirm-babel-evaluate nil) + (load-file "my-async-tests.el") + #+end_src + + +Emacs and org versions: + #+begin_src elisp + (mapcar (lambda (sb) (list sb (symbol-value sb))) + '(emacs-version org-version)) + #+end_src + + #+RESULTS: + | emacs-version | 30.0.50 | + | org-version | 9.7-pre | + +Note that we've disabled eval on export: export doesn't know it needs +to wait for asynchronous results. + +* POSIX shells +** A simple bash example + :PROPERTIES: + :header-args:bash: :execute-with my-shell-babel :nasync yes + :END: + +The package `my-async-tests.el' contains the function +`my-shell-babel-schedule' to evaluate shell script asynchronously. + +The header-args properties above request asynchronous execution for +bash (:nasync yes), and, tells ob-core to use the prefix +`my-shell-babel' when looking for functions to evaluate a source +block. Thus, org will delegate execution to `my-shell-babel-schedule'. +We don't have `my-shell-babel-execute', so, in this case, :nasync must +be yes. + +Examples taken from the org mailing list and from worg. + +A simple execution: + #+begin_src bash + date + #+end_src + + #+RESULTS: + : Wed Feb 21 15:56:23 CET 2024 + +A tricky computation takes some time: + #+begin_src bash + sleep 1; date + #+end_src + + #+RESULTS: + : Wed Feb 21 15:56:25 CET 2024 + +An example of a failure: + #+begin_src bash + sleepdd 1; false + #+end_src + + #+RESULTS: + +** On top of ob-shell + :PROPERTIES: + :header-args:bash: :execute-with my-org-babel :nasync yes + :header-args:bash+: :session sh-async + :END: + + #+begin_src bash + sleep 1; date + #+end_src + + #+RESULTS: + : Wed Feb 21 15:56:42 CET 2024 + + + #+begin_src bash :results output :session *test* :nasync yes + cd /tmp + echo "hello world" + #+end_src + + #+RESULTS: + : hello world + + #+begin_src bash :results output + # comment + # comment + #+end_src + + #+RESULTS: + + #+begin_src bash :results output + # print message + echo \"hello world\" + #+end_src + + #+RESULTS: + : "hello world" + + #+begin_src bash :results output + echo "hello" + echo "world" + #+end_src + + #+RESULTS: + : hello + : world + + + #+begin_src bash :results output + echo PID: "$$" + #+end_src + + #+RESULTS: + : PID: 22212 + + #+begin_src bash :results output + echo PID: "$$" + #+end_src + + #+RESULTS: + : PID: 22212 + + + #+begin_src bash :results output :session shared + echo PID: "$$" + X=5 + #+end_src + + #+RESULTS: + : PID: 22218 + + #+begin_src bash :results output :session shared + echo PID: "$$" + echo X was set to "$X" + #+end_src + + #+RESULTS: + : PID: 22218 + : X was set to 5 + + #+begin_src bash :nasync yes :results value scalar + echo "Execute session blocks in the background" + sleep 3 + echo "Using the :async header" + #+end_src + + #+RESULTS: + : Execute session blocks in the background + : Using the :async header + : 0 + + + + #+name: their-os + Linux + + + #+begin_src bash :results output :shebang #!/usr/bin/env bash :stdin their-os :cmdline RMS :tangle ask_for_os.sh + + # call as ./ask_for_os.sh NAME, where NAME is who to ask + + if [ -z "$1" ]; then + asked="$USER" + else + asked="$1" + fi + + echo Hi, "$asked"! What operating system are you using? + read my_os + + if [ "$asked" = "RMS" ]; then + echo You\'re using GNU/"$my_os"! + elif [ "$asked" = "Linus" ]; then + echo You\'re using "$my_os"! + else + echo You\'re using `uname -o`! + fi + #+end_src + + #+RESULTS: + : Hi, RMS! What operating system are you using? + : You're using GNU/Linux! + + + + #+begin_src bash + declare -a array + + m=4 + n=3 + for ((i=0; i<m; i++)) + do + for ((j=0; j<n; j++)) + do + a[${i},${j}]=$RANDOM + done + done + for ((i=0; i<m; i++)) + do + for ((j=0; j<n; j++)) + do + echo -ne "${a[${i},${j}]}\t" + done + echo + done + #+end_src + + #+RESULTS: + | 7592 | 13920 | 4911 | + | 7592 | 13920 | 4911 | + | 7592 | 13920 | 4911 | + | 7592 | 13920 | 4911 | + + + #+begin_src bash :results list + declare -a array + + m=4 + n=3 + for ((i=0; i<m; i++)) + do + for ((j=0; j<n; j++)) + do + a[${i},${j}]=$RANDOM + done + done + for ((i=0; i<m; i++)) + do + for ((j=0; j<n; j++)) + do + echo -ne "${a[${i},${j}]}\t" + done + echo + done + #+end_src + + #+RESULTS: + - 17593 + 384 + 1439 + - 17593 + 384 + 1439 + - 17593 + 384 + 1439 + - 17593 + 384 + 1439 + + #+begin_src bash :results file :file my_output.txt + declare -a array + + m=4 + n=3 + for ((i=0; i<m; i++)) + do + for ((j=0; j<n; j++)) + do + a[${i},${j}]=$RANDOM + done + done + for ((i=0; i<m; i++)) + do + for ((j=0; j<n; j++)) + do + echo -ne "${a[${i},${j}]}\t" + done + echo + done + #+end_src + + #+RESULTS: + [[file:my_output.txt]] + + + #+begin_src bash :results output + cat my_output.txt + #+end_src + + #+RESULTS: + : 30132 16194 19934 + : 30132 16194 19934 + : 30132 16194 19934 + : 30132 16194 19934 + + + #+begin_src bash :results output :dir /ssh:phone: :session none + if [ ! -e "foo_file" ]; + then + echo "foo" > foo_file + echo "Created foo_file" + else + echo "foo_file already exists!" + fi + #+end_src + + #+RESULTS: + : foo_file already exists! + + + #+begin_src bash :results output :dir /ssh:phone: :session *remote* + if [ ! -e "foo_file" ]; + then + echo "foo" > foo_file + echo "Created foo_file" + else + echo "foo_file already exists!" + fi + #+end_src + + #+RESULTS: + : Created foo_file + + + #+RESULTS: + + + #+begin_src bash :results none :session *my-session* + X=1 + #+end_src + + #+RESULTS: + + #+begin_src bash :results output :session *my-session* + echo X was set to "$X" + #+end_src + + #+RESULTS: + : X was set to 1 + + #+begin_src bash :results output :session *another-session* + echo X was set to "$X" + #+end_src + + #+RESULTS: + : X was set to + + #+RESULTS: + + + #+begin_src bash :results output + echo "Hello, world!" + sleep 3 + echo "Good-bye, cruel World..." + #+end_src + + #+RESULTS: + : Hello, world! + : Good-bye, cruel World... + + + + #+begin_src bash :var by_two=0 x=3 :session none + if [ "$by_two" = "0" ]; then + echo $(($x * 2)) + else + echo $(($x * 3)) + fi + #+end_src + + #+RESULTS: + : 6 + + + #+begin_src bash :results output :var arr='("apple" "banana" "cherry") + echo The first element is... + echo \"${arr[1]}\" + #+end_src + + #+RESULTS: + : The first element is... + : "banana" + +*** TODO Doesn't work yet: asynchronous that depends from other blocks + #+name: multiply_by_2 + #+begin_src bash :var data="" :results output + echo $(($data * 2)) + #+end_src + + #+RESULTS: multiply_by_2 + : bash: * 2: syntax error: operand expected (error token is "* 2") + + #+begin_src bash :post multiply_by_2(data=*this*) + echo 3 + #+end_src + + #+results: + + +* On top of ob-python + :PROPERTIES: + :header-args:python: :execute-with my-org-babel :nasync yes + :header-args:python+: :session py-async + :END: + +Used =header-args= properties: + - =:execute-with my-org-babel=: look for functions with the prefix `my-org-babel' to execute + blocks (for the asynchronous case use + `my-org-babel-schedule', and, for the synchronous case + `my-org-babel-execute'). These functions are defined in [[file:my-async-tests.el]]. + + - =:nasync yes=: by default, execute asynchronously (use `my-org-babel-schedule'). + + - =:session py-async= by default, use a session named "py-async". + +** basic examples +*** async with a session +A very simple test: + #+begin_src python + 2+3 + #+end_src + + #+RESULTS: + : 5 + +Let's import the module time in our session. + #+begin_src python :results silent + import time + #+end_src + + #+RESULTS: + +(Yes, =:results silent= needs some work.) + + + +A table that requires some time to compute: + #+begin_src python + start = time.time() + time.sleep(1) + end = time.time() + ["%.1fs" % t for t in [start, end, end-start]] + #+end_src + + #+RESULTS: + | 1708515666.8s | 1708515667.8s | 1.0s | + + +An error (click on the error , <mouse-1>, to see the details): + #+begin_src python + 2/0 + #+end_src + + #+RESULTS: + + +*** async with no session + :PROPERTIES: + :header-args:python+: :session none + :END: + +A very simple test: + #+begin_src python + 2+3 + #+end_src + + #+RESULTS: + : 5 + +Let's import the module time in our session. + #+begin_src python :results silent + import time + #+end_src + + #+RESULTS: + +(Yes, =:results silent= needs some work.) + + + +A table that requires some time to compute: + #+begin_src python + start = time.time() + time.sleep(1) + end = time.time() + ["%.1fs" % t for t in [start, end, end-start]] + #+end_src + + #+RESULTS: + | 1708083470.9s | 1708083471.9s | 1.0s | + +Yes, it failed, as expected. "import time" was done in its own +temporary session. The old result is preserved; the error is display +as an overlay. Click on it to get more info about the error. + + +Let's fix it, adding the import line: + #+begin_src python + import time + start = time.time() + time.sleep(1) + end = time.time() + ["%.1fs" % t for t in [start, end, end-start]] + #+end_src + + #+RESULTS: + | 1708515915.0s | 1708515916.0s | 1.0s | + + +An error (click on the error , <mouse-1>, to see the details): + #+begin_src python + 2/0 + #+end_src + + #+RESULTS: + + + +*** sync with a session + :PROPERTIES: + :header-args:python+: :session py-sync-session :nasync no + :END: + +A very simple test: + #+begin_src python + 2+3 + #+end_src + + #+RESULTS: + : 5 + +Let's import the module time in our session. + #+begin_src python :results silent + import time + #+end_src + +(Yes, =:results silent= needs some work.) + + + +A table that requires some time to compute: + #+begin_src python + start = time.time() + time.sleep(1) + end = time.time() + ["%.1fs" % t for t in [start, end, end-start]] + #+end_src + + #+RESULTS: + | 1708102997.5s | 1708102998.5s | 1.0s | + + + +An error (click on the error , <mouse-1>, to see the details): + #+begin_src python + 2/0 + #+end_src + + #+RESULTS: + + +*** sync with no session + :PROPERTIES: + :header-args:python+: :session none :nasync no + :END: + +A very simple test: + #+begin_src python + 2+3 + #+end_src + + #+RESULTS: + : 5 + +Let's import the module time in our session. + #+begin_src python :results silent + import time + #+end_src + +(Yes, =:results silent= needs some work.) + + + +A table that requires some time to compute: + #+begin_src python + start = time.time() + time.sleep(1) + end = time.time() + ["%.1fs" % t for t in [start, end, end-start]] + #+end_src + + #+RESULTS: + | 1708083470.9s | 1708083471.9s | 1.0s | + +Yes, that fails (no session), displaying the details in a popup. Let's +fix it: + #+begin_src python + import time + start = time.time() + time.sleep(1) + end = time.time() + ["%.1fs" % t for t in [start, end, end-start]] + #+end_src + + #+RESULTS: + | 1708103039.0s | 1708103040.0s | 1.0s | + + + +An error (click on the error , <mouse-1>, to see the details): + #+begin_src python + 2/0 + #+end_src + + #+RESULTS: + + +** worg examples + +Let's import matplotlib in our session. + + #+begin_src python + import matplotlib + import matplotlib.pyplot as plt + #+end_src + + #+RESULTS: + : None + +A figure in a PDF, asynchronous case. + #+begin_src python :results file link + fig=plt.figure(figsize=(3,2)) + plt.plot([1,3,2]) + fig.tight_layout() + + fname = 'myfig-async.pdf' + plt.savefig(fname) + fname # return this to org-mode + #+end_src + + #+RESULTS: + [[file:myfig-async.pdf]] + + +A figure in a PDF, synchronous case. + #+begin_src python :results file link :nasync no + fig=plt.figure(figsize=(3,2)) + plt.plot([1,3,2]) + fig.tight_layout() + + fname = 'myfig-sync.pdf' + plt.savefig(fname) + fname # return this to org-mode + #+end_src + + #+RESULTS: + [[file:myfig-sync.pdf]] + + + +A PNG figure, asynchronous case. + #+begin_src python :results graphics file output :file boxplot.png + fig=plt.figure(figsize=(3,2)) + plt.plot([1,3,2]) + fig.tight_layout() + fig + #+end_src + + #+RESULTS: + [[file:boxplot.png]] + +Same, but using the =:return= keyword. + #+begin_src python :return "plt.gcf()" :results graphics file output :file boxplot.png + fig=plt.figure(figsize=(3,2)) + plt.plot([1,3,2]) + fig.tight_layout() + #+end_src + + #+RESULTS: + [[file:boxplot.png]] + +Same, asynchronous but without a session this time. + #+begin_src python :return "plt.gcf()" :results graphics file output :file boxplot-no-sess-a-y.png :session none + import matplotlib + import matplotlib.pyplot as plt + fig=plt.figure(figsize=(3,2)) + plt.plot([1,3,2]) + fig.tight_layout() + #+end_src + + #+RESULTS: + [[file:boxplot-no-sess-a-y.png]] + + +Lists are table, + #+begin_src python + [1,2,3] + #+end_src + + #+RESULTS: + | 1 | 2 | 3 | + +unless requested otherwise. + #+begin_src python :results verbatim + [1,2,3] + #+end_src + + #+RESULTS: + : [1, 2, 3] + + +Dictionaries are tables too. + #+begin_src python :results table + {"a": 1, "b": 2} + #+end_src + + #+RESULTS: + | a | 1 | + | b | 2 | + + +Let's try the example with Panda. + #+begin_src python :results none + import pandas as pd + import numpy as np + #+end_src + + #+RESULTS: + : None + + #+begin_src python :results table + pd.DataFrame(np.array([[1,2,3],[4,5,6]]), + columns=['a','b','c']) + #+end_src + + #+RESULTS: + | | a | b | c | + |---+---+---+---| + | 0 | 1 | 2 | 3 | + | 1 | 4 | 5 | 6 | + +And the synchronous case? + + #+begin_src python :results table :nasync no + pd.DataFrame(np.array([[1,2,3],[4,5,6]]), + columns=['a','b','c']) + #+end_src + + #+RESULTS: + | | a | b | c | + |---+---+---+---| + | 0 | 1 | 2 | 3 | + | 1 | 4 | 5 | 6 | + + + +Without session ? + + #+begin_src python :results table :session none + pd.DataFrame(np.array([[1,2,3],[4,5,6]]), + columns=['a','b','c']) + #+end_src + + #+RESULTS: + | | a | b | c | + |---+---+---+---| + | 0 | 1 | 2 | 3 | + | 1 | 4 | 5 | 6 | + +Right, we need to import the libraries (no session). + + #+begin_src python :results table :session none + import pandas as pd + import numpy as np + pd.DataFrame(np.array([[1,2,3],[4,5,6]]), + columns=['a','b','c']) + #+end_src + + #+RESULTS: + | | a | b | c | + |---+---+---+---| + | 0 | 1 | 2 | 3 | + | 1 | 4 | 5 | 6 | + + +** inline examples + + A simple asynchronous inline src_python{3*2} {{{results(=6=)}}}. + + An other one containing a mistake src_python{2/0} {{{results(=6=)}}} + (click on the error to see the details). + + + Some very slow inline asynchronous computations that all run in + the same session. You need to execute the 3 of them at once. Here + is the first one src_python[:return "\"OK1\""]{import time; + time.sleep(5)} {{{results(=OK1=)}}} and a second one + src_python[:return "\"OK1 bis\""]{import time; time.sleep(5)} + {{{results(=OK1 bis=)}}} and the third one src_python[:return + "\"OK2\""]{import time; time.sleep(5)} {{{results(=OK2=)}}}. + + Yes, the previous paragraph is unreadable; it's on purpose, to + check that ob-core can figure it out. + + Let's repeat, in a more readable way, and making the last one + synchronous. + + Some very slow inline computations that all run in the same + session. Here is the first asynchronous one + src_python[:return"\"OK1\""]{import time; time.sleep(5)} {{{results(=None=)}}} + and a second one, asynchronous too: + src_python[:return "\"OK1 bis\""]{import time; time.sleep(5)} {{{results(=OK1 bis=)}}} + and finally, a third one, synchronous this one: + src_python[:nasync no :return "\"OK2\""]{import time; time.sleep(5)} {{{results(=OK2=)}}}. + + Note that, once the user executes the last synchronous block, the + user is blocked until the synchronous execution can start + (i.e. all previous asynchronous executions are done) and until + it's done. The display is updated though, to see the asynchronous + progress. -- 2.43.0