keter-2.1.0: src/Keter/Cli.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RecordWildCards #-}
module Keter.Cli
( launchCli
, CliStates(..)
) where
import Keter.Common
import Keter.Context
import Keter.AppManager
import Control.Concurrent (forkFinally)
import qualified Control.Exception as E
import Control.Monad (unless, forever, void, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.IO.Unlift (withRunInIO)
import Control.Monad.Trans.Class (MonadTrans, lift)
import Control.Monad.Logger
import Control.Monad.Reader (ask)
import qualified Data.ByteString as S
import Network.Socket
import Network.Socket.ByteString (recv, sendAll)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Options.Applicative
import Data.Foldable
import GHC.Conc
data Commands = CmdListRunningApps
| CmdExit
data CliStates = MkCliStates
{ csAppManager :: !AppManager
, csPort :: !Port
}
launchCli :: KeterM CliStates ()
launchCli = do
MkCliStates{..} <- ask
void $ withRunInIO $ \rio -> forkIO $
withSocketsDo $ do
addr <- resolve $ show csPort
E.bracket (open addr) close $ \x -> rio $ do
$logInfo $ T.pack $ "Bound cli to " <> show addr
loop x
commandParser :: Parser Commands
commandParser = hsubparser $
fold [
command "exit"
(info (pure CmdExit)
(progDesc "List all ports"))
,
command "apps"
(info (pure CmdListRunningApps)
(progDesc "Exit the program"))
]
resolve :: ServiceName -> IO AddrInfo
resolve port = do
let hints = defaultHints {
addrFlags = [AI_PASSIVE]
, addrSocketType = Stream
}
addr:_ <- getAddrInfo (Just hints) Nothing (Just port)
return addr
open :: AddrInfo -> IO Socket
open addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
setSocketOption sock ReuseAddr 1
-- If the prefork technique is not used,
-- set CloseOnExec for the security reasons.
withFdSocket sock $ setCloseOnExecIfNeeded
bind sock (addrAddress addr)
listen sock 10
return sock
loop :: Socket -> KeterM CliStates b
loop sock = forever $ do
(conn, peer) <- liftIO $ accept sock
$logInfo $ T.pack $ "CLI Connection from " <> show peer
void $ withRunInIO $ \rio ->
forkFinally (rio $ talk conn) (\_ -> close conn)
listRunningApps :: Socket -> KeterM CliStates ()
listRunningApps conn = do
MkCliStates{..} <- ask
txt <- liftIO $ atomically $ renderApps csAppManager
liftIO $ sendAll conn $ T.encodeUtf8 txt <> "\n"
talk :: Socket -> KeterM CliStates ()
talk conn = do
msg <- liftIO $ recv conn 1024
unless (S.null msg) $ do
case T.decodeUtf8' msg of
Left exception -> liftIO $ sendAll conn ("decode error: " <> T.encodeUtf8 (T.pack $ show exception))
Right txt -> do
let res = execParserPure defaultPrefs (info (commandParser <**> helper)
(fullDesc <> header "server repl" <> progDesc (
"repl for inspecting program state. You can connect to a socket and ask predefined questions")) ) (T.unpack <$> T.words txt)
isLoop <- case res of
(Success (CmdListRunningApps)) -> True <$ listRunningApps conn
(Success (CmdExit )) -> False <$ liftIO (sendAll conn "bye\n")
(CompletionInvoked x) -> True <$ liftIO (sendAll conn "completion ignored \n")
Failure failure ->
True <$ liftIO (sendAll conn (T.encodeUtf8 (T.pack $ fst $ renderFailure failure "") <> "\n"))
when isLoop $ talk conn