{-# 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"