on master help sign in
Merge pull request #94 from dynamicboid/use-hdaemonize
6048113Jappie Klooster
ghc-9.2.5.stack.yaml−1
@@ -2,5 +2,4 @@ resolver: lts-20.3
2packages:
2packages:
3 - .
3 - .
4extra-deps:
4extra-deps:
5− - daemons-0.3.0@sha256:356edaf06a8cf5e9f19685f9ce793ad6d2b6bce2e55379f6b2aff1b0c07a633f,3178
6 - timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092
5 - timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092
ghc-9.2.5.stack.yaml.lock−7
@@ -4,13 +4,6 @@
4# https://docs.haskellstack.org/en/stable/lock_files
4# https://docs.haskellstack.org/en/stable/lock_files
5
5
6packages:
6packages:
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
14- completed:
7- completed:
15 hackage: timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092
8 hackage: timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092
16 pantry-tree:
9 pantry-tree:
odd-jobs.cabal+43−11
@@ -1,10 +1,10 @@
1cabal-version: 1.12
1cabal-version: 1.12
2
2
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.
4--
4--
5-- see: https://github.com/sol/hpack
5-- see: https://github.com/sol/hpack
6--
6--
7−-- hash: 81dabf5fa65d737ec98a4f07762968283cccc36292941ff7fff10a9a398e84b0
7+-- hash: dff1ced317b40e54688799203ec3743b119bf71f2fc98109a5e79f12c6e10fba
8
8
9name: odd-jobs
9name: odd-jobs
10version: 0.2.2
10version: 0.2.2
@@ -61,19 +61,27 @@ library
61 Paths_odd_jobs
61 Paths_odd_jobs
62 hs-source-dirs:
62 hs-source-dirs:
63 src
63 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
65 ghc-options: -Wall -fno-warn-orphans -fno-warn-unused-imports -fno-warn-dodgy-exports -Werror=missing-fields -Werror=incomplete-patterns
72 ghc-options: -Wall -fno-warn-orphans -fno-warn-unused-imports -fno-warn-dodgy-exports -Werror=missing-fields -Werror=incomplete-patterns
66 build-depends:
73 build-depends:
67 aeson
74 aeson
75+ , async ==2.2.4
68 , base >=4.7 && <5
76 , base >=4.7 && <5
69 , bytestring
77 , bytestring
70− , daemons
71 , directory
78 , directory
72 , either
79 , either
73 , fast-logger
80 , fast-logger
74 , filepath
81 , filepath
75 , friendly-time
82 , friendly-time
76 , generic-deriving
83 , generic-deriving
84+ , hdaemonize
77 , hostname
85 , hostname
78 , lucid
86 , lucid
79 , monad-control
87 , monad-control
@@ -81,7 +89,7 @@ library
81 , mtl
89 , mtl
82 , optparse-applicative
90 , optparse-applicative
83 , postgresql-simple
91 , postgresql-simple
84− , resource-pool >= 0.2.0.0 && < 0.4.0.0
92+ , resource-pool
85 , safe
93 , safe
86 , servant
94 , servant
87 , servant-lucid
95 , servant-lucid
@@ -115,13 +123,20 @@ executable devel
115 hs-source-dirs:
123 hs-source-dirs:
116 dev
124 dev
117 src
125 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
119 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
134 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
120 build-depends:
135 build-depends:
121 aeson
136 aeson
137+ , async ==2.2.4
122 , base >=4.7 && <5
138 , base >=4.7 && <5
123 , bytestring
139 , bytestring
124− , daemons
125 , directory
140 , directory
126 , either
141 , either
127 , fast-logger
142 , fast-logger
@@ -129,6 +144,7 @@ executable devel
129 , foreign-store
144 , foreign-store
130 , friendly-time
145 , friendly-time
131 , generic-deriving
146 , generic-deriving
147+ , hdaemonize
132 , hostname
148 , hostname
133 , lucid
149 , lucid
134 , monad-control
150 , monad-control
@@ -162,19 +178,27 @@ executable odd-jobs-cli-example
162 Paths_odd_jobs
178 Paths_odd_jobs
163 hs-source-dirs:
179 hs-source-dirs:
164 examples
180 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
166 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
189 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
167 build-depends:
190 build-depends:
168 aeson
191 aeson
192+ , async ==2.2.4
169 , base >=4.7 && <5
193 , base >=4.7 && <5
170 , bytestring
194 , bytestring
171− , daemons
172 , directory
195 , directory
173 , either
196 , either
174 , fast-logger
197 , fast-logger
175 , filepath
198 , filepath
176 , friendly-time
199 , friendly-time
177 , generic-deriving
200 , generic-deriving
201+ , hdaemonize
178 , hostname
202 , hostname
179 , lucid
203 , lucid
180 , monad-control
204 , monad-control
@@ -219,20 +243,28 @@ test-suite jobrunner
219 hs-source-dirs:
243 hs-source-dirs:
220 test
244 test
221 src
245 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
223 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
254 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
224 build-depends:
255 build-depends:
225 aeson
256 aeson
257+ , async ==2.2.4
226 , base >=4.7 && <5
258 , base >=4.7 && <5
227 , bytestring
259 , bytestring
228 , containers
260 , containers
229− , daemons
230 , directory
261 , directory
231 , either
262 , either
232 , fast-logger
263 , fast-logger
233 , filepath
264 , filepath
234 , friendly-time
265 , friendly-time
235 , generic-deriving
266 , generic-deriving
267+ , hdaemonize
236 , hedgehog
268 , hedgehog
237 , hostname
269 , hostname
238 , lifted-async
270 , lifted-async
package.yaml+2−1
@@ -58,6 +58,8 @@ default-extensions:
58
58
59dependencies:
59dependencies:
60 - base >= 4.7 && < 5
60 - base >= 4.7 && < 5
61+ - async ==2.2.4
62+ - hdaemonize
61 - text
63 - text
62 - postgresql-simple
64 - postgresql-simple
63 - bytestring
65 - bytestring
@@ -85,7 +87,6 @@ dependencies:
85 - warp
87 - warp
86 - unordered-containers
88 - unordered-containers
87 - optparse-applicative
89 - optparse-applicative
88− - daemons
89 - filepath
90 - filepath
90 - directory
91 - directory
91 - generic-deriving
92 - generic-deriving
src/OddJobs/Cli.hs+50−38
@@ -5,13 +5,17 @@
5module OddJobs.Cli where
5module OddJobs.Cli where
6
6
7import Options.Applicative as Opts
7import 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)
8import Data.Text
12import Data.Text
9import OddJobs.Job (startJobRunner, Config(..), LogLevel(..), LogEvent(..))
13import OddJobs.Job (startJobRunner, Config(..), LogLevel(..), LogEvent(..))
10import OddJobs.Types (UIConfig(..), Seconds(..), delaySeconds)
14import OddJobs.Types (UIConfig(..), Seconds(..), delaySeconds)
11−-- import System.Daemonize (DaemonOptions(..), daemonize)
12−import qualified System.Posix.Daemon as Daemon
15+import qualified System.Posix.Daemonize as Daemonize
13−import System.FilePath (FilePath)
16+import System.FilePath (FilePath, takeBaseName, takeDirectory)
14import System.Posix.Process (getProcessID)
17import System.Posix.Process (getProcessID)
18+import System.Posix.Signals (Handler(CatchOnce), installHandler, sigTERM)
15import qualified System.Directory as Dir
19import qualified System.Directory as Dir
16import qualified System.Exit as Exit
20import qualified System.Exit as Exit
17import System.Environment (getProgName)
21import System.Environment (getProgName)
@@ -101,6 +105,33 @@ runCli cliType = do
101 Status ->
105 Status ->
102 Prelude.error "not implemented yet"
106 Prelude.error "not implemented yet"
103
107
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+
104{-| Used by 'defaultMain' if the 'Start' command is issued via the CLI. If
135{-| Used by 'defaultMain' if the 'Start' command is issued via the CLI. If
105@--daemonize@ switch is also passed, it checks for 'startPidFile':
136@--daemonize@ switch is also passed, it checks for 'startPidFile':
106
137
@@ -116,7 +147,12 @@ defaultStartCommand :: CommonStartArgs
116 -> IO ()
147 -> IO ()
117defaultStartCommand CommonStartArgs{..} mUIArgs cliType = do
148defaultStartCommand CommonStartArgs{..} mUIArgs cliType = do
118 if startDaemonize then do
149 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+ }
120 else
156 else
121 coreStartupFn
157 coreStartupFn
122 where
158 where
@@ -155,7 +191,7 @@ defaultWebUI UIStartArgs{..} uicfg@UIConfig{..} = do
155 Warp.run uistartPort $ Servant.serveWithContext api ctx app
191 Warp.run uistartPort $ Servant.serveWithContext api ctx app
156
192
157{-| Used by 'defaultMain' if 'Stop' command is issued via the CLI. Sends a
193{-| 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
159of 'shutTimeout' seconds (controller by @--timeout@) for the daemon to shutdown
195of 'shutTimeout' seconds (controller by @--timeout@) for the daemon to shutdown
160gracefully, after which a @SIGKILL@ is issued
196gracefully, after which a @SIGKILL@ is issued
161-}
197-}
@@ -163,27 +199,15 @@ defaultStopCommand :: StopArgs
163 -> IO ()
199 -> IO ()
164defaultStopCommand StopArgs{..} = do
200defaultStopCommand StopArgs{..} = do
165 progName <- getProgName
201 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 <>
170 " and waiting " <> show (unSeconds shutTimeout) <> " seconds for graceful stop"
203 " and waiting " <> show (unSeconds shutTimeout) <> " seconds for graceful stop"
171− readFile shutPidFile >>= Sig.signalProcess Sig.sigINT . read
204+ flip Daemonize.serviced' Daemonize.Stop
172− Async.race (delaySeconds shutTimeout) checkProcessStatus >>= \case
205+ $ Daemonize.simpleDaemon
173− Right _ -> do
206+ { Daemonize.name = Just $ takeBaseName shutPidFile
174− putStrLn $ progName <> " seems to have exited gracefully."
207+ , Daemonize.pidfileDirectory = Just $ takeDirectory shutPidFile
175− Exit.exitSuccess
208+ , Daemonize.killWait = Just $ coerce shutTimeout
176− Left _ -> do
209+ }
177− putStrLn $ progName <> " has still not exited. Sending sigKILL for forced exit."
210+
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 ()
187
211
188-- * Default CLI parsers
212-- * Default CLI parsers
189--
213--
@@ -298,7 +322,7 @@ webUiAuthParser = basicAuthParser <|> noAuthParser
298-- 'shutTimeout' seconds. If the daemon doesn't shut down cleanly within that
322-- 'shutTimeout' seconds. If the daemon doesn't shut down cleanly within that
299-- time, it sends a @SIGKILL@ to kill immediately.
323-- time, it sends a @SIGKILL@ to kill immediately.
300data StopArgs = StopArgs
324data 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
302 -- @SIGKILL@
326 -- @SIGKILL@
303 shutTimeout :: !Seconds
327 shutTimeout :: !Seconds
304 -- | PID file of the deamon. Ref: 'pidFileParser'
328 -- | PID file of the deamon. Ref: 'pidFileParser'
@@ -310,8 +334,6 @@ stopParser = fmap Stop $ StopArgs
310 <$> option (Seconds <$> auto) ( long "timeout" <>
334 <$> option (Seconds <$> auto) ( long "timeout" <>
311 metavar "TIMEOUT" <>
335 metavar "TIMEOUT" <>
312 help "Maximum seconds to wait before force-killing the background daemon."
336 help "Maximum seconds to wait before force-killing the background daemon."
313− -- value defaultTimeout <>
314− -- showDefaultWith (show . unSeconds)
315 )
337 )
316 <*> pidFileParser
338 <*> pidFileParser
317
339
@@ -344,16 +366,6 @@ defaultCliInfo :: CliType -> ParserInfo Args
344defaultCliInfo cliType =
366defaultCliInfo cliType =
345 info (argParser cliType <**> helper) fullDesc
367 info (argParser cliType <**> helper) fullDesc
346
368
347−-- defaultDaemonOptions :: DaemonOptions
348−-- defaultDaemonOptions = DaemonOptions
349−-- { daemonShouldChangeDirectory = False
350−-- , daemonShouldCloseStandardStreams = False
351−-- , daemonShouldIgnoreSignals = True
352−-- , daemonUserToChangeTo = Nothing
353−-- , daemonGroupToChangeTo = Nothing
354−-- }
355−
356−
357-- ** Auth implementations for the default Web UI
369-- ** Auth implementations for the default Web UI
358
370
359-- *** Basic Auth
371-- *** Basic Auth
stack.yaml−1
@@ -2,5 +2,4 @@ resolver: lts-20.3
2packages:
2packages:
3 - .
3 - .
4extra-deps:
4extra-deps:
5− - daemons-0.3.0@sha256:356edaf06a8cf5e9f19685f9ce793ad6d2b6bce2e55379f6b2aff1b0c07a633f,3178
6 - timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092
5 - timing-convenience-0.1@sha256:7ff807a9a9e5596f2b18d45c5a01aefb91d4a98f6a1008d183b5c550f68f7cb7,2092