/ docker-process.el
docker-process.el
  1  ;;; docker-process.el --- Docker process  -*- lexical-binding: t -*-
  2  
  3  ;; Author: Philippe Vaucher <philippe.vaucher@gmail.com>
  4  
  5  ;; This file is NOT part of GNU Emacs.
  6  
  7  ;; This program is free software; you can redistribute it and/or modify
  8  ;; it under the terms of the GNU General Public License as published by
  9  ;; the Free Software Foundation; either version 3, or (at your option)
 10  ;; any later version.
 11  ;;
 12  ;; This program is distributed in the hope that it will be useful,
 13  ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  ;; GNU General Public License for more details.
 16  ;;
 17  ;; You should have received a copy of the GNU General Public License
 18  ;; along with GNU Emacs; see the file COPYING.  If not, write to the
 19  ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 20  ;; Boston, MA 02110-1301, USA.
 21  
 22  ;;; Commentary:
 23  
 24  ;;; Code:
 25  (eval-when-compile
 26    (setq-local byte-compile-warnings '(not docstrings)))
 27  
 28  (require 's)
 29  (require 'aio)
 30  (require 'dash)
 31  
 32  (require 'docker-group)
 33  (require 'docker-utils)
 34  
 35  (defcustom docker-run-as-root nil
 36    "Run docker as root."
 37    :group 'docker
 38    :type 'boolean)
 39  
 40  (defcustom docker-show-messages t
 41    "If non-nil `message' docker commands which are run."
 42    :group 'docker
 43    :type 'boolean)
 44  
 45  (defcustom docker-terminal-backend 'auto
 46    "Terminal backend used for commands that need a live buffer.
 47  When set to `auto', prefer eat, then vterm, then shell."
 48    :group 'docker
 49    :type '(choice (const :tag "Auto (eat > vterm > shell)" auto)
 50                   (const :tag "Eat" eat)
 51                   (const :tag "Vterm" vterm)
 52                   (const :tag "Shell" shell)))
 53  
 54  (defcustom docker-run-async-with-buffer-function nil
 55    "Obsolete; use `docker-terminal-backend' instead."
 56    :group 'docker
 57    :type 'symbol)
 58  
 59  (make-obsolete-variable 'docker-run-async-with-buffer-function 'docker-terminal-backend "2.5.0")
 60  
 61  
 62  (defmacro docker-with-sudo (&rest body)
 63    "Ensure `default-directory' is set correctly according to `docker-run-as-root' then execute BODY."
 64    (declare (indent defun))
 65    `(let ((default-directory (if (and docker-run-as-root (not (file-remote-p default-directory)))
 66                                  "/sudo::"
 67                                default-directory)))
 68       ,@body))
 69  
 70  (defun docker-run-start-file-process-shell-command (program &rest args)
 71    "Execute \"PROGRAM ARGS\" and return the process."
 72    (docker-with-sudo
 73      (let* ((process-args (-remove 's-blank? (-flatten args)))
 74             (command (s-join " " (-insert-at 0 program process-args))))
 75        (when docker-show-messages (message "Running: %s" command))
 76        (start-file-process-shell-command command (apply #'docker-utils-generate-new-buffer-name program process-args) command))))
 77  
 78  (defun docker-run-async (program &rest args)
 79    "Execute \"PROGRAM ARGS\" and return a promise with the results."
 80    (let* ((process (apply #'docker-run-start-file-process-shell-command program args))
 81           (promise (aio-promise)))
 82      (set-process-query-on-exit-flag process nil)
 83      (set-process-sentinel process (-partial #'docker-process-sentinel promise))
 84      promise))
 85  
 86  (defun docker-run-async-with-buffer (program interactive &rest args)
 87    "Execute \"PROGRAM ARGS\" and display output in a new buffer.
 88  INTERACTIVE selects an interactive terminal buffer when non-nil.
 89  Prefer `docker-run-async-with-buffer-interactive' or
 90  `docker-run-async-with-buffer-noninteractive'."
 91    (if docker-run-async-with-buffer-function
 92        (apply docker-run-async-with-buffer-function program interactive args)
 93      (apply #'docker-run-async-with-buffer-dispatch
 94             (docker--terminal-backend)
 95             program interactive args)))
 96  
 97  (defun docker-run-async-with-buffer-interactive (program &rest args)
 98    "Execute \"PROGRAM ARGS\" and display output in an interactive buffer."
 99    (apply #'docker-run-async-with-buffer program t args))
100  
101  (defun docker-run-async-with-buffer-noninteractive (program &rest args)
102    "Execute \"PROGRAM ARGS\" and display output in a non-interactive buffer."
103    (apply #'docker-run-async-with-buffer program nil args))
104  
105  (defun docker--terminal-backend-available-p (backend)
106    "Return non-nil when BACKEND is available."
107    (pcase backend
108      ('eat (fboundp 'eat-other-window))
109      ('vterm (fboundp 'vterm-other-window))
110      ('shell t)
111      (_ nil)))
112  
113  (defun docker--terminal-backend ()
114    "Return the selected backend symbol."
115    (pcase docker-terminal-backend
116      ('auto (cond
117              ((docker--terminal-backend-available-p 'eat) 'eat)
118              ((docker--terminal-backend-available-p 'vterm) 'vterm)
119              (t 'shell)))
120      (_ docker-terminal-backend)))
121  
122  (defun docker-run-async-with-buffer-dispatch (backend program interactive &rest args)
123    "Dispatch PROGRAM to BACKEND and display output in a new buffer."
124    (pcase backend
125      ('eat (if (docker--terminal-backend-available-p 'eat)
126                (apply #'docker-run-async-with-buffer-eat program interactive args)
127              (error "The eat package is not installed")))
128      ('vterm (if (docker--terminal-backend-available-p 'vterm)
129                  (apply #'docker-run-async-with-buffer-vterm program interactive args)
130                (error "The vterm package is not installed")))
131      ('shell (apply #'docker-run-async-with-buffer-shell program interactive args))
132      (_ (error "Unsupported docker terminal backend: %s" backend))))
133  
134  (defun docker-run-async-with-buffer-shell (program &optional interactive &rest args)
135    "Execute \"PROGRAM ARGS\" and display output in a new buffer.
136  If INTERACTIVE is non-nil, use a `shell' buffer for interactive use.
137  Otherwise, use a non-interactive buffer with ANSI color support."
138    (let* ((process (apply #'docker-run-start-file-process-shell-command program args))
139           (buffer (process-buffer process)))
140      (set-process-query-on-exit-flag process nil)
141      (if interactive
142          (with-current-buffer buffer (shell-mode))
143        (with-current-buffer buffer (special-mode)))
144      (set-process-filter process
145                          (if interactive
146                              'comint-output-filter
147                            'docker-process-filter-noninteractive))
148      (switch-to-buffer-other-window buffer)))
149  
150  (defun docker-run-async-with-buffer-vterm (program &optional interactive &rest args)
151    "Execute \"PROGRAM ARGS\" and display output in a new `vterm' buffer.
152  If INTERACTIVE is nil, fall back to shell mode since vterm is interactive."
153    (if (not interactive)
154        ;; vterm is interactive only, fall back to shell for non-interactive output
155        (apply #'docker-run-async-with-buffer-shell program nil args)
156      (defvar vterm-kill-buffer-on-exit)
157      (defvar vterm-shell)
158      (if (fboundp 'vterm-other-window)
159          (let* ((process-args (-remove 's-blank? (-flatten args)))
160                 (vterm-shell (s-join " " (-insert-at 0 program process-args)))
161                 (vterm-kill-buffer-on-exit nil))
162            (vterm-other-window
163             (apply #'docker-utils-generate-new-buffer-name program process-args)))
164        (error "The vterm package is not installed"))))
165  
166  (defun docker-run-async-with-buffer-eat (program &optional interactive &rest args)
167    "Execute \"PROGRAM ARGS\" and display output in a new `eat' buffer.
168  If INTERACTIVE is nil, fall back to shell mode since eat is interactive."
169    (if (not interactive)
170        (apply #'docker-run-async-with-buffer-shell program nil args)
171      (defvar eat-buffer-name)
172      (if (fboundp 'eat-other-window)
173          (let* ((process-args (-remove 's-blank? (-flatten args)))
174                 (command (s-join " " (-insert-at 0 program process-args)))
175                 (eat-buffer-name (apply #'docker-utils-generate-new-buffer-name
176                                         program process-args)))
177            (eat-other-window command))
178        (error "The eat package is not installed"))))
179  
180  (defun docker-process-filter-noninteractive (proc string)
181    "Process filter for non-interactive streaming buffers.
182  Strips carriage returns and applies ANSI color codes."
183    (when (buffer-live-p (process-buffer proc))
184      (with-current-buffer (process-buffer proc)
185        (let ((inhibit-read-only t)
186              (moving (= (point) (process-mark proc))))
187          (save-excursion
188            (goto-char (process-mark proc))
189            (insert (ansi-color-apply (replace-regexp-in-string "\r" "" string)))
190            (set-marker (process-mark proc) (point)))
191          (when moving (goto-char (process-mark proc)))))))
192  
193  (defun docker-process-sentinel (promise process event)
194    "Sentinel that resolves the PROMISE using PROCESS and EVENT."
195    (when (memq (process-status process) '(exit signal))
196      (setq event (substring event 0 -1))
197      (if (not (string-equal event "finished"))
198          (aio-resolve promise
199                       (lambda ()
200                         (error "Error running: \"%s\" (%s)" (process-name process) event)))
201        (aio-resolve promise
202                     (lambda ()
203                       (when docker-show-messages
204                         (message "Finished: %s" (process-name process)))
205                       (run-with-timer 2 nil (lambda () (message nil)))
206                       (with-current-buffer (process-buffer process)
207                         (prog1 (buffer-substring-no-properties (point-min) (point-max))
208                           (kill-buffer))))))))
209  
210  (provide 'docker-process)
211  
212  ;;; docker-process.el ends here