packages feed

purescript-0.9.2: psc-ide-server/Main.hs

-----------------------------------------------------------------------------
--
-- Module      : Main
-- Description : The server accepting commands for psc-ide
-- Copyright   : Christoph Hegemann 2016
-- License     : MIT (http://opensource.org/licenses/MIT)
--
-- Maintainer  : Christoph Hegemann <christoph.hegemann1337@gmail.com>
-- Stability   : experimental
--
-- |
-- The server accepting commands for psc-ide
-----------------------------------------------------------------------------

{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE PackageImports        #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE NoImplicitPrelude     #-}

module Main where

import           Protolude

import qualified Data.Aeson as Aeson
import           Control.Concurrent.STM
import           "monad-logger" Control.Monad.Logger
import qualified Data.Text.IO                      as T
import qualified Data.ByteString.Lazy.Char8        as BS8
import           Data.Version                      (showVersion)
import           Language.PureScript.Ide
import           Language.PureScript.Ide.Util
import           Language.PureScript.Ide.Error
import           Language.PureScript.Ide.Types
import           Language.PureScript.Ide.Watcher
import           Network                           hiding (socketPort, accept)
import           Network.BSD                       (getProtocolNumber)
import           Network.Socket                    hiding (PortNumber, Type,
                                                    sClose)
import           Options.Applicative               hiding ((<>))
import           System.Directory
import           System.FilePath
import           System.IO                         hiding (putStrLn, print)
import           System.IO.Error                   (isEOFError)

import qualified Paths_purescript                  as Paths

listenOnLocalhost :: PortNumber -> IO Socket
listenOnLocalhost port = do
  proto <- getProtocolNumber "tcp"
  localhost <- inet_addr "127.0.0.1"
  bracketOnError
    (socket AF_INET Stream proto)
    sClose
    (\sock -> do
      setSocketOption sock ReuseAddr 1
      bindSocket sock (SockAddrInet port localhost)
      listen sock maxListenQueue
      pure sock)

data Options = Options
  { optionsDirectory  :: Maybe FilePath
  , optionsGlobs      :: [FilePath]
  , optionsOutputPath :: FilePath
  , optionsPort       :: PortNumber
  , optionsNoWatch    :: Bool
  , optionsDebug      :: Bool
  }

main :: IO ()
main = do
  Options dir globs outputPath port noWatch debug <- execParser opts
  maybe (pure ()) setCurrentDirectory dir
  ideState <- newTVarIO emptyIdeState
  cwd <- getCurrentDirectory
  let fullOutputPath = cwd </> outputPath

  unlessM (doesDirectoryExist fullOutputPath) $ do
    putStrLn ("Your output directory didn't exist. I'll create it at: " <> fullOutputPath)
    createDirectory fullOutputPath
    putText "This usually means you didn't compile your project yet."
    putText "psc-ide needs you to compile your project (for example by running pulp build)"

  unless noWatch $
    void (forkFinally (watcher ideState fullOutputPath) print)

  let conf = Configuration {confDebug = debug, confOutputPath = outputPath, confGlobs = globs}
      env = IdeEnvironment {ideStateVar = ideState, ideConfiguration = conf}
  startServer port env
  where
    parser =
      Options
        <$> optional (strOption (long "directory" `mappend` short 'd'))
        <*> many (argument str (metavar "Source GLOBS..."))
        <*> strOption (long "output-directory" `mappend` value "output/")
        <*> (fromIntegral <$>
             option auto (long "port" `mappend` short 'p' `mappend` value (4242 :: Integer)))
        <*> switch (long "no-watch")
        <*> switch (long "debug")
    opts = info (version <*> helper <*> parser) mempty
    version = abortOption
      (InfoMsg (showVersion Paths.version))
      (long "version" `mappend` help "Show the version number")

startServer :: PortNumber -> IdeEnvironment -> IO ()
startServer port env = withSocketsDo $ do
  sock <- listenOnLocalhost port
  runLogger (runReaderT (forever (loop sock)) env)
  where
    runLogger = runStdoutLoggingT . filterLogger (\_ _ -> confDebug (ideConfiguration env))

    loop :: (Ide m, MonadLogger m) => Socket -> m ()
    loop sock = do
      accepted <- runExceptT $ acceptCommand sock
      case accepted of
        Left err -> $(logDebug) err
        Right (cmd, h) -> do
          case decodeT cmd of
            Just cmd' -> do
              result <- runExceptT (handleCommand cmd')
              -- $(logDebug) ("Answer was: " <> T.pack (show result))
              liftIO (hFlush stdout)
              case result of
                Right r  -> liftIO $ BS8.hPutStrLn h (Aeson.encode r)
                Left err -> liftIO $ BS8.hPutStrLn h (Aeson.encode err)
            Nothing -> do
              $(logDebug) ("Parsing the command failed. Command: " <> cmd)
              liftIO $ do
                T.hPutStrLn h (encodeT (GeneralError "Error parsing Command."))
                hFlush stdout
          liftIO (hClose h)


acceptCommand :: (MonadIO m, MonadLogger m, MonadError Text m)
                 => Socket -> m (Text, Handle)
acceptCommand sock = do
  h <- acceptConnection
  $(logDebug) "Accepted a connection"
  cmd' <- liftIO (catchJust
                  -- this means that the connection was
                  -- terminated without receiving any input
                  (\e -> if isEOFError e then Just () else Nothing)
                  (Just <$> T.hGetLine h)
                  (const (pure Nothing)))
  case cmd' of
    Nothing -> throwError "Connection was closed before any input arrived"
    Just cmd -> do
      $(logDebug) cmd
      pure (cmd, h)
  where
   acceptConnection = liftIO $ do
     -- Use low level accept to prevent accidental reverse name resolution
     (s,_) <- accept sock
     h     <- socketToHandle s ReadWriteMode
     hSetEncoding h utf8
     hSetBuffering h LineBuffering
     pure h