on master help sign in
Merge pull request #94 from dynamicboid/use-hdaemonize

Use `hdaemonize`

commit60481132b229d22eda8efcd3392dd1df237bd968 authorJappie Klooster <jappieklooster@hotmail.com> dateSat 13 May 2023, 13:32 (3 years ago) parentcc05b6450d27b4
6 files changed+95−59
ghc-9.2.5.stack.yaml−1 ghc-9.2.5.stack.yaml.lock−7 odd-jobs.cabal+43−11 package.yaml+2−1 src/OddJobs/Cli.hs+50−38 stack.yaml−1
ghc-9.2.5.stack.yaml−1
@@ -2,5 +2,4 @@ resolver: lts-20.3
22packages:
33 - .
44extra-deps:
5− - daemons-0.3.0@sha256:356edaf06a8cf5e9f19685f9ce793ad6d2b6bce2e55379f6b2aff1b0c07a633f,3178
65 - timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092
ghc-9.2.5.stack.yaml.lock−7
@@ -4,13 +4,6 @@
44# https://docs.haskellstack.org/en/stable/lock_files
55
66packages:
7−- completed:
8− hackage: daemons-0.3.0@sha256:356edaf06a8cf5e9f19685f9ce793ad6d2b6bce2e55379f6b2aff1b0c07a633f,3178
9− pantry-tree:
10− sha256: 26b1a5279be340c896edbc72baa6d95b071384b9bf7e19cd146025cf13d21769
11− size: 918
12− original:
13− hackage: daemons-0.3.0@sha256:356edaf06a8cf5e9f19685f9ce793ad6d2b6bce2e55379f6b2aff1b0c07a633f,3178
147- completed:
158 hackage: timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092
169 pantry-tree:
odd-jobs.cabal+43−11
@@ -1,10 +1,10 @@
11cabal-version: 1.12
22
3−-- This file has been generated from package.yaml by hpack version 0.33.0.
3+-- This file has been generated from package.yaml by hpack version 0.35.2.
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 81dabf5fa65d737ec98a4f07762968283cccc36292941ff7fff10a9a398e84b0
7+-- hash: dff1ced317b40e54688799203ec3743b119bf71f2fc98109a5e79f12c6e10fba
88
99name: odd-jobs
1010version: 0.2.2
@@ -61,19 +61,27 @@ library
6161 Paths_odd_jobs
6262 hs-source-dirs:
6363 src
64− default-extensions: NamedFieldPuns LambdaCase TemplateHaskell ScopedTypeVariables GeneralizedNewtypeDeriving QuasiQuotes OverloadedStrings
64+ default-extensions:
65+ NamedFieldPuns
66+ LambdaCase
67+ TemplateHaskell
68+ ScopedTypeVariables
69+ GeneralizedNewtypeDeriving
70+ QuasiQuotes
71+ OverloadedStrings
6572 ghc-options: -Wall -fno-warn-orphans -fno-warn-unused-imports -fno-warn-dodgy-exports -Werror=missing-fields -Werror=incomplete-patterns
6673 build-depends:
6774 aeson
75+ , async ==2.2.4
6876 , base >=4.7 && <5
6977 , bytestring
70− , daemons
7178 , directory
7279 , either
7380 , fast-logger
7481 , filepath
7582 , friendly-time
7683 , generic-deriving
84+ , hdaemonize
7785 , hostname
7886 , lucid
7987 , monad-control
@@ -81,7 +89,7 @@ library
8189 , mtl
8290 , optparse-applicative
8391 , postgresql-simple
84− , resource-pool >= 0.2.0.0 && < 0.4.0.0
92+ , resource-pool
8593 , safe
8694 , servant
8795 , servant-lucid
@@ -115,13 +123,20 @@ executable devel
115123 hs-source-dirs:
116124 dev
117125 src
118− default-extensions: NamedFieldPuns LambdaCase TemplateHaskell ScopedTypeVariables GeneralizedNewtypeDeriving QuasiQuotes OverloadedStrings
126+ default-extensions:
127+ NamedFieldPuns
128+ LambdaCase
129+ TemplateHaskell
130+ ScopedTypeVariables
131+ GeneralizedNewtypeDeriving
132+ QuasiQuotes
133+ OverloadedStrings
119134 ghc-options: -Wall -fno-warn-orphans -fno-warn-unused-imports -fno-warn-dodgy-exports -Werror=missing-fields -Werror=incomplete-patterns -threaded -with-rtsopts=-N -main-is DevelMain
120135 build-depends:
121136 aeson
137+ , async ==2.2.4
122138 , base >=4.7 && <5
123139 , bytestring
124− , daemons
125140 , directory
126141 , either
127142 , fast-logger
@@ -129,6 +144,7 @@ executable devel
129144 , foreign-store
130145 , friendly-time
131146 , generic-deriving
147+ , hdaemonize
132148 , hostname
133149 , lucid
134150 , monad-control
@@ -162,19 +178,27 @@ executable odd-jobs-cli-example
162178 Paths_odd_jobs
163179 hs-source-dirs:
164180 examples
165− default-extensions: NamedFieldPuns LambdaCase TemplateHaskell ScopedTypeVariables GeneralizedNewtypeDeriving QuasiQuotes OverloadedStrings
181+ default-extensions:
182+ NamedFieldPuns
183+ LambdaCase
184+ TemplateHaskell
185+ ScopedTypeVariables
186+ GeneralizedNewtypeDeriving
187+ QuasiQuotes
188+ OverloadedStrings
166189 ghc-options: -Wall -fno-warn-orphans -fno-warn-unused-imports -fno-warn-dodgy-exports -Werror=missing-fields -Werror=incomplete-patterns -threaded -with-rtsopts=-N -main-is OddJobsCliExample
167190 build-depends:
168191 aeson
192+ , async ==2.2.4
169193 , base >=4.7 && <5
170194 , bytestring
171− , daemons
172195 , directory
173196 , either
174197 , fast-logger
175198 , filepath
176199 , friendly-time
177200 , generic-deriving
201+ , hdaemonize
178202 , hostname
179203 , lucid
180204 , monad-control
@@ -219,20 +243,28 @@ test-suite jobrunner
219243 hs-source-dirs:
220244 test
221245 src
222− default-extensions: NamedFieldPuns LambdaCase TemplateHaskell ScopedTypeVariables GeneralizedNewtypeDeriving QuasiQuotes OverloadedStrings
246+ default-extensions:
247+ NamedFieldPuns
248+ LambdaCase
249+ TemplateHaskell
250+ ScopedTypeVariables
251+ GeneralizedNewtypeDeriving
252+ QuasiQuotes
253+ OverloadedStrings
223254 ghc-options: -Wall -fno-warn-orphans -fno-warn-unused-imports -fno-warn-dodgy-exports -Werror=missing-fields -Werror=incomplete-patterns -threaded -with-rtsopts=-N -main-is Test
224255 build-depends:
225256 aeson
257+ , async ==2.2.4
226258 , base >=4.7 && <5
227259 , bytestring
228260 , containers
229− , daemons
230261 , directory
231262 , either
232263 , fast-logger
233264 , filepath
234265 , friendly-time
235266 , generic-deriving
267+ , hdaemonize
236268 , hedgehog
237269 , hostname
238270 , lifted-async
package.yaml+2−1
@@ -58,6 +58,8 @@ default-extensions:
5858
5959dependencies:
6060 - base >= 4.7 && < 5
61+ - async ==2.2.4
62+ - hdaemonize
6163 - text
6264 - postgresql-simple
6365 - bytestring
@@ -85,7 +87,6 @@ dependencies:
8587 - warp
8688 - unordered-containers
8789 - optparse-applicative
88− - daemons
8990 - filepath
9091 - directory
9192 - generic-deriving
src/OddJobs/Cli.hs+50−38
@@ -5,13 +5,17 @@
55module OddJobs.Cli where
66
77import Options.Applicative as Opts
8+import Control.Concurrent.Async (race)
9+import Control.Concurrent.MVar (newEmptyMVar, takeMVar, tryPutMVar, tryTakeMVar)
10+import Data.Coerce (coerce)
11+import Data.Functor (void)
812import Data.Text
913import OddJobs.Job (startJobRunner, Config(..), LogLevel(..), LogEvent(..))
1014import OddJobs.Types (UIConfig(..), Seconds(..), delaySeconds)
11−-- import System.Daemonize (DaemonOptions(..), daemonize)
12−import qualified System.Posix.Daemon as Daemon
13−import System.FilePath (FilePath)
15+import qualified System.Posix.Daemonize as Daemonize
16+import System.FilePath (FilePath, takeBaseName, takeDirectory)
1417import System.Posix.Process (getProcessID)
18+import System.Posix.Signals (Handler(CatchOnce), installHandler, sigTERM)
1519import qualified System.Directory as Dir
1620import qualified System.Exit as Exit
1721import System.Environment (getProgName)
@@ -101,6 +105,33 @@ runCli cliType = do
101105 Status ->
102106 Prelude.error "not implemented yet"
103107
108+-- | Ensure that @SIGTERM@ is handled gracefully, because it's how containers are stopped.
109+--
110+-- @action@ will receive an 'AsyncCancelled' exception if @SIGTERM@ is received by the process.
111+--
112+-- Typical use:
113+--
114+-- > main :: IO ()
115+-- > main = withGracefulTermination_ $ do
116+--
117+-- Note that although the Haskell runtime handles @SIGINT@ it doesn't do anything with @SIGTERM@.
118+-- Therefore, the program will be killed immediately and no cleanup will be performed. In particular,
119+-- exception handlers in `bracket`, `finally`, `onException`, etc. won't be run. However, if the
120+-- program is running as PID 1 in a container, @SIGTERM@ will be ignored and the program will keep
121+-- running. This will likely result in a @SIGKILL@ being sent a short while later and cleanup still
122+-- won't be performed.
123+withGracefulTermination :: IO a -> IO (Maybe a)
124+withGracefulTermination ioAction = do
125+ var <- newEmptyMVar
126+ let terminate = void $ tryPutMVar var ()
127+ waitForTermination = takeMVar var
128+ void $ installHandler sigTERM (CatchOnce terminate) Nothing
129+ either (const Nothing) Just <$> race waitForTermination ioAction
130+
131+-- | Like 'withGracefulTermination' but ignoring the return value
132+withGracefulTermination_ :: IO a -> IO ()
133+withGracefulTermination_ = void . withGracefulTermination
134+
104135{-| Used by 'defaultMain' if the 'Start' command is issued via the CLI. If
105136@--daemonize@ switch is also passed, it checks for 'startPidFile':
106137
@@ -116,7 +147,12 @@ defaultStartCommand :: CommonStartArgs
116147 -> IO ()
117148defaultStartCommand CommonStartArgs{..} mUIArgs cliType = do
118149 if startDaemonize then do
119− Daemon.runDetached (Just startPidFile) (Daemon.ToFile "/tmp/oddjobs.out") coreStartupFn
150+ Daemonize.serviced
151+ $ Daemonize.simpleDaemon
152+ { Daemonize.program = \() -> withGracefulTermination_ coreStartupFn
153+ , Daemonize.name = Just $ takeBaseName startPidFile
154+ , Daemonize.pidfileDirectory = Just $ takeDirectory startPidFile
155+ }
120156 else
121157 coreStartupFn
122158 where
@@ -155,7 +191,7 @@ defaultWebUI UIStartArgs{..} uicfg@UIConfig{..} = do
155191 Warp.run uistartPort $ Servant.serveWithContext api ctx app
156192
157193{-| Used by 'defaultMain' if 'Stop' command is issued via the CLI. Sends a
158−@SIGINT@ signal to the process indicated by 'shutPidFile'. Waits for a maximum
194+@SIGTERM@ signal to the process indicated by 'shutPidFile'. Waits for a maximum
159195of 'shutTimeout' seconds (controller by @--timeout@) for the daemon to shutdown
160196gracefully, after which a @SIGKILL@ is issued
161197-}
@@ -163,27 +199,15 @@ defaultStopCommand :: StopArgs
163199 -> IO ()
164200defaultStopCommand StopArgs{..} = do
165201 progName <- getProgName
166− -- pid <- read <$> (readFile shutPidFile)
167− if shutTimeout == Seconds 0
168− then Daemon.brutalKill shutPidFile
169− else do putStrLn $ "Sending sigINT to " <> show progName <>
202+ putStrLn $ "Sending SIGTERM to " <> show progName <>
170203 " and waiting " <> show (unSeconds shutTimeout) <> " seconds for graceful stop"
171− readFile shutPidFile >>= Sig.signalProcess Sig.sigINT . read
172− Async.race (delaySeconds shutTimeout) checkProcessStatus >>= \case
173− Right _ -> do
174− putStrLn $ progName <> " seems to have exited gracefully."
175− Exit.exitSuccess
176− Left _ -> do
177− putStrLn $ progName <> " has still not exited. Sending sigKILL for forced exit."
178− Daemon.brutalKill shutPidFile
179− where
180− checkProcessStatus = do
181− Daemon.isRunning shutPidFile >>= \case
182− True -> do
183− delaySeconds (Seconds 1)
184− checkProcessStatus
185− False -> do
186− pure ()
204+ flip Daemonize.serviced' Daemonize.Stop
205+ $ Daemonize.simpleDaemon
206+ { Daemonize.name = Just $ takeBaseName shutPidFile
207+ , Daemonize.pidfileDirectory = Just $ takeDirectory shutPidFile
208+ , Daemonize.killWait = Just $ coerce shutTimeout
209+ }
210+
187211
188212-- * Default CLI parsers
189213--
@@ -298,7 +322,7 @@ webUiAuthParser = basicAuthParser <|> noAuthParser
298322-- 'shutTimeout' seconds. If the daemon doesn't shut down cleanly within that
299323-- time, it sends a @SIGKILL@ to kill immediately.
300324data StopArgs = StopArgs
301− { -- | After sending a @SIGINT@, how many seconds to wait before sending a
325+ { -- | After sending a @SIGTERM@, how many seconds to wait before sending a
302326 -- @SIGKILL@
303327 shutTimeout :: !Seconds
304328 -- | PID file of the deamon. Ref: 'pidFileParser'
@@ -310,8 +334,6 @@ stopParser = fmap Stop $ StopArgs
310334 <$> option (Seconds <$> auto) ( long "timeout" <>
311335 metavar "TIMEOUT" <>
312336 help "Maximum seconds to wait before force-killing the background daemon."
313− -- value defaultTimeout <>
314− -- showDefaultWith (show . unSeconds)
315337 )
316338 <*> pidFileParser
317339
@@ -344,16 +366,6 @@ defaultCliInfo :: CliType -> ParserInfo Args
344366defaultCliInfo cliType =
345367 info (argParser cliType <**> helper) fullDesc
346368
347−-- defaultDaemonOptions :: DaemonOptions
348−-- defaultDaemonOptions = DaemonOptions
349−-- { daemonShouldChangeDirectory = False
350−-- , daemonShouldCloseStandardStreams = False
351−-- , daemonShouldIgnoreSignals = True
352−-- , daemonUserToChangeTo = Nothing
353−-- , daemonGroupToChangeTo = Nothing
354−-- }
355−
356−
357369-- ** Auth implementations for the default Web UI
358370
359371-- *** Basic Auth
stack.yaml−1
@@ -2,5 +2,4 @@ resolver: lts-20.3
22packages:
33 - .
44extra-deps:
5− - daemons-0.3.0@sha256:356edaf06a8cf5e9f19685f9ce793ad6d2b6bce2e55379f6b2aff1b0c07a633f,3178
65 - timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092