Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split serialisation from IO #5049

Merged
merged 8 commits into from
Apr 6, 2023
3 changes: 2 additions & 1 deletion cardano-api/cardano-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ library
Cardano.Api.GenesisParameters
Cardano.Api.Hash
Cardano.Api.HasTypeProxy
Cardano.Api.InMode
Cardano.Api.IO
Cardano.Api.IPC
Cardano.Api.IPC.Monad
Cardano.Api.InMode
Cardano.Api.IPC.Version
Cardano.Api.Json
Cardano.Api.Keys.Byron
Expand Down
17 changes: 16 additions & 1 deletion cardano-api/src/Cardano/Api.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ module Cardano.Api (
cardanoEraStyle,
shelleyBasedToCardanoEra,

-- ** IO
OutputFile(..),

writeByteStringFileWithOwnerPermissions,
writeByteStringFile,
writeByteStringOutput,

writeLazyByteStringFileWithOwnerPermissions,
writeLazyByteStringFile,
writeLazyByteStringOutput,

writeTextFileWithOwnerPermissions,
writeTextFile,
writeTextOutput,

-- ** Deprecated
Byron,
Shelley,
Expand Down Expand Up @@ -523,7 +538,6 @@ module Cardano.Api (
deserialiseFromTextEnvelope,
readFileTextEnvelope,
writeFileTextEnvelope,
writeFileTextEnvelopeWithOwnerPermissions,
readTextEnvelopeFromFile,
readTextEnvelopeOfTypeFromFile,

Expand Down Expand Up @@ -800,6 +814,7 @@ import Cardano.Api.GenesisParameters
import Cardano.Api.Hash
import Cardano.Api.HasTypeProxy
import Cardano.Api.InMode
import Cardano.Api.IO
import Cardano.Api.IPC
import Cardano.Api.IPC.Monad
import Cardano.Api.Keys.Byron
Expand Down
161 changes: 161 additions & 0 deletions cardano-api/src/Cardano/Api/IO.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Cardano.Api.IO
( OutputFile(..)

, writeByteStringFileWithOwnerPermissions
, writeByteStringFile
, writeByteStringOutput

, writeLazyByteStringFileWithOwnerPermissions
, writeLazyByteStringFile
, writeLazyByteStringOutput

, writeTextFileWithOwnerPermissions
, writeTextFile
, writeTextOutput

) where

#if !defined(mingw32_HOST_OS)
#define UNIX
#endif

#ifdef UNIX
import Control.Exception (IOException, bracket, bracketOnError, try)
import System.Directory ()
import System.IO (hClose)
import System.Posix.Files (ownerModes, setFdOwnerAndGroup)
import System.Posix.IO (OpenMode (..), closeFd, defaultFileFlags, fdToHandle, openFd)
import System.Posix.User (getRealUserID)
#else
import Control.Exception (bracketOnError)
import System.Directory (removeFile, renameFile)
import System.FilePath (splitFileName, (<.>))
import System.IO (hClose, openTempFile)
#endif

import Cardano.Api.Error (FileError (..))

import Control.Monad.Except (runExceptT)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Except.Extra (handleIOExceptT)
import Data.Aeson.Types (FromJSON, ToJSON)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Lazy as LBSC
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text.IO as Text
import GHC.Generics (Generic)
import System.IO (Handle)

handleFileForWritingWithOwnerPermission
:: FilePath
-> (Handle -> IO ())
-> IO (Either (FileError ()) ())
handleFileForWritingWithOwnerPermission path f = do
#ifdef UNIX
-- On a unix based system, we grab a file descriptor and set ourselves as owner.
-- Since we're holding the file descriptor at this point, we can be sure that
-- what we're about to write to is owned by us if an error didn't occur.
user <- getRealUserID
ownedFile <- try $
-- We only close the FD on error here, otherwise we let it leak out, since
-- it will be immediately turned into a Handle (which will be closed when
-- the Handle is closed)
bracketOnError
(openFd path WriteOnly (Just ownerModes) defaultFileFlags)
closeFd
(\fd -> setFdOwnerAndGroup fd user (-1) >> pure fd)
case ownedFile of
Left (err :: IOException) -> do
pure $ Left $ FileIOError path err
Right fd -> do
bracket
(fdToHandle fd)
hClose
(runExceptT . handleIOExceptT (FileIOError path) . f)
#else
-- On something other than unix, we make a _new_ file, and since we created it,
-- we must own it. We then place it at the target location. Unfortunately this
-- won't work correctly with pseudo-files.
bracketOnError
(openTempFile targetDir $ targetFile <.> "tmp")
(\(tmpPath, h) -> do
hClose h >> removeFile tmpPath
return . Left $ FileErrorTempFile path tmpPath h)
(\(tmpPath, h) -> do
f h
hClose h
renameFile tmpPath path
return $ Right ())
where
(targetDir, targetFile) = splitFileName path
#endif

newtype OutputFile = OutputFile
{ unOutputFile :: FilePath
}
deriving Generic
deriving newtype (Eq, Ord, Show, IsString, ToJSON, FromJSON)

writeByteStringFile :: MonadIO m => FilePath -> ByteString -> m (Either (FileError ()) ())
writeByteStringFile fp bs = runExceptT $
handleIOExceptT (FileIOError fp) $ BS.writeFile fp bs

writeByteStringFileWithOwnerPermissions
:: FilePath
-> BS.ByteString
-> IO (Either (FileError ()) ())
writeByteStringFileWithOwnerPermissions fp bs =
handleFileForWritingWithOwnerPermission fp $ \h ->
BS.hPut h bs

writeByteStringOutput :: MonadIO m => Maybe FilePath -> ByteString -> m (Either (FileError ()) ())
Copy link
Contributor

@carbolymer carbolymer Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first argument is a bit unclear, it takes a moment to understand that Nothing means stdout. Maybe encoding that information in the type would be better? For example:

data OutputFileDescriptor
  = OutputFile FilePath
  | OutputStream Handle

where Handle can be stdout or stderr (which doesn't really matter here).

Consequently, if you open a handle to a file path, I think the other functions, which then could operate on Handles, could be unified and simplified I think (instead of matching Maybe in every one of them).

Copy link
Contributor Author

@newhoggy newhoggy Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There have been multiple attempts to do such a thing, but they've been piecemeal.

The purpose of this PR is not to introduce such an abstraction. We already have code that uses Maybe FilePath and keeping this code to that convention minimises difference.

We may introduce an appropriate abstraction in a separate PR and do it across the entire codebase to handle consistently.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may introduce an appropriate abstraction in a separate PR and do it across the entire codebase to handle consistently.

@carbolymer this would be a good issue to create and tackle next.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created: #5062

writeByteStringOutput mOutput bs = runExceptT $
case mOutput of
Just fp -> handleIOExceptT (FileIOError fp) $ BS.writeFile fp bs
Nothing -> liftIO $ BSC.putStr bs

writeLazyByteStringFile :: MonadIO m => FilePath -> LBS.ByteString -> m (Either (FileError ()) ())
writeLazyByteStringFile fp bs = runExceptT $
handleIOExceptT (FileIOError fp) $ LBS.writeFile fp bs

writeLazyByteStringFileWithOwnerPermissions
:: FilePath
-> LBS.ByteString
-> IO (Either (FileError ()) ())
writeLazyByteStringFileWithOwnerPermissions fp lbs =
handleFileForWritingWithOwnerPermission fp $ \h ->
LBS.hPut h lbs

writeLazyByteStringOutput :: MonadIO m => Maybe FilePath -> LBS.ByteString -> m (Either (FileError ()) ())
writeLazyByteStringOutput mOutput bs = runExceptT $
case mOutput of
Just fp -> handleIOExceptT (FileIOError fp) $ LBS.writeFile fp bs
Nothing -> liftIO $ LBSC.putStr bs

writeTextFile :: MonadIO m => FilePath -> Text -> m (Either (FileError ()) ())
writeTextFile fp t = runExceptT $
handleIOExceptT (FileIOError fp) $ Text.writeFile fp t

writeTextFileWithOwnerPermissions
:: FilePath
-> Text
-> IO (Either (FileError ()) ())
writeTextFileWithOwnerPermissions fp t =
handleFileForWritingWithOwnerPermission fp $ \h ->
Text.hPutStr h t

writeTextOutput :: MonadIO m => Maybe FilePath -> Text -> m (Either (FileError ()) ())
writeTextOutput mOutput t = runExceptT $
case mOutput of
Just fp -> handleIOExceptT (FileIOError fp) $ Text.writeFile fp t
Nothing -> liftIO $ Text.putStr t
87 changes: 3 additions & 84 deletions cardano-api/src/Cardano/Api/SerialiseTextEnvelope.hs
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

#if !defined(mingw32_HOST_OS)
#define UNIX
#endif

-- | TextEnvelope Serialisation
--
module Cardano.Api.SerialiseTextEnvelope
Expand All @@ -23,7 +18,6 @@ module Cardano.Api.SerialiseTextEnvelope
, deserialiseFromTextEnvelope
, readFileTextEnvelope
, writeFileTextEnvelope
, writeFileTextEnvelopeWithOwnerPermissions
, readTextEnvelopeFromFile
, readTextEnvelopeOfTypeFromFile
, textEnvelopeToJSON
Expand Down Expand Up @@ -60,24 +54,10 @@ import Cardano.Binary (DecoderError)

import Cardano.Api.Error
import Cardano.Api.HasTypeProxy
import Cardano.Api.IO
import Cardano.Api.SerialiseCBOR
import Cardano.Api.Utils (readFileBlocking)

#ifdef UNIX
import Control.Exception (IOException, bracket, bracketOnError, try)
import System.Directory ()
import System.IO (hClose)
import System.Posix.Files (ownerModes, setFdOwnerAndGroup)
import System.Posix.IO (OpenMode (..), closeFd, defaultFileFlags, fdToHandle, openFd)
import System.Posix.User (getRealUserID)
#else
import Control.Exception (bracketOnError)
import System.Directory (removeFile, renameFile)
import System.FilePath (splitFileName, (<.>))
import System.IO (hClose, openTempFile)
#endif


-- ----------------------------------------------------------------------------
-- Text envelopes
--
Expand Down Expand Up @@ -226,74 +206,13 @@ deserialiseFromTextEnvelopeAnyOf types te =

matching (FromSomeType ttoken _f) = actualType == textEnvelopeType ttoken

writeFileWithOwnerPermissions
:: FilePath
-> LBS.ByteString
-> IO (Either (FileError ()) ())
#ifdef UNIX
-- On a unix based system, we grab a file descriptor and set ourselves as owner.
-- Since we're holding the file descriptor at this point, we can be sure that
-- what we're about to write to is owned by us if an error didn't occur.
writeFileWithOwnerPermissions path a = do
user <- getRealUserID
ownedFile <- try $
-- We only close the FD on error here, otherwise we let it leak out, since
-- it will be immediately turned into a Handle (which will be closed when
-- the Handle is closed)
bracketOnError
(openFd path WriteOnly (Just ownerModes) defaultFileFlags)
closeFd
(\fd -> setFdOwnerAndGroup fd user (-1) >> pure fd)
case ownedFile of
Left (err :: IOException) -> do
pure $ Left $ FileIOError path err
Right fd -> do
bracket
(fdToHandle fd)
hClose
(\handle -> runExceptT $ handleIOExceptT (FileIOError path) $ LBS.hPut handle a)
#else
-- On something other than unix, we make a _new_ file, and since we created it,
-- we must own it. We then place it at the target location. Unfortunately this
-- won't work correctly with pseudo-files.
writeFileWithOwnerPermissions targetPath a =
bracketOnError
(openTempFile targetDir $ targetFile <.> "tmp")
(\(tmpPath, fHandle) -> do
hClose fHandle >> removeFile tmpPath
return . Left $ FileErrorTempFile targetPath tmpPath fHandle)
(\(tmpPath, fHandle) -> do
LBS.hPut fHandle a
hClose fHandle
renameFile tmpPath targetPath
return $ Right ())
where
(targetDir, targetFile) = splitFileName targetPath
#endif

writeFileTextEnvelope :: HasTextEnvelope a
=> FilePath
-> Maybe TextEnvelopeDescr
-> a
-> IO (Either (FileError ()) ())
writeFileTextEnvelope path mbDescr a =
runExceptT $ do
handleIOExceptT (FileIOError path) $ LBS.writeFile path content
where
content = textEnvelopeToJSON mbDescr a


writeFileTextEnvelopeWithOwnerPermissions
:: HasTextEnvelope a
=> FilePath
-> Maybe TextEnvelopeDescr
-> a
-> IO (Either (FileError ()) ())
writeFileTextEnvelopeWithOwnerPermissions targetPath mbDescr a =
writeFileWithOwnerPermissions targetPath content
where
content = textEnvelopeToJSON mbDescr a

writeFileTextEnvelope outputFile mbDescr a =
writeLazyByteStringFile outputFile (textEnvelopeToJSON mbDescr a)

textEnvelopeToJSON :: HasTextEnvelope a => Maybe TextEnvelopeDescr -> a -> LBS.ByteString
textEnvelopeToJSON mbDescr a =
Expand Down
5 changes: 0 additions & 5 deletions cardano-cli/src/Cardano/CLI/Shelley/Commands.hs
Original file line number Diff line number Diff line change
Expand Up @@ -514,11 +514,6 @@ data MetadataFile = MetadataFileJSON FilePath

deriving Show

newtype OutputFile = OutputFile
{ unOutputFile :: FilePath
}
deriving Show

newtype PoolMetadataFile = PoolMetadataFile
{ unPoolMetadataFile :: FilePath }
deriving Show
Expand Down
6 changes: 3 additions & 3 deletions cardano-cli/src/Cardano/CLI/Shelley/Run/Address.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Cardano.CLI.Shelley.Key (PaymentVerifier (..), StakeIdentifier
StakeVerifier (..), VerificationKeyTextOrFile,
VerificationKeyTextOrFileError (..), generateKeyPair, readVerificationKeyOrFile,
readVerificationKeyTextOrFileAnyOf, renderVerificationKeyTextOrFileError)
import Cardano.CLI.Shelley.Parsers (AddressCmd (..), AddressKeyType (..), OutputFile (..))
import Cardano.CLI.Shelley.Parsers (AddressCmd (..), AddressKeyType (..))
import Cardano.CLI.Shelley.Run.Address.Info (ShelleyAddressInfoError, runAddressInfo)
import Cardano.CLI.Shelley.Run.Read
import Cardano.CLI.Types
Expand Down Expand Up @@ -91,8 +91,8 @@ writePaymentKeyFiles
-> ExceptT ShelleyAddressCmdError IO ()
writePaymentKeyFiles (VerificationKeyFile vkeyPath) (SigningKeyFile skeyPath) vkey skey = do
firstExceptT ShelleyAddressCmdWriteFileError $ do
newExceptT $ writeFileTextEnvelope skeyPath (Just skeyDesc) skey
newExceptT $ writeFileTextEnvelope vkeyPath (Just vkeyDesc) vkey
newExceptT $ writeLazyByteStringFile skeyPath $ textEnvelopeToJSON (Just skeyDesc) skey
newExceptT $ writeLazyByteStringFile vkeyPath $ textEnvelopeToJSON (Just vkeyDesc) vkey
where
skeyDesc, vkeyDesc :: TextEnvelopeDescr
skeyDesc = "Payment Signing Key"
Expand Down
1 change: 0 additions & 1 deletion cardano-cli/src/Cardano/CLI/Shelley/Run/Address/Info.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module Cardano.CLI.Shelley.Run.Address.Info
) where

import Cardano.Api
import Cardano.CLI.Shelley.Parsers (OutputFile (..))

import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Except (ExceptT)
Expand Down
Loading