fpco-api-1.1.0: src/library/FP/Server.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
-- | Main entry point to the server.
module FP.Server where
import FP.API
import FP.API.Run
import FP.API.Types
import FP.Server.Spans
import FP.Server.Types
import Control.Applicative
import Control.Concurrent.Lifted
import Control.Exception.Lifted as E hiding (handle)
import Control.Monad.Extra
import Control.Monad.Logger
import Control.Monad.Reader
import Data.Aeson
import qualified Data.ByteString.Char8 as S8
import Data.ByteString.Lazy (fromChunks)
import qualified Data.ByteString.Lazy.Char8 as L8
import Data.IORef
import qualified Data.Map as M
import Data.Map (Map)
import Data.Maybe
import Data.Monoid
import Data.Text (Text, pack, unpack)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.IO as T
import Data.Typeable
import Network
import Network.HTTP.Conduit
import Prelude hiding (span)
import System.Directory
import System.FilePath
import System.IO
import Texts.English
-- | Run the given Server command with the config. Good for testing in the repl.
runWithConfig :: Server b -> Config -> IO b
runWithConfig m config = do
manager <- newManager conduitManagerSettings
pollers <- newMVar mempty
jar <- newIORef mempty
tokensVar <- newMVar mempty
tokens <- newMVar tokensVar
runReaderT ((if configDebug config then runStderrLoggingT else runSilentLoggingT) m)
(ServerReader (CC (stripSlash (configUrl config))
(configToken config)
manager
jar
(configAgent config))
config
pollers
tokens)
where runSilentLoggingT m' =
runLoggingT m' (\_loc _src _lvl _str -> return ())
-- | Start the server.
startServer :: Bool -> Server ()
startServer forkOnceListening = do
Config{..} <- asks serverConfig
io (hSetBuffering stdout NoBuffering)
sock <- io (listenOn (PortNumber (fromIntegral configPort)))
$(logInfo) ("Server started on port " <> pack (show configPort) <>
", remote URL is: " <> configUrl)
(if forkOnceListening then void . fork else id)
(forever (acceptConnection sock))
-- | Accept a connection on the given socket.
acceptConnection :: Socket -> Server ()
acceptConnection sock = do
(handle,remote,port) <- io (accept sock)
io (hSetBuffering handle NoBuffering)
$(logInfo) ("Connection accepted from: " <> pack remote <>
":" <> pack (show port) <>
" (" <> pack (show handle) <> ")")
void (fork (handleLine handle))
-- | Handle a line of input.
handleLine :: Handle -> Server ()
handleLine handle = flip finally (close handle) $ do
result <- io (try (S8.hGetLine handle))
case result of
Left (_ :: IOException) -> do
$(logError) "Unable to get line from handle."
Right line -> do
case decode (fromChunks [line]) of
Nothing -> do
$(logError) ("Unable to parse JSON from line: " <> decodeUtf8 line)
Just msg -> do
$(logDebug) ("<- " <> pack (show msg))
handleMessage handle msg
-- | Handle any incoming message from the client.
handleMessage :: Handle -> Msg -> Server ()
handleMessage h msg =
case msg of
MsgSaveModule fpid root filename ->
saveTheModule h fpid root filename
MsgCheckModule fpid root filename path ->
checkModule h fpid root filename path
MsgTypeInfo fpid filename sl sc el ec ->
typeInfo h fpid filename sl sc el ec
MsgGetDefinition fpid root filename sl sc el ec ->
getDefinition h fpid root filename sl sc el ec
MsgAutoComplete fpid filename prefix ->
autoComplete h fpid filename prefix
MsgHoogleIdent fpid filename name ->
hoogleIdent h fpid filename name
MsgHoogleDb fpid name ->
hoogleDb h fpid name
MsgDownloadFiles fpid fp ->
downloadFiles h fpid fp
MsgWriteEmacsConfig fpid fp ->
writeEmacsConfig h fpid fp
-- Message handlers
-- | Write out the .dir-locals.el file for the project.
writeEmacsConfig :: Handle -> Either Text FayProjectId -> FilePath -> Server ()
writeEmacsConfig h pid root = do
fpid <- getFayProjectId pid
io (writeFile (root </> ".dir-locals.el")
(unlines (src fpid)))
reply h (ReplyOK ())
where src fpid =
["((nil . ((fpco-pid . " ++ unpack (unFayProjectId fpid) ++ ")"
," (eval . (set (make-local-variable 'fpco-root)"
," (expand-file-name"
," (locate-dominating-file buffer-file-name \".dir-locals.el\")))))))))"]
-- | Download all files in the project, overwriting any local copies.
downloadFiles :: Handle -> Either Text FayProjectId -> FilePath -> Server ()
downloadFiles h pid root = do
fpid <- getFayProjectId pid
ipi <- getInitialProjectInfo fpid
forM_ (ipiFiles ipi) $ \(name,_) -> do
FileContent text _ <- getFile name fpid
io (writeCreateFile (root </> T.unpack (unFayFileName name))
(fromMaybe "" text))
reply h (ReplyOK ())
where writeCreateFile fp text = do
io (createDirectoryIfMissing True (takeDirectory fp))
T.writeFile (root </> fp) text
-- | Hoogle search whole database.
hoogleDb :: Handle -> FayProjectId -> Text -> Server ()
hoogleDb h fpid q = do
void (ideHoogleSearch Nothing exact count offset limit q fpid)
wait fpid
(Callback
(\_mid msg ->
case msg of
HoogleResults _ results _ ->
do reply h (ReplyHoogleResults results)
return Done
_ -> return NotDone))
where exact = False
count = 10
offset = 0
limit = count
-- | Hoogle search for an identifier in a module.
hoogleIdent :: Handle -> FayProjectId -> String -> Text -> Server ()
hoogleIdent h fpid filename q = do
hid <- ideHoogleSearch (Just mname) exact count offset limit q fpid
wait fpid
(Callback
(\mid msg ->
case msg of
HoogleResults _ results _
| toHoogleId mid == Just hid ->
do case results of
(result:_) -> reply h (ReplyHoogleResult result)
_ -> return ()
return Done
_ -> return NotDone))
where mname = FayFileName (pack filename)
exact = True
count = 10
offset = 0
limit = count
toHoogleId = fmap HoogleId
-- | Autocomplete the given prefix replying with a list of
-- completions.
autoComplete :: Handle
-> FayProjectId
-> FilePath
-> Text
-> Server ()
autoComplete h fpid filename prefix = do
case T.strip prefix of
"" -> reply h (ReplyCompletions [])
_ -> do
getAutocompletions input fpid
wait fpid
(Callback
(\_ msg ->
case msg of
AutoCompleteResults input' completions
| Just input == input' -> do reply h (ReplyCompletions completions)
return Done
_ -> return NotDone))
where input = AutoCompleteInput (FayFileName (pack filename))
prefix
-- | Get the definition location of the identifier at the given span.
getDefinition :: Handle
-> FayProjectId -> FilePath
-> FilePath
-> Int -> Int
-> Int -> Int
-> Server ()
getDefinition h fpid root filename sl sc el ec = do
getDefinitionSource defSpan fpid
wait fpid
(Callback (\_ msg ->
case msg of
IdInfoResults i ->
case i of
NoIdInfo src
| src == defSpan -> return Done
IdInfo src _ info
| src == defSpan -> do reply h (ReplyLocation (makeDef info))
return Done
_ -> return NotDone
_ -> return NotDone))
where defSpan =
SourceSpan (FayFileName (pack filename))
sl sc
el ec
makeDef (DefinitionLocal span) =
DefinitionLoc (makeLoc root span)
makeDef (DefinitionTextSpan t1 t2) =
DefinitionUseless (t1 <> " " <> t2 <> " (nowhere known to go)")
makeDef (DefinitionImported text m1 m2 es1 es2) =
DefinitionImport text
(makeModuleId m1)
(makeModuleId m2)
(makeEitherLoc root es1)
(makeEitherLoc root es2)
makeDef (DefinitionWiredIn text) =
DefinitionUseless ("Wired-in: " <> text <> " (nowhere to go!)")
makeDef (DefinitionBinder text) =
DefinitionUseless ("Binder: " <> text <> " (you're already there!)")
-- | Print a package:module pair.
makeModuleId :: FayModuleId -> ModuleId
makeModuleId (FayModuleId _ mname pkg) =
(ModuleId (packageName pkg) (unFayModuleName mname))
-- | Get type info of span.
typeInfo :: Handle
-> FayProjectId -> String
-> Int -> Int
-> Int -> Int
-> Server ()
typeInfo h fpid filename sl sc el ec = do
getTypeInfo span 0 fpid
wait fpid
(Callback
(\_ msg -> do
case msg of
SubExprsResults span' infos
| span' == span -> do reply h (ReplyTypeInfo (map toSpanType (concat infos)))
return Done
_ -> return NotDone))
where span =
SourceSpan (FayFileName (pack filename))
sl sc
el ec
-- | Save the given module.
saveTheModule :: Handle -> FayProjectId -> FilePath -> FilePath -> Server ()
saveTheModule h fpid root filename =
withTokens (\tokensVar -> do
let fname = FayFileName (pack filename)
token <- getToken tokensVar fpid root filename
text <- io (T.readFile (root </> filename))
E.catch (do SaveFileOutput token' _ <- saveFile fname text token fpid
updateToken tokensVar root filename token'
reply h (ReplySaveStatus False))
-- A command exception will be thrown when the file is out of
-- date. So we just immediately grab the new version of the
-- file, overwrite out local copy. Emacs will prompt the user
-- about it at the right time.
(\(_ :: CommandException) ->
do updateFileContents tokensVar fpid root filename
reply h (ReplySaveStatus True)))
-- | Check the given module. Necessary for flycheck.
checkModule :: Handle -> FayProjectId -> FilePath -> FilePath -> FilePath -> Server ()
checkModule h fpid root filename filepath =
withTokens (\tokensVar -> do
let fname = FayFileName (pack filename)
token <- getToken tokensVar fpid root filename
text <- io (T.readFile filepath)
result <- try (saveFile fname text token fpid)
case result of
-- If there's an out of date error, don't break the flychecker,
-- just return no errors for now.
Left (_ :: CommandException) -> reply h (ReplyCompileInfos [])
Right (SaveFileOutput token' (CompileChanged mcid _)) -> do
updateToken tokensVar root filename token'
case mcid of
Nothing -> reply h (ReplyCompileInfos [])
Just _ -> do
wait fpid
(Callback
(\_ msg ->
case msg of
StatusSnapshot snapshot _ ->
case snapCompileStatus snapshot of
RunnerNotCompiling -> return NotDone
RunnerCompiling _ _ -> return NotDone
RunnerCompileDone _ infos -> do
reply h
(ReplyCompileInfos infos)
return Done
_ -> return NotDone)))
-- The communication API
-- | Reply with the given value.
reply :: (ToJSON a,Show a) => Handle -> a -> Server ()
reply h r = do
$(logDebug) ("-> " <> ellipsize 140 (pack (show r)))
io (L8.hPutStrLn h (encode r))
-- | Close the given handle.
close :: Handle -> Server ()
close h = do
$(logDebug) ("Connection closed to " <> pack (show h))
io (hClose h)
-- | Start poller if there isn't already one running for the given
-- project, and in any case add the given callback to the list. This
-- blocks on the result.
wait :: FayProjectId -- ^ The project to poll on.
-> Callback -- ^ Take the message or pass it back.
-> Server ()
wait fpid (Callback callback) = do
waiter <- newEmptyMVar
let wcallback = waiting waiter
psvar <- asks serverPollers
start <- modifyMVar psvar $ \pollers ->
case M.lookup fpid pollers of
-- Either insert or append
Just{} -> return (M.insertWith (++) fpid [wcallback] pollers,False)
Nothing -> return (M.insert fpid [wcallback] pollers,True)
when start $
void $ fork $ poll fpid
takeMVar waiter
where waiting waiter = Callback $ \mid msg ->
do ret <- callback mid msg
case ret of
Done -> putMVar waiter ()
_ -> return ()
return ret
-- | Remove a poller from the polling list and any broadcast
-- callbacks.
removePoller :: FayProjectId -> Server ()
removePoller fpid = do
psvar <- asks serverPollers
modifyMVar_ psvar (return . M.delete fpid)
-- | Poll for new messages and apply any queued callbacks to them.
poll :: FayProjectId -> Server ()
poll fpid = do
getIPI fpid
$(logDebug) ("Polling on project " <> pack (show fpid))
go PMRImmediateStatusNoMessages
where go statusHash = do
result <- try (getProjectMessages statusHash fpid)
case result of
Left (SomeException ge) ->
case fmap Left (cast ge) <|> fmap Right (cast ge) of
Nothing ->
do removePoller fpid
throw ge
Just (e :: Either HttpException CommandException) ->
do $(logError) ("Error while polling for messages: " <> pack (show e))
$(logError) ("Waiting 10 seconds before polling again ...")
threadDelay (1000 * 1000 * 10)
go PMRImmediateStatusNoMessages
Right (ProjectMessagesOutput nextfilt messages) ->
do psvar <- asks serverPollers
modifyMVar_ psvar $ \pollers ->
case M.lookup fpid pollers of
Nothing -> return pollers
Just callbacks -> do
callbacks' <- foldM applyCallbacks callbacks messages
return (M.insert fpid callbacks' pollers)
go (newRequest nextfilt messages)
newRequest nextfilt messages =
fromMaybe (if null messages
then PMRImmediateStatusNoMessages
else PMRImmediateStatusWithMessages nextfilt)
(latestHash messages)
latestHash = listToMaybe . mapMaybe latest . reverse where
latest (_,StatusSnapshot _ hash) = Just (PMRNextStatusWithMessages PMFilterAll hash)
latest _ = Nothing
-- | Get initial project information.
getIPI :: FayProjectId -> Server ()
getIPI fpid = do
_ <- getInitialProjectInfo fpid
return ()
-- | Applies the given callbacks to the given message. Returns a new
-- list of callbacks. If any of the callbacks are now done, they will
-- be removed from the list.
applyCallbacks :: [Callback] -> (Maybe Int, RunnerMessage) -> Server [Callback]
applyCallbacks callbacks (mtag,msg) = do
fmap catMaybes $ forM callbacks $ \callback@(Callback call) -> do
result <- try (call mtag msg)
case result of
Right Done -> return Nothing
Right NotDone -> return (Just callback)
-- Callbacks that throw exceptions are discarded.
Left (e :: SomeException) -> do
$(logError) ("Callback threw exception: " <> pack (show e))
return Nothing
-- Misc
-- | Get the token of the given file.
getToken :: MVar (Map FilePath FayTutorialToken) -> FayProjectId -> FilePath -> FilePath -> Server FayTutorialToken
getToken tokensVar fpid root file = do
modifyMVar tokensVar
(\tokens ->
case M.lookup key tokens of
Nothing -> do token <- getFileToken (FayFileName (T.pack file)) fpid
return (M.insert key token tokens,token)
Just token -> return (tokens,token))
where key = root </> file
-- | Update the contents of the given file from the server.
updateFileContents :: MVar (Map FilePath FayTutorialToken) -> FayProjectId -> FilePath -> FilePath
-> Server ()
updateFileContents tokensVar fpid root filename = do
FileContent text token <- getFile (FayFileName (T.pack filename)) fpid
updateToken tokensVar root filename token
io (T.writeFile (root </> filename)
(fromMaybe "" text))
-- | Update the token of the given file.
updateToken :: MVar (Map FilePath FayTutorialToken) -> FilePath -> FilePath
-> FayTutorialToken -> Server ()
updateToken tokensVar root file token = do
$(logDebug) ("Updating file token: " <> T.pack key <> ": " <> pack (show token))
modifyMVar_ tokensVar
(return . M.insert key token)
where key = root </> file
-- | Do something exclusively with tokens. Due to the fact that
-- flychecking and buffer saving *can* occur simultaneously from
-- Emacs, we don't want those two racing save capabilities.
--
-- On the other hand it doesn't matter what order they occur because
-- they'll be saving the same content. So we simply require that any
-- command that uses module tokens needs to happen in an exclusion.
withTokens :: (MVar (Map FilePath FayTutorialToken) -> Server a) -> Server a
withTokens cont = do
tokensVar <- asks serverTokens
withMVar tokensVar cont
-- | Get the project ID from either a URL or a project ID.
getFayProjectId :: Either Text FayProjectId -> Server FayProjectId
getFayProjectId = either getProjectId return
-- | Strip the trailing slash.
stripSlash :: Text -> Text
stripSlash = T.reverse . T.dropWhile (=='/') . T.reverse
-- | Convert an API message to a more structurally convenient reply message.
convertMsg :: FilePath -> SourceInfo -> CompileMessage
convertMsg root SourceInfo{..} =
CompileMessage (printEitherSpan root infoSpan)
kind
infoMsg
where kind =
case infoKind of
KindError -> "error"
KindWarning -> "warning"
KindHint -> "hint"