@@ -7,7 +7,10 @@ module OddJobs.Cli where
77import Options.Applicative as Opts
88import Data.Text
99import OddJobs.Job (startJobRunner, Config(..))
10−import System.Daemonize (DaemonOptions(..), daemonize)
10+import OddJobs.Types (UIConfig(..))
11+import OddJobs.Job(LogLevel(..), LogEvent(..))
12+-- import System.Daemonize (DaemonOptions(..), daemonize)
13+import qualified System.Posix.Daemon as Daemon
1114import System.FilePath (FilePath)
1215import System.Posix.Process (getProcessID)
1316import qualified System.Directory as Dir
@@ -16,6 +19,8 @@ import System.Environment (getProgName)
1619import OddJobs.Types (Seconds(..), delaySeconds)
1720import qualified System.Posix.Signals as Sig
1821import qualified UnliftIO.Async as Async
22+import UnliftIO (bracket_)
23+import Safe (fromJustNote)
1924import qualified OddJobs.Endpoints as UI
2025import Servant.Server as Servant
2126import Servant.API
@@ -23,6 +28,7 @@ import Data.Proxy
2328import Data.Text.Encoding (decodeUtf8)
2429import Network.Wai.Handler.Warp as Warp
2530import Debug.Trace
31+import Data.String.Conv (toS)
2632
2733-- * Introduction
2834--
@@ -57,6 +63,11 @@ import Debug.Trace
5763-- #defaultBehaviour#
5864--
5965
66+
67+data CliType = CliOnlyJobRunner { cliStartJobRunner :: (Config -> Config) -> IO () }
68+ | CliOnlyWebUi { cliStartWebUI :: UIStartArgs -> (UIConfig -> UIConfig) -> IO () }
69+ | CliBoth { cliStartJobRunner :: (Config -> Config) -> IO (), cliStartWebUI :: UIStartArgs -> (UIConfig -> UIConfig) -> IO () }
70+
6071{-|
6172Please do not get scared by the type-signature of the first argument.
6273Conceptually, it's a callback, within another callback.
@@ -81,15 +92,12 @@ Please take a look at the [getting started
8192guide](https://www.haskelltutorials.com/odd-jobs/guide.html#getting-started) for
8293an example of how to use this function.
8394-}
84−defaultMain :: ((Config -> IO ()) -> IO ())
85− -- ^ A callback function that will be executed once the dameon has
86− -- forked into the background.
87− -> IO ()
88−defaultMain startFn = do
89− Args{argsCommand} <- customExecParser defaultCliParserPrefs defaultCliInfo
95+runCli :: CliType -> IO ()
96+runCli cliType = do
97+ Args{argsCommand} <- customExecParser defaultCliParserPrefs (defaultCliInfo cliType)
9098 case argsCommand of
91− Start cmdArgs -> do
92− defaultStartCommand cmdArgs startFn
99+ Start commonArgs mUIArgs ->
100+ defaultStartCommand commonArgs mUIArgs cliType
93101 Stop cmdArgs -> do
94102 defaultStopCommand cmdArgs
95103 Status ->
@@ -104,54 +112,50 @@ defaultMain startFn = do
104112* If it exists, it refuses to start, to prevent multiple invocations of the same
105113 background daemon.
106114-}
107−defaultStartCommand :: StartArgs
108− -> ((Config -> IO ()) -> IO ())
109− -- ^ the same callback-within-callback function described in
110− -- 'defaultMain'
115+defaultStartCommand :: CommonStartArgs
116+ -> Maybe UIStartArgs
117+ -> CliType
111118 -> IO ()
112−defaultStartCommand args@StartArgs{..} startFn = do
113− progName <- getProgName
119+defaultStartCommand CommonStartArgs{..} mUIArgs cliType = do
114120 case startDaemonize of
115− False -> do
116− startFn coreStartupFn
121+ False ->
122+ coreStartupFn
117123 True -> do
118− (Dir.doesPathExist startPidFile) >>= \case
119− True -> do
120− putStrLn $
121− "PID file already exists. Please check if " <> progName <> " is still running in the background." <>
122− " If not, you can safely delete this file and start " <> progName <> " again: " <> startPidFile
123− Exit.exitWith (Exit.ExitFailure 1)
124− False -> do
125− daemonize defaultDaemonOptions (pure ()) $ const $ do
126− pid <- getProcessID
127− writeFile startPidFile (show pid)
128− putStrLn $ "Started " <> progName <> " in background with PID=" <> show pid <> ". PID written to " <> startPidFile
129− startFn $ \cfg -> coreStartupFn cfg{cfgPidFile = Just startPidFile}
124+ Daemon.runDetached (Just startPidFile) (Daemon.ToFile "/tmp/oddjobs.out") coreStartupFn
130125 where
131− coreStartupFn cfg = do
132− Async.withAsync (defaultWebUi args cfg) $ \_ -> do
133− startJobRunner cfg
134−
135−
136−defaultWebUi :: StartArgs
137− -> Config
126+ uiArgs = fromJustNote "Please specify Web UI Startup Args" $ traceShowId mUIArgs
127+ coreStartupFn =
128+ case cliType of
129+ CliOnlyJobRunner{..} -> do
130+ cliStartJobRunner Prelude.id
131+ CliOnlyWebUi{..} -> do
132+ traceM "CliOnlyWebUi before"
133+ cliStartWebUI uiArgs Prelude.id
134+ CliBoth{..} -> do
135+ traceM "CliBoth before"
136+ Async.withAsync (cliStartWebUI uiArgs Prelude.id) $ \_ -> do
137+ traceM "CliBoth inside withAsync"
138+ cliStartJobRunner Prelude.id
139+ traceM "CliBoth end"
140+
141+defaultWebUI :: UIStartArgs
142+ -> UIConfig
138143 -> IO ()
139−defaultWebUi StartArgs{..} cfg@Config{..} = do
140− env <- UI.mkEnv cfg ("/" <>)
141− case startWebUiAuth of
142− Nothing -> pure ()
143− Just AuthNone ->
144− let app = UI.server cfg env Prelude.id
145− in Warp.run startWebUiPort $
146− Servant.serve (Proxy :: Proxy UI.FinalAPI) app
147− Just (AuthBasic u p) ->
144+defaultWebUI UIStartArgs{..} uicfg@UIConfig{..} = do
145+ env <- UI.mkEnv uicfg ("/" <>)
146+ case uistartAuth of
147+ AuthNone -> do
148+ let app = UI.server uicfg env Prelude.id
149+ uicfgLogger LevelInfo $ LogText $ "Starting admin UI on port " <> (toS $ show uistartPort)
150+ Warp.run uistartPort $ Servant.serve (Proxy :: Proxy UI.FinalAPI) app
151+ (AuthBasic u p) -> do
148152 let api = Proxy :: Proxy (BasicAuth "OddJobs Admin UI" OddJobsUser :> UI.FinalAPI)
149153 ctx = defaultBasicAuth (u, p) :. EmptyContext
150154 -- Now the app will receive an extra argument for OddJobsUser,
151155 -- which we aren't really interested in.
152− app _ = UI.server cfg env Prelude.id
153− in Warp.run startWebUiPort $
154− Servant.serveWithContext api ctx app
156+ app _ = UI.server uicfg env Prelude.id
157+ uicfgLogger LevelInfo $ LogText $ "Starting admin UI on port " <> (toS $ show uistartPort)
158+ Warp.run uistartPort $ Servant.serveWithContext api ctx app
155159
156160{-| Used by 'defaultMain' if 'Stop' command is issued via the CLI. Sends a
157161@SIGINT@ signal to the process indicated by 'shutPidFile'. Waits for a maximum
@@ -162,26 +166,22 @@ defaultStopCommand :: StopArgs
162166 -> IO ()
163167defaultStopCommand StopArgs{..} = do
164168 progName <- getProgName
165− pid <- read <$> (readFile shutPidFile)
169+ -- pid <- read <$> (readFile shutPidFile)
166170 if (shutTimeout == Seconds 0)
167− then forceKill pid
168− else do putStrLn $ "Sending SIGINT to pid=" <> show pid <>
171+ then Daemon.brutalKill shutPidFile
172+ else do putStrLn $ "Sending sigINT to " <> show progName <>
169173 " and waiting " <> (show $ unSeconds shutTimeout) <> " seconds for graceful stop"
170− Sig.signalProcess Sig.sigINT pid
174+ readFile shutPidFile >>= (pure . read) >>= Sig.signalProcess Sig.sigINT
171175 (Async.race (delaySeconds shutTimeout) checkProcessStatus) >>= \case
172176 Right _ -> do
173177 putStrLn $ progName <> " seems to have exited gracefully."
174178 Exit.exitSuccess
175179 Left _ -> do
176− putStrLn $ progName <> " has still not exited."
177− forceKill pid
180+ putStrLn $ progName <> " has still not exited. Sending sigKILL for forced exit."
181+ Daemon.brutalKill shutPidFile
178182 where
179− forceKill pid = do
180− putStrLn $ "Sending SIGKILL to pid=" <> show pid
181− Sig.signalProcess Sig.sigKILL pid
182−
183183 checkProcessStatus = do
184− Dir.doesPathExist shutPidFile >>= \case
184+ Daemon.isRunning shutPidFile >>= \case
185185 True -> do
186186 delaySeconds (Seconds 1)
187187 checkProcessStatus
@@ -206,47 +206,43 @@ data Args = Args
206206
207207
208208-- | The top-level command-line parser
209−argParser :: Parser Args
210−argParser = Args <$> commandParser
209+argParser :: CliType -> Parser Args
210+argParser cliType = Args <$> (commandParser cliType)
211211
212212-- ** Top-level command parser
213213
214214-- | CLI commands are parsed into this data-structure by 'commandParser'
215215data Command
216− = Start StartArgs
216+ = Start CommonStartArgs (Maybe UIStartArgs)
217217 | Stop StopArgs
218218 | Status
219219 deriving (Eq, Show)
220220
221221-- Parser for 'argsCommand'
222−commandParser :: Parser Command
223−commandParser = hsubparser
224− ( command "start" (info startParser (progDesc "start the odd-jobs runner")) <>
225− command "stop" (info stopParser (progDesc "stop the odd-jobs runner")) <>
222+commandParser :: CliType -> Parser Command
223+commandParser cliType = hsubparser
224+ ( command "start" (info (startCmdParser cliType) (progDesc "start the odd-jobs runner and/or admin UI")) <>
225+ command "stop" (info stopParser (progDesc "stop the odd-jobs runner and/or admin UI")) <>
226226 command "status" (info statusParser (progDesc "print status of all active jobs"))
227227 )
228228
229+
230+data UIStartArgs = UIStartArgs
231+ { uistartAuth :: !WebUiAuth
232+ , uistartPort :: !Int
233+ } deriving (Eq, Show)
234+
229235-- ** Start command
230236
231237-- | @start@ command is parsed into this data-structure by 'startParser'
232−data StartArgs = StartArgs
233− {
234− -- | Switch to pick the authentication mechanism for web UI. __Note:__ We
235− -- don't have a separate switch to enable\/disable the web UI. Picking an
236− -- authentication mechanism by specifying the relevant options,
237− -- automatically enables the web UI. Ref: 'webUiAuthParser'
238− startWebUiAuth :: !(Maybe WebUiAuth)
239− -- | Port on which the web UI will run.
240− , startWebUiPort :: !Int
241− -- | You'll need to pass the @--daemonize@ switch to fork the job-runner as
242− -- a background daemon, else it will keep running as a foreground process.
243− , startDaemonize :: !Bool
238+data CommonStartArgs = CommonStartArgs
239+ { startDaemonize :: !Bool
244240 -- | PID file for the background dameon. Ref: 'pidFileParser'
245241 , startPidFile :: !FilePath
246242 } deriving (Eq, Show)
247243
248−startParser :: Parser Command
249−startParser = fmap Start $ StartArgs
244+uiStartArgsParser :: Parser UIStartArgs
245+uiStartArgsParser = UIStartArgs
250246 <$> webUiAuthParser
251247 <*> option auto ( long "web-ui-port" <>
252248 metavar "PORT" <>
@@ -254,11 +250,23 @@ startParser = fmap Start $ StartArgs
254250 showDefault <>
255251 help "The port on which the Web UI listens. Please note, to actually enable the Web UI you need to pick one of the available auth schemes"
256252 )
257− <*> switch ( long "daemonize" <>
253+
254+commonStartArgsParser :: Parser CommonStartArgs
255+commonStartArgsParser = CommonStartArgs
256+ <$> switch ( long "daemonize" <>
258257 help "Fork the job-runner as a background daemon. If omitted, the job-runner remains in the foreground."
259258 )
260259 <*> pidFileParser
261260
261+startCmdParser :: CliType -> Parser Command
262+startCmdParser cliType = Start
263+ <$> commonStartArgsParser
264+ <*> (case cliType of
265+ CliOnlyJobRunner _ -> pure Nothing
266+ CliOnlyWebUi _ -> Just <$> uiStartArgsParser
267+ CliBoth _ _ -> (Just <$> uiStartArgsParser) <|> (pure Nothing)
268+ )
269+
262270data WebUiAuth
263271 = AuthNone
264272 | AuthBasic !Text !Text
@@ -269,10 +277,10 @@ data WebUiAuth
269277-- * No auth - @--web-ui-no-auth@ __NOT RECOMMENDED__
270278-- * Basic auth - @--web-ui-basic-auth-user <USER>@ and
271279-- @--web-ui-basic-auth-password <PASS>@
272−webUiAuthParser :: Parser (Maybe WebUiAuth)
273−webUiAuthParser = basicAuthParser <|> noAuthParser <|> (pure Nothing)
280+webUiAuthParser :: Parser WebUiAuth
281+webUiAuthParser = basicAuthParser <|> noAuthParser
274282 where
275− basicAuthParser = fmap Just $ AuthBasic
283+ basicAuthParser = AuthBasic
276284 <$> strOption ( long "web-ui-basic-auth-user" <>
277285 metavar "USER" <>
278286 help "Username for basic auth"
@@ -281,7 +289,7 @@ webUiAuthParser = basicAuthParser <|> noAuthParser <|> (pure Nothing)
281289 metavar "PASS" <>
282290 help "Password for basic auth"
283291 )
284− noAuthParser = flag' (Just AuthNone)
292+ noAuthParser = flag' AuthNone
285293 ( long "web-ui-no-auth" <>
286294 help "Start the web UI with any authentication. NOT RECOMMENDED."
287295 )
@@ -335,18 +343,18 @@ defaultCliParserPrefs = prefs $
335343 showHelpOnError <>
336344 showHelpOnEmpty
337345
338−defaultCliInfo :: ParserInfo Args
339−defaultCliInfo =
340− info (argParser <**> helper) fullDesc
341−
342−defaultDaemonOptions :: DaemonOptions
343−defaultDaemonOptions = DaemonOptions
344− { daemonShouldChangeDirectory = False
345− , daemonShouldCloseStandardStreams = False
346− , daemonShouldIgnoreSignals = True
347− , daemonUserToChangeTo = Nothing
348− , daemonGroupToChangeTo = Nothing
349− }
346+defaultCliInfo :: CliType -> ParserInfo Args
347+defaultCliInfo cliType =
348+ info ((argParser cliType) <**> helper) fullDesc
349+
350+-- defaultDaemonOptions :: DaemonOptions
351+-- defaultDaemonOptions = DaemonOptions
352+-- { daemonShouldChangeDirectory = False
353+-- , daemonShouldCloseStandardStreams = False
354+-- , daemonShouldIgnoreSignals = True
355+-- , daemonUserToChangeTo = Nothing
356+-- , daemonGroupToChangeTo = Nothing
357+-- }
350358
351359
352360-- ** Auth implementations for the default Web UI