{-# LANGUAGE OverloadedStrings #-}
module Gargoyle.PostgreSQL where
import Control.Monad
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Search as BS
import Data.Function
import Data.Maybe
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import System.Directory
import System.Exit
import System.IO
import System.Posix.Signals
import System.Process
import System.Process.Internals
import Gargoyle
-- | A 'Gargoyle' that assumes `initdb` and `postgres` are in the path and
-- will perform a 'fast shutdown' on termination (see below).
defaultPostgres :: Gargoyle ProcessHandle ByteString
defaultPostgres = mkPostgresGargoyle "initdb" "postgres" shutdownPostgresFast
-- | Create a gargoyle by telling it where the relevant PostgreSQL executables are and
-- what it should do in order to shut down the server. This module provides two options:
-- 'shutdownPostgresSmart' and 'shutdownPostgresFast'.
mkPostgresGargoyle :: FilePath -- ^ Path to `initdb`
-> FilePath -- ^ Path to `postgres`
-> (ProcessHandle -> IO ()) -- ^ Shutdown function
-> Gargoyle ProcessHandle ByteString
-- ^ The 'Gargoyle' returned provides to client code the connection
-- string that can be used to connect to the PostgreSQL server
mkPostgresGargoyle initdbPath postgresPath shutdownFun = Gargoyle
{ _gargoyle_exec = "gargoyle-postgres-monitor"
, _gargoyle_init = initLocalPostgres initdbPath
, _gargoyle_start = startLocalPostgres postgresPath
, _gargoyle_stop = shutdownFun
, _gargoyle_getInfo = getLocalPostgresConnectionString
}
-- | Create a new PostgreSQL database in a local folder. This is a low level function used to
-- define the PostgreSQL 'Gargoyle'.
initLocalPostgres :: FilePath -- ^ Path to PostgreSQL `initdb` executable
-> FilePath -- ^ Path in which to initialize PostgreSQL Server
-> IO ()
initLocalPostgres binPath dbDir = do
(_, _, _, initdb) <- runInteractiveProcess binPath
[ "-D", dbDir
, "-U", "postgres"
, "--no-locale"
, "-E", "UTF8"
] Nothing Nothing
ExitSuccess <- waitForProcess initdb
return ()
-- | Produces the connection string for a local postgresql database. This is a low level function
-- used to define the PostgreSQL 'Gargoyle'
getLocalPostgresConnectionString :: FilePath -> IO ByteString
getLocalPostgresConnectionString dbDir = do
absoluteDbDir <- makeAbsolute dbDir
return $ mconcat $
[ "postgresql://postgres@"
, (LBS.toStrict $ BS.replace "/" ("%2F" :: LBS.ByteString) $ T.encodeUtf8 $ T.pack absoluteDbDir)
, "/postgres"
]
-- | Start a postgres server that is assumed to be in the given folder. This is a low level function
-- used to define the PostgreSQL 'Gargoyle'
startLocalPostgres :: FilePath -- ^ Path to PostgreSQL `postgres` executable
-> FilePath -- ^ Path where the server to start is located
-> IO ProcessHandle -- ^ handle of the PostgreSQL server
startLocalPostgres binPath dbDir = do
absoluteDbDir <- makeAbsolute dbDir
(_, _, err, postgres) <- runInteractiveProcess binPath
[ "-h", ""
, "-D", absoluteDbDir
, "-k", absoluteDbDir
] Nothing Nothing
fix $ \loop -> do
l <- hGetLine err
let (tag, rest) = span (/= ':') l
when (tag /= "LOG") $ fail $ "startLocalPostgres: Unexpected output from postgres: " <> show l
when (rest /= ": database system is ready to accept connections") loop
return postgres
-- | Perform a "Smart Shutdown" of Postgres;
-- see http://www.postgresql.org/docs/current/static/server-shutdown.html
shutdownPostgresSmart :: ProcessHandle -- ^ handle of the PostgreSQL server
-> IO ()
shutdownPostgresSmart postgres = do
terminateProcess postgres
_ <- waitForProcess postgres
return ()
-- | Perform a "Fast Shutdown" of Postgres;
-- see http://www.postgresql.org/docs/current/static/server-shutdown.html
shutdownPostgresFast :: ProcessHandle -- ^ handle of the PostgreSQL server
-> IO ()
shutdownPostgresFast postgres = do
withProcessHandle postgres $ \p -> do
case p of
ClosedHandle _ -> return ()
OpenHandle h -> signalProcess sigINT h
_ <- waitForProcess postgres
return ()
-- | Run `psql` against a Gargoyle managed db.
psqlLocal :: Gargoyle pid ByteString -- ^ 'Gargoyle' against which to run
-> FilePath -- ^ The path to `psql`
-> FilePath -- ^ The path where the managed daemon is expected
-> Maybe String
-- ^ Optionally provide stdin input instead of an inheriting current stdin.
-- It will have a newline and quit command appended to it.
-> IO ()
psqlLocal g psqlPath dbPath minput = withGargoyle g dbPath $ \dbUri -> do
void $ installHandler keyboardSignal Ignore Nothing
let psqlProc = (proc psqlPath [ T.unpack $ T.decodeUtf8 dbUri ])
{ std_in = case minput of
Nothing -> Inherit
Just _ -> CreatePipe
, std_out = Inherit
, std_err = Inherit
}
(mStdin, _, _, psql) <- createProcess psqlProc
case minput of
Nothing -> return ()
Just input -> hPutStrLn (fromJust mStdin) (input ++ "\n\\q")
ExitSuccess <- waitForProcess psql
return ()