git-annex/Utility/SimpleProtocol.hs

157 lines
4.4 KiB
Haskell
Raw Normal View History

{- Simple line-based protocols.
-
- Copyright 2013-2024 Joey Hess <id@joeyh.name>
-
- License: BSD-2-clause
-}
{-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Utility.SimpleProtocol (
Sendable(..),
Receivable(..),
parseMessage,
Serializable(..),
Parser,
parseFail,
parse0,
parse1,
parse2,
parse3,
parse4,
parse5,
parseList,
2015-04-03 19:33:28 +00:00
dupIoHandles,
getProtocolLine,
) where
import Data.Char
import GHC.IO.Handle
import Text.Read
import Common
-- Messages that can be sent.
class Sendable m where
formatMessage :: m -> [String]
-- Messages that can be received.
class Receivable m where
-- Passed the first word of the message, returns
-- a Parser that can be be fed the rest of the message to generate
-- the value.
parseCommand :: String -> Parser m
parseMessage :: (Receivable m) => String -> Maybe m
parseMessage s = parseCommand command rest
where
(command, rest) = splitWord s
class Serializable a where
serialize :: a -> String
deserialize :: String -> Maybe a
instance Serializable [Char] where
serialize = id
deserialize = Just
instance Serializable Integer where
serialize = show
deserialize = readMaybe
instance Serializable ExitCode where
serialize ExitSuccess = "0"
serialize (ExitFailure n) = show n
deserialize "0" = Just ExitSuccess
deserialize s = ExitFailure <$> readMaybe s
{- Parsing the parameters of messages. Using the right parseN ensures
- that the string is split into exactly the requested number of words,
- which allows the last parameter of a message to contain arbitrary
- whitespace, etc, without needing any special quoting.
-}
type Parser a = String -> Maybe a
parseFail :: Parser a
parseFail _ = Nothing
parse0 :: a -> Parser a
parse0 mk "" = Just mk
parse0 _ _ = Nothing
parse1 :: Serializable p1 => (p1 -> a) -> Parser a
parse1 mk p1 = mk <$> deserialize p1
parse2 :: (Serializable p1, Serializable p2) => (p1 -> p2 -> a) -> Parser a
parse2 mk s = mk <$> deserialize p1 <*> deserialize p2
where
(p1, p2) = splitWord s
parse3 :: (Serializable p1, Serializable p2, Serializable p3) => (p1 -> p2 -> p3 -> a) -> Parser a
parse3 mk s = mk <$> deserialize p1 <*> deserialize p2 <*> deserialize p3
where
(p1, rest) = splitWord s
(p2, p3) = splitWord rest
parse4 :: (Serializable p1, Serializable p2, Serializable p3, Serializable p4) => (p1 -> p2 -> p3 -> p4 -> a) -> Parser a
parse4 mk s = mk <$> deserialize p1 <*> deserialize p2 <*> deserialize p3 <*> deserialize p4
where
(p1, rest) = splitWord s
(p2, rest') = splitWord rest
(p3, p4) = splitWord rest'
parse5 :: (Serializable p1, Serializable p2, Serializable p3, Serializable p4, Serializable p5) => (p1 -> p2 -> p3 -> p4 -> p5 -> a) -> Parser a
parse5 mk s = mk <$> deserialize p1 <*> deserialize p2 <*> deserialize p3 <*> deserialize p4 <*> deserialize p5
where
(p1, rest) = splitWord s
(p2, rest') = splitWord rest
(p3, rest'') = splitWord rest'
(p4, p5) = splitWord rest''
splitWord :: String -> (String, String)
splitWord = separate isSpace
{- Only safe to use when the serialization does not include whitespace. -}
parseList :: Serializable p => ([p] -> a) -> Parser a
parseList mk v = mk <$> mapM deserialize (words v)
{- When a program speaks a simple protocol over stdio, any other output
- to stdout (or anything that attempts to read from stdin)
2015-10-12 20:32:52 +00:00
- will mess up the protocol. To avoid that, close stdin,
- and duplicate stderr to stdout. Return two new handles
- that are duplicates of the original (stdin, stdout). -}
2015-04-03 19:33:28 +00:00
dupIoHandles :: IO (Handle, Handle)
dupIoHandles = do
readh <- hDuplicate stdin
writeh <- hDuplicate stdout
nullh <- openFile devNull ReadMode
nullh `hDuplicateTo` stdin
stderr `hDuplicateTo` stdout
return (readh, writeh)
{- Reads a line, but to avoid super-long lines eating memory, returns
- Nothing if 32 kb have been read without seeing a '\n'
-
- If there is a '\r' before the '\n', it is removed, to support
- systems using "\r\n" at ends of lines
-
- This implementation is not super efficient, but as long as the Handle
- supports buffering, it avoids reading a character at a time at the
- syscall level.
-
- Throws isEOFError when no more input is available.
-}
getProtocolLine :: Handle -> IO (Maybe String)
getProtocolLine h = go (32768 :: Int) []
where
go 0 _ = return Nothing
go n l = do
c <- hGetChar h
if c == '\n'
then return $ Just $ reverse $
case l of
('\r':rest) -> rest
_ -> l
else go (n-1) (c:l)