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

402 lines
13 KiB
Markdown

# 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)