packages feed

keter-1.6: Keter/Cli.hs

{-# LANGUAGE OverloadedStrings #-}
module Keter.Cli
    ( launchCli
    , CliStates(..)
    ) where

import Keter.Types.Common
import Keter.AppManager
import Control.Concurrent (forkFinally)
import qualified Control.Exception as E
import Control.Monad (unless, forever, void, when)
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
  , csLog        :: !(LogMessage -> IO ())
  , csPort       :: !Port
  }

launchCli :: CliStates -> IO ()
launchCli states = void $ forkIO $ withSocketsDo $ do
    addr <- resolve $ show $ csPort states
    E.bracket (open addr) close $ \x -> do
                                    csLog states $ BindCli addr
                                    loop states 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 :: CliStates -> Socket -> IO b
loop states sock = forever $ do
    (conn, peer) <- accept sock
    csLog states $ ReceivedCliConnection peer
    void $ forkFinally (talk states conn) (\_ -> close conn)

listRunningApps :: CliStates -> Socket -> IO ()
listRunningApps states conn = do
  txt <- atomically $ renderApps $ csAppManager states
  sendAll conn $ T.encodeUtf8 txt <> "\n"

talk :: CliStates -> Socket -> IO ()
talk states conn = do
    msg <- recv conn 1024
    unless (S.null msg) $ do
      case T.decodeUtf8' msg of
        Left exception -> 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 states conn
            (Success (CmdExit   )) -> False <$ sendAll conn "bye\n"
            (CompletionInvoked x) -> True <$ sendAll conn "completion ignored \n"
            Failure failure        ->
              True <$ sendAll conn (T.encodeUtf8 (T.pack $ fst $ renderFailure failure "") <> "\n")
          when isLoop $ talk states conn