docs and tasks
This commit is contained in:
401
tasks/09-remote-shell.md
Normal file
401
tasks/09-remote-shell.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user