Skip to content

Commit e07c7e3

Browse files
committed
[feat] websockets
Allow the user to set up an lsp server that talks to the user using the websocket protocol instead of standard IO and do enough refactoring to make the setup with websockets possible
1 parent bd0217c commit e07c7e3

File tree

2 files changed

+207
-50
lines changed

2 files changed

+207
-50
lines changed

lsp/lsp.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ library
7979
, unliftio ^>=0.2
8080
, unliftio-core ^>=0.2
8181
, unordered-containers ^>=0.2
82+
, websockets ^>=0.13
8283

8384
executable lsp-demo-reactor-server
8485
import: warnings

lsp/src/Language/LSP/Server/Control.hs

Lines changed: 206 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
1+
{-# LANGUAGE DerivingStrategies #-}
12
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE RecordWildCards #-}
24

35
module Language.LSP.Server.Control (
46
-- * Running
5-
runServer,
67
runServerWith,
7-
runServerWithHandles,
8+
runServerWithConfig,
9+
ServerConfig (..),
810
LspServerLog (..),
11+
12+
-- ** Using standard 'IO' 'Handle's
13+
runServer,
14+
15+
-- ** Using 'Handle's
16+
runServerWithHandles,
17+
prependHeader,
18+
parseHeaders,
19+
20+
-- ** Using websockets
21+
WebsocketConfig (..),
22+
withWebsocket,
23+
withWebsocketRunServer,
924
) where
1025

1126
import Colog.Core (LogAction (..), Severity (..), WithSeverity (..), (<&))
1227
import Colog.Core qualified as L
1328
import Control.Applicative ((<|>))
1429
import Control.Concurrent
30+
import Control.Concurrent.Async
1531
import Control.Concurrent.STM.TChan
32+
import Control.Exception (finally)
1633
import Control.Monad
1734
import Control.Monad.IO.Class
1835
import Control.Monad.STM
@@ -31,6 +48,7 @@ import Language.LSP.Protocol.Message
3148
import Language.LSP.Server.Core
3249
import Language.LSP.Server.Processing qualified as Processing
3350
import Language.LSP.VFS
51+
import Network.WebSockets qualified as WS
3452
import Prettyprinter
3553
import System.IO
3654

@@ -42,6 +60,7 @@ data LspServerLog
4260
| Starting
4361
| ParsedMsg T.Text
4462
| SendMsg TL.Text
63+
| WebsocketLog WebsocketLog
4564
deriving (Show)
4665

4766
instance Pretty LspServerLog where
@@ -60,6 +79,7 @@ instance Pretty LspServerLog where
6079
pretty Starting = "Starting server"
6180
pretty (ParsedMsg msg) = "---> " <> pretty msg
6281
pretty (SendMsg msg) = "<--2-- " <> pretty msg
82+
pretty (WebsocketLog msg) = "Websocket:" <+> pretty msg
6383

6484
-- ---------------------------------------------------------------------
6585

@@ -71,18 +91,20 @@ instance Pretty LspServerLog where
7191
runServer :: forall config. ServerDefinition config -> IO Int
7292
runServer =
7393
runServerWithHandles
74-
ioLogger
75-
lspLogger
94+
defaultIOLogger
95+
defaultLspLogger
7696
stdin
7797
stdout
98+
99+
defaultIOLogger :: LogAction IO (WithSeverity LspServerLog)
100+
defaultIOLogger = L.cmap (show . prettyMsg) L.logStringStderr
78101
where
79102
prettyMsg l = "[" <> viaShow (L.getSeverity l) <> "] " <> pretty (L.getMsg l)
80-
ioLogger :: LogAction IO (WithSeverity LspServerLog)
81-
ioLogger = L.cmap (show . prettyMsg) L.logStringStderr
82-
lspLogger :: LogAction (LspM config) (WithSeverity LspServerLog)
83-
lspLogger =
84-
let clientLogger = L.cmap (fmap (T.pack . show . pretty)) defaultClientLogger
85-
in clientLogger <> L.hoistLogAction liftIO ioLogger
103+
104+
defaultLspLogger :: LogAction (LspM config) (WithSeverity LspServerLog)
105+
defaultLspLogger =
106+
let clientLogger = L.cmap (fmap (T.pack . show . pretty)) defaultClientLogger
107+
in clientLogger <> L.hoistLogAction liftIO defaultIOLogger
86108

87109
{- | Starts a language server over the specified handles.
88110
This function will return once the @exit@ notification is received.
@@ -116,27 +138,54 @@ runServerWithHandles ioLogger logger hin hout serverDefinition = do
116138

117139
{- | Starts listening and sending requests and responses
118140
using the specified I/O.
141+
142+
Assumes that the client sends (and wants to receive) the Content-Length
143+
header. If you do not want this to be the case, use 'runServerWithConfig'
119144
-}
120145
runServerWith ::
121146
-- | The logger to use outside the main body of the server where we can't assume the ability to send messages.
122147
LogAction IO (WithSeverity LspServerLog) ->
123148
-- | The logger to use once the server has started and can successfully send messages.
124149
LogAction (LspM config) (WithSeverity LspServerLog) ->
125150
-- | Client input.
126-
IO BS.ByteString ->
151+
IO BS.StrictByteString ->
127152
-- | Function to provide output to.
128-
(BSL.ByteString -> IO ()) ->
153+
(BSL.LazyByteString -> IO ()) ->
129154
ServerDefinition config ->
130155
IO Int -- exit code
131-
runServerWith ioLogger logger clientIn clientOut serverDefinition = do
156+
runServerWith ioLogger lspLogger inwards outwards =
157+
runServerWithConfig ServerConfig{prepareOutwards = prependHeader, parseInwards = parseHeaders, ..}
158+
159+
-- ---------------------------------------------------------------------
160+
161+
data ServerConfig config = ServerConfig
162+
{ ioLogger :: LogAction IO (WithSeverity LspServerLog)
163+
-- ^ The logger to use outside the main body of the server where we can't assume the ability to send messages.
164+
, lspLogger :: LogAction (LspM config) (WithSeverity LspServerLog)
165+
-- ^ The logger to use once the server has started and can successfully send messages.
166+
, inwards :: IO BS.StrictByteString
167+
-- ^ Client input.
168+
, outwards :: BSL.LazyByteString -> IO ()
169+
-- ^ Function to provide output to.
170+
, prepareOutwards :: BSL.LazyByteString -> BSL.LazyByteString
171+
-- ^ how to prepare an outgoing response for sending. This can be used, to e.g. prepend the Content-Length header, c.f. 'prependHeader'
172+
, parseInwards :: Attoparsec.Parser BS.StrictByteString
173+
-- ^ how to parse the input. This can be used to consume the Content-Length and Content-Type headers, c.f. 'parseHeaders'
174+
}
175+
176+
runServerWithConfig ::
177+
ServerConfig config ->
178+
ServerDefinition config ->
179+
IO Int
180+
runServerWithConfig ServerConfig{..} serverDefinition = do
132181
ioLogger <& Starting `WithSeverity` Info
133182

134183
cout <- atomically newTChan :: IO (TChan J.Value)
135-
_rhpid <- forkIO $ sendServer ioLogger cout clientOut
184+
_rhpid <- forkIO $ sendServer ioLogger cout outwards prepareOutwards
136185

137186
let sendMsg msg = atomically $ writeTChan cout $ J.toJSON msg
138187

139-
ioLoop ioLogger logger clientIn serverDefinition emptyVFS sendMsg
188+
ioLoop ioLogger lspLogger inwards parseInwards serverDefinition emptyVFS sendMsg
140189

141190
return 1
142191

@@ -146,12 +195,13 @@ ioLoop ::
146195
forall config.
147196
LogAction IO (WithSeverity LspServerLog) ->
148197
LogAction (LspM config) (WithSeverity LspServerLog) ->
149-
IO BS.ByteString ->
198+
IO BS.StrictByteString ->
199+
Attoparsec.Parser BS.StrictByteString ->
150200
ServerDefinition config ->
151201
VFS ->
152202
(FromServerMessage -> IO ()) ->
153203
IO ()
154-
ioLoop ioLogger logger clientIn serverDefinition vfs sendMsg = do
204+
ioLoop ioLogger logger clientIn parser serverDefinition vfs sendMsg = do
155205
minitialize <- parseOne ioLogger clientIn (parse parser "")
156206
case minitialize of
157207
Nothing -> pure ()
@@ -167,7 +217,7 @@ ioLoop ioLogger logger clientIn serverDefinition vfs sendMsg = do
167217
pioLogger = L.cmap (fmap LspProcessingLog) ioLogger
168218
pLogger = L.cmap (fmap LspProcessingLog) logger
169219

170-
loop :: Result BS.ByteString -> LspM config ()
220+
loop :: Result BS.StrictByteString -> LspM config ()
171221
loop = go
172222
where
173223
go r = do
@@ -178,30 +228,11 @@ ioLoop ioLogger logger clientIn serverDefinition vfs sendMsg = do
178228
Processing.processMessage pLogger $ BSL.fromStrict msg
179229
go (parse parser remainder)
180230

181-
parser = do
182-
try contentType <|> return ()
183-
len <- contentLength
184-
try contentType <|> return ()
185-
_ <- string _ONE_CRLF
186-
Attoparsec.take len
187-
188-
contentLength = do
189-
_ <- string "Content-Length: "
190-
len <- decimal
191-
_ <- string _ONE_CRLF
192-
return len
193-
194-
contentType = do
195-
_ <- string "Content-Type: "
196-
skipWhile (/= '\r')
197-
_ <- string _ONE_CRLF
198-
return ()
199-
200231
parseOne ::
201232
MonadIO m =>
202233
LogAction m (WithSeverity LspServerLog) ->
203-
IO BS.ByteString ->
204-
Result BS.ByteString ->
234+
IO BS.StrictByteString ->
235+
Result BS.StrictByteString ->
205236
m (Maybe (BS.ByteString, BS.ByteString))
206237
parseOne logger clientIn = go
207238
where
@@ -223,30 +254,155 @@ parseOne logger clientIn = go
223254

224255
-- ---------------------------------------------------------------------
225256

257+
data WebsocketLog
258+
= WebsocketShutDown
259+
| WebsocketNewConnection
260+
| WebsocketConnectionClosed
261+
| WebsocketPing
262+
| WebsocketStarted
263+
| WebsocketIncomingRequest
264+
| WebsocketOutgoingResponse
265+
deriving stock (Show)
266+
267+
instance Pretty WebsocketLog where
268+
pretty l = case l of
269+
WebsocketPing -> "Ping"
270+
WebsocketStarted -> "Started Server, waiting for connections"
271+
WebsocketShutDown -> "Shut down server"
272+
WebsocketNewConnection -> "New connection established"
273+
WebsocketIncomingRequest -> "Received request"
274+
WebsocketConnectionClosed -> "Closed connection to client"
275+
WebsocketOutgoingResponse -> "Sent response"
276+
277+
-- | 'host' and 'port' of the websocket server to set up
278+
data WebsocketConfig = WebsocketConfig
279+
{ host :: !String
280+
-- ^ the host of the websocket server, e.g. @"localhost"@
281+
, port :: !Int
282+
-- ^ the port of the websocket server, e.g. @8080@
283+
}
284+
285+
-- | Set up a websocket server, then call call the continuation (in our case this corresponds to the language server) after accepting a connection
286+
withWebsocket ::
287+
-- | The logger
288+
LogAction IO (WithSeverity LspServerLog) ->
289+
-- | The configuration of the websocket server
290+
WebsocketConfig ->
291+
-- | invoke the lsp server, passing communication functions
292+
(IO BS.StrictByteString -> (BSL.LazyByteString -> IO ()) -> IO r) ->
293+
IO ()
294+
withWebsocket logger conf startLspServer = do
295+
let wsLogger = L.cmap (fmap WebsocketLog) logger
296+
297+
WS.runServer (host conf) (port conf) $ \pending -> do
298+
conn <- WS.acceptRequest pending
299+
wsLogger <& WebsocketNewConnection `WithSeverity` Debug
300+
301+
outChan <- newChan
302+
inChan <- newChan
303+
304+
let inwards = readChan inChan
305+
outwards = writeChan outChan
306+
307+
WS.withPingThread conn 30 (wsLogger <& WebsocketPing `WithSeverity` Debug) $ do
308+
withAsync (startLspServer inwards outwards) $ \_lspAsync ->
309+
race_
310+
( forever $ do
311+
msg <- readChan outChan
312+
wsLogger <& WebsocketOutgoingResponse `WithSeverity` Debug
313+
WS.sendTextData conn msg
314+
)
315+
( forever $ do
316+
msg <- WS.receiveData conn
317+
wsLogger <& WebsocketIncomingRequest `WithSeverity` Debug
318+
writeChan inChan msg
319+
)
320+
`finally` do
321+
wsLogger <& WebsocketConnectionClosed `WithSeverity` Debug
322+
323+
withWebsocketRunServer ::
324+
-- | Configuration for the websocket
325+
WebsocketConfig ->
326+
-- | How to set up a new 'ServerDefinition' for a specific configuration. z
327+
-- This is passed as CPS'd 'IO' to allow for setting (- and cleaning) up
328+
-- a server per websocket connection
329+
((ServerDefinition config -> IO Int) -> IO Int) ->
330+
-- | The 'IO' logger
331+
LogAction IO (WithSeverity LspServerLog) ->
332+
-- | The logger that logs in 'LspM' to the client
333+
LogAction (LspM config) (WithSeverity LspServerLog) ->
334+
IO ()
335+
withWebsocketRunServer wsConf withLspDefinition ioLogger lspLogger =
336+
withWebsocket ioLogger wsConf $ \inwards outwards -> do
337+
withLspDefinition $ \lspDefinition ->
338+
runServerWithConfig
339+
ServerConfig
340+
{ ioLogger
341+
, lspLogger
342+
, inwards
343+
, outwards
344+
, -- NOTE: if you run the language server on websockets, you do not
345+
-- need to prepend headers to requests and responses, because
346+
-- the chunking is already handled by the websocket, i.e. there's
347+
-- no situation where the client or the server has to rely on input/
348+
-- output chunking
349+
prepareOutwards = id
350+
, parseInwards = Attoparsec.takeByteString
351+
}
352+
lspDefinition
353+
354+
-- ---------------------------------------------------------------------
355+
226356
-- | Simple server to make sure all output is serialised
227-
sendServer :: LogAction IO (WithSeverity LspServerLog) -> TChan J.Value -> (BSL.ByteString -> IO ()) -> IO ()
228-
sendServer _logger msgChan clientOut = do
357+
sendServer :: LogAction IO (WithSeverity LspServerLog) -> TChan J.Value -> (BSL.LazyByteString -> IO ()) -> (BSL.LazyByteString -> BSL.LazyByteString) -> IO ()
358+
sendServer _logger msgChan clientOut prepareMessage = do
229359
forever $ do
230360
msg <- atomically $ readTChan msgChan
231361

232362
-- We need to make sure we only send over the content of the message,
233363
-- and no other tags/wrapper stuff
234364
let str = J.encode msg
235-
236-
let out =
237-
BSL.concat
238-
[ TL.encodeUtf8 $ TL.pack $ "Content-Length: " ++ show (BSL.length str)
239-
, BSL.fromStrict _TWO_CRLF
240-
, str
241-
]
365+
let out = prepareMessage str
242366

243367
clientOut out
244368

245369
-- TODO: figure out how to re-enable
246370
-- This can lead to infinite recursion in logging, see https://github.com/haskell/lsp/issues/447
247371
-- logger <& SendMsg (TL.decodeUtf8 str) `WithSeverity` Debug
248372

249-
_ONE_CRLF :: BS.ByteString
373+
-- | prepend a Content-Length header to the given message
374+
prependHeader :: BSL.LazyByteString -> BSL.LazyByteString
375+
prependHeader str =
376+
BSL.concat
377+
[ TL.encodeUtf8 $ TL.pack $ "Content-Length: " ++ show (BSL.length str)
378+
, BSL.fromStrict _TWO_CRLF
379+
, str
380+
]
381+
382+
{- | parse Content-Length and Content-Type headers and then consume
383+
input with length of the Content-Length
384+
-}
385+
parseHeaders :: Attoparsec.Parser BS.StrictByteString
386+
parseHeaders = do
387+
try contentType <|> return ()
388+
len <- contentLength
389+
try contentType <|> return ()
390+
_ <- string _ONE_CRLF
391+
Attoparsec.take len
392+
where
393+
contentLength = do
394+
_ <- string "Content-Length: "
395+
len <- decimal
396+
_ <- string _ONE_CRLF
397+
return len
398+
399+
contentType = do
400+
_ <- string "Content-Type: "
401+
skipWhile (/= '\r')
402+
_ <- string _ONE_CRLF
403+
return ()
404+
405+
_ONE_CRLF :: BS.StrictByteString
250406
_ONE_CRLF = "\r\n"
251-
_TWO_CRLF :: BS.ByteString
407+
_TWO_CRLF :: BS.StrictByteString
252408
_TWO_CRLF = "\r\n\r\n"

0 commit comments

Comments
 (0)