funflow-1.0.0: src/Control/Funflow/Steps.hs
{-# LANGUAGE Arrows #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
-- | Miscallaneous steps to form part of Funflow computations.
module Control.Funflow.Steps
( -- * Error handling
retry
-- * Store manipulation
, assignAliasInStore
, copyDirToStore
, copyFileToStore
, listDirContents
, lookupAliasInStore
, mergeDirs
, mergeFiles
, putInStoreAt
, readString
, readString_
, readYaml
, writeExecutableString
, writeString
, writeString_
, writeYaml
, writeYaml_
-- * Docker
, docker
-- * Testing and debugging
, promptFor
, printS
, failStep
, worstBernoulli
, pauseWith
, melancholicLazarus
)
where
import Control.Arrow
import Control.Arrow.Free (catch)
import Control.Exception.Safe (Exception, throwM)
import Control.Funflow.Base (SimpleFlow)
import Control.Funflow.Class
import Control.Funflow.ContentHashable (ContentHashable,
DirectoryContent (..),
FileContent (..))
import Control.Funflow.ContentStore (Content ((:</>)))
import qualified Control.Funflow.ContentStore as CS
import qualified Control.Funflow.External.Docker as Docker
import Data.Foldable (for_)
import Data.Store
import Data.Traversable (for)
import Data.Typeable (Typeable)
import qualified Data.Yaml as Yaml
import GHC.Conc (threadDelay)
import Path
import Path.IO
import System.Posix.Files (accessModes, createLink,
setFileMode)
import System.Random
promptFor :: (Read a, ArrowFlow eff ex arr) => arr String a
promptFor = proc s -> do
() <- stepIO putStr -< (s++"> ")
s' <- stepIO (const getLine) -< ()
returnA -< read s'
printS :: (Show a, ArrowFlow eff ex arr) => arr a ()
printS = stepIO $ \s-> print s
failStep :: ArrowFlow eff ex arr => arr () ()
failStep = stepIO $ \_ -> fail "failStep"
worstBernoulli :: (Exception ex, ArrowFlow eff ex arr) => (String -> ex) -> arr Double Double
worstBernoulli errorC = stepIO $ \p -> do
r <- randomRIO (0,1)
if r < p
then return r
else throwM . errorC $ "worstBernoulli fail with "++ show r++ " > "++show p
-- | pause for a given number of seconds. Thread through a value to ensure
-- delay does not happen inparallel with other processing
pauseWith :: (Store a, ArrowFlow eff ex arr) => arr (Int, a) a
pauseWith = stepIO $ \(secs,a) -> do
threadDelay (secs*1000000)
return a
-- | on first invocation die and leave a suicide note
-- on second invocation it is resurrected and destroys suicide note, returning contents
melancholicLazarus :: ArrowFlow eff ex arr => arr String String
melancholicLazarus = stepIO $ \s -> do
let fnm = [absfile|/tmp/lazarus_note|]
ex <- doesFileExist fnm
if ex
then do s1 <- readFile (fromAbsFile fnm)
removeFile fnm
return s1
else do writeFile (fromAbsFile fnm) s
fail "lazarus fail"
-- | `retry n s f` reruns `f` on failure at most n times with a delay of `s`
-- seconds between retries
retry :: forall arr eff ex a b. (Exception ex, Store a, ArrowFlow eff ex arr)
=> Int -> Int -> arr a b -> arr a b
retry 0 _ f = f
retry n secs f = catch f $ proc (x, _ :: ex) -> do
x1 <- pauseWith -< (secs,x)
x2 <- retry (n-1) secs f -< x1
returnA -< x2
lookupAliasInStore :: ArrowFlow eff ex arr => arr CS.Alias (Maybe CS.Item)
lookupAliasInStore = internalManipulateStore CS.lookupAlias
assignAliasInStore :: ArrowFlow eff ex arr => arr (CS.Alias, CS.Item) ()
assignAliasInStore = internalManipulateStore $ \store (alias, item) ->
CS.assignAlias store alias item
putInStoreAt :: (ContentHashable IO a, Typeable t, ArrowFlow eff ex arr)
=> (Path Abs t -> a -> IO ()) -> arr (a, Path Rel t) (CS.Content t)
putInStoreAt f = proc (a, p) -> do
item <- putInStore (\d (a, p) -> do
createDirIfMissing True (parent $ d </> p)
f (d </> p) a
) -< (a, p)
returnA -< item :</> p
-- | @copyFileToStore (fIn, fOut)@ copies the contents of @fIn@ into the store
-- under the relative path @fOut@ within the subtree.
copyFileToStore :: ArrowFlow eff ex arr => arr (FileContent, Path Rel File) (CS.Content File)
copyFileToStore = putInStoreAt $ \p (FileContent inFP) -> copyFile inFP p
-- | @copyDirToStore (dIn, Nothing)@ copies the contents of @dIn@ into the store
-- right under the subtree.
--
-- | @copyDirToStore (dIn, Just dOut)@ copies the contents of @dIn@ into the store
-- under relative path @dOut@ within the subtree
copyDirToStore :: ArrowFlow eff ex arr => arr (DirectoryContent, Maybe (Path Rel Dir)) (CS.Content Dir)
copyDirToStore = proc (inDir, mbOutDir) ->
case mbOutDir of
Nothing -> do
item <- putInStore (\d (DirectoryContent inDir) ->
copyDirRecur inDir d
) -< inDir
returnA -< CS.All item
Just outDir ->
putInStoreAt (\p (DirectoryContent inDir) ->
copyDirRecur inDir p
) -< (inDir, outDir)
-- | List the contents of a directory within the store
listDirContents :: ArrowFlow eff ex arr => arr (CS.Content Dir)
([CS.Content Dir], [CS.Content File])
listDirContents = internalManipulateStore
( \store dir -> let
item = CS.contentItem dir
itemRoot = CS.itemPath store item
in do
(dirs, files) <- listDir $ CS.contentPath store dir
relDirs <- for dirs (stripProperPrefix itemRoot)
relFiles <- for files (stripProperPrefix itemRoot)
return ( (item :</>) <$> relDirs
, (item :</>) <$> relFiles
)
)
-- | Merge a number of store directories together into a single output directory.
-- This uses hardlinks to avoid duplicating the data on disk.
mergeDirs :: ArrowFlow eff ex arr => arr [CS.Content Dir] (CS.Content Dir)
mergeDirs = proc inDirs -> do
paths <- internalManipulateStore
( \store items -> return $ CS.contentPath store <$> items) -< inDirs
arr CS.All <<< putInStore
( \d inDirs -> for_ inDirs $ \inDir -> do
(subDirs, files) <- listDirRecur inDir
for_ subDirs $ \absSubDir -> do
relSubDir <- stripProperPrefix inDir absSubDir
createDirIfMissing True (d </> relSubDir)
for_ files $ \absFile -> do
relFile <- stripProperPrefix inDir absFile
createLink (toFilePath absFile) (toFilePath $ d </> relFile)
) -< paths
-- | Merge a number of files into a single output directory.
mergeFiles :: ArrowFlow eff ex arr => arr [CS.Content File] (CS.Content Dir)
mergeFiles = proc inFiles -> do
absFiles <- internalManipulateStore
( \store items -> return $ CS.contentPath store <$> items) -< inFiles
arr CS.All <<< putInStore
(\d inFiles -> for_ inFiles $ \inFile ->
createLink (toFilePath inFile) (toFilePath $ d </> filename inFile)
) -< absFiles
-- | Read the contents of the given file in the store.
readString :: ArrowFlow eff ex arr => arr (CS.Content File) String
readString = getFromStore $ readFile . fromAbsFile
-- | Read the contents of a file named @out@ within the given item.
readString_ :: ArrowFlow eff ex arr => arr CS.Item String
readString_ = arr (:</> [relfile|out|]) >>> readString
-- | Create and write into a file under the given path in the store.
writeString :: ArrowFlow eff ex arr => arr (String, Path Rel File) (CS.Content File)
writeString = putInStoreAt $ writeFile . fromAbsFile
writeExecutableString :: ArrowFlow eff ex arr => arr (String, Path Rel File) (CS.Content File)
writeExecutableString = putInStoreAt $ \p i -> do
writeFile (fromAbsFile p) i
setFileMode (fromAbsFile p) accessModes
-- | Create and write into a file named @out@ within the given item.
writeString_ :: ArrowFlow eff ex arr => arr String (CS.Content File)
writeString_ = Control.Funflow.Steps.writeString <<< arr (, [relfile|out|])
-- | Read a YAML file from the given file in the store.
readYaml :: Yaml.FromJSON a
=> SimpleFlow (CS.Content File) (Either Yaml.ParseException a)
readYaml = getFromStore (Yaml.decodeFileEither . fromAbsFile)
-- | Write a YAML file under the given name to the store.
writeYaml :: (ContentHashable IO a, Yaml.ToJSON a)
=> SimpleFlow (a, Path Rel File) (CS.Content File)
writeYaml = putInStoreAt $ Yaml.encodeFile . fromAbsFile
-- | Write a YAML file named @out.yaml@ to the store.
writeYaml_ :: (ContentHashable IO a, Yaml.ToJSON a)
=> SimpleFlow a (CS.Content File)
writeYaml_ = writeYaml <<< arr (, [relfile|out.yaml|])
docker :: (ContentHashable IO a, ArrowFlow eff ex arr) => (a -> Docker.Config) -> arr a CS.Item
docker f = external $ Docker.toExternal . f