git-annex/Backend/External.hs
Joey Hess 5f4228dc2b
types for async protocol extension
renamed AsyncMessage to ExceptionalMessage to make way for this new
extension.
2020-08-12 12:04:12 -04:00

372 lines
13 KiB
Haskell

{- git-annex external backend
-
- Copyright 2020 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE TypeSynonymInstances #-}
module Backend.External (makeBackend) where
import Annex.Common
import Annex.ExternalAddonProcess
import Backend.Utilities
import Types.Key
import Types.Backend
import Types.KeySource
import Utility.Metered
import qualified Utility.SimpleProtocol as Proto
import qualified Data.ByteString as S
import qualified Data.Map.Strict as M
import Data.Char
import Control.Concurrent
import System.IO.Unsafe (unsafePerformIO)
import System.Log.Logger (debugM)
newtype ExternalBackendName = ExternalBackendName S.ByteString
deriving (Show, Eq, Ord)
-- Makes Backend representing an external backend of any type.
-- If the program is not available or doesn't work, makes a Backend
-- that cannot generate or verify keys, but that still lets the keys be
-- basically used.
makeBackend :: S.ByteString -> HasExt -> Annex Backend
makeBackend bname hasext =
withExternalState ebname hasext (return . externalBackend)
where
ebname = ExternalBackendName bname
makeBackend' :: ExternalBackendName -> HasExt -> Either ExternalAddonStartError ExternalAddonProcess -> Annex Backend
makeBackend' ebname@(ExternalBackendName bname) hasext (Right p) = do
let st = ExternalState
{ externalAddonProcess = Right p
, externalBackend = unavailBackend ebname hasext
}
canverify <- handleRequest st CANVERIFY (pure False) $ \case
CANVERIFY_YES -> result True
CANVERIFY_NO -> result False
_ -> Nothing
isstable <- handleRequest st ISSTABLE (pure False) $ \case
ISSTABLE_YES -> result True
ISSTABLE_NO -> result False
_ -> Nothing
iscryptographicallysecure <- handleRequest st ISCRYPTOGRAPHICALLYSECURE (pure False) $ \case
ISCRYPTOGRAPHICALLYSECURE_YES -> result True
ISCRYPTOGRAPHICALLYSECURE_NO -> result False
_ -> Nothing
return $ Backend
{ backendVariety = ExternalKey bname hasext
, genKey = Just $ genKeyExternal ebname hasext
, verifyKeyContent = if canverify
then Just $ verifyKeyContentExternal ebname hasext
-- The protocol supports PROGRESS here,
-- but it's not actually used. It was put
-- in to avoid needing a protocol version
-- bump if progress handling is later added.
nullMeterUpdate
else Nothing
, canUpgradeKey = Nothing
, fastMigrate = Nothing
, isStableKey = const isstable
, isCryptographicallySecure = const iscryptographicallysecure
}
makeBackend' ebname hasext (Left _) = return $ unavailBackend ebname hasext
unavailBackend :: ExternalBackendName -> HasExt -> Backend
unavailBackend (ExternalBackendName bname) hasext =
Backend
{ backendVariety = ExternalKey bname hasext
, genKey = Nothing
, verifyKeyContent = Nothing
, canUpgradeKey = Nothing
, fastMigrate = Nothing
, isStableKey = const False
, isCryptographicallySecure = const False
}
genKeyExternal :: ExternalBackendName -> HasExt -> KeySource -> MeterUpdate -> Annex Key
genKeyExternal ebname hasext ks meterupdate =
withExternalState ebname hasext $ \st ->
handleRequest st req notavail go
where
req = GENKEY (fromRawFilePath (contentLocation ks))
notavail = giveup $ "Cannot generate a key, since " ++ externalBackendProgram ebname ++ " is not available."
go (GENKEY_SUCCESS pk) = Just $ Result <$> fromProtoKey pk hasext ks
go (GENKEY_FAILURE msg) = Just $ giveup $
"External backend program failed to generate a key: " ++ msg
go (PROGRESS bytesprocessed) = Just $ do
liftIO $ meterupdate bytesprocessed
return $ GetNextMessage go
go _ = Nothing
verifyKeyContentExternal :: ExternalBackendName -> HasExt -> MeterUpdate -> Key -> FilePath -> Annex Bool
verifyKeyContentExternal ebname hasext meterupdate k f =
withExternalState ebname hasext $ \st ->
handleRequest st req notavail go
where
req = VERIFYKEYCONTENT (toProtoKey k) f
-- This should not be able to happen, because CANVERIFY is checked
-- before this function is enable, and so the external program
-- is available. But if it does, fail the verification.
notavail = return False
go VERIFYKEYCONTENT_SUCCESS = result True
go VERIFYKEYCONTENT_FAILURE = result False
go (PROGRESS bytesprocessed) = Just $ do
liftIO $ meterupdate bytesprocessed
return $ GetNextMessage go
go _ = Nothing
-- State about a running external backend program.
data ExternalState = ExternalState
{ externalAddonProcess :: Either ExternalAddonStartError ExternalAddonProcess
, externalBackend :: Backend
}
handleRequest :: ExternalState -> Request -> Annex a -> ResponseHandler a -> Annex a
handleRequest st req whenunavail responsehandler =
withExternalAddon st whenunavail $ \p -> do
sendMessage p req
let loop = receiveResponse p responsehandler
(Just . handleExceptionalMessage loop)
loop
where
handleExceptionalMessage _ (ERROR err) = do
warning ("external special remote error: " ++ err)
whenunavail
handleExceptionalMessage loop (DEBUG msg) = do
liftIO $ debugM "external" msg
loop
withExternalAddon :: ExternalState -> a -> (ExternalAddonProcess -> a) -> a
withExternalAddon st whenunavail a = case externalAddonProcess st of
Right addon -> a addon
Left _ -> whenunavail
sendMessage :: Proto.Sendable m => ExternalAddonProcess -> m -> Annex ()
sendMessage p m = liftIO $ do
protocolDebug p True line
hPutStrLn (externalSend p) line
hFlush (externalSend p)
where
line = unwords $ Proto.formatMessage m
{- A response handler can yeild a result, or it can request that another
- message be consumed from the external. -}
data ResponseHandlerResult a
= Result a
| GetNextMessage (ResponseHandler a)
type ResponseHandler a = Response -> Maybe (Annex (ResponseHandlerResult a))
result :: a -> Maybe (Annex (ResponseHandlerResult a))
result = Just . return . Result
{- Waits for a message from the external backend, and passes it to the
- apppropriate handler.
-
- If the handler returns Nothing, this is a protocol error.
-}
receiveResponse
:: ExternalAddonProcess
-> ResponseHandler a
-> (ExceptionalMessage -> Maybe (Annex a))
-> Annex a
receiveResponse p handleresponse handleexceptional =
go =<< liftIO (catchMaybeIO $ hGetLine $ externalReceive p)
where
go Nothing = protocolError False ""
go (Just s) = do
liftIO $ protocolDebug p False s
case Proto.parseMessage s :: Maybe Response of
Just resp -> case handleresponse resp of
Nothing -> protocolError True s
Just callback -> callback >>= \case
Result a -> return a
GetNextMessage handleresponse' ->
receiveResponse p handleresponse' handleexceptional
Nothing -> case Proto.parseMessage s :: Maybe ExceptionalMessage of
Just msg -> maybe (protocolError True s) id (handleexceptional msg)
Nothing -> protocolError False s
protocolError parsed s = giveup $ "external backend protocol error, unexpectedly received \"" ++ s ++ "\" " ++
if parsed
then "(message not allowed at this time)"
else "(unable to parse message)"
-- Information about pools of of running external backends that are
-- available to use is stored in this global.
{-# NOINLINE poolVar #-}
poolVar :: MVar (M.Map ExternalBackendName (ExternalAddonPID, [ExternalState]))
poolVar = unsafePerformIO $ newMVar M.empty
-- Starts a new instance of an external backend.
-- Does not add it to the poolVar; caller should add it once it's done
-- using it.
newExternalState :: ExternalBackendName -> HasExt -> ExternalAddonPID -> Annex ExternalState
newExternalState ebname hasext pid = do
st <- startExternalAddonProcess basecmd pid
st' <- case st of
Left (ProgramNotInstalled msg) -> warnonce msg >> return st
Left (ProgramFailure msg) -> warnonce msg >> return st
Right p -> do
sendMessage p GETVERSION
v <- receiveResponse p
(\resp -> case resp of
VERSION v -> result v
_ -> Nothing
)
(const Nothing)
if v `notElem` supportedProtocolVersions
then do
warnonce (basecmd ++ " uses an unsupported version of the external backend protocol")
return $ Left (ProgramFailure "bad protocol version")
else return (Right p)
backend <- makeBackend' ebname hasext st'
return $ ExternalState
{ externalAddonProcess = st'
, externalBackend = backend
}
where
basecmd = externalBackendProgram ebname
warnonce msg = when (pid == 1) $
warning msg
externalBackendProgram :: ExternalBackendName -> String
externalBackendProgram (ExternalBackendName bname) = "git-annex-backend-X" ++ decodeBS' bname
-- Runs an action with an ExternalState, starting a new external backend
-- process if necessary. It is returned to the pool once the action
-- finishes successfully. On exception, it's shut down.
withExternalState :: ExternalBackendName -> HasExt -> (ExternalState -> Annex a) -> Annex a
withExternalState bname hasext a = do
st <- get
r <- a st `onException` shutdown st
put st -- only when no exception is thrown
return r
where
get = do
m <- liftIO $ takeMVar poolVar
case fromMaybe (1, []) (M.lookup bname m) of
(pid, []) -> do
let m' = M.insert bname (succ pid, []) m
liftIO $ putMVar poolVar m'
newExternalState bname hasext pid
(pid, (st:rest)) -> do
let m' = M.insert bname (pid, rest) m
liftIO $ putMVar poolVar m'
return st
put st = liftIO $ modifyMVar_ poolVar $
pure . M.adjust (\(pid, l) -> (pid, st:l)) bname
shutdown st = liftIO $
withExternalAddon st noop (flip externalShutdown False)
-- This is a key as seen by the protocol consumer. When the "E" variant
-- of the external backend is in use, it does not include an extension.
-- And it's assumed not to contain spaces or newlines, or anything besides
-- ascii alphanumerics, because the protocol does not allow keys containing
-- such things.
newtype ProtoKey = ProtoKey Key
deriving (Show)
fromProtoKey :: ProtoKey -> HasExt -> KeySource -> Annex Key
fromProtoKey (ProtoKey k) (HasExt False) _ = pure k
fromProtoKey (ProtoKey k) hasext@(HasExt True) source =
addE source (setHasExt hasext) k
toProtoKey :: Key -> ProtoKey
toProtoKey k = ProtoKey $ alterKey k $ \d -> d
-- The extension can be easily removed, because the protocol
-- documentation does not allow '.' to be used in the keyName,
-- so the first one is the extension.
{ keyName = S.takeWhile (/= dot) (keyName d)
, keyVariety = setHasExt (HasExt False) (keyVariety d)
}
where
dot = fromIntegral (ord '.')
setHasExt :: HasExt -> KeyVariety -> KeyVariety
setHasExt hasext (ExternalKey name _) = ExternalKey name hasext
setHasExt _ v = v
instance Proto.Serializable ProtoKey where
serialize (ProtoKey k) = Proto.serialize k
deserialize = fmap ProtoKey . Proto.deserialize
data Request
= GETVERSION
| CANVERIFY
| ISSTABLE
| ISCRYPTOGRAPHICALLYSECURE
| GENKEY FilePath
| VERIFYKEYCONTENT ProtoKey FilePath
deriving (Show)
data Response
= VERSION ProtocolVersion
| CANVERIFY_YES
| CANVERIFY_NO
| ISSTABLE_YES
| ISSTABLE_NO
| ISCRYPTOGRAPHICALLYSECURE_YES
| ISCRYPTOGRAPHICALLYSECURE_NO
| GENKEY_SUCCESS ProtoKey
| GENKEY_FAILURE ErrorMsg
| VERIFYKEYCONTENT_SUCCESS
| VERIFYKEYCONTENT_FAILURE
| PROGRESS BytesProcessed
deriving (Show)
data ExceptionalMessage
= ERROR ErrorMsg
| DEBUG String
deriving (Show)
type ErrorMsg = String
newtype ProtocolVersion = ProtocolVersion Int
deriving (Show, Eq)
supportedProtocolVersions :: [ProtocolVersion]
supportedProtocolVersions = [ProtocolVersion 1]
instance Proto.Serializable ProtocolVersion where
serialize (ProtocolVersion n) = show n
deserialize = ProtocolVersion <$$> readish
instance Proto.Sendable ExceptionalMessage where
formatMessage (ERROR err) = ["ERROR", Proto.serialize err]
formatMessage (DEBUG msg) = ["DEBUG", Proto.serialize msg]
instance Proto.Receivable ExceptionalMessage where
parseCommand "ERROR" = Proto.parse1 ERROR
parseCommand "DEBUG" = Proto.parse1 DEBUG
parseCommand _ = Proto.parseFail
instance Proto.Sendable Request where
formatMessage GETVERSION = ["GETVERSION"]
formatMessage CANVERIFY = ["CANVERIFY"]
formatMessage ISSTABLE = ["ISSTABLE"]
formatMessage ISCRYPTOGRAPHICALLYSECURE = ["ISCRYPTOGRAPHICALLYSECURE"]
formatMessage (GENKEY file) = ["GENKEY", Proto.serialize file]
formatMessage (VERIFYKEYCONTENT key file) =
["VERIFYKEYCONTENT", Proto.serialize key, Proto.serialize file]
instance Proto.Receivable Response where
parseCommand "VERSION" = Proto.parse1 VERSION
parseCommand "CANVERIFY-YES" = Proto.parse0 CANVERIFY_YES
parseCommand "CANVERIFY-NO" = Proto.parse0 CANVERIFY_NO
parseCommand "ISSTABLE-YES" = Proto.parse0 ISSTABLE_YES
parseCommand "ISSTABLE-NO" = Proto.parse0 ISSTABLE_NO
parseCommand "ISCRYPTOGRAPHICALLYSECURE-YES" = Proto.parse0 ISCRYPTOGRAPHICALLYSECURE_YES
parseCommand "ISCRYPTOGRAPHICALLYSECURE-NO" = Proto.parse0 ISCRYPTOGRAPHICALLYSECURE_NO
parseCommand "GENKEY-SUCCESS" = Proto.parse1 GENKEY_SUCCESS
parseCommand "GENKEY-FAILURE" = Proto.parse1 GENKEY_FAILURE
parseCommand "VERIFYKEYCONTENT-SUCCESS" = Proto.parse0 VERIFYKEYCONTENT_SUCCESS
parseCommand "VERIFYKEYCONTENT-FAILURE" = Proto.parse0 VERIFYKEYCONTENT_FAILURE
parseCommand "PROGRESS" = Proto.parse1 PROGRESS
parseCommand _ = Proto.parseFail