packages feed

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"