Files
ob-elixir/tasks/09-remote-shell.md

13 KiB

Task 09: Remote Shell (remsh) Support

Phase: 4 - Advanced Features Priority: Medium Estimated Time: 2-3 hours Dependencies: Task 07 (Session Support)

Objective

Implement remote shell support to connect to running Elixir/Erlang nodes and execute code against them.

Prerequisites

  • Session support implemented (Task 07)
  • Understanding of Erlang distribution
  • Running Elixir node for testing

Background

Elixir/Erlang nodes can connect to each other for distributed computing. The --remsh flag allows IEx to connect to a running node:

iex --sname console --remsh my_app@hostname --cookie secret

This is useful for:

  • Inspecting production systems
  • Running code against a live application
  • Debugging distributed systems

Steps

Step 1: Add remote shell configuration

Add to ob-elixir.el:

;;; Remote Shell Configuration

(defconst org-babel-header-args:elixir
  '((mix-project . :any)
    (mix-env . :any)
    (remsh . :any)        ; Remote node to connect to
    (node-name . :any)    ; --name for local node
    (node-sname . :any)   ; --sname for local node  
    (cookie . :any))      ; Erlang cookie
  "Elixir-specific header arguments.")

(defcustom ob-elixir-default-cookie nil
  "Default Erlang cookie for remote connections.

Set this if all your nodes use the same cookie.
Can be overridden with :cookie header argument."
  :type '(choice (const nil) string)
  :group 'ob-elixir)

Step 2: Implement remote session creation

;;; Remote Shell Sessions

(defun ob-elixir--start-remote-session (buffer-name session-name params)
  "Start a remote shell session in BUFFER-NAME.

Connects to the node specified in PARAMS."
  (let* ((remsh (cdr (assq :remsh params)))
         (node-name (cdr (assq :node-name params)))
         (node-sname (cdr (assq :node-sname params)))
         (cookie (or (cdr (assq :cookie params))
                     ob-elixir-default-cookie))
         (buffer (get-buffer-create buffer-name))
         (local-name (or node-sname
                         node-name
                         (format "ob_elixir_%d" (random 99999))))
         (process-environment (cons "TERM=dumb" process-environment)))
    
    (unless remsh
      (error "No remote node specified. Use :remsh header argument"))
    
    (with-current-buffer buffer
      ;; Build command arguments
      (let ((args (append
                   ;; Local node name
                   (if node-name
                       (list "--name" node-name)
                     (list "--sname" local-name))
                   ;; Cookie
                   (when cookie
                     (list "--cookie" cookie))
                   ;; Remote shell
                   (list "--remsh" remsh))))
        
        (apply #'make-comint-in-buffer
               (format "ob-elixir-remsh-%s" session-name)
               buffer
               ob-elixir-iex-command
               nil
               args))
      
      ;; Wait for connection
      (unless (ob-elixir--wait-for-prompt buffer 30)
        (error "Failed to connect to remote node: %s" remsh))
      
      ;; Configure session
      (ob-elixir--configure-session buffer)
      
      buffer)))

(defun ob-elixir--is-remote-session-p (params)
  "Return t if PARAMS specify a remote shell connection."
  (not (null (cdr (assq :remsh params)))))

Step 3: Update session creation dispatcher

Modify ob-elixir--start-session:

(defun ob-elixir--start-session (buffer-name session-name params)
  "Start a new IEx session in BUFFER-NAME."
  (cond
   ;; Remote shell
   ((ob-elixir--is-remote-session-p params)
    (ob-elixir--start-remote-session buffer-name session-name params))
   
   ;; Mix project
   ((ob-elixir--resolve-mix-project params)
    (ob-elixir--start-mix-session buffer-name session-name params))
   
   ;; Plain IEx
   (t
    (ob-elixir--start-plain-session buffer-name session-name params))))

(defun ob-elixir--start-plain-session (buffer-name session-name params)
  "Start a plain IEx session in BUFFER-NAME."
  (let* ((buffer (get-buffer-create buffer-name))
         (process-environment (cons "TERM=dumb" process-environment)))
    (with-current-buffer buffer
      (make-comint-in-buffer
       (format "ob-elixir-%s" session-name)
       buffer
       ob-elixir-iex-command
       nil)
      
      (ob-elixir--wait-for-prompt buffer 10)
      (ob-elixir--configure-session buffer)
      buffer)))

(defun ob-elixir--start-mix-session (buffer-name session-name params)
  "Start an IEx session with Mix in BUFFER-NAME."
  (let* ((mix-project (ob-elixir--resolve-mix-project params))
         (mix-env (cdr (assq :mix-env params)))
         (buffer (get-buffer-create buffer-name))
         (default-directory mix-project)
         (process-environment
          (append
           (list "TERM=dumb")
           (when mix-env (list (format "MIX_ENV=%s" mix-env)))
           process-environment)))
    (with-current-buffer buffer
      (make-comint-in-buffer
       (format "ob-elixir-%s" session-name)
       buffer
       ob-elixir-iex-command
       nil
       "-S" "mix")
      
      (ob-elixir--wait-for-prompt buffer 30)
      (ob-elixir--configure-session buffer)
      buffer)))

Step 4: Add connection verification

(defun ob-elixir--verify-remote-connection (buffer remsh)
  "Verify that BUFFER is connected to remote node REMSH."
  (with-current-buffer buffer
    (let ((result (ob-elixir--send-and-receive buffer "Node.self()")))
      (when (string-match-p (regexp-quote remsh) result)
        t))))

(defun ob-elixir--send-and-receive (buffer command)
  "Send COMMAND to BUFFER and return the response."
  (let ((start-pos nil)
        (result nil))
    (with-current-buffer buffer
      (goto-char (point-max))
      (setq start-pos (point))
      (ob-elixir--send-command buffer command)
      (ob-elixir--wait-for-prompt buffer 10)
      (setq result (buffer-substring-no-properties start-pos (point))))
    (ob-elixir--clean-session-output result)))

Step 5: Add safety checks

(defcustom ob-elixir-remsh-confirm t
  "Whether to confirm before connecting to remote nodes.

When non-nil, ask for confirmation before connecting.
This is a safety measure for production systems."
  :type 'boolean
  :group 'ob-elixir)

(defun ob-elixir--confirm-remsh (node)
  "Confirm remote shell connection to NODE."
  (or (not ob-elixir-remsh-confirm)
      (yes-or-no-p 
       (format "Connect to remote Elixir node '%s'? " node))))

(defun ob-elixir--start-remote-session (buffer-name session-name params)
  "Start a remote shell session in BUFFER-NAME."
  (let ((remsh (cdr (assq :remsh params))))
    (unless (ob-elixir--confirm-remsh remsh)
      (user-error "Remote shell connection cancelled"))
    ;; ... rest of implementation
    ))

Step 6: Add helper commands

(defun ob-elixir-connect-to-node (node &optional cookie)
  "Interactively connect to a remote Elixir NODE.

Optional COOKIE specifies the Erlang cookie."
  (interactive
   (list (read-string "Remote node: ")
         (when current-prefix-arg
           (read-string "Cookie: "))))
  (let ((params `((:remsh . ,node)
                  ,@(when cookie `((:cookie . ,cookie))))))
    (org-babel-elixir-initiate-session "remote" params)))

(defun ob-elixir-list-sessions ()
  "List all active ob-elixir sessions."
  (interactive)
  (if (= 0 (hash-table-count ob-elixir--sessions))
      (message "No active sessions")
    (with-output-to-temp-buffer "*ob-elixir sessions*"
      (princ "Active ob-elixir sessions:\n\n")
      (maphash (lambda (name buffer-name)
                 (let ((buffer (get-buffer buffer-name)))
                   (princ (format "  %s -> %s (%s)\n"
                                  name
                                  buffer-name
                                  (if (and buffer
                                           (get-buffer-process buffer))
                                      "running"
                                    "dead")))))
               ob-elixir--sessions))))

Step 7: Add tests

Create test/test-ob-elixir-remsh.el:

;;; test-ob-elixir-remsh.el --- Remote shell tests -*- lexical-binding: t; -*-

(require 'ert)
(require 'ob-elixir)

;; Note: These tests require a running Elixir node
;; Start one with: iex --sname testnode --cookie testcookie

(defvar ob-elixir-test-remote-node "testnode@localhost"
  "Remote node for testing. Set to your test node.")

(defvar ob-elixir-test-remote-cookie "testcookie"
  "Cookie for test node.")

(ert-deftest ob-elixir-test-is-remote-session ()
  "Test remote session detection."
  (should (ob-elixir--is-remote-session-p 
           '((:remsh . "node@host"))))
  (should-not (ob-elixir--is-remote-session-p 
               '((:session . "test")))))

(ert-deftest ob-elixir-test-remote-session-creation ()
  "Test remote session creation."
  (skip-unless (executable-find ob-elixir-iex-command))
  (skip-unless (getenv "OB_ELIXIR_TEST_REMSH")) ; Only run if explicitly enabled
  (let ((ob-elixir-remsh-confirm nil))
    (unwind-protect
        (let* ((params `((:remsh . ,ob-elixir-test-remote-node)
                         (:cookie . ,ob-elixir-test-remote-cookie)))
               (buffer (org-babel-elixir-initiate-session "test-remote" params)))
          (should buffer)
          (should (org-babel-comint-buffer-livep buffer)))
      (ob-elixir-kill-session "test-remote"))))

(ert-deftest ob-elixir-test-remote-execution ()
  "Test code execution on remote node."
  (skip-unless (executable-find ob-elixir-iex-command))
  (skip-unless (getenv "OB_ELIXIR_TEST_REMSH"))
  (let ((ob-elixir-remsh-confirm nil))
    (unwind-protect
        (let* ((params `((:remsh . ,ob-elixir-test-remote-node)
                         (:cookie . ,ob-elixir-test-remote-cookie)))
               (result (ob-elixir--evaluate-in-session 
                        "test-remote-exec"
                        "Node.self() |> to_string()"
                        'value
                        params)))
          (should (string-match-p ob-elixir-test-remote-node result)))
      (ob-elixir-kill-session "test-remote-exec"))))

(provide 'test-ob-elixir-remsh)

Step 8: Document usage

Add to documentation:

* Remote Shell Usage

** Connecting to a running node

#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :cookie secret
Node.self()
#+END_SRC

** With explicit node name

#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :node-sname console :cookie secret
# Local node will be named 'console'
Node.list()
#+END_SRC

** With distributed name

#+BEGIN_SRC elixir :session remote :remsh myapp@myhost.example.com :node-name console@myhost.example.com :cookie secret
# Use full node names for cross-machine connections
Application.get_env(:my_app, :some_setting)
#+END_SRC

Acceptance Criteria

  • :remsh node@host connects to remote node
  • :cookie sets the Erlang cookie
  • :node-sname and :node-name set local node name
  • Connection failures produce clear error messages
  • Safety confirmation before connecting (configurable)
  • ob-elixir-connect-to-node interactive command works
  • Remote session appears in ob-elixir-list-sessions
  • All tests pass

Header Arguments Reference

Argument Values Description
:remsh node@host Remote node to connect to
:cookie string Erlang cookie for authentication
:node-sname name Short name for local node
:node-name name@host Full name for local node

Security Considerations

  1. Cookies: Never commit cookies to version control
  2. Confirmation: Enable ob-elixir-remsh-confirm for production
  3. Network: Ensure proper firewall rules for Erlang distribution port (4369 + dynamic)
  4. Scope: Consider what code could be executed on production systems

Troubleshooting

Cannot connect to node

  1. Verify node is running: epmd -names
  2. Check cookie matches
  3. Verify network connectivity on port 4369
  4. Check that node allows remote connections

Connection times out

Increase timeout or check for network issues:

(setq ob-elixir-session-timeout 60)

Node not found

Ensure using correct node name format:

  • Short names: node@hostname (same machine or subnet)
  • Long names: node@hostname.domain.com (cross-network)

Files Modified

  • ob-elixir.el - Add remote shell support
  • test/test-ob-elixir-remsh.el - Add remote shell tests

References