526 lines
17 KiB
EmacsLisp
Executable file
526 lines
17 KiB
EmacsLisp
Executable file
;;; derl.el --- Distributed Erlang networking code.
|
|
|
|
;;; Commentary:
|
|
;;
|
|
;; This module implements a useful subset of the Erlang distribution
|
|
;; protocol, and provides a small API for sending messages to remote
|
|
;; nodes.
|
|
|
|
(require 'net-fsm)
|
|
(require 'epmd)
|
|
(require 'erlext)
|
|
(require 'md5)
|
|
(eval-when-compile
|
|
(require 'cl))
|
|
|
|
(defvar erl-nodeup-hook nil
|
|
"Called with two args, NODE and FSM. NODE is a string of the form
|
|
\"mynode@cockatoo\", FSM is the net-fsm process of the connection.")
|
|
|
|
(defvar erl-nodedown-hook nil
|
|
"Called with one arg, NODE, a string of the form \"mynode@cockatoo\"")
|
|
|
|
(defcustom derl-use-trace-buffer t
|
|
"*Store erlang message communication in a trace buffer."
|
|
:type 'boolean
|
|
:group 'distel)
|
|
|
|
(defvar derl-cookie nil
|
|
"*Cookie to use in distributed erlang connections, or NIL.
|
|
When NIL, we read ~/.erlang.cookie.")
|
|
|
|
;; Local variables
|
|
|
|
(make-variable-buffer-local
|
|
(defvar derl-connection-node nil
|
|
"Local variable recording the node name of the connection."))
|
|
|
|
(make-variable-buffer-local
|
|
(defvar derl-hdrlen 2
|
|
"Size in bytes of length headers of packets. Set to 2 during
|
|
handshake, 4 when connected."))
|
|
|
|
(make-variable-buffer-local
|
|
(defvar derl-alive nil
|
|
"Local variable set to t after handshaking."))
|
|
|
|
(make-variable-buffer-local
|
|
(defvar derl-shutting-down nil
|
|
"Set to T during shutdown, when no longer servicing requests."))
|
|
|
|
(make-variable-buffer-local
|
|
(defvar derl-request-queue nil
|
|
"Messages waiting to be sent to node."))
|
|
|
|
(make-variable-buffer-local
|
|
(defvar derl-remote-links '()
|
|
"List of (LOCAL-PID . REMOTE-PID) for all distributed links (per-node.)
|
|
Used for sending exit signals when the node goes down."))
|
|
|
|
;; Optional feature flags
|
|
(defconst derl-flag-published #x01)
|
|
(defconst derl-flag-atom-cache #x02)
|
|
(defconst derl-flag-extended-references #x04)
|
|
(defconst derl-flag-dist-monitor #x08)
|
|
(defconst derl-flag-fun-tags #x10)
|
|
(defconst derl-flag-dist-monitor-name #x20)
|
|
(defconst derl-flag-hidden-atom-cache #x40)
|
|
(defconst derl-flag-new-fun-tags #x80)
|
|
(defconst derl-flag-extended-pids-ports #x100)
|
|
|
|
;; ------------------------------------------------------------
|
|
;; External API
|
|
;; ------------------------------------------------------------
|
|
|
|
(defun erl-connect (node)
|
|
"Asynchronously connect to NODE. If the connection succeeds,
|
|
`erl-nodeup-hook' is run. If the connection fails, or goes down
|
|
some time later, `erl-nodedown-hook' is run."
|
|
(when (eq node erl-node-name)
|
|
(error "Remote node has the same node name as Emacs: %S" node))
|
|
(let* ((name (derl-node-name node))
|
|
(host (derl-node-host node))
|
|
(buffer (get-buffer-create (derl-buffer-name node)))
|
|
;; faking a closure with backtick. fun eh?
|
|
;; NB: (funcall '(lambda () 1))
|
|
;; => 1
|
|
;; (let ((n 1)) `(lambda () ,n))
|
|
;; => (lambda () 1)
|
|
(fail-cont `(lambda ()
|
|
(kill-buffer ,buffer)
|
|
(derl-nodedown ',node))))
|
|
(epmd-port-please name host
|
|
;; success continuation
|
|
`(lambda (port)
|
|
(fsm-connect ,host port #'derl-state0 ',node
|
|
nil
|
|
,fail-cont
|
|
,buffer))
|
|
fail-cont)))
|
|
|
|
(defun erl-dist-send (pid msg)
|
|
"Send a message to a process on a remote node."
|
|
(derl-dist-request (erl-pid-node pid) #'derl-send pid msg))
|
|
|
|
(defun erl-dist-reg-send (node name msg)
|
|
"Send a message to a registered process on a remote node."
|
|
(derl-dist-request node #'derl-reg-send erl-self name msg))
|
|
|
|
(defun erl-dist-link (pid)
|
|
"Link the current process with the remote PID."
|
|
(derl-dist-request (erl-pid-node pid) #'derl-link erl-self pid))
|
|
|
|
(defun erl-dist-unlink (pid)
|
|
"Link the current process with the remote PID."
|
|
(derl-dist-request (erl-pid-node pid) #'derl-unlink erl-self pid))
|
|
|
|
(defun erl-dist-exit (from to reason)
|
|
"Send an exit signal to a remote process."
|
|
(derl-dist-request (erl-pid-node to) #'derl-exit from to reason))
|
|
|
|
(defun erl-dist-exit2 (from to reason)
|
|
"Send an `exit2' signal to a remote process.
|
|
Use the distribution protocol's EXIT2 message."
|
|
;; I don't know exactly how EXIT2 differs from EXIT. Browsing the
|
|
;; emulator code, it looks like EXIT is for propagating a process
|
|
;; crash, and EXIT2 is for the exit/2 BIF (where FROM isn't
|
|
;; necessarily linked with TO).
|
|
(derl-dist-request (erl-pid-node to) #'derl-exit2 from to reason))
|
|
|
|
;; -----------------------------------------------------------
|
|
;; Handshake protocol states. These follow the protocol diagram in
|
|
;; the distributed_handshake.txt file of lib/kernel/internal_doc/ in
|
|
;; Erlang/OTP.
|
|
;; -----------------------------------------------------------
|
|
|
|
(defun derl-state0 (event node-name)
|
|
"Start state: send-name and then transition."
|
|
(check-event event 'init)
|
|
(setq derl-connection-node node-name)
|
|
(setq fsm-put-data-in-buffer t)
|
|
;; Do nodedown when the buffer is killed in an unexpected way
|
|
;; (e.g. by user)
|
|
(add-hook 'kill-buffer-hook
|
|
(lambda () (when derl-alive (derl-nodedown derl-connection-node))))
|
|
(derl-send-name)
|
|
(fsm-change-state #'derl-recv-status))
|
|
|
|
(defun derl-recv-status (event data)
|
|
"Wait for status message."
|
|
(check-event event 'data)
|
|
(let ((msg (derl-take-msg)))
|
|
(when msg
|
|
(if (string= msg "sok")
|
|
(fsm-change-state #'derl-recv-challenge t)
|
|
(fsm-fail)))))
|
|
|
|
(defun derl-recv-challenge (event data)
|
|
"Receive challenge message, send response and our challenge."
|
|
(check-event event 'data)
|
|
(when (derl-have-msg)
|
|
(goto-char (point-min))
|
|
(erlext-read2) ; skip length
|
|
(let ((tag (erlext-read1)))
|
|
(unless (equal 110 tag) ; tag-check (n)
|
|
(fsm-fail (format nil "wrong-tag: %S" tag))))
|
|
(let ((version (erlext-read2))
|
|
(flags (erlext-read4))
|
|
(challenge (erlext-readn 4))
|
|
(rem-node (buffer-substring (point) (derl-msg-end))))
|
|
(derl-eat-msg)
|
|
(derl-send-challenge-reply challenge)
|
|
(fsm-change-state #'derl-recv-challenge-ack))))
|
|
|
|
(defun derl-string-make-unibyte (string)
|
|
(if (fboundp 'string-make-unibyte)
|
|
(string-make-unibyte string)
|
|
string))
|
|
|
|
(defun derl-recv-challenge-ack (event data)
|
|
"Receive and check challenge ack. If it's OK then the handshake is
|
|
complete and we become live."
|
|
(if (equal event 'closed)
|
|
(message "Distel thinks the cookie is %s. Erlang seems to disagree."
|
|
(erl-cookie)))
|
|
(check-event event 'data)
|
|
(when (derl-have-msg)
|
|
(goto-char (point-min))
|
|
(erlext-read2) ; skip length
|
|
(unless (equal 97 (erlext-read1)) ; tag-check (a)
|
|
(fsm-fail 'wrong-tag))
|
|
(let ((digest (buffer-substring (point) (+ (point) 16))))
|
|
(derl-eat-msg)
|
|
(if (equal (derl-string-make-unibyte (derl-gen-digest (string 0 0 0 42))) digest)
|
|
(derl-go-live)
|
|
(fsm-fail)))))
|
|
|
|
;; Handshake support code
|
|
|
|
(defun derl-send-name ()
|
|
(erase-buffer)
|
|
(derl-send-msg
|
|
(fsm-build-message
|
|
(fsm-encode1 110) ; tag (n)
|
|
(fsm-encode2 5) ; version
|
|
(fsm-encode4 (logior derl-flag-extended-references
|
|
derl-flag-extended-pids-ports))
|
|
(fsm-insert (symbol-name erl-node-name)))))
|
|
|
|
(defun derl-send-challenge-reply (challenge)
|
|
(derl-send-msg (fsm-build-message
|
|
(fsm-encode1 114) ; 114 = ?r
|
|
(fsm-encode4 42)
|
|
(fsm-insert (derl-gen-digest challenge)))))
|
|
|
|
(defun derl-gen-digest (challenge)
|
|
"Generate a message digest as required for the specification's
|
|
gen_digest() function:
|
|
(md5 (concat challenge-as-ascii-decimal cookie))"
|
|
(derl-hexstring-to-binstring
|
|
(md5 (concat (erl-cookie) (derl-int32-to-decimal challenge)))))
|
|
|
|
(defun erl-cookie ()
|
|
(or derl-cookie
|
|
(with-temp-buffer
|
|
(insert-file-contents (concat (getenv "HOME") "/.erlang.cookie"))
|
|
(while (search-forward "\n" nil t)
|
|
(replace-match ""))
|
|
(buffer-string))))
|
|
|
|
;; ------------------------------------------------------------
|
|
;; Alive/connected state
|
|
;; ------------------------------------------------------------
|
|
|
|
(defun derl-go-live ()
|
|
(setq derl-alive t)
|
|
(setq derl-hdrlen 4)
|
|
(derl-nodeup derl-connection-node)
|
|
(mapc #'derl-do-request derl-request-queue)
|
|
(setq derl-request-queue nil)
|
|
(fsm-change-state #'derl-alive t))
|
|
|
|
(defun derl-alive (event data)
|
|
(check-event event 'data 'closed)
|
|
(if (eq event 'closed)
|
|
(progn (derl-nodedown derl-connection-node)
|
|
(setq derl-alive nil)
|
|
(fsm-fail))
|
|
(while (derl-handle-tick))
|
|
(when (derl-have-msg)
|
|
(let ((msg (derl-take-msg))
|
|
ctl
|
|
req)
|
|
;; Decode the control message, and the request if it's present
|
|
(let (default-enable-multibyte-characters)
|
|
(with-temp-buffer
|
|
(insert msg)
|
|
(goto-char (point-min))
|
|
(assert (= (erlext-read1) 112)) ; type = pass through..
|
|
(setq ctl (erlext-read-whole-obj))
|
|
(when (< (point) (point-max))
|
|
(setq req (erlext-read-whole-obj)))))
|
|
(ecase (tuple-elt ctl 1)
|
|
((1) ;; link: [1 FROM TO]
|
|
(let ((from (tuple-elt ctl 2))
|
|
(to (tuple-elt ctl 3)))
|
|
(derl-trace-input "LINK: %S %S" from to)
|
|
(add-to-list 'derl-remote-links (cons to from))
|
|
(erl-add-link to from)))
|
|
((2) ;; send: [2 COOKIE TO-PID]
|
|
(let ((to-pid (tuple-elt ctl 3)))
|
|
(derl-trace-input "SEND: %S %S" to-pid req)
|
|
(erl-send to-pid req)))
|
|
((3) ;; exit: [FROM TO REASON]
|
|
(let ((from (tuple-elt ctl 1))
|
|
(to (tuple-elt ctl 2))
|
|
(rsn (tuple-elt ctl 3)))
|
|
(derl-trace-input "EXIT: %S %S %S" from to rsn)
|
|
(erl-send-exit from to rsn)))
|
|
((4) ;; unlink: [4 FROM TO]
|
|
(let ((from (tuple-elt ctl 2))
|
|
(to (tuple-elt ctl 3)))
|
|
(derl-trace-input "UNLINK: %S %S %S" from to)
|
|
(erl-remove-link to from)))
|
|
((6) ;; reg_send: [6 FROM COOKIE NAME]
|
|
(let ((from (tuple-elt ctl 2))
|
|
(name (tuple-elt ctl 4)))
|
|
(derl-trace-input "REG_SEND: %S %S %S" from name req)
|
|
(condition-case data
|
|
(erl-send name req)
|
|
(erl-exit-signal
|
|
;; Ignore the error if the name isn't registered -
|
|
;; that's what the real nodes do. Seems reasonable,
|
|
;; since the send is async, and who knows what the
|
|
;; sender is up to now.
|
|
t))))))
|
|
;; Recursively handle other messages
|
|
(fsm-event 'data 'continue))))
|
|
|
|
(defun derl-handle-tick ()
|
|
(when (derl-have-tick)
|
|
(derl-eat-msg)
|
|
(derl-send-msg "")
|
|
t))
|
|
|
|
(defun derl-have-tick ()
|
|
(goto-char (point-min))
|
|
(and (>= (buffer-size) derl-hdrlen)
|
|
(= 0 (erlext-read4))))
|
|
|
|
;; ------------------------------------------------------------
|
|
;; Message buffer helpers
|
|
;; ------------------------------------------------------------
|
|
|
|
(defun derl-send-msg (string)
|
|
"Send a message (with a length header)."
|
|
(fsm-send-string (fsm-build-message
|
|
(fsm-encode (length string) derl-hdrlen)
|
|
(fsm-insert string))))
|
|
|
|
(defun derl-take-msg ()
|
|
"Read and return a message, removing it from the input buffer. If no
|
|
complete message is available, nil is returned and the buffer isn't
|
|
modified."
|
|
(when (derl-have-msg)
|
|
(goto-char (point-min))
|
|
(let* ((length (erlext-read derl-hdrlen))
|
|
(start (point))
|
|
(end (+ start length)))
|
|
(prog1 (buffer-substring start end)
|
|
(derl-eat-msg)))))
|
|
|
|
(defun derl-have-msg ()
|
|
(goto-char (point-min))
|
|
(when (>= (buffer-size) derl-hdrlen)
|
|
(let ((len (erlext-read derl-hdrlen)))
|
|
(>= (buffer-size) (+ derl-hdrlen len)))))
|
|
|
|
(defun derl-msg-end ()
|
|
(goto-char (point-min))
|
|
(+ (point-min) derl-hdrlen (erlext-read derl-hdrlen)))
|
|
|
|
(defun derl-eat-msg ()
|
|
(delete-region (point-min) (derl-msg-end)))
|
|
|
|
;; ------------------------------------------------------------
|
|
;; Distributed erlang protocol requests
|
|
;; ------------------------------------------------------------
|
|
|
|
(defun derl-dist-request (node &rest request)
|
|
"Make REQUEST to NODE. If the node isn't live, a connection is
|
|
initiated if necessary and the request is queued."
|
|
(let ((derl-bufname (derl-buffer-name node)))
|
|
(unless (get-buffer derl-bufname)
|
|
(erl-connect node))
|
|
(with-current-buffer derl-bufname
|
|
(cond (derl-shutting-down
|
|
nil)
|
|
(derl-alive
|
|
(derl-do-request request))
|
|
(t
|
|
(push request derl-request-queue))))))
|
|
|
|
(defun derl-do-request (req)
|
|
(apply (car req) (cdr req)))
|
|
|
|
(defun derl-send (pid msg)
|
|
(derl-trace-output "SEND: %S %S" pid msg)
|
|
(derl-send-request (tuple 2 empty-symbol pid) msg))
|
|
|
|
(defun derl-reg-send (from to term)
|
|
(derl-trace-output "REG_SEND: %S %S %S" from to term)
|
|
(derl-send-request (tuple 6 from empty-symbol to) term))
|
|
|
|
(defun derl-link (from to)
|
|
(derl-trace-output "LINK: %S %S" from to)
|
|
(add-to-list 'derl-remote-links (cons from to))
|
|
(derl-send-request (tuple 1 from to) nil t))
|
|
|
|
(defun derl-unlink (from to)
|
|
(derl-trace-output "UNLINK: %S %S" from to)
|
|
(derl-send-request (tuple 4 from to) nil t))
|
|
|
|
(defun derl-exit (from to reason)
|
|
(derl-trace-output "EXIT: %S %S %S" from to reason)
|
|
(derl-send-request (tuple 3 from to reason) nil t))
|
|
|
|
(defun derl-exit2 (from to reason)
|
|
(derl-trace-output "EXIT2: %S %S %S" from to reason)
|
|
(derl-send-request (tuple 8 from to reason) nil t))
|
|
|
|
(defun derl-send-request (control message &optional skip-message)
|
|
(let* ((ctl (erlext-term-to-binary control))
|
|
(msg (if skip-message "" (erlext-term-to-binary message)))
|
|
(len (+ 1 (length ctl) (length msg))))
|
|
(fsm-send-string
|
|
(fsm-build-message
|
|
(fsm-encode4 len)
|
|
(fsm-encode1 121) ; type = pass-through (whatever that means..)
|
|
(fsm-insert ctl)
|
|
(fsm-insert msg)))))
|
|
|
|
;; Tracing
|
|
|
|
(defface derl-trace-output-face
|
|
'((t (:inherit font-lock-string-face)))
|
|
"Face for outgoing messages in the distributed erlang trace
|
|
buffer.")
|
|
|
|
(defface derl-trace-input-face
|
|
'((t (:inherit font-lock-comment-face)))
|
|
"Face for incoming messages in the distributed erlang trace
|
|
buffer.")
|
|
|
|
(defun derl-trace-output (fmt &rest args)
|
|
(let ((msg (format ">> %s" (apply #'format (cons fmt args)))))
|
|
(put-text-property 0 (length msg) 'face 'derl-trace-output-face msg)
|
|
(derl-trace msg)))
|
|
|
|
(defun derl-trace-input (fmt &rest args)
|
|
(let ((msg (format "<< %s" (apply #'format (cons fmt args)))))
|
|
(put-text-property 0 (length msg) 'face 'derl-trace-input-face msg)
|
|
(derl-trace msg)))
|
|
|
|
(defun derl-trace (string)
|
|
(if derl-use-trace-buffer
|
|
(with-current-buffer (get-buffer-create
|
|
(format "*trace %S*" derl-connection-node))
|
|
(goto-char (point-max))
|
|
(insert string)
|
|
(insert "\n"))))
|
|
|
|
;; ------------------------------------------------------------
|
|
;; Utility
|
|
;; ------------------------------------------------------------
|
|
|
|
(defun derl-nodedown (node)
|
|
(setq derl-shutting-down t)
|
|
(dolist (link derl-remote-links)
|
|
(let ((local (car link))
|
|
(remote (cdr link)))
|
|
(message "LOCAL: %S REMOTE %S" local remote)
|
|
(erl-send-exit remote local 'noconnection)))
|
|
(run-hook-with-args 'erl-nodedown-hook node))
|
|
|
|
(defun derl-nodeup (node)
|
|
;; NB: only callable from the state machine
|
|
(run-hook-with-args 'erl-nodeup-hook node fsm-process))
|
|
|
|
(eval-and-compile
|
|
(defun derl-int32-to-decimal (s)
|
|
"Converts a 32-bit number (represented as a 4-byte string) into its
|
|
decimal printed representation."
|
|
(format "%.0f" (+ (+ (aref s 3) (* 256 (aref s 2)))
|
|
(* (+ 0.0 (aref s 1) (* 256 (aref s 0)))
|
|
65536)))))
|
|
|
|
;; Try to establish whether we have enough precision in floating-point
|
|
;; The test is pretty lame, even if it succeeds we cannot be sure
|
|
;; it'll work for all int32's
|
|
;; alas, i'm too ignorant to write a good test
|
|
;; the previous version of the test was nicer, but FSFmacs-specific :<
|
|
|
|
(unless (string= "1819634533" (derl-int32-to-decimal "luke"))
|
|
(error "Can't use Emacs's floating-point for `derl-int32-to-decimal'."))
|
|
|
|
(defun derl-hexstring-to-binstring (s)
|
|
"Convert the hexidecimal string S into a binary number represented
|
|
as a string of octets."
|
|
(let ((halves (mapcar #'derl-hexchar-to-int (string-to-list s))))
|
|
(derl-merge-halves halves)))
|
|
|
|
(defun derl-merge-halves (halves &optional acc)
|
|
(if (null halves)
|
|
(apply #'string (reverse acc))
|
|
(derl-merge-halves (cddr halves)
|
|
(cons (+ (ash (car halves) 4)
|
|
(cadr halves))
|
|
acc))))
|
|
|
|
(defun derl-hexchar-to-int (c)
|
|
(cond ((and (<= ?0 c) (<= c ?9))
|
|
(- c ?0))
|
|
((and (<= ?a c) (<= c ?f))
|
|
(+ 10 (- c ?a)))
|
|
(t
|
|
(error "Not hexchar" c))))
|
|
|
|
(defun derl-node-p (node)
|
|
"Check if `node' is a node name, e.g. \"foo@bar\". The @ character
|
|
is not allowed in the node or host name."
|
|
(and (symbolp node)
|
|
(string-match "^[^@]+@[^@]+$" (symbol-name node))))
|
|
|
|
(defun derl-node-name (node)
|
|
"Take the atom node part of a node name, e.g.
|
|
(derl-node-name \"foo@bar\") => \"foo\""
|
|
(assert (derl-node-p node))
|
|
(let ((string (symbol-name node)))
|
|
(string-match "^[^@]+" string)
|
|
(match-string 0 string)))
|
|
|
|
(defun derl-node-host (node)
|
|
"Take the host part of a node name, e.g.
|
|
(derl-node-host \"foo@bar\") => \"bar\""
|
|
(assert (derl-node-p node))
|
|
(let ((string (symbol-name node)))
|
|
(string-match "[^@]+$" string)
|
|
(match-string 0 string)))
|
|
|
|
(defun derl-buffer-name (node)
|
|
(format "*derl %s*" node))
|
|
|
|
;; ------------------------------------------------------------
|
|
;; Testing and playing around
|
|
;; ------------------------------------------------------------
|
|
|
|
(defun derl-go (port)
|
|
(fsm-connect "localhost" port #'derl-state0
|
|
nil
|
|
(lambda (result)
|
|
(message "RESULT: %S" result))
|
|
(lambda ()
|
|
(message "FAIL"))))
|
|
|
|
(provide 'derl)
|
|
|