fpco-api-1.0.1: 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 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,catch)
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 def
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: " <> pack 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 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))
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 :: String -> String
stripSlash = reverse . dropWhile (=='/') . 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"