# 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: ```bash 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`: ```elisp ;;; 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 ```elisp ;;; 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`: ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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`: ```elisp ;;; 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: ```org * 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: ```elisp (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 - [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Remote Shell section - [Erlang Distribution](https://www.erlang.org/doc/reference_manual/distributed.html) - [IEx Remote Shell](https://hexdocs.pm/iex/IEx.html#module-remote-shells)