Skip to content

Commit

Permalink
Merge pull request #4664 from input-output-hk/newhoggy/cardano-ping
Browse files Browse the repository at this point in the history
New cardano-cli ping command.
  • Loading branch information
newhoggy committed Mar 24, 2023
2 parents 3888d54 + f270530 commit a2fda17
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 12 deletions.
2 changes: 1 addition & 1 deletion cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ index-state: 2023-03-06T05:24:58Z

index-state:
, hackage.haskell.org 2023-03-06T05:24:58Z
, cardano-haskell-packages 2023-02-28T09:20:07Z
, cardano-haskell-packages 2023-03-21T10:00:52Z

packages:
cardano-api
Expand Down
19 changes: 12 additions & 7 deletions cardano-cli/cardano-cli.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ library
Cardano.CLI.Shelley.Run.Read
Cardano.CLI.Shelley.Run.Validate

Cardano.CLI.Ping

Cardano.CLI.TopHandler

other-modules: Paths_cardano_cli
Expand All @@ -100,35 +102,38 @@ library
, aeson-pretty >= 0.8.5
, ansi-terminal
, attoparsec
, base16-bytestring >= 1.0
, bech32 >= 1.1.0
, binary
, bytestring
, base16-bytestring >= 1.0
, canonical-json
, cardano-api
, cardano-binary
, cardano-git-rev
, cardano-crypto
, cardano-crypto-class ^>= 2.0
, cardano-crypto-wrapper ^>= 1.4
, cardano-data ^>= 0.1
, cardano-git-rev
, cardano-ledger-alonzo ^>= 0.1
, cardano-ledger-byron ^>= 0.1
, cardano-ledger-conway
, cardano-ledger-core ^>= 0.1
, cardano-ledger-shelley ^>= 0.1
, cardano-ledger-shelley-ma ^>= 0.1
, cardano-ping
, cardano-prelude
, cardano-protocol-tpraos ^>= 0.1
, cardano-slotting ^>= 0.1
, vector-map ^>= 0.1
, contra-tracer
, cardano-strict-containers ^>= 0.1
, cborg >= 0.2.4 && < 0.3
, containers
, contra-tracer
, cryptonite
, deepseq
, directory
, filepath
, formatting
, io-classes
, iproute
, mtl
, network
Expand All @@ -144,17 +149,17 @@ library
, prettyprinter
, prettyprinter-ansi-terminal
, random
, cardano-ledger-shelley ^>= 0.1
, set-algebra ^>= 0.1
, split
, cardano-strict-containers ^>= 0.1
, strict-stm
, text
, time
, transformers
, transformers-except ^>= 0.1.3
, unliftio-core
, utf8-string
, vector
, vector-map ^>= 0.1
, yaml

executable cardano-cli
Expand All @@ -177,9 +182,9 @@ test-suite cardano-cli-test
type: exitcode-stdio-1.0

build-depends: aeson
, bech32 >= 1.1.0
, base16-bytestring
, bytestring
, bech32 >= 1.1.0
, cardano-api
, cardano-api:gen
, cardano-cli
Expand Down
5 changes: 5 additions & 0 deletions cardano-cli/src/Cardano/CLI/Parsers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Cardano.CLI.Parsers
) where

import Cardano.CLI.Byron.Parsers (backwardsCompatibilityCommands, parseByronCommands)
import Cardano.CLI.Ping (parsePingCmd)
import Cardano.CLI.Render (customRenderHelp)
import Cardano.CLI.Run (ClientCommand (..))
import Cardano.CLI.Shelley.Parsers (parseShelleyCommands)
Expand Down Expand Up @@ -50,6 +51,7 @@ parseClientCommand =
-- so we list it first.
[ parseShelley
, parseByron
, parsePing
, parseDeprecatedShelleySubcommand
, backwardsCompatibilityCommands
, parseDisplayVersion opts
Expand All @@ -67,6 +69,9 @@ parseByron =
parseByronCommands
]

parsePing :: Parser ClientCommand
parsePing = CliPingCommand <$> parsePingCmd

-- | Parse Shelley-related commands at the top level of the CLI.
parseShelley :: Parser ClientCommand
parseShelley = ShelleyCommand <$> parseShelleyCommands
Expand Down
198 changes: 198 additions & 0 deletions cardano-cli/src/Cardano/CLI/Ping.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeApplications #-}

{- HLINT ignore "Move brackets to avoid $" -}

module Cardano.CLI.Ping
( PingCmd(..)
, PingClientCmdError(..)
, renderPingClientCmdError
, runPingCmd
, parsePingCmd
) where

import Control.Applicative ((<|>))
import Control.Concurrent.Class.MonadSTM.Strict (StrictTMVar)
import qualified Control.Concurrent.Class.MonadSTM.Strict as STM
import Control.Exception (SomeException)
import Control.Monad (forM, unless)
import Control.Monad.Class.MonadAsync (MonadAsync (async, wait, waitCatch))
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT)
import Control.Monad.Trans.Except.Extra (left)
import Control.Tracer (Tracer (..))
import Data.List (foldl')
import qualified Data.List as L
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word32)
import Network.Socket (AddrInfo)
import qualified Network.Socket as Socket
import qualified Options.Applicative as Opt
import qualified Prettyprinter as PP
import qualified System.Exit as IO
import qualified System.IO as IO

import qualified Cardano.Network.Ping as CNP

newtype PingClientCmdError = PingClientCmdError [(AddrInfo, SomeException)]

data EndPoint = HostEndPoint String | UnixSockEndPoint String deriving (Eq, Show)

maybeHostEndPoint :: EndPoint -> Maybe String
maybeHostEndPoint = \case
HostEndPoint host -> Just host
UnixSockEndPoint _ -> Nothing

maybeUnixSockEndPoint :: EndPoint -> Maybe String
maybeUnixSockEndPoint = \case
HostEndPoint _ -> Nothing
UnixSockEndPoint sock -> Just sock

data PingCmd = PingCmd
{ pingCmdCount :: !Word32
, pingCmdEndPoint :: !EndPoint
, pingCmdPort :: !String
, pingCmdMagic :: !Word32
, pingCmdJson :: !Bool
, pingCmdQuiet :: !Bool
} deriving (Eq, Show)

pingClient :: Tracer IO CNP.LogMsg -> Tracer IO String -> PingCmd -> [CNP.NodeVersion] -> AddrInfo -> IO ()
pingClient stdout stderr cmd = CNP.pingClient stdout stderr opts
where opts = CNP.PingOpts
{ CNP.pingOptsQuiet = pingCmdQuiet cmd
, CNP.pingOptsJson = pingCmdJson cmd
, CNP.pingOptsCount = pingCmdCount cmd
, CNP.pingOptsHost = maybeHostEndPoint (pingCmdEndPoint cmd)
, CNP.pingOptsUnixSock = maybeUnixSockEndPoint (pingCmdEndPoint cmd)
, CNP.pingOptsPort = pingCmdPort cmd
, CNP.pingOptsMagic = pingCmdMagic cmd
}

runPingCmd :: PingCmd -> ExceptT PingClientCmdError IO ()
runPingCmd options = do
let hints = Socket.defaultHints { Socket.addrSocketType = Socket.Stream }

msgQueue <- liftIO STM.newEmptyTMVarIO

-- 'addresses' are all the endpoints to connect to and 'versions' are the node protocol versions
-- to ping with.
(addresses, versions) <- case pingCmdEndPoint options of
HostEndPoint host -> do
addrs <- liftIO $ Socket.getAddrInfo (Just hints) (Just host) (Just (pingCmdPort options))
return (addrs, CNP.supportedNodeToNodeVersions $ pingCmdMagic options)
UnixSockEndPoint fname -> do
let addr = Socket.AddrInfo
[] Socket.AF_UNIX Socket.Stream
Socket.defaultProtocol (Socket.SockAddrUnix fname) Nothing
return ([addr], CNP.supportedNodeToClientVersions $ pingCmdMagic options)

-- Logger async thread handle
laid <- liftIO . async $ CNP.logger msgQueue $ pingCmdJson options
-- Ping client thread handles
caids <- forM addresses $ liftIO . async . pingClient (Tracer $ doLog msgQueue) (Tracer doErrLog) options versions
res <- L.zip addresses <$> mapM (liftIO . waitCatch) caids
liftIO $ doLog msgQueue CNP.LogEnd
liftIO $ wait laid

-- Collect errors 'es' from failed pings and 'addrs' from successful pings.
let (es, addrs) = foldl' partition ([],[]) res

-- Report any errors
case (es, addrs) of
([], _) -> liftIO IO.exitSuccess
(_, []) -> left $ PingClientCmdError es
(_, _) -> do
unless (pingCmdQuiet options) $ mapM_ (liftIO . IO.hPrint IO.stderr) es
liftIO IO.exitSuccess

where
partition :: ([(AddrInfo, SomeException)], [AddrInfo])
-> (AddrInfo, Either SomeException ())
-> ([(AddrInfo, SomeException)], [AddrInfo])
partition (es, as) (a, Left e) = ((a, e) : es, as)
partition (es, as) (a, Right _) = (es, a : as)

doLog :: StrictTMVar IO CNP.LogMsg -> CNP.LogMsg -> IO ()
doLog msgQueue msg = STM.atomically $ STM.putTMVar msgQueue msg

doErrLog :: String -> IO ()
doErrLog = IO.hPutStrLn IO.stderr

renderPingClientCmdError :: PingClientCmdError -> Text
renderPingClientCmdError = \case
PingClientCmdError es -> T.intercalate "\n" $ T.pack . show <$> es

parsePingCmd :: Opt.Parser PingCmd
parsePingCmd = Opt.hsubparser $ mconcat
[ Opt.metavar "ping"
, Opt.command "ping" $ Opt.info pPing $ Opt.progDescDoc $ Just $ mconcat
[ PP.pretty @String "Ping a cardano node either using node-to-node or node-to-client protocol. "
, PP.pretty @String "It negotiates a handshake and keeps sending keep alive messages."
]
]

pHost :: Opt.Parser String
pHost =
Opt.strOption $ mconcat
[ Opt.long "host"
, Opt.short 'h'
, Opt.metavar "HOST"
, Opt.help "Hostname/IP, e.g. relay.iohk.example."
]

pUnixSocket :: Opt.Parser String
pUnixSocket =
Opt.strOption $ mconcat
[ Opt.long "unixsock"
, Opt.short 'u'
, Opt.metavar "SOCKET"
, Opt.help "Unix socket, e.g. file.socket."
]

pEndPoint :: Opt.Parser EndPoint
pEndPoint = fmap HostEndPoint pHost <|> fmap UnixSockEndPoint pUnixSocket

pPing :: Opt.Parser PingCmd
pPing = PingCmd
<$> ( Opt.option Opt.auto $ mconcat
[ Opt.long "count"
, Opt.short 'c'
, Opt.metavar "COUNT"
, Opt.help $ mconcat
[ "Stop after sending count requests and receiving count responses. "
, "If this option is not specified, ping will operate until interrupted. "
]
, Opt.value maxBound
]
)
<*> pEndPoint
<*> ( Opt.strOption $ mconcat
[ Opt.long "port"
, Opt.short 'p'
, Opt.metavar "PORT"
, Opt.help "Port number, e.g. 1234."
, Opt.value "3001"
]
)
<*> ( Opt.option Opt.auto $ mconcat
[ Opt.long "magic"
, Opt.short 'm'
, Opt.metavar "MAGIC"
, Opt.help "Network magic."
, Opt.value CNP.mainnetMagic
]
)
<*> ( Opt.switch $ mconcat
[ Opt.long "json"
, Opt.short 'j'
, Opt.help "JSON output flag."
]
)
<*> ( Opt.switch $ mconcat
[ Opt.long "quiet"
, Opt.short 'q'
, Opt.help "Quiet flag, CSV/JSON only output"
]
)
7 changes: 7 additions & 0 deletions cardano-cli/src/Cardano/CLI/Run.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import qualified System.IO as IO
import Cardano.CLI.Byron.Commands (ByronCommand)
import Cardano.CLI.Byron.Run (ByronClientCmdError, renderByronClientCmdError,
runByronClientCommand)
import Cardano.CLI.Ping (PingCmd (..), PingClientCmdError (..), renderPingClientCmdError, runPingCmd)
import Cardano.CLI.Shelley.Commands (ShelleyCommand)
import Cardano.CLI.Shelley.Run (ShelleyClientCmdError, renderShelleyClientCmdError,
runShelleyClientCommand)
Expand Down Expand Up @@ -48,16 +49,20 @@ data ClientCommand =
-- now-deprecated \"shelley\" subcommand.
| DeprecatedShelleySubcommand ShelleyCommand

| CliPingCommand PingCmd

| forall a. Help ParserPrefs (ParserInfo a)
| DisplayVersion

data ClientCommandErrors
= ByronClientError ByronClientCmdError
| ShelleyClientError ShelleyCommand ShelleyClientCmdError
| PingClientError PingClientCmdError

runClientCommand :: ClientCommand -> ExceptT ClientCommandErrors IO ()
runClientCommand (ByronCommand c) = firstExceptT ByronClientError $ runByronClientCommand c
runClientCommand (ShelleyCommand c) = firstExceptT (ShelleyClientError c) $ runShelleyClientCommand c
runClientCommand (CliPingCommand c) = firstExceptT PingClientError $ runPingCmd c
runClientCommand (DeprecatedShelleySubcommand c) =
firstExceptT (ShelleyClientError c)
$ runShelleyClientCommandWithDeprecationWarning
Expand All @@ -70,6 +75,8 @@ renderClientCommandError (ByronClientError err) =
renderByronClientCmdError err
renderClientCommandError (ShelleyClientError cmd err) =
renderShelleyClientCmdError cmd err
renderClientCommandError (PingClientError err) =
renderPingClientCmdError err

-- | Combine an 'ExceptT' that will write a warning message to @stderr@ with
-- the provided 'ExceptT'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module Cardano.Node.Tracing.Tracers.NodeToNode
) where

import Cardano.Logging
import Data.Aeson (Value (String), ToJSON (..), (.=))
import Data.Aeson (ToJSON (..), Value (String), (.=))
import Data.Proxy (Proxy (..))
import Data.Text (pack)
import Network.TypedProtocol.Codec (AnyMessageAndAgency (..))
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a2fda17

Please sign in to comment.