Skip to content

Commit 3e1b408

Browse files
authored
Merge pull request #40 from clojure-lsp/deprecate-socket-server
Deprecate lsp4clj.socket-server
2 parents ac2deb7 + e775db0 commit 3e1b408

File tree

5 files changed

+94
-23
lines changed

5 files changed

+94
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Deprecate `lsp4clj.socket-server` and document preferred alternative in the README.
6+
57
## v1.7.3
68

79
## v1.7.2

README.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A [Language Server Protocol](https://microsoft.github.io/language-server-protoco
44

55
[![Clojars Project](https://img.shields.io/clojars/v/com.github.clojure-lsp/lsp4clj.svg)](https://clojars.org/com.github.clojure-lsp/lsp4clj)
66

7-
lsp4clj reads and writes from stdio, parsing JSON-RPC according to the LSP spec. It provides tools to allow server implementors to receive, process, and respond to any of the methods defined in the LSP spec, and to send their own requests and notifications to clients.
7+
lsp4clj reads and writes from io streams, parsing JSON-RPC according to the LSP spec. It provides tools to allow server implementors to receive, process, and respond to any of the methods defined in the LSP spec, and to send their own requests and notifications to clients.
88

99
## Usage
1010

@@ -119,15 +119,25 @@ First, if the server's input is closed, it will shut down too. Second, if you ca
119119

120120
When a server shuts down it stops reading input, finishes processing the messages it has in flight, and then closes is output. Finally it closes its `:log-ch` and `:trace-ch`. As such, it should probably not be shut down until the LSP `exit` notification (as opposed to the `shutdown` request) to ensure all messages are received. `lsp4clj.server/shutdown` will not return until all messages have been processed, or until 10 seconds have passed, whichever happens sooner. It will return `:done` in the first case and `:timeout` in the second.
121121

122-
### Socket server
122+
## Other types of servers
123123

124-
The `stdio-server` is the most commonly used, but the library also provides a `lsp4clj.socket-server/server`.
124+
So far the examples have focused on `lsp4clj.io-server/stdio-server`, because many clients communicate over stdio by default. The client opens a subprocess for the LSP server, then starts sending messages to the process via the process's stdin and reading messages from it on its stdout.
125+
126+
Many clients can also communicate over a socket. Typically the client starts a socket server, then passes a command-line argument to the LSP subprocess, telling it what port to connect to. The server is expected to connect to that port and use it to send and receive messages. In lsp4clj, that can be accomplished with `lsp4clj.io-server/server`:
125127

126128
```clojure
127-
(lsp4clj.socket-server/server {:port 61235})
129+
(defn socket-server [{:keys [host port]}]
130+
{:pre [(or (nil? host) (string? host))
131+
(and (int? port) (<= 1024 port 65535))]}
132+
(let [addr (java.net.InetAddress/getByName host) ;; nil host == loopback
133+
sock (java.net.Socket. ^java.net.InetAddress addr ^int port)]
134+
(lsp4clj.io-server/server {:in sock
135+
:out sock})))
128136
```
129137

130-
This will start listening on the provided port, blocking until a client makes a connection. When the connection is made it returns a lsp4clj server that has the same behavior as a `stdio-server`, except that messages are exchanged over the socket. When the server is shut down, the connection will be closed.
138+
`lsp4clj.io-server/server` accepts a pair of options `:in` and `:out`. These will be coerced to a `java.io.InputStream` and `java.io.OutputStream` via `clojure.java.io/input-stream` and `clojure.java.io/output-stream`, respectively. The example above works because a `java.net.Socket` can be coerced to both an input and output stream via this mechanism.
139+
140+
A similar approach can be used to connect over pipes.
131141

132142
## Development details
133143

@@ -156,7 +166,11 @@ You may also find `lsp4clj.server/chan-server` a useful alternative to `stdio-se
156166

157167
## Caveats
158168

159-
You must not print to stdout while a `stdio-server` is running. This will corrupt its output stream and clients will receive malformed messages. To protect a block of code from writing to stdout, wrap it with `lsp4clj.server/discarding-stdout`. The `receive-notification` and `receive-request` multimethods are already protected this way, but tasks started outside of these multimethods need this protection added. Consider using a `lsp4clj.socket-server/server` to avoid this problem.
169+
You must not print to stdout while a `stdio-server` is running. This will corrupt its output stream. Clients will receive malformed messages, and either throw errors or stop responding.
170+
171+
From experience, it's dismayingly easy to leave in an errant `prn` or `time` and end up with a non-responsive client. For this reason, we highly recommend supporting communication over sockets (see [other types of servers](#other-types-of-servers)) which are immune to this problem. However, since the choice of whether to use sockets or stdio is ultimately up to the client, you may have no choice but to support both.
172+
173+
lsp4clj provides one tool to avoid accidental writes to stdout (or rather to `*out*`, which is usually the same as `System.out`). To protect a block of code from writing to `*out*`, wrap it with `lsp4clj.server/discarding-stdout`. The `receive-notification` and `receive-request` multimethods are already protected this way, but tasks started outside of these multimethods or that run in separate threads need this protection added.
160174

161175
## Known lsp4clj users
162176

src/lsp4clj/io_chan.clj

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
[clojure.java.io :as io]
88
[clojure.string :as string])
99
(:import
10-
(java.io EOFException InputStream OutputStream)))
10+
(java.io
11+
EOFException
12+
IOException
13+
InputStream
14+
OutputStream)))
1115

1216
(set! *warn-on-reflection* true)
1317

@@ -72,15 +76,18 @@
7276
(defn ^:private read-header-line
7377
"Reads a line of input. Blocks if there are no messages on the input."
7478
[^InputStream input]
75-
(let [s (java.lang.StringBuilder.)]
76-
(loop []
77-
(let [b (.read input)] ;; blocks, presumably waiting for next message
78-
(case b
79-
-1 ::eof ;; end of stream
80-
#_lf 10 (str s) ;; finished reading line
81-
#_cr 13 (recur) ;; ignore carriage returns
82-
(do (.append s (char b)) ;; byte == char because header is in US-ASCII
83-
(recur)))))))
79+
(try
80+
(let [s (java.lang.StringBuilder.)]
81+
(loop []
82+
(let [b (.read input)] ;; blocks, presumably waiting for next message
83+
(case b
84+
-1 ::eof ;; end of stream
85+
#_lf 10 (str s) ;; finished reading line
86+
#_cr 13 (recur) ;; ignore carriage returns
87+
(do (.append s (char b)) ;; byte == char because header is in US-ASCII
88+
(recur))))))
89+
(catch IOException _e
90+
::eof)))
8491

8592
(defn input-stream->input-chan
8693
"Returns a channel which will yield parsed messages that have been read off

src/lsp4clj/socket_server.clj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
(ns lsp4clj.socket-server
2+
"DEPRECATED: Start a socket and listen on it.
3+
4+
This namespace is deprecated and is scheduled for deletion. See the README for
5+
recommendations on how to communicate with an LSP client over a socket.
6+
7+
This namespace was implemented based on a misconception. The LSP spec says
8+
servers should accept a `--port` CLI argument for socket-based communication.
9+
We assumed that the server was expected to start a socket server on that port,
10+
then wait for the client to connect and start sending messages. However, this
11+
is not how clients actually work. Instead, the _client_ opens the socket
12+
server, and tells the LSP server which port to connect to. That is, the LSP
13+
client acts as a socket server and the LSP server acts as a socket client.
14+
15+
This namespace, which largely deals with starting a socket server, is
16+
therefore unusable in an LSP server. If the LSP server did try to open a
17+
server on the provided `--port`, it should always fail, because the client
18+
should already be listening on that port."
19+
{:deprecated "1.7.4"}
220
(:require
321
[lsp4clj.io-server :as io-server])
422
(:import

test/lsp4clj/io_server_test.clj

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
[lsp4clj.io-server :as io-server]
77
[lsp4clj.lsp.requests :as lsp.requests]
88
[lsp4clj.server :as server]
9-
[lsp4clj.test-helper :as h]))
9+
[lsp4clj.test-helper :as h])
10+
(:import
11+
[java.io PipedInputStream PipedOutputStream]
12+
[java.net InetAddress ServerSocket Socket]))
1013

1114
(set! *warn-on-reflection* true)
1215

13-
(deftest server-test
14-
(let [server-input-stream (java.io.PipedInputStream.)
15-
server-output-stream (java.io.PipedOutputStream.)
16-
client-input-stream (java.io.PipedInputStream. server-output-stream)
17-
client-output-stream (java.io.PipedOutputStream. server-input-stream)
18-
server (io-server/server {:in server-input-stream :out server-output-stream})
16+
(deftest should-communicate-over-io-streams
17+
(let [client-input-stream (PipedInputStream.)
18+
client-output-stream (PipedOutputStream.)
19+
server-input-stream (PipedInputStream. client-output-stream)
20+
server-output-stream (PipedOutputStream. client-input-stream)
1921
client-input-ch (io-chan/input-stream->input-chan client-input-stream)
2022
client-output-ch (io-chan/output-stream->output-chan client-output-stream)
23+
server (io-server/server {:in server-input-stream :out server-output-stream})
2124
join (server/start server nil)]
2225
;; client initiates request
2326
(async/put! client-output-ch (lsp.requests/request 1 "foo" {}))
@@ -37,3 +40,30 @@
3740
(h/assert-take client-input-ch)))
3841
(server/shutdown server)
3942
(is (= :done @join))))
43+
44+
(deftest should-communicate-through-socket
45+
(let [addr (InetAddress/getLoopbackAddress)
46+
;; ephermeral port, translated to real port via .getLocalPort
47+
port 0]
48+
(with-open [socket-server (ServerSocket. port 0 addr)
49+
socket-for-server (Socket. addr (.getLocalPort socket-server))
50+
socket-for-client (.accept socket-server)]
51+
(let [client-input-ch (io-chan/input-stream->input-chan socket-for-client)
52+
client-output-ch (io-chan/output-stream->output-chan socket-for-client)
53+
server (io-server/server {:in socket-for-server
54+
:out socket-for-server})
55+
join (server/start server nil)]
56+
(async/put! client-output-ch (lsp.requests/request 1 "foo" {}))
57+
(is (= {:jsonrpc "2.0",
58+
:id 1,
59+
:error {:code -32601,
60+
:message "Method not found",
61+
:data {:method "foo"}}}
62+
(h/assert-take client-input-ch)))
63+
(server/send-notification server "bar" {:key "value"})
64+
(is (= {:jsonrpc "2.0",
65+
:method "bar",
66+
:params {:key "value"}}
67+
(h/assert-take client-input-ch)))
68+
(server/shutdown server)
69+
(is (= :done @join))))))

0 commit comments

Comments
 (0)