purescript-0.11.5: app/Command/Ide.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 RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Command.Ide (command) 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.Char8 as BS8
import qualified Data.ByteString.Lazy.Char8 as BSL8
import GHC.IO.Exception (IOErrorType(..), IOException(..))
import Language.PureScript.Ide
import Language.PureScript.Ide.Command
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 qualified Options.Applicative as Opts
import System.Directory
import System.Info as SysInfo
import System.FilePath
import System.IO hiding (putStrLn, print)
import System.IO.Error (isEOFError)
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
bind sock (SockAddrInet port localhost)
listen sock maxListenQueue
pure sock)
data ServerOptions = ServerOptions
{ _serverDirectory :: Maybe FilePath
, _serverGlobs :: [FilePath]
, _serverOutputPath :: FilePath
, _serverPort :: PortNumber
, _serverNoWatch :: Bool
, _serverPolling :: Bool
, _serverLoglevel :: IdeLogLevel
} deriving (Show)
data ClientOptions = ClientOptions
{ clientPort :: PortID
}
command :: Opts.Parser (IO ())
command = Opts.helper <*> subcommands where
subcommands :: Opts.Parser (IO ())
subcommands = (Opts.subparser . fold)
[ Opts.command "server"
(Opts.info (fmap server serverOptions <**> Opts.helper)
(Opts.progDesc "Start a server process"))
, Opts.command "client"
(Opts.info (fmap client clientOptions <**> Opts.helper)
(Opts.progDesc "Connect to a running server"))
]
client :: ClientOptions -> IO ()
client ClientOptions{..} = do
hSetEncoding stdin utf8
hSetEncoding stdout utf8
let handler (SomeException e) = do
T.putStrLn ("Couldn't connect to purs ide server on port " <> show clientPort <> ":")
print e
exitFailure
h <- connectTo "127.0.0.1" clientPort `catch` handler
T.hPutStrLn h =<< T.getLine
BS8.putStrLn =<< BS8.hGetLine h
hFlush stdout
hClose h
clientOptions :: Opts.Parser ClientOptions
clientOptions = ClientOptions . PortNumber . fromIntegral <$>
Opts.option Opts.auto (Opts.long "port" <> Opts.short 'p' <> Opts.value (4242 :: Integer))
server :: ServerOptions -> IO ()
server opts'@(ServerOptions dir globs outputPath port noWatch polling logLevel) = do
when (logLevel == LogDebug || logLevel == LogAll)
(putText "Parsed Options:" *> print opts')
maybe (pure ()) setCurrentDirectory dir
ideState <- newTVarIO emptyIdeState
cwd <- getCurrentDirectory
let fullOutputPath = cwd </> outputPath
unlessM (doesDirectoryExist fullOutputPath) $ do
putText "Your output directory didn't exist. 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 polling logLevel ideState fullOutputPath) print)
let conf = IdeConfiguration {confLogLevel = logLevel, confOutputPath = outputPath, confGlobs = globs}
env = IdeEnvironment {ideStateVar = ideState, ideConfiguration = conf}
startServer port env
serverOptions :: Opts.Parser ServerOptions
serverOptions =
ServerOptions
<$> optional (Opts.strOption (Opts.long "directory" `mappend` Opts.short 'd'))
<*> many (Opts.argument Opts.str (Opts.metavar "Source GLOBS..."))
<*> Opts.strOption (Opts.long "output-directory" `mappend` Opts.value "output/")
<*> (fromIntegral <$>
Opts.option Opts.auto (Opts.long "port" `mappend` Opts.short 'p' `mappend` Opts.value (4242 :: Integer)))
<*> Opts.switch (Opts.long "no-watch")
<*> flipIfWindows (Opts.switch (Opts.long "polling"))
<*> (parseLogLevel <$> Opts.strOption
(Opts.long "log-level"
`mappend` Opts.value ""
`mappend` Opts.help "One of \"debug\", \"perf\", \"all\" or \"none\""))
parseLogLevel s = case s of
"debug" -> LogDebug
"perf" -> LogPerf
"all" -> LogAll
"none" -> LogNone
_ -> LogDefault
-- polling is the default on Windows and the flag turns it off. See
-- #2209 and #2414 for explanations
flipIfWindows = map (if SysInfo.os == "mingw32" then not else identity)
startServer :: PortNumber -> IdeEnvironment -> IO ()
startServer port env = withSocketsDo $ do
sock <- listenOnLocalhost port
runLogger (confLogLevel (ideConfiguration env)) (runReaderT (forever (loop sock)) env)
where
loop :: (Ide m, MonadLogger m) => Socket -> m ()
loop sock = do
accepted <- runExceptT (acceptCommand sock)
case accepted of
Left err -> $(logError) err
Right (cmd, h) -> do
case decodeT cmd of
Just cmd' -> do
let message duration =
"Command "
<> commandName cmd'
<> " took "
<> displayTimeSpec duration
logPerf message $ do
result <- runExceptT (handleCommand cmd')
liftIO $ catchGoneHandle $ BSL8.hPutStrLn h $ case result of
Right r -> Aeson.encode r
Left err -> Aeson.encode err
liftIO (hFlush stdout)
Nothing -> do
$(logError) ("Parsing the command failed. Command: " <> cmd)
liftIO $ do
catchGoneHandle (T.hPutStrLn h (encodeT (GeneralError "Error parsing Command.")))
hFlush stdout
liftIO $ catchGoneHandle (hClose h)
catchGoneHandle :: IO () -> IO ()
catchGoneHandle =
handle (\e -> case e of
IOError { ioe_type = ResourceVanished } ->
putText ("[Error] psc-ide-server tried interact with the handle, but the connection was already gone.")
_ -> throwIO e)
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) ("Received command: " <> 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