-
-
Notifications
You must be signed in to change notification settings - Fork 391
Send unhandled exceptions to the user #2484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
83ae7af
90a4b14
8479178
870df21
a044156
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,8 +38,12 @@ import Development.IDE.Core.Tracing | |
import Development.IDE.LSP.HoverDefinition | ||
import Development.IDE.Types.Logger | ||
|
||
import Control.Monad.IO.Unlift (MonadUnliftIO) | ||
import System.IO.Unsafe (unsafeInterleaveIO) | ||
|
||
issueTrackerUrl :: T.Text | ||
issueTrackerUrl = "https://github.com/haskell/haskell-language-server/issues" | ||
|
||
runLanguageServer | ||
:: forall config. (Show config) | ||
=> LSP.Options | ||
|
@@ -54,11 +58,16 @@ runLanguageServer | |
runLanguageServer options inH outH getHieDbLoc defaultConfig onConfigurationChange userHandlers getIdeState = do | ||
|
||
-- This MVar becomes full when the server thread exits or we receive exit message from client. | ||
-- LSP loop will be canceled when it's full. | ||
-- LSP server will be canceled when it's full. | ||
clientMsgVar <- newEmptyMVar | ||
-- Forcefully exit | ||
let exit = void $ tryPutMVar clientMsgVar () | ||
|
||
-- An MVar to control the lifetime of the reactor loop. | ||
-- The loop will be stopped and resources freed when it's full | ||
reactorLifetime <- newEmptyMVar | ||
let stopReactorLoop = void $ tryPutMVar reactorLifetime () | ||
michaelpj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
-- The set of requests ids that we have received but not finished processing | ||
pendingRequests <- newTVarIO Set.empty | ||
-- The set of requests that have been cancelled and are also in pendingRequests | ||
|
@@ -93,7 +102,7 @@ runLanguageServer options inH outH getHieDbLoc defaultConfig onConfigurationChan | |
[ ideHandlers | ||
, cancelHandler cancelRequest | ||
, exitHandler exit | ||
, shutdownHandler | ||
, shutdownHandler stopReactorLoop | ||
] | ||
-- Cancel requests are special since they need to be handled | ||
-- out of order to be useful. Existing handlers are run afterwards. | ||
|
@@ -102,25 +111,23 @@ runLanguageServer options inH outH getHieDbLoc defaultConfig onConfigurationChan | |
let serverDefinition = LSP.ServerDefinition | ||
{ LSP.onConfigurationChange = onConfigurationChange | ||
, LSP.defaultConfig = defaultConfig | ||
, LSP.doInitialize = handleInit exit clearReqId waitForCancel clientMsgChan | ||
, LSP.doInitialize = handleInit reactorLifetime exit clearReqId waitForCancel clientMsgChan | ||
, LSP.staticHandlers = asyncHandlers | ||
, LSP.interpretHandler = \(env, st) -> LSP.Iso (LSP.runLspT env . flip runReaderT (clientMsgChan,st)) liftIO | ||
, LSP.options = modifyOptions options | ||
} | ||
|
||
void $ waitAnyCancel =<< traverse async | ||
[ void $ LSP.runServerWithHandles | ||
void $ untilMVar clientMsgVar $ | ||
void $ LSP.runServerWithHandles | ||
inH | ||
outH | ||
serverDefinition | ||
, void $ readMVar clientMsgVar | ||
] | ||
|
||
where | ||
handleInit | ||
:: IO () -> (SomeLspId -> IO ()) -> (SomeLspId -> IO ()) -> Chan ReactorMessage | ||
:: MVar () -> IO () -> (SomeLspId -> IO ()) -> (SomeLspId -> IO ()) -> Chan ReactorMessage | ||
-> LSP.LanguageContextEnv config -> RequestMessage Initialize -> IO (Either err (LSP.LanguageContextEnv config, IdeState)) | ||
handleInit exitClientMsg clearReqId waitForCancel clientMsgChan env (RequestMessage _ _ m params) = otTracedHandler "Initialize" (show m) $ \sp -> do | ||
handleInit lifetime exitClientMsg clearReqId waitForCancel clientMsgChan env (RequestMessage _ _ m params) = otTracedHandler "Initialize" (show m) $ \sp -> do | ||
traceWithSpan sp params | ||
let root = LSP.resRootPath env | ||
dir <- maybe getCurrentDirectory return root | ||
|
@@ -138,58 +145,71 @@ runLanguageServer options inH outH getHieDbLoc defaultConfig onConfigurationChan | |
registerIdeConfiguration (shakeExtras ide) initConfig | ||
|
||
let handleServerException (Left e) = do | ||
logError (ideLogger ide) $ | ||
logError logger $ | ||
pepeiborra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
T.pack $ "Fatal error in server thread: " <> show e | ||
sendErrorMessage e | ||
exitClientMsg | ||
handleServerException _ = pure () | ||
handleServerException (Right _) = pure () | ||
|
||
sendErrorMessage (e :: SomeException) = do | ||
LSP.runLspT env $ LSP.sendNotification SWindowShowMessage $ | ||
ShowMessageParams MtError $ T.unlines | ||
[ "Unhandled exception, please [report](" <> issueTrackerUrl <> "): " | ||
, T.pack(show e) | ||
] | ||
|
||
exceptionInHandler e = do | ||
logError logger $ T.pack $ | ||
"Unexpected exception, please report!\n" ++ | ||
"Exception: " ++ show e | ||
sendErrorMessage e | ||
|
||
logger = ideLogger ide | ||
_ <- flip forkFinally handleServerException $ runWithDb logger dbLoc $ \hiedb hieChan -> do | ||
|
||
checkCancelled _id act k = | ||
flip finally (clearReqId _id) $ | ||
catch (do | ||
-- We could optimize this by first checking if the id | ||
-- is in the cancelled set. However, this is unlikely to be a | ||
-- bottleneck and the additional check might hide | ||
-- issues with async exceptions that need to be fixed. | ||
cancelOrRes <- race (waitForCancel _id) act | ||
case cancelOrRes of | ||
Left () -> do | ||
logDebug (ideLogger ide) $ T.pack $ "Cancelled request " <> show _id | ||
k $ ResponseError RequestCancelled "" Nothing | ||
Right res -> pure res | ||
) $ \(e :: SomeException) -> do | ||
exceptionInHandler e | ||
k $ ResponseError InternalError (T.pack $ show e) Nothing | ||
_ <- flip forkFinally handleServerException $ untilMVar lifetime $ runWithDb logger dbLoc $ \hiedb hieChan -> do | ||
putMVar dbMVar (hiedb,hieChan) | ||
forever $ do | ||
msg <- readChan clientMsgChan | ||
-- We dispatch notifications synchronously and requests asynchronously | ||
-- This is to ensure that all file edits and config changes are applied before a request is handled | ||
case msg of | ||
ReactorNotification act -> do | ||
catch act $ \(e :: SomeException) -> | ||
logError (ideLogger ide) $ T.pack $ | ||
"Unexpected exception on notification, please report!\n" ++ | ||
"Exception: " ++ show e | ||
ReactorRequest _id act k -> void $ async $ | ||
checkCancelled ide clearReqId waitForCancel _id act k | ||
ReactorNotification act -> handle exceptionInHandler act | ||
ReactorRequest _id act k -> void $ async $ checkCancelled _id act k | ||
pure $ Right (env,ide) | ||
|
||
checkCancelled | ||
:: IdeState -> (SomeLspId -> IO ()) -> (SomeLspId -> IO ()) -> SomeLspId | ||
-> IO () -> (ResponseError -> IO ()) -> IO () | ||
checkCancelled ide clearReqId waitForCancel _id act k = | ||
flip finally (clearReqId _id) $ | ||
catch (do | ||
-- We could optimize this by first checking if the id | ||
-- is in the cancelled set. However, this is unlikely to be a | ||
-- bottleneck and the additional check might hide | ||
-- issues with async exceptions that need to be fixed. | ||
cancelOrRes <- race (waitForCancel _id) act | ||
case cancelOrRes of | ||
Left () -> do | ||
logDebug (ideLogger ide) $ T.pack $ "Cancelled request " <> show _id | ||
k $ ResponseError RequestCancelled "" Nothing | ||
Right res -> pure res | ||
) $ \(e :: SomeException) -> do | ||
logError (ideLogger ide) $ T.pack $ | ||
"Unexpected exception on request, please report!\n" ++ | ||
"Exception: " ++ show e | ||
k $ ResponseError InternalError (T.pack $ show e) Nothing | ||
|
||
-- | Runs the action until it ends or until the given MVar is put. | ||
-- Rethrows any exceptions. | ||
untilMVar :: MonadUnliftIO m => MVar () -> m () -> m () | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kind of surprised these sorts of utilities aren't available elsewhere. I couldn't find them, anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Especially the pattern of forking a thread with an MVar to control terminating it 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They may exist, I'm not sure either. |
||
untilMVar mvar io = void $ | ||
waitAnyCancel =<< traverse async [ io , readMVar mvar ] | ||
|
||
cancelHandler :: (SomeLspId -> IO ()) -> LSP.Handlers (ServerM c) | ||
cancelHandler cancelRequest = LSP.notificationHandler SCancelRequest $ \NotificationMessage{_params=CancelParams{_id}} -> | ||
liftIO $ cancelRequest (SomeLspId _id) | ||
|
||
shutdownHandler :: LSP.Handlers (ServerM c) | ||
shutdownHandler = LSP.requestHandler SShutdown $ \_ resp -> do | ||
shutdownHandler :: IO () -> LSP.Handlers (ServerM c) | ||
shutdownHandler stopReactor = LSP.requestHandler SShutdown $ \_ resp -> do | ||
(_, ide) <- ask | ||
liftIO $ logDebug (ideLogger ide) "Received exit message" | ||
liftIO $ logDebug (ideLogger ide) "Received shutdown message" | ||
-- stop the reactor to free up the hiedb connection | ||
liftIO stopReactor | ||
-- flush out the Shake session to record a Shake profile if applicable | ||
liftIO $ shakeShut ide | ||
resp $ Right Empty | ||
|
Uh oh!
There was an error while loading. Please reload this page.