Skip to content

Commit e25304f

Browse files
authored
Reject messages that come in after 'shutdown' (#564)
* Reject messages that come in after 'shutdown' This is mandated by the spec so we can do it. We also expose the shutdown barrier, which I think can be convenient. For more sophisticated usecases people should just install a proper shutdown handler. I tried to write some tests for this, but I gave up because it requires some singificant surgery on `lsp-test`, which stops recording messages when it receives `shutdown` :( * Fix formatting
1 parent 7a87841 commit e25304f

File tree

5 files changed

+106
-34
lines changed

5 files changed

+106
-34
lines changed

lsp/ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Revision history for lsp
22

3+
## Unreleased
4+
5+
- The server will now reject messages sent after `shutdown` has been received.
6+
- There is a `shutdownBarrier` member in the server state which can be used to
7+
conveniently run actions when shutdown is triggered.
8+
39
## 2.4.0.0
410

511
- Server-created progress now will not send reports until and unless the client

lsp/lsp.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ library
5959
, data-default ^>=0.7
6060
, directory ^>=1.3
6161
, exceptions ^>=0.10
62+
, extra ^>=1.7
6263
, filepath >=1.4 && < 1.6
6364
, hashable ^>=1.4
6465
, lens >=5.1 && <5.3

lsp/src/Language/LSP/Server.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ module Language.LSP.Server (
3535
requestConfigUpdate,
3636
tryChangeConfig,
3737

38+
-- * Shutdown
39+
isShuttingDown,
40+
waitShuttingDown,
41+
3842
-- * VFS
3943
getVirtualFile,
4044
getVirtualFiles,

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Colog.Core (
2121
)
2222
import Control.Applicative
2323
import Control.Concurrent.Async
24+
import Control.Concurrent.Extra as C
2425
import Control.Concurrent.MVar
2526
import Control.Concurrent.STM
2627
import Control.Exception qualified as E
@@ -211,6 +212,10 @@ data LanguageContextState config = LanguageContextState
211212
, resRegistrationsNot :: !(TVar (RegistrationMap Notification))
212213
, resRegistrationsReq :: !(TVar (RegistrationMap Request))
213214
, resLspId :: !(TVar Int32)
215+
, resShutdown :: !(C.Barrier ())
216+
-- ^ Has the server received 'shutdown'? Can be used to conveniently trigger e.g. thread termination,
217+
-- but if you need a cleanup action to terminate before exiting, then you should install a full
218+
-- 'shutdown' handler
214219
}
215220

216221
type ResponseMap = IxMap LspId (Product SMethod ServerResponseCallback)
@@ -903,6 +908,25 @@ requestConfigUpdate logger = do
903908
Left err -> logger <& BadConfigurationResponse err `WithSeverity` Error
904909
else logger <& ConfigurationNotSupported `WithSeverity` Debug
905910

911+
--------------------------------------------------------------------------------
912+
-- CONFIG
913+
--------------------------------------------------------------------------------
914+
915+
-- | Checks if the server has received a 'shutdown' request.
916+
isShuttingDown :: (m ~ LspM config) => m Bool
917+
isShuttingDown = do
918+
b <- resShutdown . resState <$> getLspEnv
919+
r <- liftIO $ C.waitBarrierMaybe b
920+
pure $ case r of
921+
Just _ -> True
922+
Nothing -> False
923+
924+
-- | Blocks until the server receives a 'shutdown' request.
925+
waitShuttingDown :: (m ~ LspM config) => m ()
926+
waitShuttingDown = do
927+
b <- resShutdown . resState <$> getLspEnv
928+
liftIO $ C.waitBarrier b
929+
906930
{- Note [LSP configuration]
907931
LSP configuration is a huge mess.
908932
- The configuration model of the client is not specified
@@ -988,3 +1012,16 @@ The 'cancellable' property that we can set when making progress reports just
9881012
affects whether the client should show a 'Cancel' button to the user in the UI.
9891013
The client can still always choose to cancel for another reason.
9901014
-}
1015+
1016+
{- Note [Shutdown]
1017+
The 'shutdown' request basically tells the server to clean up and stop doing things.
1018+
In particular, it allows us to ignore or reject all further messages apart from 'exit'.
1019+
1020+
We also provide a `Barrier` that indicates whether or not we are shutdown, this can
1021+
be convenient, e.g. you can race a thread against `waitBarrier` to have it automatically
1022+
be cancelled when we receive `shutdown`.
1023+
1024+
Shutdown is a request, and the client won't send `exit` until a server responds, so if you
1025+
want to be sure that some cleanup happens, you need to ensure we don't respond to `shutdown`
1026+
until it's done. The best way to do this is just to install a specific `shutdown` handler.
1027+
-}

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

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Colog.Core (
2121
(<&),
2222
)
2323

24+
import Control.Concurrent.Extra as C
2425
import Control.Concurrent.STM
2526
import Control.Exception qualified as E
2627
import Control.Lens hiding (Empty)
@@ -69,6 +70,8 @@ data LspProcessingLog
6970
| MessageProcessingError BSL.ByteString String
7071
| forall m. MissingHandler Bool (SClientMethod m)
7172
| ProgressCancel ProgressToken
73+
| forall m. MessageDuringShutdown (SClientMethod m)
74+
| ShuttingDown
7275
| Exiting
7376

7477
deriving instance Show LspProcessingLog
@@ -85,7 +88,9 @@ instance Pretty LspProcessingLog where
8588
]
8689
pretty (MissingHandler _ m) = "LSP: no handler for:" <+> pretty m
8790
pretty (ProgressCancel tid) = "LSP: cancelling action for token:" <+> pretty tid
88-
pretty Exiting = "LSP: Got exit, exiting"
91+
pretty (MessageDuringShutdown m) = "LSP: received message during shutdown:" <+> pretty m
92+
pretty ShuttingDown = "LSP: received shutdown"
93+
pretty Exiting = "LSP: received exit"
8994

9095
processMessage :: (m ~ LspM config) => LogAction m (WithSeverity LspProcessingLog) -> BSL.ByteString -> m ()
9196
processMessage logger jsonStr = do
@@ -164,6 +169,7 @@ initializeRequestHandler logger ServerDefinition{..} vfs sendFunc req = do
164169
resRegistrationsNot <- newTVarIO mempty
165170
resRegistrationsReq <- newTVarIO mempty
166171
resLspId <- newTVarIO 0
172+
resShutdown <- C.newBarrier
167173
pure LanguageContextState{..}
168174

169175
-- Call the 'duringInitialization' callback to let the server kick stuff up
@@ -414,13 +420,21 @@ inferServerCapabilities _clientCaps o h =
414420
{- | Invokes the registered dynamic or static handlers for the given message and
415421
method, as well as doing some bookkeeping.
416422
-}
417-
handle :: (m ~ LspM config) => LogAction m (WithSeverity LspProcessingLog) -> SClientMethod meth -> TClientMessage meth -> m ()
423+
handle :: forall m config meth. (m ~ LspM config) => LogAction m (WithSeverity LspProcessingLog) -> SClientMethod meth -> TClientMessage meth -> m ()
418424
handle logger m msg =
419425
case m of
420426
SMethod_WorkspaceDidChangeWorkspaceFolders -> handle' logger (Just updateWorkspaceFolders) m msg
421427
SMethod_WorkspaceDidChangeConfiguration -> handle' logger (Just $ handleDidChangeConfiguration logger) m msg
422428
-- See Note [LSP configuration]
423429
SMethod_Initialized -> handle' logger (Just $ \_ -> initialDynamicRegistrations logger >> requestConfigUpdate (cmap (fmap LspCore) logger)) m msg
430+
SMethod_Shutdown -> handle' logger (Just $ \_ -> signalShutdown) m msg
431+
where
432+
-- See Note [Shutdown]
433+
signalShutdown :: LspM config ()
434+
signalShutdown = do
435+
logger <& ShuttingDown `WithSeverity` Info
436+
b <- resShutdown . resState <$> getLspEnv
437+
liftIO $ signalBarrier b ()
424438
SMethod_TextDocumentDidOpen -> handle' logger (Just $ vfsFunc logger openVFS) m msg
425439
SMethod_TextDocumentDidChange -> handle' logger (Just $ vfsFunc logger changeFromClientVFS) m msg
426440
SMethod_TextDocumentDidClose -> handle' logger (Just $ vfsFunc logger closeVFS) m msg
@@ -445,48 +459,40 @@ handle' logger mAction m msg = do
445459

446460
env <- getLspEnv
447461
let Handlers{reqHandlers, notHandlers} = resHandlers env
448-
449-
let mkRspCb :: TRequestMessage (m1 :: Method ClientToServer Request) -> Either ResponseError (MessageResult m1) -> IO ()
450-
mkRspCb req (Left err) =
451-
runLspT env $
452-
sendToClient $
453-
FromServerRsp (req ^. L.method) $
454-
TResponseMessage "2.0" (Just (req ^. L.id)) (Left err)
455-
mkRspCb req (Right rsp) =
456-
runLspT env $
457-
sendToClient $
458-
FromServerRsp (req ^. L.method) $
459-
TResponseMessage "2.0" (Just (req ^. L.id)) (Right rsp)
462+
shutdown <- isShuttingDown
460463

461464
case splitClientMethod m of
465+
-- See Note [Shutdown]
466+
IsClientNot | shutdown, not (allowedMethod m) -> notificationDuringShutdown
467+
where
468+
allowedMethod SMethod_Exit = True
469+
allowedMethod _ = False
462470
IsClientNot -> case pickHandler dynNotHandlers notHandlers of
463471
Just h -> liftIO $ h msg
464472
Nothing
465473
| SMethod_Exit <- m -> exitNotificationHandler logger msg
466-
| otherwise -> do
467-
reportMissingHandler
474+
| otherwise -> missingNotificationHandler
475+
-- See Note [Shutdown]
476+
IsClientReq | shutdown, not (allowedMethod m) -> requestDuringShutdown msg
477+
where
478+
allowedMethod SMethod_Shutdown = True
479+
allowedMethod _ = False
468480
IsClientReq -> case pickHandler dynReqHandlers reqHandlers of
469-
Just h -> liftIO $ h msg (mkRspCb msg)
481+
Just h -> liftIO $ h msg (runLspT env . sendResponse msg)
470482
Nothing
471-
| SMethod_Shutdown <- m -> liftIO $ shutdownRequestHandler msg (mkRspCb msg)
472-
| otherwise -> do
473-
let errorMsg = T.pack $ unwords ["lsp:no handler for: ", show m]
474-
err = ResponseError (InR ErrorCodes_MethodNotFound) errorMsg Nothing
475-
sendToClient $
476-
FromServerRsp (msg ^. L.method) $
477-
TResponseMessage "2.0" (Just (msg ^. L.id)) (Left err)
483+
| SMethod_Shutdown <- m -> liftIO $ shutdownRequestHandler msg (runLspT env . sendResponse msg)
484+
| otherwise -> missingRequestHandler msg
478485
IsClientEither -> case msg of
486+
-- See Note [Shutdown]
487+
NotMess _ | shutdown -> notificationDuringShutdown
479488
NotMess noti -> case pickHandler dynNotHandlers notHandlers of
480489
Just h -> liftIO $ h noti
481-
Nothing -> reportMissingHandler
490+
Nothing -> missingNotificationHandler
491+
-- See Note [Shutdown]
492+
ReqMess req | shutdown -> requestDuringShutdown req
482493
ReqMess req -> case pickHandler dynReqHandlers reqHandlers of
483-
Just h -> liftIO $ h req (mkRspCb req)
484-
Nothing -> do
485-
let errorMsg = T.pack $ unwords ["lsp:no handler for: ", show m]
486-
err = ResponseError (InR ErrorCodes_MethodNotFound) errorMsg Nothing
487-
sendToClient $
488-
FromServerRsp (req ^. L.method) $
489-
TResponseMessage "2.0" (Just (req ^. L.id)) (Left err)
494+
Just h -> liftIO $ h req (runLspT env . sendResponse req)
495+
Nothing -> missingRequestHandler req
490496
where
491497
-- \| Checks to see if there's a dynamic handler, and uses it in favour of the
492498
-- static handler, if it exists.
@@ -496,14 +502,32 @@ handle' logger mAction m msg = do
496502
(Nothing, Just (ClientMessageHandler h)) -> Just h
497503
(Nothing, Nothing) -> Nothing
498504

505+
sendResponse :: forall m1. TRequestMessage (m1 :: Method ClientToServer Request) -> Either ResponseError (MessageResult m1) -> m ()
506+
sendResponse req res = sendToClient $ FromServerRsp (req ^. L.method) $ TResponseMessage "2.0" (Just (req ^. L.id)) res
507+
508+
requestDuringShutdown :: forall m1. TRequestMessage (m1 :: Method ClientToServer Request) -> m ()
509+
requestDuringShutdown req = do
510+
logger <& MessageDuringShutdown m `WithSeverity` Warning
511+
sendResponse req (Left (ResponseError (InR ErrorCodes_InvalidRequest) "Server is shutdown" Nothing))
512+
513+
notificationDuringShutdown :: m ()
514+
notificationDuringShutdown = logger <& MessageDuringShutdown m `WithSeverity` Warning
515+
499516
-- '$/' notifications should/could be ignored by server.
500517
-- Don't log errors in that case.
501518
-- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#-notifications-and-requests.
502-
reportMissingHandler :: m ()
503-
reportMissingHandler =
519+
missingNotificationHandler :: m ()
520+
missingNotificationHandler =
504521
let optional = isOptionalMethod (SomeMethod m)
505522
in logger <& MissingHandler optional m `WithSeverity` if optional then Warning else Error
506523

524+
missingRequestHandler :: TRequestMessage (m1 :: Method ClientToServer Request) -> m ()
525+
missingRequestHandler req = do
526+
logger <& MissingHandler False m `WithSeverity` Error
527+
let errorMsg = T.pack $ unwords ["No handler for: ", show m]
528+
err = ResponseError (InR ErrorCodes_MethodNotFound) errorMsg Nothing
529+
sendResponse req (Left err)
530+
507531
progressCancelHandler :: (m ~ LspM config) => LogAction m (WithSeverity LspProcessingLog) -> TMessage Method_WindowWorkDoneProgressCancel -> m ()
508532
progressCancelHandler logger (TNotificationMessage _ _ (WorkDoneProgressCancelParams tid)) = do
509533
pdata <- getsState (progressCancel . resProgressData)

0 commit comments

Comments
 (0)