tmp-postgres-0.1.1.1: src/Database/Postgres/Temp/Internal.hs
{-# LANGUAGE RecordWildCards, LambdaCase, ScopedTypeVariables, DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
module Database.Postgres.Temp.Internal where
import System.IO.Temp
import System.Process
import System.Process.Internals
import Control.Concurrent
import System.IO
import System.Exit
import System.Directory
import Network.Socket
import Control.Exception
import Data.Typeable
import GHC.Generics
import System.Posix.Signals
import qualified Database.PostgreSQL.Simple as SQL
import qualified Data.ByteString.Char8 as BSC
openFreePort :: IO Int
openFreePort = bracket (socket AF_INET Stream defaultProtocol) close $ \s -> do
localhost <- inet_addr "127.0.0.1"
bind s (SockAddrInet aNY_PORT localhost)
listen s 1
fmap fromIntegral $ socketPort s
waitForDB :: String -> IO ()
waitForDB connStr = do
eresult <- try $ bracket (SQL.connectPostgreSQL (BSC.pack connStr)) SQL.close $ \_ -> return ()
case eresult of
Left (_ :: IOError) -> threadDelay 10000 >> waitForDB connStr
Right _ -> return ()
data DB = DB
{ mainDir :: FilePath
-- ^ Temporary directory where the unix socket, logs and data directory live.
, connectionString :: String
-- ^ PostgreSQL connection string.
, pid :: ProcessHandle
-- ^ The process handle for the @postgres@ process.
}
data SocketClass = Localhost | Unix
deriving (Show, Eq, Read, Ord, Enum, Bounded, Generic, Typeable)
-- | start postgres and use the current processes stdout and stderr
start :: [(String, String)]
-- ^ Extra options which override the defaults
-> IO (Either StartError DB)
start options = startWithHandles Unix options stdout stderr
-- | start postgres and use the current processes stdout and stderr
-- but use TCP on localhost instead of a unix socket.
startLocalhost :: [(String, String)]
-- ^ Extra options which override the defaults
-> IO (Either StartError DB)
startLocalhost options = startWithHandles Localhost options stdout stderr
fourth :: (a, b, c, d) -> d
fourth (_, _, _, x) = x
procWith :: Handle -> Handle -> String -> [String] -> CreateProcess
procWith stdOut stdErr cmd args =
(proc cmd args)
{ std_err = UseHandle stdErr
, std_out = UseHandle stdOut
}
config :: Maybe FilePath -> String
config mMainDir = unlines $
[ "shared_buffers = 12MB"
, "fsync = off"
, "synchronous_commit = off"
, "full_page_writes = off"
, "log_min_duration_statement = 0"
, "log_connections = on"
, "log_disconnections = on"
, "client_min_messages = ERROR"
] ++ maybe ["listen_addresses = '127.0.0.1'"] (\x -> ["unix_socket_directories = '" ++ x ++ "'", "listen_addresses = ''"]) mMainDir
data StartError
= InitDBFailed ExitCode
| CreateDBFailed ExitCode
deriving (Show, Eq, Typeable)
instance Exception StartError
throwIfError :: (ExitCode -> StartError) -> ExitCode -> IO ()
throwIfError f e = case e of
ExitSuccess -> return ()
_ -> throwIO $ f e
pidString :: ProcessHandle -> IO String
pidString phandle = withProcessHandle phandle (\case
OpenHandle p -> return $ show p
ClosedHandle _ -> return ""
)
runProcessWith :: Handle -> Handle -> String -> String -> [String] -> IO ExitCode
runProcessWith stdOut stdErr name cmd args
= createProcess_ name (procWith stdOut stdErr cmd args)
>>= waitForProcess . fourth
-- | Start postgres and pass in handles for stdout and stderr
startWithHandles :: SocketClass
-> [(String, String)]
-- ^ Extra options which override the defaults
-> Handle
-- ^ @stdout@
-> Handle
-- ^ @stderr@
-> IO (Either StartError DB)
startWithHandles socketClass options stdOut stdErr = do
mainDir <- createTempDirectory "/tmp" "tmp-postgres"
startWithHandlesAndDir socketClass options mainDir stdOut stdErr
startWithHandlesAndDir :: SocketClass
-> [(String, String)]
-> FilePath
-> Handle
-> Handle
-> IO (Either StartError DB)
startWithHandlesAndDir = startWithLogger $ \_ -> return ()
data Event
= InitDB
| WriteConfig
| FreePort
| StartPostgres
| WaitForDB
| CreateDB
| Finished
deriving (Show, Eq, Enum, Bounded, Ord)
rmDirIgnoreErrors :: FilePath -> IO ()
rmDirIgnoreErrors mainDir =
removeDirectoryRecursive mainDir `catch` (\(_ :: IOException) -> return ())
startWithLogger :: (Event -> IO ())
-> SocketClass
-> [(String, String)]
-> FilePath
-> Handle
-> Handle
-> IO (Either StartError DB)
startWithLogger logger socketType options mainDir stdOut stdErr = try $ flip onException (rmDirIgnoreErrors mainDir) $ do
let dataDir = mainDir ++ "/data"
logger InitDB
initDBExitCode <- runProcessWith stdOut stdErr "initdb"
"initdb" ["-E", "UNICODE", "-A", "trust", "-D", dataDir]
throwIfError InitDBFailed initDBExitCode
logger WriteConfig
writeFile (dataDir ++ "/postgresql.conf") $ config $ if socketType == Unix then Just mainDir else Nothing
logger FreePort
port <- openFreePort
-- slight race here, the port might not be free anymore!
let host = case socketType of
Localhost -> "127.0.0.1"
Unix -> mainDir
let makeConnectionString dbName = "postgresql:///"
++ dbName ++ "?host=" ++ host ++ "&port=" ++ show port
connectionString = makeConnectionString "test"
logger StartPostgres
let extraOptions = map (\(key, value) -> "--" ++ key ++ "=" ++ value) options
bracketOnError ( fmap (DB mainDir connectionString . fourth)
$ createProcess_ "postgres"
( procWith stdOut stdErr
"postgres"
$ ["-D", dataDir, "-p", show port] ++ extraOptions
)
)
stop
$ \result -> do
logger WaitForDB
waitForDB $ makeConnectionString "template1"
logger CreateDB
let createDBHostArgs = case socketType of
Unix -> ["-h", mainDir]
Localhost -> ["-h", "127.0.0.1"]
throwIfError CreateDBFailed =<<
runProcessWith stdOut stdErr "createDB"
"createdb" (createDBHostArgs ++ ["-p", show port, "test"])
logger Finished
return result
-- | Start postgres and log it's all stdout to {'mainDir'}\/output.txt and {'mainDir'}\/error.txt
startAndLogToTmp :: [(String, String)]
-- ^ Extra options which override the defaults
-> IO (Either StartError DB)
startAndLogToTmp options = do
mainDir <- createTempDirectory "/tmp" "tmp-postgres"
stdOutFile <- openFile (mainDir ++ "/" ++ "output.txt") WriteMode
stdErrFile <- openFile (mainDir ++ "/" ++ "error.txt") WriteMode
startWithHandlesAndDir Unix options mainDir stdOutFile stdErrFile
-- | Stop postgres and clean up the temporary database folder.
stop :: DB -> IO ExitCode
stop DB {..} = do
withProcessHandle pid (\case
OpenHandle p -> signalProcess sigINT p
ClosedHandle _ -> return ()
)
result <- waitForProcess pid
removeDirectoryRecursive mainDir
return result