{-# 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
{ Gargoyle pid a -> FilePath
_gargoyle_exec :: FilePath
, Gargoyle pid a -> FilePath -> IO ()
_gargoyle_init :: FilePath -> IO ()
, Gargoyle pid a -> FilePath -> IO pid
_gargoyle_start :: FilePath -> IO pid
, Gargoyle pid a -> pid -> IO ()
_gargoyle_stop :: pid -> IO ()
, 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 (m :: * -> *) a. Monad m => a -> m a
return ())
withGargoyle :: Gargoyle pid a
-> FilePath
-> (a -> IO b)
-> IO b
withGargoyle :: Gargoyle pid a -> FilePath -> (a -> IO b) -> IO b
withGargoyle Gargoyle pid a
g FilePath
daemonDir a -> IO b
b = do
IO ()
checkThreadedRuntime
Bool
daemonExists <- FilePath -> IO Bool
doesDirectoryExist FilePath
daemonDir
if Bool
daemonExists
then do
let oldWrk :: FilePath
oldWrk = FilePath -> FilePath
gOldWorkDir FilePath
daemonDir
wrk :: FilePath
wrk = FilePath -> FilePath
gWorkDir FilePath
daemonDir
Bool
oldWorkDirExists <- FilePath -> IO Bool
doesDirectoryExist FilePath
oldWrk
Bool
workDirExists <- FilePath -> IO Bool
doesDirectoryExist FilePath
wrk
Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Bool
oldWorkDirExists Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
workDirExists) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath -> FilePath -> IO ()
renameDirectory FilePath
oldWrk FilePath
wrk
else do
FilePath -> IO ()
createDirectory FilePath
daemonDir
Gargoyle pid a -> FilePath -> IO ()
forall pid a. Gargoyle pid a -> FilePath -> IO ()
_gargoyle_init Gargoyle pid a
g (FilePath -> FilePath
gWorkDir FilePath
daemonDir)
Socket
s <- Family -> SocketType -> ProtocolNumber -> IO Socket
socket Family
AF_UNIX SocketType
Stream ProtocolNumber
defaultProtocol
let acquire :: IO ()
acquire = do
Either IOError ()
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 Either IOError ()
connectResult of
Right () -> () -> IO ()
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 :: StdStream
std_in = StdStream
CreatePipe
, std_out :: StdStream
std_out = StdStream
CreatePipe
, std_err :: StdStream
std_err = StdStream
Inherit
, close_fds :: Bool
close_fds = Bool
True
, new_session :: Bool
new_session = Bool
True
}
(Just Handle
monIn, Just Handle
monOut, Maybe Handle
Nothing, ProcessHandle
monHnd) <- CreateProcess
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
createProcess CreateProcess
monProc
IO ThreadId -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO ThreadId -> IO ()) -> IO ThreadId -> IO ()
forall a b. (a -> b) -> a -> b
$ IO () -> IO ThreadId
forkOS (IO () -> IO ThreadId) -> IO () -> IO ThreadId
forall a b. (a -> b) -> a -> b
$ IO ExitCode -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO ExitCode -> IO ()) -> IO ExitCode -> IO ()
forall a b. (a -> b) -> a -> b
$ ProcessHandle -> IO ExitCode
waitForProcess ProcessHandle
monHnd
Handle -> IO ()
hClose Handle
monIn
FilePath
r <- Handle -> IO FilePath
hGetLine Handle
monOut
case FilePath
r of
FilePath
"retry" -> do
Int -> IO ()
threadDelay Int
500000
IO ()
acquire
FilePath
"ready" -> IO ()
acquire
FilePath
_ -> FilePath -> IO ()
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. Exception e => e -> IO a
throwIO IOError
e
IO () -> IO () -> IO b -> IO b
forall a b c. IO a -> IO b -> IO c -> IO c
bracket_ IO ()
acquire (Socket -> ShutdownCmd -> IO ()
shutdown Socket
s ShutdownCmd
ShutdownBoth IO () -> IO () -> IO ()
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Socket -> IO ()
close Socket
s) (IO b -> IO b) -> IO b -> IO b
forall a b. (a -> b) -> a -> b
$
a -> IO b
b (a -> IO b) -> IO a -> IO b
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< Gargoyle pid a -> FilePath -> IO a
forall pid a. Gargoyle pid a -> FilePath -> IO a
_gargoyle_getInfo Gargoyle pid a
g (FilePath -> FilePath
gWorkDir FilePath
daemonDir)
gargoyleMain :: Gargoyle pid a
-> IO ()
gargoyleMain :: 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
[FilePath
daemonDir] <- IO [FilePath]
getArgs IO [FilePath] -> ([FilePath] -> IO [FilePath]) -> IO [FilePath]
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
x :: [FilePath]
x@[FilePath
_] -> [FilePath] -> IO [FilePath]
forall (m :: * -> *) a. Monad m => a -> m a
return [FilePath]
x
[FilePath]
_ -> FilePath -> IO [FilePath]
forall (m :: * -> *) a. MonadFail m => FilePath -> m a
fail FilePath
"Gargoyle monitor received unexpected number of arguments"
let lockPath :: FilePath
lockPath = FilePath -> FilePath
gLockDir FilePath
daemonDir
IO () -> (IOError -> IO ()) -> IO ()
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
catch (FilePath -> IOMode -> IO Handle
openFile FilePath
lockPath IOMode
WriteMode IO Handle -> (Handle -> IO ()) -> IO ()
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Handle -> IO ()
hClose) ((IOError -> IO ()) -> IO ()) -> (IOError -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \(IOError
e :: IOException) -> if
| IOError -> Bool
isAlreadyInUseError IOError
e -> () -> IO ()
forall (m :: * -> *) a. Monad m => a -> m a
return ()
| IOError -> Bool
isDoesNotExistError IOError
e -> IOError -> IO ()
forall e a. Exception e => e -> IO a
throwIO IOError
e
| IOError -> Bool
isPermissionError IOError
e -> IOError -> IO ()
forall e a. Exception e => e -> IO a
throwIO IOError
e
| Bool
otherwise -> IOError -> IO ()
forall e a. Exception e => e -> IO a
throwIO IOError
e
FileLock
lock <- FilePath -> SharedExclusive -> IO (Maybe FileLock)
tryLockFile FilePath
lockPath SharedExclusive
Exclusive IO (Maybe FileLock)
-> (Maybe FileLock -> IO FileLock) -> IO FileLock
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Just FileLock
x -> FileLock -> IO FileLock
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
Socket
controlSocket <- Family -> SocketType -> ProtocolNumber -> IO Socket
socket Family
AF_UNIX SocketType
Stream ProtocolNumber
defaultProtocol
let socketPath :: FilePath
socketPath = FilePath -> FilePath
gControlDir FilePath
daemonDir
createSocket :: IO ()
createSocket = do
Either IOError ()
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 Either IOError ()
result of
Right () -> () -> IO ()
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. Exception e => e -> IO a
throwIO IOError
e
IO () -> (() -> IO ()) -> (() -> IO ()) -> IO ()
forall a b c. IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket IO ()
createSocket (\()
_ -> FilePath -> IO ()
removeFile FilePath
socketPath) ((() -> IO ()) -> IO ()) -> (() -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \()
_ -> do
Socket -> Int -> IO ()
listen Socket
controlSocket Int
128
MVar Int
numClientsVar <- Int -> IO (MVar Int)
forall a. a -> IO (MVar a)
newMVar (Int
0 :: Int)
MVar ()
shutdownVar <- IO (MVar ())
forall a. IO (MVar a)
newEmptyMVar
IO ThreadId -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO ThreadId -> IO ()) -> IO ThreadId -> IO ()
forall a b. (a -> b) -> a -> b
$ IO () -> IO ThreadId
forkOS (IO () -> IO ThreadId) -> IO () -> IO ThreadId
forall a b. (a -> b) -> a -> b
$ IO ThreadId -> IO ()
forall (f :: * -> *) a b. Applicative f => f a -> f b
forever (IO ThreadId -> IO ()) -> IO ThreadId -> IO ()
forall a b. (a -> b) -> a -> b
$ do
(Socket
s, SockAddr
_) <- Socket -> IO (Socket, SockAddr)
accept Socket
controlSocket
ThreadId
acceptThread <- IO ThreadId
myThreadId
MVar Int -> (Int -> IO Int) -> IO ()
forall a. MVar a -> (a -> IO a) -> IO ()
modifyMVar_ MVar Int
numClientsVar ((Int -> IO Int) -> IO ()) -> (Int -> IO Int) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Int
n -> do
Int -> IO Int
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
IO () -> IO ThreadId
forkOS (IO () -> IO ThreadId) -> IO () -> IO ThreadId
forall a b. (a -> b) -> a -> b
$ do
Handle
h <- Socket -> IOMode -> IO Handle
socketToHandle Socket
s IOMode
ReadMode
(IOError -> Maybe ()) -> IO () -> (() -> IO ()) -> IO ()
forall e b a.
Exception e =>
(e -> Maybe b) -> IO a -> (b -> IO a) -> IO a
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)
(Handle -> IO Char
hGetChar Handle
h IO Char -> IO () -> IO ()
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Handle -> FilePath -> IO ()
hPutStrLn Handle
stderr FilePath
"Warning: client sent data over the control socket")
() -> IO ()
forall (m :: * -> *) a. Monad m => a -> m a
return
IO () -> IO ()
forall a. IO a -> IO a
mask_ (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
Int
n <- MVar Int -> IO Int
forall a. MVar a -> IO a
takeMVar MVar Int
numClientsVar
case Int -> Int
forall a. Enum a => a -> a
pred Int
n of
Int
0 -> do
Socket -> ShutdownCmd -> IO ()
shutdown Socket
controlSocket ShutdownCmd
ShutdownBoth
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'
IO pid -> (pid -> IO ()) -> (pid -> IO ()) -> IO ()
forall a b c. IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket (Gargoyle pid a -> FilePath -> IO pid
forall pid a. Gargoyle pid a -> FilePath -> IO pid
_gargoyle_start Gargoyle pid a
g (FilePath -> FilePath
gWorkDir FilePath
daemonDir)) (Gargoyle pid a -> pid -> IO ()
forall pid a. Gargoyle pid a -> pid -> IO ()
_gargoyle_stop Gargoyle pid a
g) ((pid -> IO ()) -> IO ()) -> (pid -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \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
FileLock -> IO ()
unlockFile FileLock
lock