packages feed

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