Skip to content

Commit ad69ecf

Browse files
committed
Modularise the server input and output
The goal here is to make the `Control` module as boring and dispensible as possible, so that users can put the pieces together as they like. Thisi s a step in that direction, tackling the server in/out threads.
1 parent 6ed8fe8 commit ad69ecf

File tree

5 files changed

+179
-154
lines changed

5 files changed

+179
-154
lines changed

lsp/lsp.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ library
4747
Language.LSP.Server.Control
4848
Language.LSP.Server.Core
4949
Language.LSP.Server.Processing
50+
Language.LSP.Server.IO
5051

5152
ghc-options: -Wall
5253
build-depends:

lsp/src/Language/LSP/Server.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{-# LANGUAGE TypeOperators #-}
22
module Language.LSP.Server
33
( module Language.LSP.Server.Control
4+
, module Language.LSP.Server.IO
45
, VFSData(..)
56
, ServerDefinition(..)
67

@@ -66,3 +67,4 @@ module Language.LSP.Server
6667

6768
import Language.LSP.Server.Control
6869
import Language.LSP.Server.Core
70+
import Language.LSP.Server.IO
Lines changed: 27 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{-# LANGUAGE OverloadedStrings #-}
22
{-# LANGUAGE ScopedTypeVariables #-}
33
{-# LANGUAGE RankNTypes #-}
4-
{-# LANGUAGE LambdaCase #-}
54

65
-- So we can keep using the old prettyprinter modules (which have a better
76
-- compatibility range) for now.
@@ -17,57 +16,38 @@ module Language.LSP.Server.Control
1716
) where
1817

1918
import qualified Colog.Core as L
20-
import Colog.Core (LogAction (..), WithSeverity (..), Severity (..), (<&))
21-
import Control.Concurrent
19+
import Colog.Core (LogAction (..), WithSeverity (..), Severity (..), (<&), cmap)
20+
import qualified Control.Concurrent.Async as Async
2221
import Control.Concurrent.STM.TChan
2322
import Control.Applicative((<|>))
2423
import Control.Monad
2524
import Control.Monad.STM
2625
import Control.Monad.IO.Class
2726
import qualified Data.Aeson as J
28-
import qualified Data.Attoparsec.ByteString as Attoparsec
29-
import Data.Attoparsec.ByteString.Char8
3027
import qualified Data.ByteString as BS
3128
import Data.ByteString.Builder.Extra (defaultChunkSize)
32-
import qualified Data.ByteString.Lazy as BSL
33-
import qualified Data.Text.Lazy as TL
34-
import qualified Data.Text.Lazy.Encoding as TL
3529
import qualified Data.Text as T
3630
import Data.Text.Prettyprint.Doc
37-
import Data.List
3831
import Language.LSP.Server.Core
3932
import qualified Language.LSP.Server.Processing as Processing
4033
import Language.LSP.Protocol.Message
4134
import Language.LSP.VFS
35+
import qualified Language.LSP.Server.IO as IO
4236
import Language.LSP.Logging (defaultClientLogger)
4337
import System.IO
4438

4539
data LspServerLog =
4640
LspProcessingLog Processing.LspProcessingLog
47-
| DecodeInitializeError String
48-
| HeaderParseFail [String] String
49-
| EOF
41+
| LspIoLog IO.LspIoLog
5042
| Starting
51-
| ParsedMsg T.Text
52-
| SendMsg TL.Text
43+
| Stopping
5344
deriving (Show)
5445

5546
instance Pretty LspServerLog where
5647
pretty (LspProcessingLog l) = pretty l
57-
pretty (DecodeInitializeError err) =
58-
vsep [
59-
"Got error while decoding initialize:"
60-
, pretty err
61-
]
62-
pretty (HeaderParseFail ctxs err) =
63-
vsep [
64-
"Failed to parse message header:"
65-
, pretty (intercalate " > " ctxs) <> ": " <+> pretty err
66-
]
67-
pretty EOF = "Got EOF"
48+
pretty (LspIoLog l) = pretty l
6849
pretty Starting = "Starting server"
69-
pretty (ParsedMsg msg) = "---> " <> pretty msg
70-
pretty (SendMsg msg) = "<--2-- " <> pretty msg
50+
pretty Stopping = "Stopping server"
7151

7252
-- ---------------------------------------------------------------------
7353

@@ -116,7 +96,7 @@ runServerWithHandles ioLogger logger hin hout serverDefinition = do
11696
clientIn = BS.hGetSome hin defaultChunkSize
11797

11898
clientOut out = do
119-
BSL.hPut hout out
99+
BS.hPut hout out
120100
hFlush hout
121101

122102
runServerWith ioLogger logger clientIn clientOut serverDefinition
@@ -130,134 +110,34 @@ runServerWith ::
130110
-- ^ The logger to use once the server has started and can successfully send messages.
131111
-> IO BS.ByteString
132112
-- ^ Client input.
133-
-> (BSL.ByteString -> IO ())
113+
-> (BS.ByteString -> IO ())
134114
-- ^ Function to provide output to.
135115
-> ServerDefinition config
136116
-> IO Int -- exit code
137117
runServerWith ioLogger logger clientIn clientOut serverDefinition = do
138118

139119
ioLogger <& Starting `WithSeverity` Info
140120

141-
cout <- atomically newTChan :: IO (TChan J.Value)
142-
_rhpid <- forkIO $ sendServer ioLogger cout clientOut
121+
cout <- atomically newTChan
122+
cin <- atomically newTChan
143123

144-
let sendMsg msg = atomically $ writeTChan cout $ J.toJSON msg
124+
let serverOut = IO.serverOut (cmap (fmap LspIoLog) ioLogger) (atomically $ readTChan cout) clientOut
125+
serverIn = IO.serverIn (cmap (fmap LspIoLog) ioLogger) (atomically . writeTChan cin) clientIn
145126

146-
initVFS $ \vfs -> do
147-
ioLoop ioLogger logger clientIn serverDefinition vfs sendMsg
127+
sendMsg msg = atomically $ writeTChan cout $ J.toJSON msg
128+
recvMsg = atomically $ readTChan cin
148129

149-
return 1
150-
151-
-- ---------------------------------------------------------------------
152-
153-
ioLoop ::
154-
forall config
155-
. LogAction IO (WithSeverity LspServerLog)
156-
-> LogAction (LspM config) (WithSeverity LspServerLog)
157-
-> IO BS.ByteString
158-
-> ServerDefinition config
159-
-> VFS
160-
-> (FromServerMessage -> IO ())
161-
-> IO ()
162-
ioLoop ioLogger logger clientIn serverDefinition vfs sendMsg = do
163-
minitialize <- parseOne ioLogger clientIn (parse parser "")
164-
case minitialize of
165-
Nothing -> pure ()
166-
Just (msg,remainder) -> do
167-
case J.eitherDecode $ BSL.fromStrict msg of
168-
Left err -> ioLogger <& DecodeInitializeError err `WithSeverity` Error
169-
Right initialize -> do
170-
mInitResp <- Processing.initializeRequestHandler pioLogger serverDefinition vfs sendMsg initialize
171-
case mInitResp of
172-
Nothing -> pure ()
173-
Just env -> runLspT env $ loop (parse parser remainder)
174-
where
175-
176-
pioLogger = L.cmap (fmap LspProcessingLog) ioLogger
177-
pLogger = L.cmap (fmap LspProcessingLog) logger
178-
179-
loop :: Result BS.ByteString -> LspM config ()
180-
loop = go
181-
where
182-
go r = do
183-
res <- parseOne logger clientIn r
184-
case res of
185-
Nothing -> pure ()
186-
Just (msg,remainder) -> do
187-
Processing.processMessage pLogger $ BSL.fromStrict msg
188-
go (parse parser remainder)
189-
190-
parser = do
191-
try contentType <|> (return ())
192-
len <- contentLength
193-
try contentType <|> (return ())
194-
_ <- string _ONE_CRLF
195-
Attoparsec.take len
196-
197-
contentLength = do
198-
_ <- string "Content-Length: "
199-
len <- decimal
200-
_ <- string _ONE_CRLF
201-
return len
202-
203-
contentType = do
204-
_ <- string "Content-Type: "
205-
skipWhile (/='\r')
206-
_ <- string _ONE_CRLF
207-
return ()
208-
209-
parseOne ::
210-
MonadIO m
211-
=> LogAction m (WithSeverity LspServerLog)
212-
-> IO BS.ByteString
213-
-> Result BS.ByteString
214-
-> m (Maybe (BS.ByteString,BS.ByteString))
215-
parseOne logger clientIn = go
216-
where
217-
go (Fail _ ctxs err) = do
218-
logger <& HeaderParseFail ctxs err `WithSeverity` Error
219-
pure Nothing
220-
go (Partial c) = do
221-
bs <- liftIO clientIn
222-
if BS.null bs
223-
then do
224-
logger <& EOF `WithSeverity` Error
225-
pure Nothing
226-
else go (c bs)
227-
go (Done remainder msg) = do
228-
-- TODO: figure out how to re-enable
229-
-- This can lead to infinite recursion in logging, see https://github.com/haskell/lsp/issues/447
230-
-- logger <& ParsedMsg (T.decodeUtf8 msg) `WithSeverity` Debug
231-
pure $ Just (msg,remainder)
232-
233-
-- ---------------------------------------------------------------------
234-
235-
-- | Simple server to make sure all output is serialised
236-
sendServer :: LogAction IO (WithSeverity LspServerLog) -> TChan J.Value -> (BSL.ByteString -> IO ()) -> IO ()
237-
sendServer _logger msgChan clientOut = do
238-
forever $ do
239-
msg <- atomically $ readTChan msgChan
240-
241-
-- We need to make sure we only send over the content of the message,
242-
-- and no other tags/wrapper stuff
243-
let str = J.encode msg
244-
245-
let out = BSL.concat
246-
[ TL.encodeUtf8 $ TL.pack $ "Content-Length: " ++ show (BSL.length str)
247-
, BSL.fromStrict _TWO_CRLF
248-
, str ]
249-
250-
clientOut out
251-
-- TODO: figure out how to re-enable
252-
-- This can lead to infinite recursion in logging, see https://github.com/haskell/lsp/issues/447
253-
-- logger <& SendMsg (TL.decodeUtf8 str) `WithSeverity` Debug
254-
255-
-- |
256-
--
257-
--
258-
_ONE_CRLF :: BS.ByteString
259-
_ONE_CRLF = "\r\n"
260-
_TWO_CRLF :: BS.ByteString
261-
_TWO_CRLF = "\r\n\r\n"
130+
processingLoop = initVFS $ \vfs ->
131+
Processing.processingLoop
132+
(cmap (fmap LspProcessingLog) ioLogger)
133+
(cmap (fmap LspProcessingLog) logger)
134+
vfs
135+
serverDefinition
136+
sendMsg
137+
recvMsg
262138

139+
-- Bind all the threads together so that any of them terminating will terminate everything
140+
serverOut `Async.race_` serverIn `Async.race_` processingLoop
263141

142+
ioLogger <& Stopping `WithSeverity` Info
143+
return 0

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE RankNTypes #-}
3+
4+
module Language.LSP.Server.IO (serverOut, serverIn, LspIoLog) where
5+
6+
import Colog.Core (LogAction (..), WithSeverity (..), Severity (..), (<&))
7+
import Control.Monad
8+
import qualified Data.Aeson as J
9+
import qualified Data.Attoparsec.ByteString as Attoparsec
10+
import Data.Attoparsec.ByteString.Char8
11+
import qualified Data.ByteString as BS
12+
import qualified Data.ByteString.Lazy as BSL
13+
import qualified Data.Text as T
14+
import qualified Data.Text.Encoding as T
15+
import Data.Text.Prettyprint.Doc
16+
import Data.List
17+
import Control.Applicative ((<|>))
18+
19+
data LspIoLog =
20+
HeaderParseFail [String] String
21+
| BodyParseFail String
22+
| RecvMsg BS.ByteString
23+
| SendMsg BS.ByteString
24+
| EOF
25+
deriving (Show)
26+
27+
instance Pretty LspIoLog where
28+
pretty (HeaderParseFail ctxs err) =
29+
vsep [
30+
"Failed to parse message header:"
31+
, pretty (intercalate " > " ctxs) <> ": " <+> pretty err
32+
]
33+
pretty (BodyParseFail err) =
34+
vsep [
35+
"Failed to parse message body:"
36+
, pretty err
37+
]
38+
pretty (RecvMsg msg) = "---> " <> pretty (T.decodeUtf8 msg)
39+
pretty (SendMsg msg) = "<--- " <> pretty (T.decodeUtf8 msg)
40+
pretty EOF = "Got EOF"
41+
42+
-- | Process which receives messages and sends them. Output queue of messages ensures they are serialised.
43+
serverIn ::
44+
LogAction IO (WithSeverity LspIoLog)
45+
-> (J.Value -> IO ()) -- ^ Channel to send out messages on.
46+
-> IO BS.ByteString -- ^ Action to pull in new messages (e.g. from a handle).
47+
-> IO ()
48+
serverIn logger msgOut clientIn = do
49+
bs <- clientIn
50+
loop (parse parser bs)
51+
where
52+
loop :: Result BS.ByteString -> IO ()
53+
loop (Fail _ ctxs err) = do
54+
logger <& HeaderParseFail ctxs err `WithSeverity` Error
55+
pure ()
56+
loop (Partial c) = do
57+
bs <- clientIn
58+
if BS.null bs
59+
then do
60+
logger <& EOF `WithSeverity` Error
61+
pure ()
62+
else loop (c bs)
63+
loop (Done remainder parsed) = do
64+
logger <& RecvMsg parsed `WithSeverity` Debug
65+
case J.eitherDecode (BSL.fromStrict parsed) of
66+
-- Note: this is recoverable, because we can just discard the
67+
-- message and keep going, whereas a header parse failure is
68+
-- not recoverable
69+
Left err -> logger <& BodyParseFail err `WithSeverity` Error
70+
Right msg -> msgOut msg
71+
loop (parse parser remainder)
72+
73+
parser = do
74+
try contentType <|> (return ())
75+
len <- contentLength
76+
try contentType <|> (return ())
77+
_ <- string _ONE_CRLF
78+
Attoparsec.take len
79+
80+
contentLength = do
81+
_ <- string "Content-Length: "
82+
len <- decimal
83+
_ <- string _ONE_CRLF
84+
return len
85+
86+
contentType = do
87+
_ <- string "Content-Type: "
88+
skipWhile (/='\r')
89+
_ <- string _ONE_CRLF
90+
return ()
91+
92+
-- | Process which receives messages and sends them. Input queue of messages ensures they are serialised.
93+
serverOut
94+
:: LogAction IO (WithSeverity LspIoLog)
95+
-> IO J.Value -- ^ Channel to receive messages on.
96+
-> (BS.ByteString -> IO ()) -- ^ Action to send messages out on (e.g. via a handle).
97+
-> IO ()
98+
serverOut logger msgIn clientOut = forever $ do
99+
msg <- msgIn
100+
101+
-- We need to make sure we only send over the content of the message,
102+
-- and no other tags/wrapper stuff
103+
let str = J.encode msg
104+
105+
let out = BS.concat
106+
[ T.encodeUtf8 $ T.pack $ "Content-Length: " ++ show (BSL.length str)
107+
, _TWO_CRLF
108+
, BSL.toStrict str ]
109+
110+
clientOut out
111+
logger <& SendMsg out `WithSeverity` Debug
112+
113+
_ONE_CRLF :: BS.ByteString
114+
_ONE_CRLF = "\r\n"
115+
_TWO_CRLF :: BS.ByteString
116+
_TWO_CRLF = "\r\n\r\n"

0 commit comments

Comments
 (0)