{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Gargoyle
( Gargoyle (..)
, withGargoyle
, gargoyleMain
) where
import Control.Concurrent
import Control.Exception
import Control.Monad
import Network.Socket
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import System.IO.Error
import System.Environment
import System.FileLock
import System.Posix.Process
import System.Process
import Debug.Trace
data Gargoyle pid a = Gargoyle
{ forall pid a. Gargoyle pid a -> FilePath
_gargoyle_exec :: FilePath
, forall pid a. Gargoyle pid a -> FilePath -> IO ()
_gargoyle_init :: FilePath -> IO ()
, forall pid a. Gargoyle pid a -> FilePath -> IO pid
_gargoyle_start :: FilePath -> IO pid
, forall pid a. Gargoyle pid a -> pid -> IO ()
_gargoyle_stop :: pid -> IO ()
, forall pid a. Gargoyle pid a -> FilePath -> IO a
_gargoyle_getInfo :: FilePath -> IO a
}
gControlDir :: FilePath -> FilePath
gControlDir :: FilePath -> FilePath
gControlDir = (FilePath -> FilePath -> FilePath
</> FilePath
"control")
gWorkDir :: FilePath -> FilePath
gWorkDir :: FilePath -> FilePath
gWorkDir = (FilePath -> FilePath -> FilePath
</> FilePath
"work")
gOldWorkDir :: FilePath -> FilePath
gOldWorkDir :: FilePath -> FilePath
gOldWorkDir = (FilePath -> FilePath -> FilePath
</> FilePath
"db")
gLockDir :: FilePath -> FilePath
gLockDir :: FilePath -> FilePath
gLockDir = (FilePath -> FilePath -> FilePath
</> FilePath
"lock")
checkThreadedRuntime :: IO ()
checkThreadedRuntime :: IO ()
checkThreadedRuntime = Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Bool -> Bool
not Bool
rtsSupportsBoundThreads) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
Handle -> FilePath -> IO ()
hPutStrLn Handle
stderr FilePath
"Gargoyle requires threaded run-time, aborting"
Bool -> IO () -> IO ()
forall a. (?callStack::CallStack) => Bool -> a -> a
assert Bool
rtsSupportsBoundThreads (() -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ())
withGargoyle :: Gargoyle pid a
-> FilePath
-> (a -> IO b)
-> IO b
withGargoyle :: forall pid a b. Gargoyle pid a -> FilePath -> (a -> IO b) -> IO b
withGargoyle Gargoyle pid a
g FilePath
daemonDir a -> IO b
b = do
IO ()
checkThreadedRuntime
daemonExists <- FilePath -> IO Bool
doesDirectoryExist FilePath
daemonDir
if daemonExists
then do
let oldWrk = FilePath -> FilePath
gOldWorkDir FilePath
daemonDir
wrk = FilePath -> FilePath
gWorkDir FilePath
daemonDir
oldWorkDirExists <- doesDirectoryExist oldWrk
workDirExists <- doesDirectoryExist wrk
when (oldWorkDirExists && not workDirExists) $ renameDirectory oldWrk wrk
else do
createDirectory daemonDir
_gargoyle_init g (gWorkDir daemonDir)
s <- socket AF_UNIX Stream defaultProtocol
let acquire = do
connectResult <- IO () -> IO (Either IOError ())
forall e a. Exception e => IO a -> IO (Either e a)
try (IO () -> IO (Either IOError ()))
-> IO () -> IO (Either IOError ())
forall a b. (a -> b) -> a -> b
$ Socket -> SockAddr -> IO ()
connect Socket
s (SockAddr -> IO ()) -> SockAddr -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath -> SockAddr
SockAddrUnix (FilePath -> SockAddr) -> FilePath -> SockAddr
forall a b. (a -> b) -> a -> b
$ FilePath -> FilePath
gControlDir FilePath
daemonDir
case connectResult of
Right () -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Left IOError
e
| IOError -> Bool
isDoesNotExistError IOError
e -> do
let monProc :: CreateProcess
monProc = (FilePath -> [FilePath] -> CreateProcess
proc (Gargoyle pid a -> FilePath
forall pid a. Gargoyle pid a -> FilePath
_gargoyle_exec Gargoyle pid a
g) [FilePath
daemonDir])
{ std_in = CreatePipe
, std_out = CreatePipe
, std_err = Inherit
, close_fds = True
, new_session = True
}
(Just monIn, Just monOut, Nothing, monHnd) <- CreateProcess
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
createProcess CreateProcess
monProc
void $ forkOS $ void $ waitForProcess monHnd
hClose monIn
r <- hGetLine monOut
case r of
FilePath
"retry" -> do
Int -> IO ()
threadDelay Int
500000
IO ()
acquire
FilePath
"ready" -> IO ()
acquire
FilePath
_ -> FilePath -> IO ()
forall a. FilePath -> IO a
forall (m :: * -> *) a. MonadFail m => FilePath -> m a
fail (FilePath -> IO ()) -> FilePath -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath
"Unexpected gargoyle message from monitor process: " FilePath -> FilePath -> FilePath
forall a. Semigroup a => a -> a -> a
<> FilePath
r
| Bool
otherwise -> IOError -> IO ()
forall e a. (?callStack::CallStack, Exception e) => e -> IO a
throwIO IOError
e
bracket_ acquire (shutdown s ShutdownBoth >> close s) $
b =<< _gargoyle_getInfo g (gWorkDir daemonDir)
gargoyleMain :: Gargoyle pid a
-> IO ()
gargoyleMain :: forall pid a. Gargoyle pid a -> IO ()
gargoyleMain Gargoyle pid a
g = IO ProcessID -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO ProcessID -> IO ()) -> IO ProcessID -> IO ()
forall a b. (a -> b) -> a -> b
$ IO () -> IO ProcessID
forkProcess (IO () -> IO ProcessID) -> IO () -> IO ProcessID
forall a b. (a -> b) -> a -> b
$ do
IO ()
checkThreadedRuntime
[daemonDir] <- IO [FilePath]
getArgs IO [FilePath] -> ([FilePath] -> IO [FilePath]) -> IO [FilePath]
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
x :: [FilePath]
x@[FilePath
_] -> [FilePath] -> IO [FilePath]
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return [FilePath]
x
[FilePath]
_ -> FilePath -> IO [FilePath]
forall a. FilePath -> IO a
forall (m :: * -> *) a. MonadFail m => FilePath -> m a
fail FilePath
"Gargoyle monitor received unexpected number of arguments"
let lockPath = FilePath -> FilePath
gLockDir FilePath
daemonDir
catch (openFile lockPath WriteMode >>= hClose) $ \(IOError
e :: IOException) -> if
| IOError -> Bool
isAlreadyInUseError IOError
e -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
| IOError -> Bool
isDoesNotExistError IOError
e -> IOError -> IO ()
forall e a. (?callStack::CallStack, Exception e) => e -> IO a
throwIO IOError
e
| IOError -> Bool
isPermissionError IOError
e -> IOError -> IO ()
forall e a. (?callStack::CallStack, Exception e) => e -> IO a
throwIO IOError
e
| Bool
otherwise -> IOError -> IO ()
forall e a. (?callStack::CallStack, Exception e) => e -> IO a
throwIO IOError
e
lock <- tryLockFile lockPath Exclusive >>= \case
Just FileLock
x -> FileLock -> IO FileLock
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return FileLock
x
Maybe FileLock
Nothing -> do
FilePath -> IO ()
putStrLn FilePath
"retry"
Handle -> IO ()
hFlush Handle
stdout
IO FileLock
forall a. IO a
exitFailure
controlSocket <- socket AF_UNIX Stream defaultProtocol
let socketPath = FilePath -> FilePath
gControlDir FilePath
daemonDir
createSocket = do
result <- IO () -> IO (Either IOError ())
forall e a. Exception e => IO a -> IO (Either e a)
try (IO () -> IO (Either IOError ()))
-> IO () -> IO (Either IOError ())
forall a b. (a -> b) -> a -> b
$ Socket -> SockAddr -> IO ()
bind Socket
controlSocket (SockAddr -> IO ()) -> SockAddr -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath -> SockAddr
SockAddrUnix FilePath
socketPath
case result of
Right () -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Left IOError
e
| IOError -> Bool
isAlreadyInUseError IOError
e
-> do
FilePath -> IO ()
removePathForcibly FilePath
socketPath
FilePath -> IO ()
putStrLn FilePath
"retry"
Handle -> IO ()
hFlush Handle
stdout
IO ()
forall a. IO a
exitFailure
| Bool
otherwise -> IOError -> IO ()
forall e a. (?callStack::CallStack, Exception e) => e -> IO a
throwIO IOError
e
bracket createSocket (\()
_ -> FilePath -> IO ()
removeFile FilePath
socketPath) $ \()
_ -> do
Socket -> Int -> IO ()
listen Socket
controlSocket Int
128
numClientsVar <- Int -> IO (MVar Int)
forall a. a -> IO (MVar a)
newMVar (Int
0 :: Int)
shutdownVar <- newEmptyMVar
void $ forkOS $ forever $ do
(s, _) <- accept controlSocket
acceptThread <- myThreadId
modifyMVar_ numClientsVar $ \Int
n -> do
Int -> IO Int
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Int -> IO Int) -> Int -> IO Int
forall a b. (a -> b) -> a -> b
$ Int -> Int
forall a. Enum a => a -> a
succ Int
n
forkOS $ do
h <- socketToHandle s ReadMode
catchJust
(\IOError
e -> if IOError -> Bool
isEOFError IOError
e then () -> Maybe ()
forall a. a -> Maybe a
Just () else IOError
e IOError -> Maybe () -> Maybe ()
forall a b. Show a => a -> b -> b
`traceShow` Maybe ()
forall a. Maybe a
Nothing)
(hGetChar h >> hPutStrLn stderr "Warning: client sent data over the control socket")
return
mask_ $ do
n <- takeMVar numClientsVar
case pred n of
Int
0 -> do
IO () -> (IOError -> IO ()) -> IO ()
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
catch (Socket -> ShutdownCmd -> IO ()
shutdown Socket
controlSocket ShutdownCmd
ShutdownBoth) ((IOError -> IO ()) -> IO ()) -> (IOError -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \(IOError
_ :: IOException) -> () -> IO ()
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
ThreadId -> IO ()
killThread ThreadId
acceptThread
MVar () -> () -> IO ()
forall a. MVar a -> a -> IO ()
putMVar MVar ()
shutdownVar ()
Int
n' -> MVar Int -> Int -> IO ()
forall a. MVar a -> a -> IO ()
putMVar MVar Int
numClientsVar Int
n'
bracket (_gargoyle_start g (gWorkDir daemonDir)) (_gargoyle_stop g) $ \pid
_ -> do
Handle -> BufferMode -> IO ()
hSetBuffering Handle
stdout BufferMode
LineBuffering
FilePath -> IO ()
putStrLn FilePath
"ready"
MVar () -> IO ()
forall a. MVar a -> IO a
takeMVar MVar ()
shutdownVar
unlockFile lock