branch: externals/dtache commit d0b0ed41e2247d0edd369e75efae6bafbad7c2be Author: Niklas Eklund <niklas.ekl...@posteo.net> Commit: Niklas Eklund <niklas.ekl...@posteo.net>
Merge develop branch into master --- .dir-locals.el | 3 +- README.org | 50 ++++++++---- dtache-shell.el | 52 +++++++++---- dtache.el | 172 +++++++++++++++++++++++++++++++++-------- embark-dtache.el | 2 + marginalia-dtache.el | 10 +-- test/dtache-test.el | 36 +++++++-- test/marginalia-dtache-test.el | 8 +- 8 files changed, 253 insertions(+), 80 deletions(-) diff --git a/.dir-locals.el b/.dir-locals.el index 2115bd6105..846a89e9bf 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,2 +1,3 @@ ((nil . ((compile-command . "guix build --file=guix.scm"))) - (prog-mode (eval flycheck-mode))) + (prog-mode (eval flycheck-mode)) + (magit-status-mode (magit-todos-exclude-globs))) diff --git a/README.org b/README.org index 09f9924ece..925a967d30 100644 --- a/README.org +++ b/README.org @@ -7,15 +7,9 @@ :description: Why Dtache? :end: - =Dtache= allows a program to be seamlessly executed in an - environment that is isolated from =Emacs= which made possible by the - [[https://github.com/crigler/dtach][dtach]] program. This package makes sure that, even though programs - are running in isolated sessions, they get tightly integrated with - Emacs. + =Dtache= allows a program to be seamlessly executed in an environment that is isolated from =Emacs= which made possible by the [[https://github.com/crigler/dtach][dtach]] program. This package makes sure that, even though programs are running in isolated sessions, they get tightly integrated with Emacs. - The =dtache= package is split up into two, =dtache.el= and - =dtache-shell.el=. The former provides the backend implementation, - whilst the former provides the integration with =M-x shell=. + The =dtache= package is split up into two, =dtache.el= and =dtache-shell.el=. The former provides the backend implementation, whilst the former provides the integration with =M-x shell=. ** Screenshots @@ -34,8 +28,13 @@ Configuration for the =dtache= package. This package provides the backend for =d (setq dtache-db-directory (no-littering-expand-var-file-name "dtache")) (setq dtache-session-directory (expand-file-name "dtache" (temporary-file-directory))) (setq dtache-shell-history-file "~/.bash_history") + (general-def '(motion normal) dtache-log-mode-map - "q" #'quit-window)) + "q" #'kill-buffer-and-window) + (general-def '(motion normal) dtache-tail-mode-map + "q" #'dtache-quit-tail-log) + + (add-to-list 'recentf-exclude (rx (regexp "dtache.*\.log")))) #+end_src *** Dtache-shell @@ -47,9 +46,8 @@ Configuration for the =dtache-shell= package. This package provides the integrat :hook (shell-mode . dtache-shell-mode) :config (general-def dtache-shell-mode-map - "<return>" #'dtache-shell-send-input "<S-return>" #'dtache-shell-create - "<C-return>" #'dtache-shell-attach + "<C-return>" #'dtache-tail-log "C-c C-q" #'dtache-shell-detach)) #+end_src @@ -90,6 +88,24 @@ The =dtache= package supports [[https://www.gnu.org/software/emacs/manual/html_n '(:application tramp :protocol "ssh") 'remote-dtache) #+end_src +** Degraded mode + +Occasionally the =dtache= user might run into shell commands that don't play well with the package. The symptom is that the logs are not updated until the session finish, which removes a lot of the use-case for dtache. To mitigate the problem dtache can be instructed to run in a degraded mode for those commands. This is done by adding a regexp that matches the troublesome command and add it to the list =dtache-degraded-list=. + +#+begin_src elisp + (setq dtache-degraded-list '("foo")) +#+end_src + +In degraded mode =dtache= will skip the usage of =tee= and instead redirect all the outputs to the dtache log. Use =dtache-tail-log= to tail the output from the session. It's still possible to attach in the terminal but there is no output forwarded to it. + +** Notification + +Make =dtache= send a notification once a session is finished. This would only make sense to add for sessions on the localhost. Add the following advice to the config. + +#+begin_src elisp + (advice-add 'dtache-session-command :override #'dtache-session-notify-command) +#+end_src + * Commands The following is a list of commands that can be run on =dtache= sessions. @@ -98,12 +114,12 @@ The following is a list of commands that can be run on =dtache= sessions. Commands to be used in shell buffers. -| Command | Description | -|-------------------------+--------------------------------------------------| -| dtache-shell-create | Create a session | -| dtache-shell-attach | Attach to a session | -| dtache-shell-detach | Detach from a session | -| dtache-shell-send-input | Optionally create a session with prefix argument | +| Command | Description | +|-------------------------+-----------------------------| +| dtache-shell-send-input | Optionally create a session | +| dtache-shell-create | Create a session | +| dtache-shell-attach | Attach to a session | +| dtache-shell-detach | Detach from a session | ** Dtache diff --git a/dtache-shell.el b/dtache-shell.el index af149f0b95..8453ad5339 100755 --- a/dtache-shell.el +++ b/dtache-shell.el @@ -39,6 +39,10 @@ "A list of regexps to block non-supported input.") (defvar dtache-shell-silence-dtach-messages t "Filter out messages from the `dtach' program.") +(defvar dtache-shell-create-primary-function #'dtache-shell-new-session + "Primary function for creating a session.") +(defvar dtache-shell-create-secondary-function #'dtache-shell-create-session + "Secondary function for creating a session.") ;;;; Functions @@ -63,14 +67,33 @@ Optionally CREATE-SESSION with prefix argument." (interactive "P") (if create-session - (dtache-shell-create) + (funcall dtache-shell-create-primary-function) (comint-send-input))) ;;;###autoload -(defun dtache-shell-create () - "Create a session." +(defun dtache-shell-create (&optional secondary) + "Create a new session with `dtache-shell-create-primary-function'. + +If prefix argument SECONDARY call `dtache-shell-create-secondary-function'." + (interactive "P") + (if secondary + (funcall dtache-shell-create-secondary-function) + (funcall dtache-shell-create-primary-function))) + +;;;###autoload +(defun dtache-shell-create-session () + "Create a session and attach to it." + (interactive) + (let* ((dtache--dtach-mode "-c") + (comint-input-sender #'dtache-shell--create-input-sender)) + (comint-send-input))) + +;;;###autoload +(defun dtache-shell-new-session () + "Create a new session." (interactive) - (let ((comint-input-sender #'dtache-shell--create-input-sender)) + (let ((dtache--dtach-mode "-n") + (comint-input-sender #'dtache-shell--create-input-sender)) (comint-send-input))) ;;;###autoload @@ -89,25 +112,28 @@ Optionally CREATE-SESSION with prefix argument." cluttering the comint-history with dtach commands." (interactive (list (dtache-select-session))) - (if (dtache--session-active-p session) - (cl-letf ((dtache--current-session session) - (comint-input-sender #'dtache-shell--attach-input-sender) - ((symbol-function 'comint-add-to-input-history) (lambda (_) t))) - (comint-kill-input) - (comint-send-input)) - (funcall dtache-attach-alternate-function session))) + (cl-letf ((dtache--current-session session) + (comint-input-sender #'dtache-shell--attach-input-sender) + ((symbol-function 'comint-add-to-input-history) (lambda (_) t))) + (comint-kill-input) + (comint-send-input))) ;;;; Support functions +(cl-defmethod dtache--attach-to-session (session &context (major-mode shell-mode)) + "Attach to a dtache SESSION when MAJOR-MODE is `shell-mode'." + (dtache-shell-attach session)) + (defun dtache-shell--attach-input-sender (proc _string) "Attach to `dtache--session' and send the attach command to PROC." - (let* ((socket + (let* ((dtache--dtach-mode "-a") + (socket (concat (dtache--session-session-directory dtache--current-session) (dtache--session-id dtache--current-session) dtache-socket-ext)) (input - (concat dtache-program " -a " socket))) + (concat dtache-program " " dtache--dtach-mode " " socket))) (comint-simple-send proc input))) (defun dtache-shell--create-input-sender (proc string) diff --git a/dtache.el b/dtache.el index ce7b567cb9..ce963f4e36 100644 --- a/dtache.el +++ b/dtache.el @@ -40,6 +40,7 @@ (require 'emacsql-sqlite) (require 'tramp-sh) +(require 'autorevert) ;;;; Variables @@ -53,7 +54,7 @@ "The `dtach' program.") (defvar dtache-shell "bash" "Shell to run the dtach command in.") -(defvar dtache-metadata-annotators '((:git . dtache--session-git-branch)) +(defvar dtache-metadata-annotators '((:git-branch . dtache--session-git-branch)) "An alist of annotators for metadata.") (defvar dtache-max-command-length 95 "Maximum length of displayed command.") @@ -76,9 +77,14 @@ "Message printed when `dtach' finishes.") (defconst dtache-detach-character "\C-\\" "Character used to detach from a session.") +(defvar dtache-degraded-list '() + "Regexps that should run in dedgraded mode.") +(defvar dtache-tail-interval 2 + "Interval in seconds for dtache to tail.") ;;;;; Private +(defvar dtache--dtach-mode nil "Mode of operation.") (defvar dtache--session-candidates nil "An alist of session candidates.") (defvar dtache--current-session nil "The current session.") @@ -93,9 +99,9 @@ (session-directory nil :read-only t) (metadata nil :read-only t) (host nil :read-only t) + (degraded nil :read-only t) (duration nil) (log-size nil) - (stderr-p nil) (active nil)) ;;;; Functions @@ -202,21 +208,38 @@ This function also makes sure that the HISTFILE is disabled for local shells." (defun dtache-dtach-command (session) "Return a dtach command for SESSION." - (let* ((command (dtache-session-command session)) - (directory (dtache--session-session-directory session)) + (let* ((directory (dtache--session-session-directory session)) (file-name (dtache--session-id session)) - (stdout (concat directory file-name dtache-stdout-ext)) - (stderr (concat directory file-name dtache-stderr-ext)) - (log (concat directory file-name dtache-log-ext)) (socket (concat directory file-name dtache-socket-ext)) ;; Construct the command line - ;; { { echo stdout; echo stderr >&2; } >>(tee stdout ); } 2>>(tee stderr) | tee log - (commandline (format "{ { %s; }%s }%s %s" - (format "%s" command) - (format " > >(tee %s );" stdout) - (format " 2> >(tee %s )" stderr) - (format " | tee %s" log)))) - (format "%s -c %s -z %s -c %s" dtache-program socket dtache-shell (shell-quote-argument commandline)))) + (commandline (dtache--output-command session)) + (dtach-mode (if (dtache--session-degraded session) + "-n" + dtache--dtach-mode))) + (format "%s %s %s -z %s -c %s" dtache-program dtach-mode socket dtache-shell (shell-quote-argument commandline)))) + +(defun dtache-degraded-p (command) + "Return t if COMMAND should run in degreaded mode." + (if (thread-last dtache-degraded-list + (seq-filter (lambda (regexp) + (string-match-p regexp command))) + (length) + (= 0)) + nil + t)) + +(defun dtache-session-notify-command (session) + "Append notify-send to SESSION's command." + (let* ((command (dtache--session-command session)) + (emacs-icon + (concat data-directory + "images/icons/hicolor/scalable/apps/emacs.svg"))) + (if (file-remote-p default-directory) + command + (concat + command + (format " && notify-send \"Dtache finished: %s\"" command) + (format " --icon %s" emacs-icon))))) (defun dtache-metadata () "Return a property list with metadata." @@ -265,7 +288,7 @@ This function also makes sure that the HISTFILE is disabled for local shells." (with-current-buffer (get-buffer-create buffer-name) (setq-local buffer-read-only nil) (erase-buffer) - (insert-file-contents file ) + (insert-file-contents file) (setq-local default-directory (dtache--session-working-directory session)) (compilation-mode)) @@ -321,6 +344,15 @@ This function also makes sure that the HISTFILE is disabled for local shells." (list (dtache-select-session))) (dtache--open-file session 'log)) +;;;###autoload +(defun dtache-tail-log (session) + "Tail SESSION's log." + (interactive + (list (dtache-select-session))) + (if (dtache--session-active-p session) + (dtache--tail-file session 'log) + (dtache--open-file session 'log))) + ;;;###autoload (defun dtache-open-stdout (session) "Open SESSION's stdout." @@ -335,16 +367,40 @@ This function also makes sure that the HISTFILE is disabled for local shells." (list (dtache-select-session))) (dtache--open-file session 'stderr)) +;;;###autoload +(defun dtache-attach-to-session (session) + "Attach to SESSION." + (interactive + (list (dtache-select-session))) + (if (dtache--session-active-p session) + (dtache--attach-to-session session) + (funcall dtache-attach-alternate-function session))) + +;;;###autoload +(defun dtache-quit-tail-log () + "Quit `dtache' tail log. + +The log can have been updated, but that is not done by the user but +rather the tail mode. To avoid a promtp `buffer-modified-p' is set to +nil before closing." + (interactive) + (set-buffer-modified-p nil) + (kill-buffer-and-window)) + ;;;; Support functions ;;;;; Session +(cl-defgeneric dtache--attach-to-session (session) + "Attach to SESSION.") + (defun dtache--create-session (command) "Create a `dtache' session from COMMAND." (let ((session (dtache--session-create :id (dtache--create-id command) :command command :working-directory default-directory + :degraded (dtache-degraded-p command) :creation-time (time-to-seconds (current-time)) :session-directory (file-name-as-directory dtache-session-directory) :host (dtache--host) @@ -399,10 +455,6 @@ This function also makes sure that the HISTFILE is disabled for local shells." (setf (dtache--session-log-size session) (file-attribute-size (file-attributes (dtache-session-file session 'log)))) - (setf (dtache--session-stderr-p session) (> (file-attribute-size - (file-attributes - (dtache-session-file session 'stderr))) - 0)) session) (defun dtache--session-git-branch () @@ -512,6 +564,38 @@ This function also makes sure that the HISTFILE is disabled for local shells." ;;;;; Other +(defun dtache--output-command (session) + "Return output command for SESSION." + (if (dtache--session-degraded session) + (dtache--output-to-file-command session) + (dtache--output-to-both-command session))) + +(defun dtache--output-to-file-command (session) + "Return a command to send SESSION's output directly to log." + (let* ((command (dtache-session-command session)) + (directory (dtache--session-session-directory session)) + (file-name (dtache--session-id session)) + (log (concat directory file-name dtache-log-ext))) + ;; Construct the command line + ;; echo &> log + (format "{ %s; } &> %s" command log))) + +(defun dtache--output-to-both-command (session) + "Return a command to send SESSION's output to both shell and log." + (let* ((command (dtache-session-command session)) + (directory (dtache--session-session-directory session)) + (file-name (dtache--session-id session)) + (stdout (concat directory file-name dtache-stdout-ext)) + (stderr (concat directory file-name dtache-stderr-ext)) + (log (concat directory file-name dtache-log-ext))) + ;; Construct the command line + ;; { { echo stdout; echo stderr >&2; } >>(tee stdout ); } 2>>(tee stderr) | tee log + (format "{ { %s; }%s }%s %s" + (format "%s" command) + (format " > >(tee %s );" stdout) + (format " 2> >(tee %s )" stderr) + (format " | tee %s" log)))) + (defun dtache--host () "Return name of host." (if-let ((remote-host (file-remote-p default-directory)) @@ -532,8 +616,8 @@ This function also makes sure that the HISTFILE is disabled for local shells." Modification time is not reliable whilst a session is active. Instead the current time is used." - ;; TODO: This function might need to take into account that there - ;; might be a time offset between two computers + ;; TODO: Consider calculating a time offset between host and remote + ;; computer (if (dtache--session-active session) (- (time-to-seconds) (dtache--session-creation-time session)) (- (time-to-seconds @@ -544,33 +628,55 @@ the current time is used." (defun dtache--open-file (session file) "Oen SESSION's FILE." - (let* ((buffer-name - (format "*dtache-%s-%s*" file - (dtache--session-short-id session))) - (file-path + (let* ((file-path + (dtache-session-file session file))) + (if (file-exists-p file-path) + (progn + (find-file-other-window file-path) + (setq-local default-directory (dtache--session-working-directory session)) + (dtache-log-mode) + (goto-char (point-max))) + (message "Dtache can't find file: %s" file-path)))) + +(defun dtache--tail-file (session file) + "Tail SESSION's FILE." + (let* ((file-path (dtache-session-file session file))) (when (file-exists-p file-path) - (with-current-buffer (get-buffer-create buffer-name) - (erase-buffer) - (insert-file-contents file-path) - (setq-local default-directory (dtache--session-working-directory session))) - (pop-to-buffer buffer-name) - (dtache-log-mode)))) + (find-file-other-window file-path) + (dtache-tail-mode) + (goto-char (point-max))))) (defun dtache--create-id (command) "Return a hash identifier for COMMAND." (let ((current-time (current-time-string))) - (secure-hash 'md5 (concat command current-time)))) + (secure-hash 'md5 (concat command current-time)))) ;;;; Major mode (defvar dtache-log-mode-map (let ((map (make-sparse-keymap))) + (define-key map (kbd "q") #'kill-buffer-and-window) map) "Keymap for `dtache-log-mode'.") (define-derived-mode dtache-log-mode nil "Dtache Log" - "Major mode for dtache logs.") + "Major mode for dtache logs." + (read-only-mode t)) + +(defvar dtache-tail-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "q") #'dtache-quit-tail-log) + map) + "Keymap for `dtache-tail-mode'.") + +(define-derived-mode dtache-tail-mode auto-revert-tail-mode "Dtache Tail" + "Major mode for tailing dtache logs." + (setq-local auto-revert-interval dtache-tail-interval) + (auto-revert-set-timer) + (setq-local auto-revert-verbose nil) + (auto-revert-tail-mode) + (read-only-mode t)) (provide 'dtache) diff --git a/embark-dtache.el b/embark-dtache.el index 71dcde4adc..7e308d62c6 100644 --- a/embark-dtache.el +++ b/embark-dtache.el @@ -38,7 +38,9 @@ (embark-define-keymap embark-dtache-map "Keymap for Embark dtache actions." + ("a" dtache-attach-to-session) ("l" dtache-open-log) + ("t" dtache-tail-log) ("e" dtache-open-stderr) ("o" dtache-open-stdout) ("i" dtache-insert-session-command) diff --git a/marginalia-dtache.el b/marginalia-dtache.el index 612d3af8e7..cac5694fac 100644 --- a/marginalia-dtache.el +++ b/marginalia-dtache.el @@ -79,7 +79,7 @@ (let* ((session (dtache-decode-session candidate))) (marginalia--fields ((marginalia-dtache--active session) :width 3 :face 'marginalia-dtache-active) - ((marginalia-dtache--stderr-p session) :width 3 :face 'marginalia-dtache-error) + ((marginalia-dtache--degraded session) :width 3 :face 'marginalia-dtache-error) ((marginalia-dtache--git-branch session) :truncate marginalia-dtache-git-branch-length :face 'marginalia-dtache-git) ((marginalia-dtache--duration session) :truncate marginalia-dtache-duration-length :face 'marginalia-dtache-duration) ((marginalia-dtache--size session) :truncate marginalia-dtache-size-length :face 'marginalia-dtache-size) @@ -110,7 +110,7 @@ (defun marginalia-dtache--git-branch (session) "Return the git branch for SESSION." - (plist-get (dtache--session-metadata session) :git)) + (plist-get (dtache--session-metadata session) :git-branch)) (defun marginalia-dtache--active (session) "Return string if SESSION is active." @@ -118,9 +118,9 @@ "*" "")) -(defun marginalia-dtache--stderr-p (session) - "Return string if SESSION has errors." - (if (dtache--session-stderr-p session) +(defun marginalia-dtache--degraded (session) + "Return string if SESSION is degraded." + (if (dtache--session-degraded session) "!" "")) diff --git a/test/dtache-test.el b/test/dtache-test.el index e48e11ad3e..5dd7ffc0e4 100644 --- a/test/dtache-test.el +++ b/test/dtache-test.el @@ -70,13 +70,15 @@ ;;;; Tests (ert-deftest dtache-test-dtach-command () - (let* ((dtache-shell "zsh") - (dtache-program "/usr/bin/dtach") - (actual - (dtache-dtach-command - (dtache--session-create :id "12345" :session-directory "/tmp/dtache/"))) - (expected "^/usr/bin/dtach -c /tmp/dtache/12345.socket -z zsh -c .*")) - (should (string-match-p expected actual)))) + (cl-letf* (((symbol-function #'dtache--output-command) (lambda (_) "command")) + (dtache-shell "zsh") + (dtache-program "/usr/bin/dtach") + (dtache--dtach-mode "-c") + (actual + (dtache-dtach-command + (dtache--session-create :id "12345" :session-directory "/tmp/dtache/"))) + (expected "/usr/bin/dtach -c /tmp/dtache/12345.socket -z zsh -c command")) + (should (string= expected actual)))) (ert-deftest dtache-test-metadata () ;; No annotators @@ -251,6 +253,26 @@ (should (= (length sessions) 1)) (should (string= (dtache--session-id (elt sessions 0)) (dtache--session-id session3))))))) +(ert-deftest dtache-test-output-command () + ;; Degraded + (let* ((actual + (dtache--output-command + (dtache--session-create :id "12345" :session-directory "/tmp/dtache/" :command "ls" :degraded t))) + (expected "{ ls; } &> /tmp/dtache/12345.log")) + (should (string= actual expected))) + + ;; Normal + (let* ((actual + (dtache--output-command + (dtache--session-create :id "12345" :session-directory "/tmp/dtache/" :command "ls"))) + (expected "{ { ls; } > >(tee /tmp/dtache/12345.stdout ); } 2> >(tee /tmp/dtache/12345.stderr ) | tee /tmp/dtache/12345.log")) + (should (string= actual expected)))) + +(ert-deftest dtache-test-degraded-p () + (let ((dtache-degraded-list '("ls"))) + (should (not (dtache-degraded-p "cd"))) + (should (dtache-degraded-p "ls -la")))) + (provide 'dtache-test) ;;; dtache-test.el ends here diff --git a/test/marginalia-dtache-test.el b/test/marginalia-dtache-test.el index 913f087ec7..6c8c9abb14 100644 --- a/test/marginalia-dtache-test.el +++ b/test/marginalia-dtache-test.el @@ -45,16 +45,16 @@ (should (string= "1k" (marginalia-dtache--size (dtache--session-create :log-size 1024))))) (ert-deftest marginalia-dtache-git () - (should (string= "foo" (marginalia-dtache--git-branch (dtache--session-create :metadata '(:git "foo"))))) + (should (string= "foo" (marginalia-dtache--git-branch (dtache--session-create :metadata '(:git-branch "foo"))))) (should (not (marginalia-dtache--git-branch (dtache--session-create))))) (ert-deftest marginalia-dtache-active () (should (string= "*" (marginalia-dtache--active (dtache--session-create :active t)))) (should (string= "" (marginalia-dtache--active (dtache--session-create :active nil))))) -(ert-deftest marginalia-dtache-stderr-p () - (should (string= "!" (marginalia-dtache--stderr-p (dtache--session-create :stderr-p t)))) - (should (string= "" (marginalia-dtache--stderr-p (dtache--session-create :stderr-p nil))))) +(ert-deftest marginalia-dtache-degraded () + (should (string= "!" (marginalia-dtache--degraded (dtache--session-create :degraded t)))) + (should (string= "" (marginalia-dtache--degraded (dtache--session-create :degraded nil))))) (provide 'marginalia-dtache-test)