-
-
Notifications
You must be signed in to change notification settings - Fork 392
Log response errors returned from Plugins #2988
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 8 commits
0c07da6
e274527
7f45bd0
c857168
a4e87cb
9a92d79
b9ce2ca
f886d63
c14a920
b0ab884
781fe33
da55694
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 |
---|---|---|
|
@@ -10,6 +10,7 @@ module Development.IDE.Plugin.HLS | |
) where | ||
|
||
import Control.Exception (SomeException) | ||
import Control.Lens ((^.)) | ||
import Control.Monad | ||
import qualified Data.Aeson as J | ||
import Data.Bifunctor | ||
|
@@ -21,6 +22,7 @@ import qualified Data.List as List | |
import Data.List.NonEmpty (NonEmpty, nonEmpty, toList) | ||
import qualified Data.Map as Map | ||
import Data.String | ||
import Data.Text (Text) | ||
import qualified Data.Text as T | ||
import Development.IDE.Core.Shake hiding (Log) | ||
import Development.IDE.Core.Tracing | ||
|
@@ -33,9 +35,10 @@ import Ide.Plugin.Config | |
import Ide.PluginUtils (getClientConfig) | ||
import Ide.Types as HLS | ||
import qualified Language.LSP.Server as LSP | ||
import Language.LSP.VFS | ||
import Language.LSP.Types | ||
import qualified Language.LSP.Types as J | ||
import qualified Language.LSP.Types.Lens as LSP | ||
import Language.LSP.VFS | ||
import Text.Regex.TDFA.Text () | ||
import UnliftIO (MonadUnliftIO) | ||
import UnliftIO.Async (forConcurrently) | ||
|
@@ -44,20 +47,48 @@ import UnliftIO.Exception (catchAny) | |
-- --------------------------------------------------------------------- | ||
-- | ||
|
||
data Log | ||
= LogNoEnabledPlugins | ||
deriving Show | ||
data Log = LogPluginError ResponseError | ||
deriving Show | ||
|
||
instance Pretty Log where | ||
pretty = \case | ||
LogNoEnabledPlugins -> | ||
"extensibleNotificationPlugins no enabled plugins" | ||
LogPluginError err -> responseErrorToLogMessage err | ||
|
||
responseErrorToLogMessage :: ResponseError -> Doc a | ||
responseErrorToLogMessage err = errorCode <> ":" <+> errorBody | ||
where | ||
errorCode = pretty $ show $ err ^. LSP.code | ||
errorBody = pretty $ err ^. LSP.message | ||
|
||
-- various error message specific builders | ||
pluginNotEnabled :: SMethod m -> [(PluginId, b, a)] -> Text | ||
pluginNotEnabled method availPlugins = "No plugin enabled for " <> T.pack (show method) <> ", available: " <> T.pack (show $ map (\(plid,_,_) -> plid) availPlugins) | ||
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. use 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. Yeah, I hoisted these error message builders outside of the plugin handlers. Just wanted to clean up the surrounding code in the handlers. I'll go for the instance option and see if I can clean up some of these messages 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.
I just realized that these build the response error messages so I don’t think there is any reason to add |
||
|
||
pluginDoesntExist :: PluginId -> Text | ||
pluginDoesntExist (PluginId pid) = "Plugin " <> pid <> " doesn't exist" | ||
|
||
commandDoesntExist :: CommandId -> PluginId -> [PluginCommand ideState] -> Text | ||
commandDoesntExist (CommandId com) (PluginId pid) legalCmds = "Command " <> com <> " isn't defined for plugin " <> pid <> ". Legal commands are: " <> T.pack (show $ map commandId legalCmds) | ||
drsooch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
failedToParseArgs :: CommandId -- ^ command that failed to parse | ||
-> PluginId -- ^ Plugin that created the command | ||
-> String -- ^ The JSON Error message | ||
-> J.Value -- ^ The Argument Values | ||
-> Text | ||
failedToParseArgs (CommandId com) (PluginId pid) err arg = "Error while parsing args for " <> com <> " in plugin " <> pid <> ": " <> T.pack err <> "\narg = " <> T.pack (show arg) | ||
|
||
-- | Build a ResponseError and log it before returning to the caller | ||
logAndReturnError :: Recorder (WithPriority Log) -> ErrorCode -> Text -> LSP.LspT Config IO (Either ResponseError a) | ||
logAndReturnError recorder errCode msg = do | ||
let err = ResponseError errCode msg Nothing | ||
logWith recorder Error $ LogPluginError err | ||
pure $ Left err | ||
|
||
-- | Map a set of plugins to the underlying ghcide engine. | ||
asGhcIdePlugin :: Recorder (WithPriority Log) -> IdePlugins IdeState -> Plugin Config | ||
asGhcIdePlugin recorder (IdePlugins ls) = | ||
mkPlugin rulesPlugins HLS.pluginRules <> | ||
mkPlugin executeCommandPlugins HLS.pluginCommands <> | ||
mkPlugin (executeCommandPlugins recorder) HLS.pluginCommands <> | ||
mkPlugin (extensiblePlugins recorder) id <> | ||
mkPlugin (extensibleNotificationPlugins recorder) id <> | ||
mkPlugin dynFlagsPlugins HLS.pluginModifyDynflags | ||
|
@@ -91,11 +122,11 @@ dynFlagsPlugins rs = mempty | |
|
||
-- --------------------------------------------------------------------- | ||
|
||
executeCommandPlugins :: [(PluginId, [PluginCommand IdeState])] -> Plugin Config | ||
executeCommandPlugins ecs = mempty { P.pluginHandlers = executeCommandHandlers ecs } | ||
executeCommandPlugins :: Recorder (WithPriority Log) -> [(PluginId, [PluginCommand IdeState])] -> Plugin Config | ||
executeCommandPlugins recorder ecs = mempty { P.pluginHandlers = executeCommandHandlers recorder ecs } | ||
|
||
executeCommandHandlers :: [(PluginId, [PluginCommand IdeState])] -> LSP.Handlers (ServerM Config) | ||
executeCommandHandlers ecs = requestHandler SWorkspaceExecuteCommand execCmd | ||
executeCommandHandlers :: Recorder (WithPriority Log) -> [(PluginId, [PluginCommand IdeState])] -> LSP.Handlers (ServerM Config) | ||
executeCommandHandlers recorder ecs = requestHandler SWorkspaceExecuteCommand execCmd | ||
where | ||
pluginMap = Map.fromList ecs | ||
|
||
|
@@ -134,21 +165,15 @@ executeCommandHandlers ecs = requestHandler SWorkspaceExecuteCommand execCmd | |
Just (plugin, cmd) -> runPluginCommand ide plugin cmd cmdParams | ||
|
||
-- Couldn't parse the command identifier | ||
_ -> return $ Left $ ResponseError InvalidParams "Invalid command identifier" Nothing | ||
_ -> logAndReturnError recorder InvalidParams "Invalid command Identifier" | ||
|
||
runPluginCommand ide p@(PluginId p') com@(CommandId com') arg = | ||
runPluginCommand ide p com arg = | ||
case Map.lookup p pluginMap of | ||
Nothing -> return | ||
(Left $ ResponseError InvalidRequest ("Plugin " <> p' <> " doesn't exist") Nothing) | ||
Nothing -> logAndReturnError recorder InvalidRequest (pluginDoesntExist p) | ||
Just xs -> case List.find ((com ==) . commandId) xs of | ||
Nothing -> return $ Left $ | ||
ResponseError InvalidRequest ("Command " <> com' <> " isn't defined for plugin " <> p' | ||
<> ". Legal commands are: " <> T.pack(show $ map commandId xs)) Nothing | ||
Nothing -> logAndReturnError recorder InvalidRequest (commandDoesntExist com p xs) | ||
Just (PluginCommand _ _ f) -> case J.fromJSON arg of | ||
J.Error err -> return $ Left $ | ||
ResponseError InvalidParams ("error while parsing args for " <> com' <> " in plugin " <> p' | ||
<> ": " <> T.pack err | ||
<> "\narg = " <> T.pack (show arg)) Nothing | ||
J.Error err -> logAndReturnError recorder InvalidParams (failedToParseArgs com p err arg) | ||
J.Success a -> f ide a | ||
|
||
-- --------------------------------------------------------------------- | ||
|
@@ -169,19 +194,15 @@ extensiblePlugins recorder xs = mempty { P.pluginHandlers = handlers } | |
config <- Ide.PluginUtils.getClientConfig | ||
-- Only run plugins that are allowed to run on this request | ||
let fs = filter (\(_, desc, _) -> pluginEnabled m params desc config) fs' | ||
-- Clients generally don't display ResponseErrors so instead we log any that we come across | ||
case nonEmpty fs of | ||
Nothing -> do | ||
logWith recorder Info LogNoEnabledPlugins | ||
pure $ Left $ ResponseError InvalidRequest | ||
( "No plugin enabled for " <> T.pack (show m) | ||
<> ", available: " <> T.pack (show $ map (\(plid,_,_) -> plid) fs) | ||
) | ||
Nothing | ||
Nothing -> logAndReturnError recorder InvalidRequest (pluginNotEnabled m fs') | ||
Just fs -> do | ||
let msg e pid = "Exception in plugin " <> T.pack (show pid) <> "while processing " <> T.pack (show m) <> ": " <> T.pack (show e) | ||
handlers = fmap (\(plid,_,handler) -> (plid,handler)) fs | ||
es <- runConcurrently msg (show m) handlers ide params | ||
let (errs,succs) = partitionEithers $ toList es | ||
unless (null errs) $ forM_ errs $ \err -> logWith recorder Warning $ LogPluginError err | ||
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. Why 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. I think my reasoning was that in the event 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. but yes I will update to Error 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. Honestly, this code is kind of suspect. I'd be more comfortable with failing if any of them fail, rather than succeeding if any of them succeed 🤔 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. I guess the only thing I can thing of is if I trigger a code action, and a single code action fails, I don't want to fail ALL of the code actions. I'd rather get back the ones that succeed and ignore the failures 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. I guess that's true. Hopefully that will still happen, they'll just get more obvious logs... |
||
case nonEmpty succs of | ||
Nothing -> pure $ Left $ combineErrors errs | ||
Just xs -> do | ||
|
@@ -206,9 +227,7 @@ extensibleNotificationPlugins recorder xs = mempty { P.pluginHandlers = handlers | |
-- Only run plugins that are allowed to run on this request | ||
let fs = filter (\(_, desc, _) -> pluginEnabled m params desc config) fs' | ||
case nonEmpty fs of | ||
Nothing -> do | ||
logWith recorder Info LogNoEnabledPlugins | ||
pure () | ||
Nothing -> void $ logAndReturnError recorder InvalidRequest (pluginNotEnabled m fs') | ||
Just fs -> do | ||
-- We run the notifications in order, so the core ghcide provider | ||
-- (which restarts the shake process) hopefully comes last | ||
|
@@ -227,7 +246,7 @@ runConcurrently | |
-> m (NonEmpty (Either ResponseError d)) | ||
runConcurrently msg method fs a b = fmap join $ forConcurrently fs $ \(pid,f) -> otTracedProvider pid (fromString method) $ do | ||
f a b | ||
`catchAny` (\e -> pure $ pure $ Left $ ResponseError InternalError (msg e pid) Nothing) | ||
`catchAny` (\e -> pure $ pure $ Left $ ResponseError InternalError (msg e pid) Nothing) | ||
|
||
combineErrors :: [ResponseError] -> ResponseError | ||
combineErrors [x] = x | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -253,10 +253,8 @@ getNormalizedFilePath (PluginId plId) uri = handleMaybe errMsg | |
errMsg = T.unpack $ "Error(" <> plId <> "): converting " <> getUri uri <> " to NormalizedFilePath" | ||
|
||
-- --------------------------------------------------------------------- | ||
throwPluginError :: Monad m => PluginId -> String -> String -> ExceptT String m b | ||
throwPluginError (PluginId who) what where' = throwE msg | ||
where | ||
msg = (T.unpack who) <> " failed with " <> what <> " at " <> where' | ||
throwPluginError :: Monad m => String -> ExceptT String m b | ||
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. inline? 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. I plan on coming back to this (assuming people are ok with my suggestion). I want to come up with some way to tie back log messages to their context. So for instance I'm thinking this would eventually return 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. Do you rather want 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. I'm not sure It would be in a follow up PR and the main detractor is whatever the solution is, has to cross 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. I think adding a
where |
||
throwPluginError = throwE | ||
|
||
handleMaybe :: Monad m => e -> Maybe b -> ExceptT e m b | ||
handleMaybe msg = maybe (throwE msg) return | ||
|
Uh oh!
There was an error while loading. Please reload this page.