packages feed

hdaemonize 0.2 → 0.3

raw patch · 3 files changed

+215/−76 lines, 3 filesdep +filepathdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: filepath

Dependency ranges changed: base

API changes (from Hackage documentation)

- System.Posix.Daemonize: type Logger = String -> IO ()
- System.Posix.Daemonize: type Program = Logger -> IO ()
+ System.Posix.Daemonize: CreateDaemon :: IO () -> Maybe String -> Maybe String -> Maybe String -> [Option] -> Maybe FilePath -> CreateDaemon
+ System.Posix.Daemonize: data CreateDaemon
+ System.Posix.Daemonize: group :: CreateDaemon -> Maybe String
+ System.Posix.Daemonize: name :: CreateDaemon -> Maybe String
+ System.Posix.Daemonize: pidfileDirectory :: CreateDaemon -> Maybe FilePath
+ System.Posix.Daemonize: program :: CreateDaemon -> IO ()
+ System.Posix.Daemonize: simpleDaemon :: CreateDaemon
+ System.Posix.Daemonize: syslogOptions :: CreateDaemon -> [Option]
+ System.Posix.Daemonize: user :: CreateDaemon -> Maybe String
- System.Posix.Daemonize: serviced :: Program -> IO ()
+ System.Posix.Daemonize: serviced :: CreateDaemon -> IO ()

Files

README view
@@ -67,6 +67,9 @@ Changelog --------- +* 0.3+    * merged with updates by madhadron+ * 0.2     * provided documentation     * backported to older GHC versions, tested on 6.8.1
System/Posix/Daemonize.hs view
@@ -1,4 +1,43 @@-module System.Posix.Daemonize (Logger, Program, daemonize, serviced) where+module System.Posix.Daemonize (+  -- * Simple daemonization+  daemonize, +  -- * Building system services+  serviced, CreateDaemon(..), simpleDaemon+  +  -- * An example                              +  --                               +  -- | Here is an example of a full program which writes a message to+  -- syslog once a second proclaiming its continued existance, and+  -- which installs its own SIGHUP handler.  Note that you won't+  -- actually see the message once a second in the log on most+  -- systems.  @syslogd@ detects repeated messages and prints the+  -- first one, then delays for the rest and eventually writes a line+  -- about how many times it has seen it.+  --                               +  -- > module Main where+  -- >+  -- > import System.Posix.Daemonize (CreateDaemon(..), serviced, simpleDaemon)+  -- > import System.Posix.Signals (installHandler, Handler(Catch), sigHUP, fullSignalSet)+  -- > import System.Posix.Syslog (syslog, Priority(Notice))+  -- > import Control.Concurrent (threadDelay)+  -- > import Control.Monad (forever)+  -- > +  -- > main :: IO ()+  -- > main = serviced stillAlive+  -- > +  -- > stillAlive :: CreateDaemon+  -- > stillAlive = simpleDaemon { program = stillAliveMain }+  -- > +  -- > stillAliveMain :: IO ()+  -- > stillAliveMain = do+  -- >   installHandler sigHUP (Catch taunt) (Just fullSignalSet)+  -- >   forever $ do threadDelay (10^6)+  -- >                syslog Notice "I'm still alive!"+  -- >                +  -- > taunt :: IO ()+  -- > taunt = syslog Notice "I sneeze in your general direction, you and your SIGHUP."++  ) where        {- originally based on code from     http://sneakymustard.com/2008/12/11/haskell-daemons -}@@ -6,32 +45,42 @@  import Control.Concurrent import Control.Exception.Extensible+import qualified Control.Monad as M (forever) import Prelude hiding (catch) import System import System.Exit import System.Posix-import System.Posix.Syslog----- | The simplest possible interface to syslog.-type Logger = String -> IO ()----- | A program is any IO computation. It also accepts a syslog handle.-type Program = Logger -> IO ()+import System.Posix.Syslog (withSyslog,Option(..),Priority(..),Facility(..),syslog)+import System.Posix.Types (UserID, GroupID)+import System.FilePath.Posix (FilePath,joinPath)+import Data.Maybe (isNothing, fromMaybe, fromJust)  --- | Daemonizes a given IO computation by forking twice, closing---   standard file descriptors, blocking sigHUP, setting file creation---   mask, starting a new session, and changing the working directory---   to root.+-- | Turning a process into a daemon involves a fixed set of+-- operations on unix systems, described in section 13.3 of Stevens+-- and Rago, "Advanced Programming in the Unix Environment."  Since+-- they are fixed, they can be written as a single function,+-- 'daemonize' taking an 'IO' action which represents the daemon's+-- actual activity.+-- +-- Briefly, 'daemonize' sets the file creation mask to 0, forks twice,+-- changed the working directory to @/@, closes stdin, stdout, and+-- stderr, blocks 'sigHUP', and runs its argument.  Strictly, it+-- should close all open file descriptors, but this is not possible in+-- a sensible way in Haskell.+-- +-- The most trivial daemon would be+-- +-- > daemonize (forever $ return ())+-- +-- which does nothing until killed. -daemonize :: IO () ->  IO () +daemonize :: IO () -> IO ()  daemonize program = --    do setFileCreationMask 0 -       forkProcess p-       exitImmediately ExitSuccess+    +  do setFileCreationMask 0 +     forkProcess p+     exitImmediately ExitSuccess      where @@ -42,50 +91,69 @@       p' = do changeWorkingDirectory "/"               closeFileDescriptors               blockSignal sigHUP-              program+              program   --- | Turns a program into a UNIX daemon (system service) ready to be---   deployed to /etc/rc.d or similar startup folder.  The resulting---   program handles command-line arguments (start, stop, or restart).------   With start option it writes out a PID to /var/run/$name.pid where---   $name is the executable name. If PID already exists, it refuses---   to start, guaranteeing there is only one live instance.------   With stop option it reads the PID from /var/run/$name.pid and---   terminates the corresponding process (first a soft kill, SIGTERM,---   then a hard kill, SIGKILL).+++-- | 'serviced' turns a program into a UNIX daemon (system service)+--   ready to be deployed to /etc/rc.d or similar startup folder.  It+--   is meant to be used in the @main@ function of a program, such as+-- +-- > serviced simpleDaemon+-- +--   The resulting program takes one of three argments: @start@,+--   @stop@, and @restart@.  All control the status of a daemon by+--   looking for a file containing a text string holding the PID of+--   any running instance.  Conventionally, this file is in+--   @/var/run/$name.pid@, where $name is the executable's name.  For+--   obvious reasons, this file is known as a PID file. -----   Another addition over the daemonize function is dropping---   privileges.  If a system user and group with a name that matches---   the executable name exist, privileges are dropped to that user and---   group.  Otherwise, they are dropped to the standard daemon user---   and group.+--   @start@ makes the program write a PID file.  If the file already+--   exists, it refuses to start, guaranteeing there is only one+--   instance of the daemon at any time. --+--   @stop@ read the PID file, and terminates the process whose pid is+--   written therein.  First it does a soft kill, SIGTERM, giving the+--   daemon a chance to shut down cleanly, then three seconds later a+--   hard kill which the daemon cannot catch or escape.+-- +--   @restart@ is simple @stop@ followed by @start@.+-- +--   'serviced' also tries to drop privileges.  If you don't specify a+--   user the daemon should run as, it will try to switch to a user+--   with the same name as the daemon, and otherwise to user @daemon@.+--   It goes through the same sequence for group.  Just to complicate+--   matters, the name of the daemon is by default the name of the+--   executable file, but can again be set to something else in the+--   'CreateDaemon' record.+--  --   Finally, exceptions in the program are caught, logged to syslog, --   and the program restarted. -serviced :: Program -> IO ()-serviced program = do name <- getProgName-                      args <- getArgs-                      process name args+serviced :: CreateDaemon -> IO ()+serviced daemon = do +  systemName <- getProgName+  let daemon' = daemon { name = if isNothing (name daemon) +                                then Just systemName else name daemon }+  args <- getArgs+  process daemon' args     where--      program' name = withSyslog name [] DAEMON $+      +      program' daemon = withSyslog (fromJust $ name daemon) (syslogOptions daemon) DAEMON $                       do let log = syslog Notice                          log "starting"-                         pidWrite name-                         dropPrivileges name-                         forever log program+                         pidWrite daemon+                         dropPrivileges daemon+                         forever (program daemon) -      process name ["start"] = pidExists name >>= f where+      process daemon ["start"] = pidExists daemon >>= f where           f True  = do error "PID file exists. Process already running?"                        exitImmediately (ExitFailure 1)-          f False = daemonize (program' name)+          f False = daemonize (program' daemon)                  -      process name ["stop"]  = -          do pid <- pidRead name+      process daemon ["stop"]  = +          do pid <- pidRead daemon              let ifdo x f = x >>= \x -> if x then f else pass              case pid of                Nothing  -> pass@@ -96,25 +164,90 @@                             do usleep (3*10^6)                                ifdo (pidLive pid) (signalProcess sigKILL pid))                    `finally`-                   removeLink (pidFile name)+                   removeLink (pidFile daemon) -      process name ["restart"] = do process name ["stop"]-                                    process name ["start"]-      process name _ = -          putStrLn $ "usage: " ++ name ++ " {start|stop|restart}"+      process daemon ["restart"] = do process daemon ["stop"]+                                      process daemon ["start"]+      process daemon _ = +        getProgName >>= \pname -> putStrLn $ "usage: " ++ pname ++ " {start|stop|restart}" +-- | The details of any given daemon are fixed by the 'CreateDaemon'+-- record passed to 'serviced'.  You can also take a predefined form+-- of 'CreateDaemon', such as 'simpleDaemon' below, and set what+-- options you want, rather than defining the whole record yourself.+data CreateDaemon = CreateDaemon {+  program :: IO (), -- ^ The actual guts of the daemon, more or less+                    -- the @main@ function.+  name :: Maybe String, -- ^ The name of the daemon, which is used as+                        -- the name for the PID file, as the name that+                        -- appears in the system logs, and as the user+                        -- and group the daemon tries to run as if+                        -- none are explicitly specified.  In general,+                        -- this should be 'Nothing', in which case the+                        -- system defaults to the name of the+                        -- executable file containing the daemon.+  user :: Maybe String, -- ^ Most daemons are initially run as root,+                        -- and try to change to another user so they+                        -- have fewer privileges and represent less of+                        -- a security threat.  This field specifies+                        -- which user it should try to run as.  If it+                        -- is 'Nothing', or if the user does not exist+                        -- on the system, it next tries to become a+                        -- user with the same name as the daemon, and+                        -- if that fails, the user @daemon@.+  group :: Maybe String, -- ^ 'group' is the group the daemon should+                         -- try to run as, and works the same way as+                         -- the user field.+  syslogOptions :: [Option], -- ^ The options the daemon should set on+                             -- syslog.  You can safely leave this as @[]@.+  pidfileDirectory :: Maybe FilePath -- ^ The directory where the+                                     -- daemon should write and look+                                     -- for the PID file.  'Nothing'+                                     -- means @/var/run@.  Unless you+                                     -- have a good reason to do+                                     -- otherwise, leave this as+                                     -- 'Nothing'.+} +-- | The simplest possible instance of 'CreateDaemon' is +-- +-- > CreateDaemon {+-- >  program = forever $ return ()+-- >  name = Nothing,+-- >  user = Nothing,+-- >  group = Nothing,+-- >  syslogOptions = [],+-- >  pidfileDirectory = Nothing,+-- > }+-- +-- which does nothing forever with all default settings.  We give it a+-- name, 'simpleDaemon', since you may want to use it as a template+-- and modify only the fields that you need.++simpleDaemon :: CreateDaemon+simpleDaemon = CreateDaemon {+  name = Nothing,+  user = Nothing,+  group = Nothing,+  syslogOptions = [],+  pidfileDirectory = Nothing,+  program = M.forever $ return ()+}+  +++ {- implementation -} -forever :: Logger -> Program -> IO ()-forever log program =     -    program log `catch` restart where+forever :: IO () -> IO ()+forever program =     +    program `catch` restart where         restart :: SomeException -> IO ()          restart e = -            do log ("unexpected exception: " ++ show e)-               log "restarting in 5 seconds"+            do syslog Error ("unexpected exception: " ++ show e)+               syslog Error "restarting in 5 seconds"                usleep (5 * 10^6)-               forever log program+               forever program  closeFileDescriptors :: IO () closeFileDescriptors = @@ -139,30 +272,33 @@         f (Left e)    = Nothing         f (Right uid) = Just uid -dropPrivileges :: String -> IO ()-dropPrivileges name = +dropPrivileges :: CreateDaemon -> IO ()+dropPrivileges daemon =      do Just ud <- getUserID "daemon"        Just gd <- getGroupID "daemon"-       u       <- fmap (maybe ud id) $ getUserID name-       g       <- fmap (maybe gd id) $ getGroupID name+       let targetUser = fromMaybe (fromJust $ name daemon) (user daemon)+           targetGroup = fromMaybe (fromJust $ name daemon) (group daemon)+       u       <- fmap (maybe ud id) $ getUserID targetUser+       g       <- fmap (maybe gd id) $ getGroupID targetGroup        setGroupID g         setUserID u -pidFile:: String -> String-pidFile name = "/var/run/" ++ name ++ ".pid"+pidFile:: CreateDaemon -> String+pidFile daemon = joinPath [dir, (fromJust $ name daemon) ++ ".pid"]+  where dir = fromMaybe "/var/run" (pidfileDirectory daemon) -pidExists :: String -> IO Bool-pidExists name = fileExist (pidFile name)+pidExists :: CreateDaemon -> IO Bool+pidExists daemon = fileExist (pidFile daemon) -pidRead :: String -> IO (Maybe CPid)-pidRead name = pidExists name >>= choose where-    choose True  = fmap (Just . read) $ readFile (pidFile name)+pidRead :: CreateDaemon -> IO (Maybe CPid)+pidRead daemon = pidExists daemon >>= choose where+    choose True  = fmap (Just . read) $ readFile (pidFile daemon)     choose False = return Nothing -pidWrite :: String -> IO ()-pidWrite name =+pidWrite :: CreateDaemon -> IO ()+pidWrite daemon =     getProcessID >>= \pid ->-    writeFile (pidFile name) (show pid)+    writeFile (pidFile daemon) (show pid)  pidLive :: CPid -> IO Bool pidLive pid = 
hdaemonize.cabal view
@@ -1,5 +1,5 @@ Name:		hdaemonize-Version:	0.2+Version:	0.3 Cabal-Version:  >= 1.6 License:	BSD3 License-file:   LICENSE@@ -16,7 +16,7 @@ Build-Type:	Simple Extra-Source-Files:	README Library-  Build-Depends:	base, unix, haskell98, hsyslog, extensible-exceptions+  Build-Depends:	base >= 4 && < 5, unix, haskell98, hsyslog, extensible-exceptions, filepath   Exposed-modules:     System.Posix.Daemonize