Skip to content

Commit 165b75b

Browse files
committed
Merge pull request #825 from vitoshka/direct-conn
Rewrite nrepl connection logic
2 parents 52edef6 + 6e0e591 commit 165b75b

File tree

2 files changed

+114
-91
lines changed

2 files changed

+114
-91
lines changed

cider.el

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,9 @@ Return a list of the form (HOST PORT), where PORT can be nil."
191191
"Interactively select port for HOST from PORTS."
192192
(let* ((sel-port (completing-read (format "Port for %s: " host)
193193
(cider-join-with-val-prop ports)))
194-
(port (or (get-text-property 1 :val sel-port) sel-port)))
195-
(if (listp port) (second port) port)))
194+
(port (or (get-text-property 1 :val sel-port) sel-port))
195+
(port (if (listp port) (second port) port)))
196+
(if (stringp port) (string-to-number port) port)))
196197

197198
(defun cider-locate-running-nrepl-ports ()
198199
"Locate ports of running nREPL servers.

nrepl-client.el

Lines changed: 111 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,16 @@ The `nrepl-buffer-name-separator' separates cider-repl from the project name."
107107
:type 'hook
108108
:group 'nrepl)
109109

110-
(defcustom nrepl-host "127.0.0.1"
110+
(defcustom nrepl-host "localhost"
111111
"The default hostname (or IP address) to connect to."
112112
:type 'string
113113
:group 'nrepl)
114114

115+
(defcustom nrepl-force-ssh-for-remote-hosts nil
116+
"If non-nil, do not attempt a direct connection for remote hosts."
117+
:type 'boolean
118+
:group 'nrepl)
119+
115120
(defcustom nrepl-port nil
116121
"The default port to connect to."
117122
:type 'string
@@ -581,10 +586,96 @@ older requests with \"done\" status."
581586
"Handle sentinel events from PROCESS.
582587
Display MESSAGE and if the process is closed kill the
583588
process buffer and run the hook `nrepl-disconnected-hook'."
584-
(message "nREPL connection closed: %s" message)
589+
(message "nREPL: Connection closed (%s)" message)
585590
(if (equal (process-status process) 'closed)
586591
(run-hooks 'nrepl-disconnected-hook)))
587592

593+
594+
;;; Network
595+
596+
(defun nrepl-connect (host port)
597+
"Connect to machine identified by HOST and PORT.
598+
For local hosts use a direct connection. For remote hosts, if
599+
`nrepl-force-ssh-for-remote-hosts' is nil, attempt a direct connection
600+
first. If `nrepl-force-ssh-for-remote-hosts' is non-nil or the direct
601+
connection failed, try to start a SSH tunneled connection. Return a plist
602+
of the form (:proc PROC :host \"HOST\" :port PORT) that might contain
603+
additional key-values depending on the connection type."
604+
(let ((localp (if host
605+
(nrepl-local-host-p host)
606+
(not (file-remote-p default-directory)))))
607+
(if localp
608+
(nrepl--direct-connect (or host "localhost") port)
609+
(or (and host (not nrepl-force-ssh-for-remote-hosts)
610+
(nrepl--direct-connect host port 'no-error))
611+
(nrepl--ssh-tunnel-connect host port)))))
612+
613+
(defun nrepl--direct-connect (host port &optional no-error)
614+
"If HOST and PORT are given, try to `open-network-stream'.
615+
If NO-ERROR is non-nil, show messages instead of throwing an error."
616+
(if (not (and host port))
617+
(unless no-error
618+
(error "Host (%s) and port (%s) must be provided" host port))
619+
(message "nREPL: Establishing direct connection to %s:%s ..." host port)
620+
(condition-case nil
621+
(prog1 (list :proc (open-network-stream "nrepl" nil host port)
622+
:host host :port port)
623+
(message "nREPL: Direct connection established"))
624+
(error (let ((mes "nREPL: Direct connection failed"))
625+
(if no-error (message mes) (error mes))
626+
nil)))))
627+
628+
(defun nrepl--ssh-tunnel-connect (host port)
629+
"Connect to a remote machine identified by HOST and PORT through SSH tunnel."
630+
(message "nREPL: Establishing SSH tunneled connection ...")
631+
(let* ((remote-dir (if host (format "/ssh:%s:" host) default-directory))
632+
(ssh (or (executable-find "ssh")
633+
(error "nREPL: Cannot locate 'ssh' executable")))
634+
(cmd (nrepl--ssh-tunnel-command ssh remote-dir port))
635+
(tunnel-buf (nrepl-tunnel-buffer-name))
636+
(tunnel (start-process-shell-command "nrepl-tunnel" tunnel-buf cmd)))
637+
(process-put tunnel :waiting-for-port t)
638+
(set-process-filter tunnel (nrepl--ssh-tunnel-filter port))
639+
(while (and (process-live-p tunnel)
640+
(process-get tunnel :waiting-for-port))
641+
(accept-process-output nil 0.005))
642+
(if (not (process-live-p tunnel))
643+
(error "nREPL: SSH port forwarding failed. Check the '%s' buffer." tunnel-buf)
644+
(message "nREPL: SSH port forwarding established to localhost:%s" port)
645+
(let ((endpoint (nrepl--direct-connect "localhost" port)))
646+
(-> endpoint
647+
(plist-put :tunnel tunnel)
648+
(plist-put :remote-host host))))))
649+
650+
(defun nrepl--ssh-tunnel-command (ssh dir port)
651+
"Command string to open SSH tunnel to the host associated with DIR's PORT."
652+
(with-parsed-tramp-file-name dir nil
653+
;; this abuses the -v option for ssh to get output when the port
654+
;; forwarding is set up, which is used to synchronise on, so that
655+
;; the port forwarding is up when we try to connect.
656+
(format-spec
657+
"%s -v -N -L %p:localhost:%p %u'%h'"
658+
`((?s . ,ssh)
659+
(?p . ,port)
660+
(?h . ,host)
661+
(?u . ,(if user (format "-l '%s' " user) ""))))))
662+
663+
(defun nrepl--ssh-tunnel-filter (port)
664+
"Return a process filter that waits for PORT to appear in process output."
665+
(let ((port-string (format "LOCALHOST:%s" port)))
666+
(lambda (proc string)
667+
(when (string-match port-string string)
668+
(process-put proc :waiting-for-port nil))
669+
(when (and (process-live-p proc)
670+
(buffer-live-p (process-buffer proc)))
671+
(with-current-buffer (process-buffer proc)
672+
(let ((moving (= (point) (process-mark proc))))
673+
(save-excursion
674+
(goto-char (process-mark proc))
675+
(insert string)
676+
(set-marker (process-mark proc) (point)))
677+
(if moving (goto-char (process-mark proc)))))))))
678+
588679

589680
;;; Client: Process Handling
590681

@@ -598,16 +689,15 @@ If eitehr HOST or PORT are nil, pick them from the value returned by
598689
connection which is associated with a repl buffer. When non-nil,
599690
SERVER-PROC must be a running nrepl server process within Emacs. Return
600691
the newly created client connection process."
601-
(let* ((endpoint (nrepl-connection-endpoint host port))
692+
(let* ((endpoint (nrepl-connect host port))
693+
(client-proc (plist-get endpoint :proc))
602694
(host (plist-get endpoint :host))
603695
(port (plist-get endpoint :port))
604-
(server-buf (and server-proc (process-buffer server-proc)))
605696
(client-buf (if replp
606697
(cider-repl-create default-directory host port)
607-
(nrepl-create-connection-buffer default-directory host port)))
608-
(client-proc (open-network-stream "nrepl" client-buf host port))
609-
(tunnel-proc (plist-get endpoint :proc))
610-
(nrepl-connection-dispatch client-buf))
698+
(nrepl-create-connection-buffer default-directory host port))))
699+
700+
(set-process-buffer client-proc (get-buffer client-buf))
611701

612702
(set-process-filter client-proc 'nrepl-client-filter)
613703
(set-process-sentinel client-proc 'nrepl-client-sentinel)
@@ -617,15 +707,16 @@ the newly created client connection process."
617707
(process-put client-proc :response-q (nrepl-response-queue))
618708

619709
(with-current-buffer client-buf
620-
(when server-buf
710+
(-when-let (server-buf (and server-proc (process-buffer server-proc)))
621711
(setq nrepl-project-dir (buffer-local-value 'nrepl-project-dir server-buf)
622712
nrepl-server-buffer server-buf))
623713
(setq nrepl-endpoint `(,host ,port)
624714
;; FIXME: REPL and connection buffers are the same thing
625715
nrepl-connection-buffer client-buf
626716
nrepl-repl-buffer (when replp client-buf)
627717
nrepl-buffer-ns "user"
628-
nrepl-tunnel-buffer (and tunnel-proc (process-buffer tunnel-proc))
718+
nrepl-tunnel-buffer (-when-let (tunnel (plist-get endpoint :tunnel))
719+
(process-buffer tunnel))
629720
nrepl-pending-requests (make-hash-table :test 'equal)
630721
nrepl-completed-requests (make-hash-table :test 'equal)))
631722

@@ -901,69 +992,6 @@ Return a newly created process."
901992
(error "Leiningen 2.x is required by CIDER"))
902993
(t (error "Could not start nREPL server: %s" problem)))))
903994

904-
(defun nrepl-connection-endpoint (host port)
905-
"Return a connection endpoint.
906-
The returned endpoint is a `plist` of the form:
907-
908-
(:proc PROCESS :host \"hostname\" :port 1234)
909-
910-
If HOST is local :proc will be nil, :host - \"localhost\" and :port - PORT.
911-
912-
If HOST is nil and `default-directory' is remote, or if HOST is remote
913-
attempt to start an SSH tunnel and return it as :proc slot. If no `ssh'
914-
executable has been found, fall back to specifying a direct connection to
915-
the remote host."
916-
(let ((localp (if host
917-
(nrepl-local-host-p host)
918-
(not (file-remote-p default-directory)))))
919-
(if localp
920-
(list :host (or host "localhost") :port port :proc nil)
921-
(let ((ssh (executable-find "ssh")))
922-
(if ssh
923-
;; create a tunnel
924-
(let* ((remote-dir (if host (format "/ssh:%s:" host) default-directory))
925-
(cmd (nrepl--ssh-tunnel-command ssh remote-dir port))
926-
(proc (start-process-shell-command
927-
"nrepl-tunnel" (nrepl-tunnel-buffer-name) cmd)))
928-
(process-put proc :waiting-for-port t)
929-
(set-process-filter proc (nrepl--ssh-tunnel-filter port))
930-
(while (and (process-live-p proc)
931-
(process-get proc :waiting-for-port))
932-
(accept-process-output nil 0.005))
933-
(unless (process-live-p proc)
934-
(message "SSH port forwarding failed"))
935-
(list :host "localhost" :port port :proc proc))
936-
;; try a direct connection
937-
(list :host tramp-current-host :port port :proc nil))))))
938-
939-
(defun nrepl--ssh-tunnel-command (ssh dir port)
940-
"Command string to open SSH tunnel to the host associated with DIR's PORT."
941-
(with-parsed-tramp-file-name dir nil
942-
;; this abuses the -v option for ssh to get output when the port
943-
;; forwarding is set up, which is used to synchronise on, so that
944-
;; the port forwarding is up when we try to connect.
945-
(format-spec
946-
"%s -v -N -L %p:localhost:%p %u'%h'"
947-
`((?s . ,ssh)
948-
(?p . ,port)
949-
(?h . ,host)
950-
(?u . ,(if user (format "-l '%s' " user) ""))))))
951-
952-
(defun nrepl--ssh-tunnel-filter (port)
953-
"Return a filter function for waiting on PORT to appear in output."
954-
(let ((port-string (format "LOCALHOST:%s" port)))
955-
(lambda (proc string)
956-
(when (string-match port-string string)
957-
(process-put proc :waiting-for-port nil))
958-
(when (buffer-live-p (process-buffer proc))
959-
(with-current-buffer (process-buffer proc)
960-
(let ((moving (= (point) (process-mark proc))))
961-
(save-excursion
962-
(goto-char (process-mark proc))
963-
(insert string)
964-
(set-marker (process-mark proc) (point)))
965-
(if moving (goto-char (process-mark proc)))))))))
966-
967995

968996
;;; Messages
969997

@@ -1040,10 +1068,6 @@ The default buffer name is *nrepl-messages*."
10401068

10411069
;;; Connection Buffer Management
10421070

1043-
(defvar nrepl-connection-dispatch nil
1044-
"Bound to the connection a message was received on.
1045-
This is bound for the duration of the handling of that message")
1046-
10471071
(defvar nrepl-connection-list nil
10481072
"A list of connections.")
10491073

@@ -1067,8 +1091,7 @@ PROJECT-DIR, HOST and PORT are as in `nrepl-make-buffer-name'."
10671091
"The connection to use for nREPL interaction.
10681092
When NO-ERROR is non-nil, don't throw an error when no connection has been
10691093
found."
1070-
(or nrepl-connection-dispatch
1071-
nrepl-connection-buffer
1094+
(or nrepl-connection-buffer
10721095
(car (nrepl-connection-buffers))
10731096
(unless no-error
10741097
(error "No nREPL connection buffer"))))
@@ -1098,16 +1121,15 @@ Moves CONNECITON-BUFFER to the front of `nrepl-connection-list'."
10981121
(defun nrepl--close-connection-buffer (conn-buffer)
10991122
"Closes CONN-BUFFER, removing it from `nrepl-connection-list'.
11001123
Also closes associated REPL and server buffers."
1101-
(let ((nrepl-connection-dispatch conn-buffer))
1102-
(let ((buffer (get-buffer conn-buffer)))
1103-
(setq nrepl-connection-list
1104-
(delq (buffer-name buffer) nrepl-connection-list))
1105-
(when (buffer-live-p buffer)
1106-
(dolist (buf `(,(buffer-local-value 'nrepl-server-buffer buffer)
1107-
,(buffer-local-value 'nrepl-tunnel-buffer buffer)
1108-
,buffer))
1109-
(when buf
1110-
(cider--close-buffer buf)))))))
1124+
(let ((buffer (get-buffer conn-buffer)))
1125+
(setq nrepl-connection-list
1126+
(delq (buffer-name buffer) nrepl-connection-list))
1127+
(when (buffer-live-p buffer)
1128+
(dolist (buf `(,(buffer-local-value 'nrepl-server-buffer buffer)
1129+
,(buffer-local-value 'nrepl-tunnel-buffer buffer)
1130+
,buffer))
1131+
(when buf
1132+
(cider--close-buffer buf))))))
11111133

11121134

11131135
;;; Connection Browser

0 commit comments

Comments
 (0)