Skip to content

Commit 6097552

Browse files
committed
fix(server): fix broken pipe after shutdown
- Catch and report broken pipe (resource vanished) when sending responses; log up to 400 chars of the outgoing message for context. - stop client sender after a shutdown response is sent.
1 parent f893e8d commit 6097552

File tree

1 file changed

+36
-18
lines changed

1 file changed

+36
-18
lines changed

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

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE TypeFamilies #-}
23

34
module Language.LSP.Server.Control (
45
-- * Running
@@ -11,9 +12,9 @@ module Language.LSP.Server.Control (
1112
import Colog.Core (LogAction (..), Severity (..), WithSeverity (..), (<&))
1213
import Colog.Core qualified as L
1314
import Control.Applicative ((<|>))
14-
import Control.Concurrent
15+
import Control.Concurrent.Async (withAsync)
1516
import Control.Concurrent.STM.TChan
16-
import Control.Monad
17+
import Control.Exception (catchJust, throwIO)
1718
import Control.Monad.IO.Class
1819
import Control.Monad.STM
1920
import Data.Aeson qualified as J
@@ -33,18 +34,22 @@ import Language.LSP.Server.Processing qualified as Processing
3334
import Language.LSP.VFS
3435
import Prettyprinter
3536
import System.IO
37+
import System.IO.Error (isResourceVanishedError)
3638

3739
data LspServerLog
3840
= LspProcessingLog Processing.LspProcessingLog
3941
| DecodeInitializeError String
4042
| HeaderParseFail [String] String
4143
| EOF
44+
| BrokenPipeWhileSending TL.Text -- truncated outgoing message (including header)
4245
| Starting
46+
| ServerStopped
4347
| ParsedMsg T.Text
4448
| SendMsg TL.Text
4549
deriving (Show)
4650

4751
instance Pretty LspServerLog where
52+
pretty ServerStopped = "Server stopped"
4853
pretty (LspProcessingLog l) = pretty l
4954
pretty (DecodeInitializeError err) =
5055
vsep
@@ -57,7 +62,12 @@ instance Pretty LspServerLog where
5762
, pretty (intercalate " > " ctxs) <> ": " <+> pretty err
5863
]
5964
pretty EOF = "Got EOF"
60-
pretty Starting = "Starting server"
65+
pretty (BrokenPipeWhileSending msg) =
66+
vsep
67+
[ "Broken pipe while sending (client likely closed output handle):"
68+
, indent 2 (pretty msg)
69+
]
70+
pretty Starting = "Server starting"
6171
pretty (ParsedMsg msg) = "---> " <> pretty msg
6272
pretty (SendMsg msg) = "<--2-- " <> pretty msg
6373

@@ -108,9 +118,15 @@ runServerWithHandles ioLogger logger hin hout serverDefinition = do
108118
let
109119
clientIn = BS.hGetSome hin defaultChunkSize
110120

111-
clientOut out = do
112-
BSL.hPut hout out
113-
hFlush hout
121+
clientOut out =
122+
catchJust
123+
(\e -> if isResourceVanishedError e then Just e else Nothing)
124+
(BSL.hPut hout out >> hFlush hout)
125+
( \e -> do
126+
let txt = TL.toStrict $ TL.take 400 $ TL.decodeUtf8 out -- limit size
127+
ioLogger <& BrokenPipeWhileSending (TL.fromStrict txt) `WithSeverity` Error
128+
throwIO e
129+
)
114130

115131
runServerWith ioLogger logger clientIn clientOut serverDefinition
116132

@@ -130,15 +146,12 @@ runServerWith ::
130146
IO Int -- exit code
131147
runServerWith ioLogger logger clientIn clientOut serverDefinition = do
132148
ioLogger <& Starting `WithSeverity` Info
133-
134-
cout <- atomically newTChan :: IO (TChan J.Value)
135-
_rhpid <- forkIO $ sendServer ioLogger cout clientOut
136-
137-
let sendMsg msg = atomically $ writeTChan cout $ J.toJSON msg
138-
139-
ioLoop ioLogger logger clientIn serverDefinition emptyVFS sendMsg
140-
141-
return 1
149+
cout <- atomically newTChan :: IO (TChan FromServerMessage)
150+
withAsync (sendServer ioLogger cout clientOut) $ \_sendAsync -> do
151+
let sendMsg = atomically . writeTChan cout
152+
ioLoop ioLogger logger clientIn serverDefinition emptyVFS sendMsg
153+
ioLogger <& ServerStopped `WithSeverity` Info
154+
return 0
142155

143156
-- ---------------------------------------------------------------------
144157

@@ -224,9 +237,10 @@ parseOne logger clientIn = go
224237
-- ---------------------------------------------------------------------
225238

226239
-- | 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
229-
forever $ do
240+
sendServer :: LogAction IO (WithSeverity LspServerLog) -> TChan FromServerMessage -> (BSL.ByteString -> IO ()) -> IO ()
241+
sendServer _logger msgChan clientOut = go
242+
where
243+
go = do
230244
msg <- atomically $ readTChan msgChan
231245

232246
-- We need to make sure we only send over the content of the message,
@@ -241,6 +255,10 @@ sendServer _logger msgChan clientOut = do
241255
]
242256

243257
clientOut out
258+
-- close the client sender when we send out the shutdown request's response
259+
case msg of
260+
FromServerRsp SMethod_Shutdown _ -> pure ()
261+
_ -> go
244262

245263
-- TODO: figure out how to re-enable
246264
-- This can lead to infinite recursion in logging, see https://github.com/haskell/lsp/issues/447

0 commit comments

Comments
 (0)