PageRenderTime 52ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/Angel/Job.hs

http://github.com/jamwt/Angel
Haskell | 129 lines | 93 code | 24 blank | 12 comment | 8 complexity | 88283d03ff6bb68b83e76c385ba27a78 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. module Angel.Job where
  2. import Data.String.Utils (split, strip)
  3. import Data.Maybe (isJust, fromJust, fromMaybe)
  4. import System.Process (createProcess, proc, waitForProcess, ProcessHandle)
  5. import System.Process (terminateProcess, CreateProcess(..), StdStream(..))
  6. import Control.Concurrent
  7. import Control.Concurrent.STM
  8. import Control.Concurrent.STM.TVar (readTVar, writeTVar)
  9. import qualified Data.Map as M
  10. import Control.Monad (unless, when, forever)
  11. import Angel.Log (logger)
  12. import Angel.Data
  13. import Angel.Util (sleepSecs)
  14. import Angel.Files (getFile)
  15. ifEmpty :: String -> IO () -> IO () -> IO ()
  16. ifEmpty s ioa iob = if s == "" then ioa else iob
  17. -- |launch the program specified by `id`, opening (and closing) the
  18. -- |appropriate fds for logging. When the process dies, either b/c it was
  19. -- |killed by a monitor, killed by a system user, or ended execution naturally,
  20. -- |re-examine the desired run config to determine whether to re-run it. if so,
  21. -- |tail call.
  22. supervise sharedGroupConfig id = do
  23. let log = logger $ "- program: " ++ id ++ " -"
  24. log "START"
  25. cfg <- atomically $ readTVar sharedGroupConfig
  26. let my_spec = find_me cfg
  27. ifEmpty (name my_spec)
  28. (log "QUIT (missing from config on restart)" >> deleteRunning)
  29. (do
  30. (attachOut, attachErr) <- makeFiles my_spec cfg
  31. let (cmd, args) = cmdSplit $ (fromJust $ exec my_spec)
  32. (_, _, _, p) <- createProcess (proc cmd args){
  33. std_out = attachOut,
  34. std_err = attachErr,
  35. cwd = workingDir my_spec
  36. }
  37. updateRunningPid my_spec (Just p)
  38. log "RUNNING"
  39. waitForProcess p
  40. log "ENDED"
  41. updateRunningPid my_spec (Nothing)
  42. cfg <- atomically $ readTVar sharedGroupConfig
  43. if M.notMember id (spec cfg)
  44. then do
  45. log "QUIT"
  46. deleteRunning
  47. else do
  48. log "WAITING"
  49. sleepSecs $ (fromMaybe defaultDelay $ delay my_spec)
  50. log "RESTART"
  51. supervise sharedGroupConfig id
  52. )
  53. where
  54. cmdSplit fullcmd = (head parts, tail parts)
  55. where parts = (filter (/="") . map strip . split " ") fullcmd
  56. find_me cfg = M.findWithDefault defaultProgram id (spec cfg)
  57. updateRunningPid my_spec mpid = atomically $ do
  58. wcfg <- readTVar sharedGroupConfig
  59. writeTVar sharedGroupConfig wcfg{
  60. running=M.insertWith' (\n o-> n) id (my_spec, mpid) (running wcfg)
  61. }
  62. deleteRunning = atomically $ do
  63. wcfg <- readTVar sharedGroupConfig
  64. writeTVar sharedGroupConfig wcfg{
  65. running=M.delete id (running wcfg)
  66. }
  67. makeFiles my_spec cfg = do
  68. let useout = fromMaybe defaultStdout $ stdout my_spec
  69. attachOut <- UseHandle `fmap` getFile useout cfg
  70. let useerr = fromMaybe defaultStderr $ stderr my_spec
  71. attachErr <- UseHandle `fmap` getFile useerr cfg
  72. return $ (attachOut, attachErr)
  73. -- |send a TERM signal to all provided process handles
  74. killProcesses :: [ProcessHandle] -> IO ()
  75. killProcesses pids = mapM_ terminateProcess pids
  76. -- |fire up new supervisors for new program ids
  77. startProcesses :: TVar GroupConfig -> [String] -> IO ()
  78. startProcesses sharedGroupConfig starts = mapM_ spawnWatcher starts
  79. where
  80. spawnWatcher s = forkIO $ supervise sharedGroupConfig s
  81. -- |diff the requested config against the actual run state, and
  82. -- |do any start/kill action necessary
  83. syncSupervisors :: TVar GroupConfig -> IO ()
  84. syncSupervisors sharedGroupConfig = do
  85. let log = logger "process-monitor"
  86. cfg <- atomically $ readTVar sharedGroupConfig
  87. let kills = mustKill cfg
  88. let starts = mustStart cfg
  89. when (length kills > 0 || length starts > 0) $ log (
  90. "Must kill=" ++ (show $ length kills)
  91. ++ ", must start=" ++ (show $ length starts))
  92. killProcesses kills
  93. startProcesses sharedGroupConfig starts
  94. where
  95. mustKill cfg = map (fromJust . snd . snd) $ filter (runningAndDifferent $ spec cfg) $ M.assocs (running cfg)
  96. runningAndDifferent spec (id, (pg, pid)) = (isJust pid && (M.notMember id spec
  97. || M.findWithDefault defaultProgram id spec `cmp` pg))
  98. where cmp one two = one /= two
  99. mustStart cfg = map fst $ filter (isNew $ running cfg) $ M.assocs (spec cfg)
  100. isNew running (id, pg) = M.notMember id running
  101. -- |periodically run the supervisor sync independent of config reload,
  102. -- |just in case state gets funky b/c of theoretically possible timing
  103. -- |issues on reload
  104. pollStale :: TVar GroupConfig -> IO ()
  105. pollStale sharedGroupConfig = forever $ sleepSecs 10 >> syncSupervisors sharedGroupConfig