402 lines
13 KiB
Markdown
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)
|