{-# 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 ()